dscf-marketplace 0.1.5 → 0.1.6

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: 82b1976ad1b3d99364b621d51ce06c5ac51e8eb818e10527580419d61f5aeb3d
4
+ data.tar.gz: 6264cf89e35f2f24cc756f0eeaa48336af30b2e20473bab859453faf3964691e
5
5
  SHA512:
6
- metadata.gz: f864dfe924f7002b4edc7686e3f227bfbf500ed63a1ecc67b3aa0d34fc76123d0b0d900495644da1552bd28539f7ec56db6fff44aa4c4e4320e8f72f741619a8
7
- data.tar.gz: 5a5aae5e8036726d534492e592a984e2185eb8e4f4102596c7d918c61fb8c293e8b2da1c60954dbd200c5f88d709d3267fe9b523634938b91fb8137e8f162fc2
6
+ metadata.gz: 9a4e05d5b18d0bc545d036b9f623eaa65345b3e641cc069a1bde5fc0a7179fa0a0aa7bc4cd2d3243406c32dd4d5e0ee6ed3fbb05133770f9da638161a7a7341d
7
+ data.tar.gz: 0eaa365be89d92f5320e5a7fca4ee74f300f19b4c5ad325740428bf456126cad807f9fbc700565262c289a912c2fe23661246308b9c9a192631a3a4388d9211d
@@ -0,0 +1,147 @@
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
+
9
+ validates :request_for_quotation_id, presence: true
10
+ validates :business_id, presence: true
11
+ validates :total_price, numericality: {greater_than_or_equal_to: 0}, allow_nil: true
12
+ validates :delivery_date, presence: true
13
+ validates :valid_until, presence: true
14
+ enum :status, {draft: 0, sent: 1, accepted: 2, rejected: 3, expired: 4}, default: :draft
15
+ validates :notes, length: {maximum: 2000}, allow_blank: true
16
+
17
+ validate :valid_until_after_delivery_date
18
+ validate :delivery_date_not_in_past
19
+ validate :valid_until_not_in_past
20
+ scope :by_business, ->(business_id) { where(business_id: business_id) }
21
+ scope :valid_quotations, -> { where("valid_until > ?", Time.current) }
22
+ scope :expired_quotations, -> { where("valid_until <= ?", Time.current) }
23
+
24
+ def calculate_total_price
25
+ self.total_price = quotation_items.sum(&:subtotal).to_f
26
+ end
27
+
28
+ def update_total_price!
29
+ calculate_total_price
30
+ save!
31
+ end
32
+
33
+ def sent?
34
+ status == "sent"
35
+ end
36
+
37
+ def accepted?
38
+ status == "accepted"
39
+ end
40
+
41
+ def rejected?
42
+ status == "rejected"
43
+ end
44
+
45
+ def expired?
46
+ status == "expired"
47
+ end
48
+
49
+ def draft?
50
+ status == "draft"
51
+ end
52
+
53
+ def sent?
54
+ status == "sent"
55
+ end
56
+
57
+ def accepted?
58
+ status == "accepted"
59
+ end
60
+
61
+ def rejected?
62
+ status == "rejected"
63
+ end
64
+
65
+ def within_validity_period?
66
+ valid_until > Time.current && !expired?
67
+ end
68
+
69
+ def expired!
70
+ update_columns(status: "expired") if valid_until <= Time.current
71
+ end
72
+
73
+ def accept!
74
+ return false unless sent?
75
+ return false if within_validity_period? == false
76
+
77
+ # Close other quotations for this RFQ first
78
+ request_for_quotation.quotations.where.not(id: id).update_all(status: "rejected")
79
+
80
+ update_columns(status: "accepted")
81
+ request_for_quotation.update!(status: "selected", selected_quotation: self)
82
+ true
83
+ end
84
+
85
+ def reject!
86
+ return false unless sent?
87
+
88
+ update!(status: "rejected")
89
+ end
90
+
91
+ def send_quotation!
92
+ return false unless draft?
93
+
94
+ update!(status: "sent")
95
+ request_for_quotation.update!(status: "responded") if request_for_quotation.sent? || request_for_quotation.draft?
96
+ true
97
+ end
98
+
99
+ def days_until_expiry
100
+ return nil unless valid_until
101
+
102
+ days = ((valid_until - Time.current) / 1.day).ceil
103
+ days.negative? ? nil : days
104
+ end
105
+
106
+ def delivery_in_days
107
+ return nil unless delivery_date
108
+
109
+ ((delivery_date.to_time - Time.current) / 1.day).ceil
110
+ end
111
+
112
+ def complete?
113
+ quotation_items.exists? && quotation_items.all? { |item| item.unit_price.present? }
114
+ end
115
+
116
+ private
117
+
118
+ def calculate_total_price
119
+ self.total_price = quotation_items.sum(&:subtotal).to_f
120
+ end
121
+
122
+ def valid_until_after_delivery_date
123
+ return unless valid_until && delivery_date
124
+
125
+ if valid_until <= delivery_date
126
+ errors.add(:valid_until, "must be after delivery date")
127
+ end
128
+ end
129
+
130
+ def delivery_date_not_in_past
131
+ return unless delivery_date
132
+
133
+ if delivery_date < Date.current
134
+ errors.add(:delivery_date, "cannot be in the past")
135
+ end
136
+ end
137
+
138
+ def valid_until_not_in_past
139
+ return unless valid_until
140
+
141
+ if valid_until < Time.current
142
+ errors.add(:valid_until, "cannot be in the past")
143
+ end
144
+ end
145
+ end
146
+ end
147
+ 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
@@ -1,5 +1,5 @@
1
1
  module Dscf
