dscf-marketplace 0.1.5 → 0.1.7
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/models/dscf/marketplace/order.rb +105 -0
- data/app/models/dscf/marketplace/order_item.rb +30 -0
- data/app/models/dscf/marketplace/quotation.rb +184 -0
- data/app/models/dscf/marketplace/quotation_item.rb +118 -0
- data/app/models/dscf/marketplace/request_for_quotation.rb +71 -0
- data/app/models/dscf/marketplace/rfq_item.rb +81 -0
- data/db/migrate/20250828090000_create_dscf_marketplace_request_for_quotations.rb +18 -0
- data/db/migrate/20250828090100_create_dscf_marketplace_rfq_items.rb +21 -0
- data/db/migrate/20250828090200_create_dscf_marketplace_quotations.rb +24 -0
- data/db/migrate/20250828090300_create_dscf_marketplace_quotation_items.rb +25 -0
- data/db/migrate/20250828090400_change_rfq_status_to_integer.rb +35 -0
- data/db/migrate/20250828090500_change_quotation_status_to_integer.rb +35 -0
- data/db/migrate/20250828115509_create_dscf_marketplace_orders.rb +19 -0
- data/db/migrate/20250828115525_create_dscf_marketplace_order_items.rb +22 -0
- data/lib/dscf/marketplace/version.rb +1 -1
- data/spec/factories/dscf/marketplace/listings.rb +16 -4
- data/spec/factories/dscf/marketplace/order_items.rb +49 -0
- data/spec/factories/dscf/marketplace/orders.rb +36 -0
- data/spec/factories/dscf/marketplace/quotation_items.rb +18 -0
- data/spec/factories/dscf/marketplace/quotations.rb +25 -0
- data/spec/factories/dscf/marketplace/request_for_quotations.rb +8 -0
- data/spec/factories/dscf/marketplace/rfq_items.rb +9 -0
- metadata +22 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 60c2cbf7461bd253837b481338ec8105fc47e5ff3927855341efc9261bf193f5
|
4
|
+
data.tar.gz: 3d10a078bab90804d176823e1cf0e44ed74f617ca3f0adeff657339438404642
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 96051d9c835dc17d0f8c7b495c28dd48b2b45a9690711be8d9a5650f6f36643498605d4d8bfdeaf6df6a7cea2f04567b6aff8479a444c525fb68ddbc60ebf41b
|
7
|
+
data.tar.gz: 6e35c21f3b43d962d838b1f5bbfb577db65e2c004c0665e340356994a93fb1e89d594d02358209af2670a6ea8638fc909347f80a8cbdd470e40d975520433af1
|
@@ -0,0 +1,105 @@
|
|
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
|
+
|
6
|
+
belongs_to :quotation, optional: true
|
7
|
+
belongs_to :listing, optional: true
|
8
|
+
belongs_to :user, class_name: "Dscf::Core::User"
|
9
|
+
has_many :order_items, dependent: :destroy
|
10
|
+
|
11
|
+
validates :order_type, presence: true
|
12
|
+
validates :status, presence: true
|
13
|
+
validates :user, presence: true
|
14
|
+
validate :quotation_or_listing_present
|
15
|
+
|
16
|
+
before_save :calculate_total_amount
|
17
|
+
|
18
|
+
def self.create_from_quotation(quotation)
|
19
|
+
return nil unless quotation.accepted?
|
20
|
+
|
21
|
+
order = create!(
|
22
|
+
order_type: :rfq_based,
|
23
|
+
status: :pending,
|
24
|
+
quotation: quotation,
|
25
|
+
user: quotation.request_for_quotation.user,
|
26
|
+
total_amount: quotation.total_price
|
27
|
+
)
|
28
|
+
|
29
|
+
quotation.quotation_items.each do |item|
|
30
|
+
order.order_items.create!(
|
31
|
+
quotation_item: item,
|
32
|
+
product: item.product,
|
33
|
+
unit: item.unit,
|
34
|
+
quantity: item.quantity,
|
35
|
+
unit_price: item.unit_price,
|
36
|
+
status: :pending
|
37
|
+
)
|
38
|
+
end
|
39
|
+
|
40
|
+
order
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.create_from_listing(listing, user, quantity)
|
44
|
+
return nil unless listing.status == "active" && quantity <= listing.quantity
|
45
|
+
|
46
|
+
order = create!(
|
47
|
+
order_type: :direct_listing,
|
48
|
+
status: :pending,
|
49
|
+
listing: listing,
|
50
|
+
user: user,
|
51
|
+
total_amount: listing.price * quantity
|
52
|
+
)
|
53
|
+
|
54
|
+
order.order_items.create!(
|
55
|
+
listing: listing,
|
56
|
+
product: listing.supplier_product.product,
|
57
|
+
unit: listing.supplier_product.product.unit,
|
58
|
+
quantity: quantity,
|
59
|
+
unit_price: listing.price,
|
60
|
+
status: :pending
|
61
|
+
)
|
62
|
+
|
63
|
+
order
|
64
|
+
end
|
65
|
+
|
66
|
+
def total_amount
|
67
|
+
# Return stored value if it exists and is greater than 0, otherwise calculate
|
68
|
+
stored_value = super
|
69
|
+
calculated_value = order_items.sum { |item| item.quantity * item.unit_price }
|
70
|
+
|
71
|
+
# Return stored value if it's set and valid, otherwise return calculated value
|
72
|
+
return stored_value if stored_value.present? && stored_value > 0
|
73
|
+
calculated_value
|
74
|
+
end
|
75
|
+
|
76
|
+
def supplier
|
77
|
+
if rfq_based?
|
78
|
+
quotation&.business
|
79
|
+
elsif direct_listing?
|
80
|
+
listing&.business
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def confirm!
|
85
|
+
return false unless pending?
|
86
|
+
|
87
|
+
update!(status: :confirmed)
|
88
|
+
# Update order items without reloading to avoid association issues
|
89
|
+
order_items.update_all(status: OrderItem.statuses[:confirmed])
|
90
|
+
true
|
91
|
+
end
|
92
|
+
|
93
|
+
def calculate_total_amount
|
94
|
+
self.total_amount = order_items.sum { |item| item.quantity * item.unit_price }
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
def quotation_or_listing_present
|
100
|
+
unless quotation.present? || listing.present?
|
101
|
+
errors.add(:base, "Either quotation or listing must be present")
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,30 @@
|
|
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
|
+
private
|
23
|
+
|
24
|
+
def quotation_item_or_listing_present
|
25
|
+
unless quotation_item.present? || listing.present?
|
26
|
+
errors.add(:base, "Either quotation_item or listing must be present")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,184 @@
|
|
1
|
+
module Dscf
|
2
|
+
module Marketplace
|
3
|
+
class Quotation < ApplicationRecord
|
4
|
+
belongs_to :request_for_quotation, class_name: "Dscf::Marketplace::RequestForQuotation"
|
5
|
+
belongs_to :business, class_name: "Dscf::Core::Business", optional: true if defined?(Dscf::Core)
|
6
|
+
|
7
|
+
has_many :quotation_items, class_name: "Dscf::Marketplace::QuotationItem", dependent: :destroy
|
8
|
+
has_one :order, class_name: "Dscf::Marketplace::Order", dependent: :destroy
|
9
|
+
|
10
|
+
validates :request_for_quotation_id, presence: true
|
11
|
+
validates :business_id, presence: true
|
12
|
+
validates :total_price, numericality: {greater_than_or_equal_to: 0}, allow_nil: true
|
13
|
+
validates :delivery_date, presence: true
|
14
|
+
validates :valid_until, presence: true
|
15
|
+
enum :status, {draft: 0, sent: 1, accepted: 2, rejected: 3, expired: 4}, default: :draft
|
16
|
+
validates :notes, length: {maximum: 2000}, allow_blank: true
|
17
|
+
|
18
|
+
validate :valid_until_after_delivery_date
|
19
|
+
validate :delivery_date_not_in_past
|
20
|
+
validate :valid_until_not_in_past
|
21
|
+
scope :by_business, ->(business_id) { where(business_id: business_id) }
|
22
|
+
scope :valid_quotations, -> { where("valid_until > ?", Time.current) }
|
23
|
+
scope :expired_quotations, -> { where("valid_until <= ?", Time.current) }
|
24
|
+
|
25
|
+
def calculate_total_price
|
26
|
+
self.total_price = quotation_items.sum(&:subtotal).to_f
|
27
|
+
end
|
28
|
+
|
29
|
+
def update_total_price!
|
30
|
+
calculate_total_price
|
31
|
+
save!
|
32
|
+
end
|
33
|
+
|
34
|
+
def sent?
|
35
|
+
status == "sent"
|
36
|
+
end
|
37
|
+
|
38
|
+
def accepted?
|
39
|
+
status == "accepted"
|
40
|
+
end
|
41
|
+
|
42
|
+
def rejected?
|
43
|
+
status == "rejected"
|
44
|
+
end
|
45
|
+
|
46
|
+
def expired?
|
47
|
+
status == "expired"
|
48
|
+
end
|
49
|
+
|
50
|
+
def draft?
|
51
|
+
status == "draft"
|
52
|
+
end
|
53
|
+
|
54
|
+
def sent?
|
55
|
+
status == "sent"
|
56
|
+
end
|
57
|
+
|
58
|
+
def accepted?
|
59
|
+
status == "accepted"
|
60
|
+
end
|
61
|
+
|
62
|
+
def rejected?
|
63
|
+
status == "rejected"
|
64
|
+
end
|
65
|
+
|
66
|
+
def within_validity_period?
|
67
|
+
valid_until > Time.current && !expired?
|
68
|
+
end
|
69
|
+
|
70
|
+
def expired!
|
71
|
+
update_columns(status: "expired") if valid_until <= Time.current
|
72
|
+
end
|
73
|
+
|
74
|
+
def accept!
|
75
|
+
return false unless sent?
|
76
|
+
return false if within_validity_period? == false
|
77
|
+
|
78
|
+
# Close other quotations for this RFQ first
|
79
|
+
request_for_quotation.quotations.where.not(id: id).update_all(status: "rejected")
|
80
|
+
|
81
|
+
update_columns(status: "accepted")
|
82
|
+
request_for_quotation.update!(status: "selected", selected_quotation: self)
|
83
|
+
|
84
|
+
# Create order from accepted quotation
|
85
|
+
create_order_from_quotation
|
86
|
+
|
87
|
+
true
|
88
|
+
end
|
89
|
+
|
90
|
+
def create_order_from_quotation
|
91
|
+
return if order.present?
|
92
|
+
|
93
|
+
Order.create_from_quotation(self)
|
94
|
+
end
|
95
|
+
|
96
|
+
def reject!
|
97
|
+
return false unless sent?
|
98
|
+
|
99
|
+
update!(status: "rejected")
|
100
|
+
end
|
101
|
+
|
102
|
+
def send_quotation!
|
103
|
+
return false unless draft?
|
104
|
+
|
105
|
+
update!(status: "sent")
|
106
|
+
request_for_quotation.update!(status: "responded") if request_for_quotation.sent? || request_for_quotation.draft?
|
107
|
+
true
|
108
|
+
end
|
109
|
+
|
110
|
+
def days_until_expiry
|
111
|
+
return nil unless valid_until
|
112
|
+
|
113
|
+
days = ((valid_until - Time.current) / 1.day).ceil
|
114
|
+
days.negative? ? nil : days
|
115
|
+
end
|
116
|
+
|
117
|
+
def delivery_in_days
|
118
|
+
return nil unless delivery_date
|
119
|
+
|
120
|
+
((delivery_date.to_time - Time.current) / 1.day).ceil
|
121
|
+
end
|
122
|
+
|
123
|
+
def complete?
|
124
|
+
quotation_items.exists? && quotation_items.all? { |item| item.unit_price.present? }
|
125
|
+
end
|
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
|
+
|
153
|
+
private
|
154
|
+
|
155
|
+
def calculate_total_price
|
156
|
+
self.total_price = quotation_items.sum(&:subtotal).to_f
|
157
|
+
end
|
158
|
+
|
159
|
+
def valid_until_after_delivery_date
|
160
|
+
return unless valid_until && delivery_date
|
161
|
+
|
162
|
+
if valid_until <= delivery_date
|
163
|
+
errors.add(:valid_until, "must be after delivery date")
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def delivery_date_not_in_past
|
168
|
+
return unless delivery_date
|
169
|
+
|
170
|
+
if delivery_date < Date.current
|
171
|
+
errors.add(:delivery_date, "cannot be in the past")
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
def valid_until_not_in_past
|
176
|
+
return unless valid_until
|
177
|
+
|
178
|
+
if valid_until < Time.current
|
179
|
+
errors.add(:valid_until, "cannot be in the past")
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
module Dscf
|
2
|
+
module Marketplace
|
3
|
+
class QuotationItem < ApplicationRecord
|
4
|
+
belongs_to :quotation, class_name: "Dscf::Marketplace::Quotation"
|
5
|
+
belongs_to :rfq_item, class_name: "Dscf::Marketplace::RfqItem"
|
6
|
+
belongs_to :product, class_name: "Dscf::Marketplace::Product"
|
7
|
+
belongs_to :unit, class_name: "Dscf::Marketplace::Unit"
|
8
|
+
|
9
|
+
|
10
|
+
|
11
|
+
validates :quotation_id, presence: true
|
12
|
+
validates :rfq_item_id, presence: true
|
13
|
+
validates :product_id, presence: true
|
14
|
+
validates :quantity, presence: true, numericality: {greater_than: 0}
|
15
|
+
validates :unit_id, presence: true
|
16
|
+
validates :unit_price, numericality: {greater_than: 0}, allow_nil: true
|
17
|
+
validates :subtotal, numericality: {greater_than_or_equal_to: 0}, allow_nil: true
|
18
|
+
|
19
|
+
validate :product_matches_rfq_item
|
20
|
+
validate :unit_compatible_with_product
|
21
|
+
before_save :calculate_subtotal, if: :unit_price_changed?
|
22
|
+
|
23
|
+
# Only validate subtotal calculation if both unit_price and quantity are present
|
24
|
+
validate :subtotal_matches_calculation, if: :should_validate_subtotal?
|
25
|
+
|
26
|
+
scope :priced, -> { where.not(unit_price: nil) }
|
27
|
+
scope :unpriced, -> { where(unit_price: nil) }
|
28
|
+
scope :by_product, ->(product_id) { where(product_id: product_id) }
|
29
|
+
scope :by_unit, ->(unit_id) { where(unit_id: unit_id) }
|
30
|
+
|
31
|
+
def product_name
|
32
|
+
product&.name
|
33
|
+
end
|
34
|
+
|
35
|
+
def product_sku
|
36
|
+
product&.sku
|
37
|
+
end
|
38
|
+
|
39
|
+
def unit_name
|
40
|
+
unit&.name
|
41
|
+
end
|
42
|
+
|
43
|
+
def unit_code
|
44
|
+
unit&.code
|
45
|
+
end
|
46
|
+
|
47
|
+
def rfq_requested_quantity
|
48
|
+
rfq_item&.quantity
|
49
|
+
end
|
50
|
+
|
51
|
+
def quantity_difference
|
52
|
+
return nil unless quantity && rfq_requested_quantity
|
53
|
+
|
54
|
+
quantity - rfq_requested_quantity
|
55
|
+
end
|
56
|
+
|
57
|
+
def quantity_difference_percentage
|
58
|
+
return nil unless quantity && rfq_requested_quantity && rfq_requested_quantity > 0
|
59
|
+
|
60
|
+
((quantity_difference.to_f / rfq_requested_quantity) * 100).round(2)
|
61
|
+
end
|
62
|
+
|
63
|
+
def price_per_base_unit
|
64
|
+
return nil unless unit_price && quantity && quantity > 0
|
65
|
+
|
66
|
+
unit_price / quantity
|
67
|
+
end
|
68
|
+
|
69
|
+
def matches_rfq_request?
|
70
|
+
return false unless rfq_item
|
71
|
+
|
72
|
+
product_id == rfq_item.product_id &&
|
73
|
+
unit_id == rfq_item.unit_id
|
74
|
+
end
|
75
|
+
|
76
|
+
def can_create_order?
|
77
|
+
quotation.sent? || quotation.accepted?
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def calculate_subtotal
|
83
|
+
return unless unit_price && quantity
|
84
|
+
|
85
|
+
self.subtotal = unit_price * quantity
|
86
|
+
end
|
87
|
+
|
88
|
+
def product_matches_rfq_item
|
89
|
+
return unless rfq_item_id && product_id
|
90
|
+
|
91
|
+
unless product_id == rfq_item.product_id
|
92
|
+
errors.add(:product_id, "must match the product in the RFQ item")
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def unit_compatible_with_product
|
97
|
+
return unless product_id && unit_id
|
98
|
+
|
99
|
+
unless product.unit_id == unit_id || product.unit.convertible_to?(unit)
|
100
|
+
errors.add(:unit_id, "is not compatible with the selected product")
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def subtotal_matches_calculation
|
105
|
+
return unless should_validate_subtotal?
|
106
|
+
|
107
|
+
expected_subtotal = unit_price * quantity
|
108
|
+
unless subtotal == expected_subtotal
|
109
|
+
errors.add(:subtotal, "must equal unit_price * quantity")
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def should_validate_subtotal?
|
114
|
+
unit_price.present? && quantity.present? && subtotal.present?
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module Dscf
|
2
|
+
module Marketplace
|
3
|
+
class RequestForQuotation < ApplicationRecord
|
4
|
+
belongs_to :user, class_name: "Dscf::Core::User", optional: true if defined?(Dscf::Core)
|
5
|
+
belongs_to :selected_quotation, class_name: "Dscf::Marketplace::Quotation", optional: true
|
6
|
+
|
7
|
+
has_many :rfq_items, class_name: "Dscf::Marketplace::RfqItem", dependent: :destroy
|
8
|
+
has_many :quotations, class_name: "Dscf::Marketplace::Quotation", dependent: :destroy
|
9
|
+
|
10
|
+
validates :user_id, presence: true
|
11
|
+
enum :status, {draft: 0, sent: 1, responded: 2, selected: 3, closed: 4}, default: :draft
|
12
|
+
validates :notes, length: {maximum: 2000}, allow_blank: true
|
13
|
+
|
14
|
+
validate :selected_quotation_belongs_to_this_rfq
|
15
|
+
scope :by_user, ->(user_id) { where(user_id: user_id) }
|
16
|
+
|
17
|
+
def draft?
|
18
|
+
status == "draft"
|
19
|
+
end
|
20
|
+
|
21
|
+
def sent?
|
22
|
+
status == "sent"
|
23
|
+
end
|
24
|
+
|
25
|
+
def responded?
|
26
|
+
status == "responded"
|
27
|
+
end
|
28
|
+
|
29
|
+
def selected?
|
30
|
+
status == "selected"
|
31
|
+
end
|
32
|
+
|
33
|
+
def closed?
|
34
|
+
status == "closed"
|
35
|
+
end
|
36
|
+
|
37
|
+
def can_select_quotation?
|
38
|
+
sent? || responded?
|
39
|
+
end
|
40
|
+
|
41
|
+
def select_quotation!(quotation)
|
42
|
+
return false unless can_select_quotation?
|
43
|
+
return false unless quotations.include?(quotation)
|
44
|
+
|
45
|
+
update!(selected_quotation: quotation, status: "selected")
|
46
|
+
end
|
47
|
+
|
48
|
+
def total_items
|
49
|
+
rfq_items.count
|
50
|
+
end
|
51
|
+
|
52
|
+
def has_responses?
|
53
|
+
quotations.where.not(status: "draft").exists?
|
54
|
+
end
|
55
|
+
|
56
|
+
def accepted_quotations
|
57
|
+
quotations.where(status: "accepted")
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def selected_quotation_belongs_to_this_rfq
|
63
|
+
return unless selected_quotation_id
|
64
|
+
|
65
|
+
unless quotations.where(id: selected_quotation_id).exists?
|
66
|
+
errors.add(:selected_quotation_id, "must belong to this RFQ")
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module Dscf
|
2
|
+
module Marketplace
|
3
|
+
class RfqItem < ApplicationRecord
|
4
|
+
belongs_to :request_for_quotation, class_name: "Dscf::Marketplace::RequestForQuotation"
|
5
|
+
belongs_to :product, class_name: "Dscf::Marketplace::Product"
|
6
|
+
belongs_to :unit, class_name: "Dscf::Marketplace::Unit"
|
7
|
+
|
8
|
+
has_many :quotation_items, class_name: "Dscf::Marketplace::QuotationItem", dependent: :destroy
|
9
|
+
|
10
|
+
validates :request_for_quotation_id, presence: true
|
11
|
+
validates :product_id, presence: true
|
12
|
+
validates :quantity, presence: true, numericality: {greater_than: 0}
|
13
|
+
validates :unit_id, presence: true
|
14
|
+
validates :notes, length: {maximum: 1000}, allow_blank: true
|
15
|
+
|
16
|
+
validate :unit_compatible_with_product
|
17
|
+
|
18
|
+
scope :by_product, ->(product_id) { where(product_id: product_id) }
|
19
|
+
scope :by_unit, ->(unit_id) { where(unit_id: unit_id) }
|
20
|
+
|
21
|
+
def product_name
|
22
|
+
product&.name
|
23
|
+
end
|
24
|
+
|
25
|
+
def product_sku
|
26
|
+
product&.sku
|
27
|
+
end
|
28
|
+
|
29
|
+
def unit_name
|
30
|
+
unit&.name
|
31
|
+
end
|
32
|
+
|
33
|
+
def unit_code
|
34
|
+
unit&.code
|
35
|
+
end
|
36
|
+
|
37
|
+
def quotation_count
|
38
|
+
quotation_items.count
|
39
|
+
end
|
40
|
+
|
41
|
+
def has_quotations?
|
42
|
+
quotation_items.exists?
|
43
|
+
end
|
44
|
+
|
45
|
+
def lowest_quoted_price
|
46
|
+
quotation_items.where.not(unit_price: nil).minimum(:unit_price)
|
47
|
+
end
|
48
|
+
|
49
|
+
def highest_quoted_price
|
50
|
+
quotation_items.where.not(unit_price: nil).maximum(:unit_price)
|
51
|
+
end
|
52
|
+
|
53
|
+
def quoted_price_range
|
54
|
+
return nil unless has_quotations?
|
55
|
+
|
56
|
+
low = lowest_quoted_price
|
57
|
+
high = highest_quoted_price
|
58
|
+
|
59
|
+
return low if low == high
|
60
|
+
"#{low} - #{high}"
|
61
|
+
end
|
62
|
+
|
63
|
+
def total_requested_quantity
|
64
|
+
return nil unless quantity && unit
|
65
|
+
|
66
|
+
# Convert to base unit for comparison if needed
|
67
|
+
quantity
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def unit_compatible_with_product
|
73
|
+
return unless product_id && unit_id
|
74
|
+
|
75
|
+
unless product.unit_id == unit_id || product.unit.convertible_to?(unit)
|
76
|
+
errors.add(:unit_id, "is not compatible with the selected product")
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
class CreateDscfMarketplaceRequestForQuotations < ActiveRecord::Migration[8.0]
|
2
|
+
def change
|
3
|
+
create_table :dscf_marketplace_request_for_quotations do |t|
|
4
|
+
t.bigint :user_id, null: false
|
5
|
+
t.string :status, null: false
|
6
|
+
t.bigint :selected_quotation_id
|
7
|
+
t.text :notes
|
8
|
+
|
9
|
+
t.timestamps
|
10
|
+
|
11
|
+
t.index [ :user_id ], name: "user_on_dm_rfqs_indx"
|
12
|
+
t.index [ :status ], name: "status_on_dm_rfqs_indx"
|
13
|
+
t.index [ :selected_quotation_id ], name: "selected_quotation_on_dm_rfqs_indx"
|
14
|
+
end
|
15
|
+
|
16
|
+
add_foreign_key :dscf_marketplace_request_for_quotations, :dscf_core_users, column: :user_id, name: "fk_dm_rfqs_user"
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class CreateDscfMarketplaceRfqItems < ActiveRecord::Migration[8.0]
|
2
|
+
def change
|
3
|
+
create_table :dscf_marketplace_rfq_items do |t|
|
4
|
+
t.bigint :request_for_quotation_id, null: false
|
5
|
+
t.bigint :product_id, null: false
|
6
|
+
t.decimal :quantity, precision: 15, scale: 6, null: false
|
7
|
+
t.bigint :unit_id, null: false
|
8
|
+
t.text :notes
|
9
|
+
|
10
|
+
t.timestamps
|
11
|
+
|
12
|
+
t.index [ :request_for_quotation_id ], name: "rfq_on_dm_rfq_items_indx"
|
13
|
+
t.index [ :product_id ], name: "product_on_dm_rfq_items_indx"
|
14
|
+
t.index [ :unit_id ], name: "unit_on_dm_rfq_items_indx"
|
15
|
+
end
|
16
|
+
|
17
|
+
add_foreign_key :dscf_marketplace_rfq_items, :dscf_marketplace_request_for_quotations, column: :request_for_quotation_id, name: "fk_dm_rfq_items_rfq"
|
18
|
+
add_foreign_key :dscf_marketplace_rfq_items, :dscf_marketplace_products, column: :product_id, name: "fk_dm_rfq_items_product"
|
19
|
+
add_foreign_key :dscf_marketplace_rfq_items, :dscf_marketplace_units, column: :unit_id, name: "fk_dm_rfq_items_unit"
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
class CreateDscfMarketplaceQuotations < ActiveRecord::Migration[8.0]
|
2
|
+
def change
|
3
|
+
create_table :dscf_marketplace_quotations do |t|
|
4
|
+
t.bigint :request_for_quotation_id, null: false
|
5
|
+
t.bigint :business_id, null: false
|
6
|
+
t.decimal :total_price, precision: 15, scale: 2
|
7
|
+
t.date :delivery_date, null: false
|
8
|
+
t.datetime :valid_until, null: false
|
9
|
+
t.string :status, null: false
|
10
|
+
t.text :notes
|
11
|
+
|
12
|
+
t.timestamps
|
13
|
+
|
14
|
+
t.index [ :request_for_quotation_id ], name: "rfq_on_dm_quotations_indx"
|
15
|
+
t.index [ :business_id ], name: "business_on_dm_quotations_indx"
|
16
|
+
t.index [ :status ], name: "status_on_dm_quotations_indx"
|
17
|
+
t.index [ :valid_until ], name: "valid_until_on_dm_quotations_indx"
|
18
|
+
end
|
19
|
+
|
20
|
+
add_foreign_key :dscf_marketplace_quotations, :dscf_marketplace_request_for_quotations, column: :request_for_quotation_id, name: "fk_dm_quotations_rfq"
|
21
|
+
add_foreign_key :dscf_marketplace_quotations, :dscf_core_businesses, column: :business_id, name: "fk_dm_quotations_business"
|
22
|
+
add_foreign_key :dscf_marketplace_request_for_quotations, :dscf_marketplace_quotations, column: :selected_quotation_id, name: "fk_dm_rfqs_selected_quotation"
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
class CreateDscfMarketplaceQuotationItems < ActiveRecord::Migration[8.0]
|
2
|
+
def change
|
3
|
+
create_table :dscf_marketplace_quotation_items do |t|
|
4
|
+
t.bigint :quotation_id, null: false
|
5
|
+
t.bigint :rfq_item_id, null: false
|
6
|
+
t.bigint :product_id, null: false
|
7
|
+
t.decimal :quantity, precision: 15, scale: 6, null: false
|
8
|
+
t.bigint :unit_id, null: false
|
9
|
+
t.decimal :unit_price, precision: 15, scale: 2
|
10
|
+
t.decimal :subtotal, precision: 15, scale: 2
|
11
|
+
|
12
|
+
t.timestamps
|
13
|
+
|
14
|
+
t.index [ :quotation_id ], name: "quotation_on_dm_quotation_items_indx"
|
15
|
+
t.index [ :rfq_item_id ], name: "rfq_item_on_dm_quotation_items_indx"
|
16
|
+
t.index [ :product_id ], name: "product_on_dm_quotation_items_indx"
|
17
|
+
t.index [ :unit_id ], name: "unit_on_dm_quotation_items_indx"
|
18
|
+
end
|
19
|
+
|
20
|
+
add_foreign_key :dscf_marketplace_quotation_items, :dscf_marketplace_quotations, column: :quotation_id, name: "fk_dm_quotation_items_quotation"
|
21
|
+
add_foreign_key :dscf_marketplace_quotation_items, :dscf_marketplace_rfq_items, column: :rfq_item_id, name: "fk_dm_quotation_items_rfq_item"
|
22
|
+
add_foreign_key :dscf_marketplace_quotation_items, :dscf_marketplace_products, column: :product_id, name: "fk_dm_quotation_items_product"
|
23
|
+
add_foreign_key :dscf_marketplace_quotation_items, :dscf_marketplace_units, column: :unit_id, name: "fk_dm_quotation_items_unit"
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
class ChangeRfqStatusToInteger < ActiveRecord::Migration[8.0]
|
2
|
+
def up
|
3
|
+
# Map string values to integers
|
4
|
+
execute <<-SQL
|
5
|
+
UPDATE dscf_marketplace_request_for_quotations
|
6
|
+
SET status = CASE
|
7
|
+
WHEN status = 'draft' THEN 0
|
8
|
+
WHEN status = 'sent' THEN 1
|
9
|
+
WHEN status = 'responded' THEN 2
|
10
|
+
WHEN status = 'selected' THEN 3
|
11
|
+
WHEN status = 'closed' THEN 4
|
12
|
+
ELSE 0
|
13
|
+
END
|
14
|
+
SQL
|
15
|
+
|
16
|
+
change_column :dscf_marketplace_request_for_quotations, :status, :integer, using: 'status::integer', default: 0, null: false
|
17
|
+
end
|
18
|
+
|
19
|
+
def down
|
20
|
+
change_column :dscf_marketplace_request_for_quotations, :status, :string
|
21
|
+
|
22
|
+
# Map integers back to strings
|
23
|
+
execute <<-SQL
|
24
|
+
UPDATE dscf_marketplace_request_for_quotations
|
25
|
+
SET status = CASE
|
26
|
+
WHEN status = 0 THEN 'draft'
|
27
|
+
WHEN status = 1 THEN 'sent'
|
28
|
+
WHEN status = 2 THEN 'responded'
|
29
|
+
WHEN status = 3 THEN 'selected'
|
30
|
+
WHEN status = 4 THEN 'closed'
|
31
|
+
ELSE 'draft'
|
32
|
+
END
|
33
|
+
SQL
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
class ChangeQuotationStatusToInteger < ActiveRecord::Migration[8.0]
|
2
|
+
def up
|
3
|
+
# Map string values to integers
|
4
|
+
execute <<-SQL
|
5
|
+
UPDATE dscf_marketplace_quotations
|
6
|
+
SET status = CASE
|
7
|
+
WHEN status = 'draft' THEN 0
|
8
|
+
WHEN status = 'sent' THEN 1
|
9
|
+
WHEN status = 'accepted' THEN 2
|
10
|
+
WHEN status = 'rejected' THEN 3
|
11
|
+
WHEN status = 'expired' THEN 4
|
12
|
+
ELSE 0
|
13
|
+
END
|
14
|
+
SQL
|
15
|
+
|
16
|
+
change_column :dscf_marketplace_quotations, :status, :integer, using: 'status::integer', default: 0, null: false
|
17
|
+
end
|
18
|
+
|
19
|
+
def down
|
20
|
+
change_column :dscf_marketplace_quotations, :status, :string
|
21
|
+
|
22
|
+
# Map integers back to strings
|
23
|
+
execute <<-SQL
|
24
|
+
UPDATE dscf_marketplace_quotations
|
25
|
+
SET status = CASE
|
26
|
+
WHEN status = 0 THEN 'draft'
|
27
|
+
WHEN status = 1 THEN 'sent'
|
28
|
+
WHEN status = 2 THEN 'accepted'
|
29
|
+
WHEN status = 3 THEN 'rejected'
|
30
|
+
WHEN status = 4 THEN 'expired'
|
31
|
+
ELSE 'draft'
|
32
|
+
END
|
33
|
+
SQL
|
34
|
+
end
|
35
|
+
end
|
@@ -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
|
@@ -1,10 +1,22 @@
|
|
1
1
|
FactoryBot.define do
|
2
|
-
factory :dscf_marketplace_listing, class:
|
3
|
-
business
|
4
|
-
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
|
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
|
+
# Don't create order by default, let tests specify
|
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,36 @@
|
|
1
|
+
FactoryBot.define do
|
2
|
+
factory :dscf_marketplace_order, class: "Dscf::Marketplace::Order" do
|
3
|
+
order_type { :rfq_based }
|
4
|
+
status { :pending }
|
5
|
+
association :user, factory: :dscf_core_user
|
6
|
+
quotation { nil }
|
7
|
+
listing { nil }
|
8
|
+
|
9
|
+
trait :rfq_based do
|
10
|
+
order_type { :rfq_based }
|
11
|
+
association :quotation, factory: :dscf_marketplace_quotation
|
12
|
+
end
|
13
|
+
|
14
|
+
trait :direct_listing do
|
15
|
+
order_type { :direct_listing }
|
16
|
+
association :listing, factory: :dscf_marketplace_listing
|
17
|
+
quotation { nil }
|
18
|
+
end
|
19
|
+
|
20
|
+
trait :confirmed do
|
21
|
+
status { :confirmed }
|
22
|
+
end
|
23
|
+
|
24
|
+
trait :processing do
|
25
|
+
status { :processing }
|
26
|
+
end
|
27
|
+
|
28
|
+
trait :completed do
|
29
|
+
status { :completed }
|
30
|
+
end
|
31
|
+
|
32
|
+
trait :cancelled do
|
33
|
+
status { :cancelled }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
FactoryBot.define do
|
2
|
+
factory :dscf_marketplace_quotation_item, class: "Dscf::Marketplace::QuotationItem" do
|
3
|
+
association :quotation, factory: :dscf_marketplace_quotation
|
4
|
+
association :rfq_item, factory: :dscf_marketplace_rfq_item
|
5
|
+
product { rfq_item.product }
|
6
|
+
unit { rfq_item.unit }
|
7
|
+
quantity { 10.0 }
|
8
|
+
unit_price { 15.0 }
|
9
|
+
subtotal { nil }
|
10
|
+
|
11
|
+
after(:build) do |item|
|
12
|
+
# Only calculate subtotal if it's not explicitly set
|
13
|
+
if item.subtotal.nil? && item.unit_price && item.quantity
|
14
|
+
item.subtotal = item.unit_price * item.quantity
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
FactoryBot.define do
|
2
|
+
factory :dscf_marketplace_quotation, class: "Dscf::Marketplace::Quotation" do
|
3
|
+
association :request_for_quotation, factory: :dscf_marketplace_request_for_quotation
|
4
|
+
association :business, factory: :dscf_core_business
|
5
|
+
total_price { nil }
|
6
|
+
delivery_date { 5.days.from_now.to_date }
|
7
|
+
valid_until { 15.days.from_now }
|
8
|
+
status { :draft }
|
9
|
+
notes { "Sample quotation notes" }
|
10
|
+
|
11
|
+
trait :expired do
|
12
|
+
after(:create) do |quotation|
|
13
|
+
quotation.update_columns(delivery_date: 10.days.ago.to_date, valid_until: 5.days.ago, status: 4)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
trait :sent do
|
18
|
+
status { :sent }
|
19
|
+
end
|
20
|
+
|
21
|
+
trait :accepted do
|
22
|
+
status { :accepted }
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
FactoryBot.define do
|
2
|
+
factory :dscf_marketplace_rfq_item, class: 'Dscf::Marketplace::RfqItem' do
|
3
|
+
association :request_for_quotation, factory: :dscf_marketplace_request_for_quotation
|
4
|
+
association :product, factory: :dscf_marketplace_product
|
5
|
+
unit { product.unit }
|
6
|
+
quantity { 10.0 }
|
7
|
+
notes { "Sample RFQ item notes" }
|
8
|
+
end
|
9
|
+
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.
|
4
|
+
version: 0.1.7
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Asrat
|
8
8
|
bindir: bin
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-08-
|
10
|
+
date: 2025-08-31 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: rails
|
@@ -423,7 +423,13 @@ files:
|
|
423
423
|
- app/models/dscf/marketplace/application_record.rb
|
424
424
|
- app/models/dscf/marketplace/category.rb
|
425
425
|
- app/models/dscf/marketplace/listing.rb
|
426
|
+
- app/models/dscf/marketplace/order.rb
|
427
|
+
- app/models/dscf/marketplace/order_item.rb
|
426
428
|
- app/models/dscf/marketplace/product.rb
|
429
|
+
- app/models/dscf/marketplace/quotation.rb
|
430
|
+
- app/models/dscf/marketplace/quotation_item.rb
|
431
|
+
- app/models/dscf/marketplace/request_for_quotation.rb
|
432
|
+
- app/models/dscf/marketplace/rfq_item.rb
|
427
433
|
- app/models/dscf/marketplace/supplier_product.rb
|
428
434
|
- app/models/dscf/marketplace/unit.rb
|
429
435
|
- app/models/dscf/marketplace/unit_conversion.rb
|
@@ -435,6 +441,14 @@ files:
|
|
435
441
|
- db/migrate/20250828062945_create_dscf_marketplace_products.rb
|
436
442
|
- db/migrate/20250828072514_create_dscf_marketplace_supplier_products.rb
|
437
443
|
- db/migrate/20250828081657_create_dscf_marketplace_listings.rb
|
444
|
+
- db/migrate/20250828090000_create_dscf_marketplace_request_for_quotations.rb
|
445
|
+
- db/migrate/20250828090100_create_dscf_marketplace_rfq_items.rb
|
446
|
+
- db/migrate/20250828090200_create_dscf_marketplace_quotations.rb
|
447
|
+
- db/migrate/20250828090300_create_dscf_marketplace_quotation_items.rb
|
448
|
+
- db/migrate/20250828090400_change_rfq_status_to_integer.rb
|
449
|
+
- db/migrate/20250828090500_change_quotation_status_to_integer.rb
|
450
|
+
- db/migrate/20250828115509_create_dscf_marketplace_orders.rb
|
451
|
+
- db/migrate/20250828115525_create_dscf_marketplace_order_items.rb
|
438
452
|
- lib/dscf/marketplace.rb
|
439
453
|
- lib/dscf/marketplace/engine.rb
|
440
454
|
- lib/dscf/marketplace/version.rb
|
@@ -444,7 +458,13 @@ files:
|
|
444
458
|
- spec/factories/dscf/core/users.rb
|
445
459
|
- spec/factories/dscf/marketplace/categories.rb
|
446
460
|
- spec/factories/dscf/marketplace/listings.rb
|
461
|
+
- spec/factories/dscf/marketplace/order_items.rb
|
462
|
+
- spec/factories/dscf/marketplace/orders.rb
|
447
463
|
- spec/factories/dscf/marketplace/products.rb
|
464
|
+
- spec/factories/dscf/marketplace/quotation_items.rb
|
465
|
+
- spec/factories/dscf/marketplace/quotations.rb
|
466
|
+
- spec/factories/dscf/marketplace/request_for_quotations.rb
|
467
|
+
- spec/factories/dscf/marketplace/rfq_items.rb
|
448
468
|
- spec/factories/dscf/marketplace/supplier_products.rb
|
449
469
|
- spec/factories/dscf/marketplace/unit_conversions.rb
|
450
470
|
- spec/factories/dscf/marketplace/units.rb
|