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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f6b077453f450386b96a2df3aac342c13fe4ec2bc97d4e34fa48b1e4918df3ed
4
- data.tar.gz: 837589f60812fcf77b8a9958b344ec274458434ba63e695538746165349af8dd
3
+ metadata.gz: 60c2cbf7461bd253837b481338ec8105fc47e5ff3927855341efc9261bf193f5
4
+ data.tar.gz: 3d10a078bab90804d176823e1cf0e44ed74f617ca3f0adeff657339438404642
5
5
  SHA512:
6
- metadata.gz: f864dfe924f7002b4edc7686e3f227bfbf500ed63a1ecc67b3aa0d34fc76123d0b0d900495644da1552bd28539f7ec56db6fff44aa4c4e4320e8f72f741619a8
7
- data.tar.gz: 5a5aae5e8036726d534492e592a984e2185eb8e4f4102596c7d918c61fb8c293e8b2da1c60954dbd200c5f88d709d3267fe9b523634938b91fb8137e8f162fc2
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,5 +1,5 @@
1
1
  module Dscf
2
2
  module Marketplace
3
- VERSION = "0.1.5".freeze
3
+ VERSION = "0.1.7".freeze
4
4
  end
5
5
  end
@@ -1,10 +1,22 @@
1
1
  FactoryBot.define do
2
- factory :dscf_marketplace_listing, class: 'Dscf::Marketplace::Listing' do
3
- business { create(:dscf_core_business) }
4
- supplier_product { create(:dscf_marketplace_supplier_product) }
2
+ factory :dscf_marketplace_listing, class: "Dscf::Marketplace::Listing" do
3
+ association :business, factory: :dscf_core_business
4
+ association :supplier_product, factory: :dscf_marketplace_supplier_product
5
5
  price { 500.00 }
6
6
  quantity { 100.0 }
7
7
  description { "High quality product listing" }
8
- status { :draft }
8
+ # status defaults to :draft from model
9
+
10
+ trait :active do
11
+ status { :active }
12
+ end
13
+
14
+ trait :draft do
15
+ status { :draft }
16
+ end
17
+
18
+ trait :paused do
19
+ status { :paused }
20
+ end
9
21
  end
10
22
  end
@@ -0,0 +1,49 @@
1
+ FactoryBot.define do
2
+ factory :dscf_marketplace_order_item, class: "Dscf::Marketplace::OrderItem" do
3
+ # 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,8 @@
1
+ FactoryBot.define do
2
+ factory :dscf_marketplace_request_for_quotation, class: 'Dscf::Marketplace::RequestForQuotation' do
3
+ association :user, factory: :dscf_core_user
4
+ status { "draft" }
5
+ selected_quotation { nil }
6
+ notes { "Sample RFQ notes" }
7
+ end
8
+ 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.5
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-28 00:00:00.000000000 Z
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