dscf-marketplace 0.1.3 → 0.1.5
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/listing.rb +55 -0
- data/app/models/dscf/marketplace/product.rb +62 -0
- data/app/models/dscf/marketplace/supplier_product.rb +47 -0
- data/db/migrate/20250828062945_create_dscf_marketplace_products.rb +30 -0
- data/db/migrate/20250828072514_create_dscf_marketplace_supplier_products.rb +32 -0
- data/db/migrate/20250828081657_create_dscf_marketplace_listings.rb +31 -0
- data/lib/dscf/marketplace/version.rb +1 -1
- data/spec/factories/dscf/core/business_types.rb +5 -0
- data/spec/factories/dscf/core/businesses.rb +11 -0
- data/spec/factories/dscf/core/users.rb +8 -0
- data/spec/factories/dscf/marketplace/listings.rb +10 -0
- data/spec/factories/dscf/marketplace/products.rb +12 -0
- data/spec/factories/dscf/marketplace/supplier_products.rb +10 -0
- metadata +14 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f6b077453f450386b96a2df3aac342c13fe4ec2bc97d4e34fa48b1e4918df3ed
|
4
|
+
data.tar.gz: 837589f60812fcf77b8a9958b344ec274458434ba63e695538746165349af8dd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f864dfe924f7002b4edc7686e3f227bfbf500ed63a1ecc67b3aa0d34fc76123d0b0d900495644da1552bd28539f7ec56db6fff44aa4c4e4320e8f72f741619a8
|
7
|
+
data.tar.gz: 5a5aae5e8036726d534492e592a984e2185eb8e4f4102596c7d918c61fb8c293e8b2da1c60954dbd200c5f88d709d3267fe9b523634938b91fb8137e8f162fc2
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Dscf::Marketplace
|
2
|
+
class Listing < ApplicationRecord
|
3
|
+
# References
|
4
|
+
belongs_to :business, class_name: "Dscf::Core::Business"
|
5
|
+
belongs_to :supplier_product, class_name: "Dscf::Marketplace::SupplierProduct"
|
6
|
+
|
7
|
+
# Relationships
|
8
|
+
has_many :order_items, class_name: "Dscf::Marketplace::OrderItem"
|
9
|
+
|
10
|
+
# Status enum
|
11
|
+
enum :status, {active: 0, draft: 1, paused: 2, sold_out: 3}, default: :draft
|
12
|
+
|
13
|
+
# Validations
|
14
|
+
validates :price, numericality: {greater_than: 0}, presence: true
|
15
|
+
validates :quantity, numericality: {greater_than: 0}, presence: true
|
16
|
+
validates :business_id, presence: true
|
17
|
+
validates :supplier_product_id, presence: true
|
18
|
+
|
19
|
+
# Scopes
|
20
|
+
scope :active, -> { where(status: :active) }
|
21
|
+
scope :by_business, ->(business_id) { where(business_id: business_id) }
|
22
|
+
scope :owned_by, ->(business) { where(business: business) }
|
23
|
+
|
24
|
+
# Custom methods
|
25
|
+
def total_value
|
26
|
+
price * quantity
|
27
|
+
end
|
28
|
+
|
29
|
+
def margin
|
30
|
+
return nil unless supplier_product&.supplier_price
|
31
|
+
price - supplier_product.supplier_price
|
32
|
+
end
|
33
|
+
|
34
|
+
def margin_percentage
|
35
|
+
return nil unless supplier_product&.supplier_price && supplier_product.supplier_price > 0
|
36
|
+
((margin / supplier_product.supplier_price) * 100).round(2)
|
37
|
+
end
|
38
|
+
|
39
|
+
def available?
|
40
|
+
active? && quantity > 0
|
41
|
+
end
|
42
|
+
|
43
|
+
def supplier_business
|
44
|
+
supplier_product&.business
|
45
|
+
end
|
46
|
+
|
47
|
+
def product
|
48
|
+
supplier_product&.product
|
49
|
+
end
|
50
|
+
|
51
|
+
def can_be_managed_by?(business)
|
52
|
+
self.business == business
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Dscf
|
2
|
+
module Marketplace
|
3
|
+
class Product < ApplicationRecord
|
4
|
+
belongs_to :category, class_name: "Dscf::Marketplace::Category"
|
5
|
+
belongs_to :unit, class_name: "Dscf::Marketplace::Unit"
|
6
|
+
|
7
|
+
has_many :supplier_products, class_name: "Dscf::Marketplace::SupplierProduct"
|
8
|
+
has_many :listings, class_name: "Dscf::Marketplace::Listing"
|
9
|
+
has_many :order_items, class_name: "Dscf::Marketplace::OrderItem"
|
10
|
+
has_many :rfq_items, class_name: "Dscf::Marketplace::RfqItem"
|
11
|
+
has_many :quotation_items, class_name: "Dscf::Marketplace::QuotationItem"
|
12
|
+
|
13
|
+
has_one_attached :thumbnail
|
14
|
+
has_many_attached :images
|
15
|
+
|
16
|
+
validates :sku, presence: true, uniqueness: true, length: {maximum: 50}
|
17
|
+
validates :name, presence: true, length: {maximum: 200}
|
18
|
+
validates :description, length: {maximum: 2000}, allow_blank: true
|
19
|
+
validates :category_id, presence: true
|
20
|
+
validates :base_price, presence: true, numericality: {greater_than: 0}
|
21
|
+
validates :business_only, inclusion: {in: [ true, false ]}
|
22
|
+
validates :unit_id, presence: true
|
23
|
+
validates :base_quantity, presence: true, numericality: {greater_than: 0}
|
24
|
+
|
25
|
+
scope :business_only, -> { where(business_only: true) }
|
26
|
+
scope :public_products, -> { where(business_only: false) }
|
27
|
+
scope :by_category, ->(category_id) { where(category_id: category_id) }
|
28
|
+
scope :by_unit, ->(unit_id) { where(unit_id: unit_id) }
|
29
|
+
|
30
|
+
def display_name
|
31
|
+
"#{name} (#{sku})"
|
32
|
+
end
|
33
|
+
|
34
|
+
def price_per_unit
|
35
|
+
return nil unless base_price && base_quantity && base_quantity > 0
|
36
|
+
base_price / base_quantity
|
37
|
+
end
|
38
|
+
|
39
|
+
def category_path
|
40
|
+
return nil unless category
|
41
|
+
category.ancestry_path
|
42
|
+
end
|
43
|
+
|
44
|
+
def convertible_to_units
|
45
|
+
return [] unless unit
|
46
|
+
unit.convertible_units
|
47
|
+
end
|
48
|
+
|
49
|
+
def thumbnail_url
|
50
|
+
return nil unless thumbnail.attached?
|
51
|
+
Rails.application.routes.url_helpers.rails_blob_url(thumbnail, only_path: true)
|
52
|
+
end
|
53
|
+
|
54
|
+
def images_urls
|
55
|
+
return [] unless images.attached?
|
56
|
+
images.map do |image|
|
57
|
+
Rails.application.routes.url_helpers.rails_blob_url(image, only_path: true)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Dscf::Marketplace
|
2
|
+
class SupplierProduct < ApplicationRecord
|
3
|
+
# References
|
4
|
+
belongs_to :business, class_name: "Dscf::Core::Business"
|
5
|
+
belongs_to :product, class_name: "Dscf::Marketplace::Product"
|
6
|
+
|
7
|
+
# Relationships
|
8
|
+
has_many :listings, class_name: "Dscf::Marketplace::Listing"
|
9
|
+
has_many :order_items, class_name: "Dscf::Marketplace::OrderItem"
|
10
|
+
|
11
|
+
# Status enum
|
12
|
+
enum :status, {active: 0, inactive: 1, discontinued: 2}, default: :active
|
13
|
+
|
14
|
+
# Validations
|
15
|
+
validates :supplier_price, numericality: {greater_than: 0}, presence: true
|
16
|
+
validates :available_quantity, numericality: {greater_than_or_equal_to: 0}
|
17
|
+
validates :minimum_order_quantity, numericality: {greater_than: 0}, allow_nil: true
|
18
|
+
|
19
|
+
# Scopes
|
20
|
+
scope :active, -> { where(status: :active) }
|
21
|
+
scope :in_stock, -> { where("available_quantity > 0") }
|
22
|
+
scope :by_business, ->(business_id) { where(business_id: business_id) }
|
23
|
+
scope :by_product, ->(product_id) { where(product_id: product_id) }
|
24
|
+
|
25
|
+
# Custom methods
|
26
|
+
def in_stock?
|
27
|
+
available_quantity > 0
|
28
|
+
end
|
29
|
+
|
30
|
+
def can_fulfill?(quantity)
|
31
|
+
available_quantity >= quantity
|
32
|
+
end
|
33
|
+
|
34
|
+
def price_per_unit
|
35
|
+
return nil unless supplier_price && product&.base_quantity
|
36
|
+
supplier_price / product.base_quantity
|
37
|
+
end
|
38
|
+
|
39
|
+
def display_name
|
40
|
+
"#{business.name} - #{product.name}"
|
41
|
+
end
|
42
|
+
|
43
|
+
def available_for_order?(quantity = 1)
|
44
|
+
active? && can_fulfill?(quantity)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
class CreateDscfMarketplaceProducts < ActiveRecord::Migration[8.0]
|
2
|
+
def change
|
3
|
+
create_table :dscf_marketplace_products do |t|
|
4
|
+
t.string :sku, null: false, limit: 50
|
5
|
+
t.string :name, null: false, limit: 200
|
6
|
+
t.text :description, limit: 2000
|
7
|
+
t.references :category,
|
8
|
+
index: {name: "category_on_dm_products_indx"},
|
9
|
+
null: false,
|
10
|
+
foreign_key: {to_table: :dscf_marketplace_categories}
|
11
|
+
t.decimal :base_price, precision: 15, scale: 2, null: false
|
12
|
+
t.boolean :business_only, null: false, default: false
|
13
|
+
t.references :unit,
|
14
|
+
index: {name: "unit_on_dm_products_indx"},
|
15
|
+
null: false,
|
16
|
+
foreign_key: {to_table: :dscf_marketplace_units}
|
17
|
+
t.decimal :base_quantity, precision: 15, scale: 6, null: false
|
18
|
+
|
19
|
+
t.timestamps
|
20
|
+
end
|
21
|
+
|
22
|
+
add_index :dscf_marketplace_products, :sku,
|
23
|
+
unique: true,
|
24
|
+
name: "sku_on_dm_products_indx"
|
25
|
+
add_index :dscf_marketplace_products, :business_only,
|
26
|
+
name: "business_only_on_dm_products_indx"
|
27
|
+
add_index :dscf_marketplace_products, [ :category_id, :business_only ],
|
28
|
+
name: "category_business_only_on_dm_products_indx"
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
class CreateDscfMarketplaceSupplierProducts < ActiveRecord::Migration[8.0]
|
2
|
+
def change
|
3
|
+
create_table :dscf_marketplace_supplier_products do |t|
|
4
|
+
t.references :business,
|
5
|
+
index: {name: "business_on_dm_sp_indx"},
|
6
|
+
null: false,
|
7
|
+
foreign_key: {to_table: :dscf_core_businesses}
|
8
|
+
t.references :product,
|
9
|
+
index: {name: "product_on_dm_sp_indx"},
|
10
|
+
null: false,
|
11
|
+
foreign_key: {to_table: :dscf_marketplace_products}
|
12
|
+
|
13
|
+
# Ethiopian Birr pricing
|
14
|
+
t.decimal :supplier_price, precision: 15, scale: 2, null: false
|
15
|
+
|
16
|
+
# Inventory management
|
17
|
+
t.decimal :available_quantity, precision: 15, scale: 6, null: false, default: 0
|
18
|
+
t.decimal :minimum_order_quantity, precision: 15, scale: 6
|
19
|
+
|
20
|
+
# Status
|
21
|
+
t.integer :status, null: false, default: 0
|
22
|
+
|
23
|
+
t.timestamps
|
24
|
+
end
|
25
|
+
|
26
|
+
# Unique constraint - one supplier product per business-product combination
|
27
|
+
add_index :dscf_marketplace_supplier_products,
|
28
|
+
[ :business_id, :product_id ],
|
29
|
+
unique: true,
|
30
|
+
name: "unique_business_product_indx"
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
class CreateDscfMarketplaceListings < ActiveRecord::Migration[8.0]
|
2
|
+
def change
|
3
|
+
create_table :dscf_marketplace_listings do |t|
|
4
|
+
t.references :business,
|
5
|
+
index: {name: "business_on_dm_listings_indx"},
|
6
|
+
null: false,
|
7
|
+
foreign_key: {to_table: :dscf_core_businesses}
|
8
|
+
t.references :supplier_product,
|
9
|
+
index: {name: "supplier_product_on_dm_listings_indx"},
|
10
|
+
null: false,
|
11
|
+
foreign_key: {to_table: :dscf_marketplace_supplier_products}
|
12
|
+
|
13
|
+
# Ethiopian Birr pricing
|
14
|
+
t.decimal :price, precision: 15, scale: 2, null: false
|
15
|
+
|
16
|
+
# Available quantity for sale
|
17
|
+
t.decimal :quantity, precision: 15, scale: 6, null: false
|
18
|
+
|
19
|
+
# Optional listing description
|
20
|
+
t.text :description
|
21
|
+
|
22
|
+
# Status
|
23
|
+
t.integer :status, null: false, default: 1 # draft
|
24
|
+
|
25
|
+
t.timestamps
|
26
|
+
end
|
27
|
+
|
28
|
+
add_index :dscf_marketplace_listings, :status, name: "status_on_dm_listings_indx"
|
29
|
+
add_index :dscf_marketplace_listings, [ :business_id, :supplier_product_id ], name: "business_supplier_product_on_dm_listings_indx"
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
FactoryBot.define do
|
2
|
+
factory :dscf_core_business, class: 'Dscf::Core::Business' do
|
3
|
+
sequence(:name) { |n| "Test Business #{n}" }
|
4
|
+
description { "Test business description" }
|
5
|
+
contact_email { "contact@example.com" }
|
6
|
+
contact_phone { "+251911123456" }
|
7
|
+
tin_number { "1234567890" }
|
8
|
+
business_type { create(:dscf_core_business_type) }
|
9
|
+
user { create(:dscf_core_user) }
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,10 @@
|
|
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) }
|
5
|
+
price { 500.00 }
|
6
|
+
quantity { 100.0 }
|
7
|
+
description { "High quality product listing" }
|
8
|
+
status { :draft }
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
FactoryBot.define do
|
2
|
+
factory :dscf_marketplace_product, class: 'Dscf::Marketplace::Product' do
|
3
|
+
sequence(:sku) { |n| "PRD-#{n.to_s.rjust(3, '0')}" }
|
4
|
+
sequence(:name) { |n| "Product #{n}" }
|
5
|
+
description { "Product description" }
|
6
|
+
category { create(:dscf_marketplace_category) }
|
7
|
+
base_price { 99.99 }
|
8
|
+
business_only { false }
|
9
|
+
unit { create(:dscf_marketplace_unit) }
|
10
|
+
base_quantity { 1.0 }
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
FactoryBot.define do
|
2
|
+
factory :dscf_marketplace_supplier_product, class: 'Dscf::Marketplace::SupplierProduct' do
|
3
|
+
business { create(:dscf_core_business) }
|
4
|
+
product { create(:dscf_marketplace_product) }
|
5
|
+
supplier_price { 500.00 }
|
6
|
+
available_quantity { 100.0 }
|
7
|
+
minimum_order_quantity { 10.0 }
|
8
|
+
status { :active }
|
9
|
+
end
|
10
|
+
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.5
|
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-28 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: rails
|
@@ -422,6 +422,9 @@ files:
|
|
422
422
|
- app/mailers/dscf/marketplace/application_mailer.rb
|
423
423
|
- app/models/dscf/marketplace/application_record.rb
|
424
424
|
- app/models/dscf/marketplace/category.rb
|
425
|
+
- app/models/dscf/marketplace/listing.rb
|
426
|
+
- app/models/dscf/marketplace/product.rb
|
427
|
+
- app/models/dscf/marketplace/supplier_product.rb
|
425
428
|
- app/models/dscf/marketplace/unit.rb
|
426
429
|
- app/models/dscf/marketplace/unit_conversion.rb
|
427
430
|
- config/routes.rb
|
@@ -429,11 +432,20 @@ files:
|
|
429
432
|
- db/migrate/20250827173526_update_dscf_marketplace_categories_indexes.rb
|
430
433
|
- db/migrate/20250827182550_create_dscf_marketplace_units.rb
|
431
434
|
- db/migrate/20250827185656_create_dscf_marketplace_unit_conversions.rb
|
435
|
+
- db/migrate/20250828062945_create_dscf_marketplace_products.rb
|
436
|
+
- db/migrate/20250828072514_create_dscf_marketplace_supplier_products.rb
|
437
|
+
- db/migrate/20250828081657_create_dscf_marketplace_listings.rb
|
432
438
|
- lib/dscf/marketplace.rb
|
433
439
|
- lib/dscf/marketplace/engine.rb
|
434
440
|
- lib/dscf/marketplace/version.rb
|
435
441
|
- lib/tasks/dscf/marketplace_tasks.rake
|
442
|
+
- spec/factories/dscf/core/business_types.rb
|
443
|
+
- spec/factories/dscf/core/businesses.rb
|
444
|
+
- spec/factories/dscf/core/users.rb
|
436
445
|
- spec/factories/dscf/marketplace/categories.rb
|
446
|
+
- spec/factories/dscf/marketplace/listings.rb
|
447
|
+
- spec/factories/dscf/marketplace/products.rb
|
448
|
+
- spec/factories/dscf/marketplace/supplier_products.rb
|
437
449
|
- spec/factories/dscf/marketplace/unit_conversions.rb
|
438
450
|
- spec/factories/dscf/marketplace/units.rb
|
439
451
|
homepage: https://mksaddis.com/
|