dscf-marketplace 0.1.6 → 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: 82b1976ad1b3d99364b621d51ce06c5ac51e8eb818e10527580419d61f5aeb3d
4
- data.tar.gz: 6264cf89e35f2f24cc756f0eeaa48336af30b2e20473bab859453faf3964691e
3
+ metadata.gz: 60c2cbf7461bd253837b481338ec8105fc47e5ff3927855341efc9261bf193f5
4
+ data.tar.gz: 3d10a078bab90804d176823e1cf0e44ed74f617ca3f0adeff657339438404642
5
5
  SHA512:
6
- metadata.gz: 9a4e05d5b18d0bc545d036b9f623eaa65345b3e641cc069a1bde5fc0a7179fa0a0aa7bc4cd2d3243406c32dd4d5e0ee6ed3fbb05133770f9da638161a7a7341d
7
- data.tar.gz: 0eaa365be89d92f5320e5a7fca4ee74f300f19b4c5ad325740428bf456126cad807f9fbc700565262c289a912c2fe23661246308b9c9a192631a3a4388d9211d
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
@@ -5,6 +5,7 @@ module Dscf
5
5
  belongs_to :business, class_name: "Dscf::Core::Business", optional: true if defined?(Dscf::Core)
6
6
 
7
7
  has_many :quotation_items, class_name: "Dscf::Marketplace::QuotationItem", dependent: :destroy
8
+ has_one :order, class_name: "Dscf::Marketplace::Order", dependent: :destroy
8
9
 
9
10
  validates :request_for_quotation_id, presence: true
10
11
  validates :business_id, presence: true
@@ -79,9 +80,19 @@ module Dscf
79
80
 
80
81
  update_columns(status: "accepted")
81
82
  request_for_quotation.update!(status: "selected", selected_quotation: self)
83
+
84
+ # Create order from accepted quotation
85
+ create_order_from_quotation
86
+
82
87
  true
83
88
  end
84
89
 
90
+ def create_order_from_quotation
91
+ return if order.present?
92
+
93
+ Order.create_from_quotation(self)
94
+ end
95
+
85
96
  def reject!
86
97
  return false unless sent?
87
98
 
@@ -113,6 +124,32 @@ module Dscf
113
124
  quotation_items.exists? && quotation_items.all? { |item| item.unit_price.present? }
114
125
  end
115
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
+
116
153
  private
117
154
 
118
155
  def calculate_total_price
@@ -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.6".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
@@ -1,5 +1,5 @@
1
1
  FactoryBot.define do
2
- factory :dscf_marketplace_quotation_item, class: 'Dscf::Marketplace::QuotationItem' do
2
+ factory :dscf_marketplace_quotation_item, class: "Dscf::Marketplace::QuotationItem" do
3
3
  association :quotation, factory: :dscf_marketplace_quotation
4
4
  association :rfq_item, factory: :dscf_marketplace_rfq_item
5
5
  product { rfq_item.product }
@@ -1,11 +1,11 @@
1
1
  FactoryBot.define do
2
- factory :dscf_marketplace_quotation, class: 'Dscf::Marketplace::Quotation' do
2
+ factory :dscf_marketplace_quotation, class: "Dscf::Marketplace::Quotation" do
3
3
  association :request_for_quotation, factory: :dscf_marketplace_request_for_quotation
4
4
  association :business, factory: :dscf_core_business
5
5
  total_price { nil }
6
6
  delivery_date { 5.days.from_now.to_date }
7
7
  valid_until { 15.days.from_now }
8
- status { "draft" }
8
+ status { :draft }
9
9
  notes { "Sample quotation notes" }
10
10
 
11
11
  trait :expired do
@@ -15,11 +15,11 @@ FactoryBot.define do
15
15
  end
16
16
 
17
17
  trait :sent do
18
- status { "sent" }
18
+ status { :sent }
19
19
  end
20
20
 
21
21
  trait :accepted do
22
- status { "accepted" }
22
+ status { :accepted }
23
23
  end
24
24
  end
25
25
  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.6
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,6 +423,8 @@ 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
427
429
  - app/models/dscf/marketplace/quotation.rb
428
430
  - app/models/dscf/marketplace/quotation_item.rb
@@ -445,6 +447,8 @@ files:
445
447
  - db/migrate/20250828090300_create_dscf_marketplace_quotation_items.rb
446
448
  - db/migrate/20250828090400_change_rfq_status_to_integer.rb
447
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
448
452
  - lib/dscf/marketplace.rb
449
453
  - lib/dscf/marketplace/engine.rb
450
454
  - lib/dscf/marketplace/version.rb
@@ -453,12 +457,14 @@ files:
453
457
  - spec/factories/dscf/core/businesses.rb
454
458
  - spec/factories/dscf/core/users.rb
455
459
  - 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
460
460
  - spec/factories/dscf/marketplace/listings.rb
461
+ - spec/factories/dscf/marketplace/order_items.rb
462
+ - spec/factories/dscf/marketplace/orders.rb
461
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
462
468
  - spec/factories/dscf/marketplace/supplier_products.rb
463
469
  - spec/factories/dscf/marketplace/unit_conversions.rb
464
470
  - spec/factories/dscf/marketplace/units.rb