2
2
  module Marketplace
3
- VERSION = "0.1.5".freeze
3
+ VERSION = "0.1.6".freeze
4
4
  end
5
5
  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,7 +1,7 @@
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.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Asrat
@@ -424,6 +424,10 @@ files:
424
424
  - app/models/dscf/marketplace/category.rb
425
425
  - app/models/dscf/marketplace/listing.rb
426
426
  - app/models/dscf/marketplace/product.rb
427
+ - app/models/dscf/marketplace/quotation.rb
428
+ - app/models/dscf/marketplace/quotation_item.rb
429
+ - app/models/dscf/marketplace/request_for_quotation.rb
430
+ - app/models/dscf/marketplace/rfq_item.rb
427
431
  - app/models/dscf/marketplace/supplier_product.rb
428
432
  - app/models/dscf/marketplace/unit.rb
429
433
  - app/models/dscf/marketplace/unit_conversion.rb
@@ -435,6 +439,12 @@ files:
435
439
  - db/migrate/20250828062945_create_dscf_marketplace_products.rb
436
440
  - db/migrate/20250828072514_create_dscf_marketplace_supplier_products.rb
437
441
  - db/migrate/20250828081657_create_dscf_marketplace_listings.rb
442
+ - db/migrate/20250828090000_create_dscf_marketplace_request_for_quotations.rb
443
+ - db/migrate/20250828090100_create_dscf_marketplace_rfq_items.rb
444
+ - db/migrate/20250828090200_create_dscf_marketplace_quotations.rb
445
+ - db/migrate/20250828090300_create_dscf_marketplace_quotation_items.rb
446
+ - db/migrate/20250828090400_change_rfq_status_to_integer.rb
447
+ - db/migrate/20250828090500_change_quotation_status_to_integer.rb
438
448
  - lib/dscf/marketplace.rb
439
449
  - lib/dscf/marketplace/engine.rb
440
450
  - lib/dscf/marketplace/version.rb
@@ -443,6 +453,10 @@ files:
443
453
  - spec/factories/dscf/core/businesses.rb
444
454
  - spec/factories/dscf/core/users.rb
445
455
  - spec/factories/dscf/marketplace/categories.rb
456
+ - spec/factories/dscf/marketplace/dscf/marketplace/quotation_items.rb
457
+ - spec/factories/dscf/marketplace/dscf/marketplace/quotations.rb
458
+ - spec/factories/dscf/marketplace/dscf/marketplace/request_for_quotations.rb
459
+ - spec/factories/dscf/marketplace/dscf/marketplace/rfq_items.rb
446
460
  - spec/factories/dscf/marketplace/listings.rb
447
461
  - spec/factories/dscf/marketplace/products.rb
448
462
  - spec/factories/dscf/marketplace/supplier_products.rb