dscf-marketplace 0.8.0 → 0.8.1
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/controllers/dscf/marketplace/listings_controller.rb +46 -2
- data/app/models/dscf/marketplace/listing.rb +95 -2
- data/app/models/dscf/marketplace/order.rb +1 -1
- data/app/policies/dscf/marketplace/listing_policy.rb +12 -0
- data/app/serializers/dscf/marketplace/listing_serializer.rb +33 -1
- data/config/locales/en.yml +6 -0
- data/config/routes.rb +3 -0
- data/db/migrate/20260527000001_add_orchestrator_fields_to_listings.rb +22 -0
- data/lib/dscf/marketplace/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f20dc6ec95c1bd2483126bf1f0b241ba3403999673a705b9f0e72e214ec29889
|
|
4
|
+
data.tar.gz: 6233b5ae039cd138f26ca51d70db6a9458e51b25d29a2ba21d37a142b103b933
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b852b81aa1b9ea52cc219e40b1056691d4ed7d464e0fd684bf1395abf4219cd35bf07c2f7fa67ab6fc5397f438a95e420a98e826465e331002d1ebebe672ef8c
|
|
7
|
+
data.tar.gz: 0e32006533ed1f96fead2d9eb34b03cc74ed5eaf62a4ad5eb0eeb7e7c36b167d58559d0d27d5c023f5d67fef2c80a48783da0f13e08a7888c3fac93ce505cea4
|
|
@@ -33,6 +33,47 @@ module Dscf
|
|
|
33
33
|
end
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
+
def promote
|
|
37
|
+
@obj = find_record
|
|
38
|
+
authorize @obj, :promote?
|
|
39
|
+
if @obj.update(
|
|
40
|
+
orchestrator_promoted: true,
|
|
41
|
+
orchestrator_promoted_at: Time.current,
|
|
42
|
+
promoted_by: current_user
|
|
43
|
+
)
|
|
44
|
+
render_success("listings.success.promoted", data: @obj)
|
|
45
|
+
else
|
|
46
|
+
render_error("listings.errors.promote_failed")
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def unpromote
|
|
51
|
+
@obj = find_record
|
|
52
|
+
authorize @obj, :unpromote?
|
|
53
|
+
if @obj.update(
|
|
54
|
+
orchestrator_promoted: false,
|
|
55
|
+
orchestrator_promoted_at: nil,
|
|
56
|
+
promoted_by: nil
|
|
57
|
+
)
|
|
58
|
+
render_success("listings.success.unpromoted", data: @obj)
|
|
59
|
+
else
|
|
60
|
+
render_error("listings.errors.unpromote_failed")
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Public feed: only visible listings (computed by 3 conditions)
|
|
65
|
+
def visible
|
|
66
|
+
authorize @clazz.new, :visible?
|
|
67
|
+
listings = @clazz.visible.includes(eager_loaded_associations)
|
|
68
|
+
|
|
69
|
+
options = {
|
|
70
|
+
include: default_serializer_includes[:index] || [],
|
|
71
|
+
meta: { resource_type: "visible_listings" }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
render_success(data: listings, serializer_options: options)
|
|
75
|
+
end
|
|
76
|
+
|
|
36
77
|
def my_listings
|
|
37
78
|
authorize @clazz.new, :my_listings?
|
|
38
79
|
service = MyResourceService.new(current_user)
|
|
@@ -40,7 +81,7 @@ module Dscf
|
|
|
40
81
|
|
|
41
82
|
options = {
|
|
42
83
|
include: default_serializer_includes[:index] || [],
|
|
43
|
-
meta: {resource_type: "my_listings"}
|
|
84
|
+
meta: { resource_type: "my_listings" }
|
|
44
85
|
}
|
|
45
86
|
|
|
46
87
|
render_success("listings.success.index", data: listings, serializer_options: options)
|
|
@@ -50,7 +91,6 @@ module Dscf
|
|
|
50
91
|
authorize @clazz.new, :listings_by_supplier?
|
|
51
92
|
supplier_id = params[:supplier_id]
|
|
52
93
|
|
|
53
|
-
# Validate supplier exists
|
|
54
94
|
supplier = Dscf::Core::Business.find_by(id: supplier_id)
|
|
55
95
|
return render_error("listings.errors.supplier_not_found") unless supplier
|
|
56
96
|
|
|
@@ -71,6 +111,10 @@ module Dscf
|
|
|
71
111
|
|
|
72
112
|
private
|
|
73
113
|
|
|
114
|
+
def find_record
|
|
115
|
+
@clazz.find(params[:id])
|
|
116
|
+
end
|
|
117
|
+
|
|
74
118
|
def model_params
|
|
75
119
|
params.require(:listing).permit(
|
|
76
120
|
:business_id, :supplier_product_id, :price, :quantity, :status
|
|
@@ -3,6 +3,7 @@ module Dscf::Marketplace
|
|
|
3
3
|
# References
|
|
4
4
|
belongs_to :business, class_name: "Dscf::Core::Business"
|
|
5
5
|
belongs_to :supplier_product, class_name: "Dscf::Marketplace::SupplierProduct"
|
|
6
|
+
belongs_to :promoted_by, class_name: "Dscf::Core::User", optional: true
|
|
6
7
|
|
|
7
8
|
# Relationships
|
|
8
9
|
has_many :order_items, class_name: "Dscf::Marketplace::OrderItem"
|
|
@@ -20,14 +21,106 @@ module Dscf::Marketplace
|
|
|
20
21
|
scope :active, -> { where(status: :active) }
|
|
21
22
|
scope :by_business, ->(business_id) { where(business_id: business_id) }
|
|
22
23
|
scope :owned_by, ->(business) { where(business: business) }
|
|
24
|
+
scope :promoted, -> { where(orchestrator_promoted: true) }
|
|
25
|
+
scope :not_promoted, -> { where(orchestrator_promoted: false) }
|
|
26
|
+
|
|
27
|
+
# Returns listings visible to end users based on 3 conditions:
|
|
28
|
+
# 1. Orchestrator promoted (manual override)
|
|
29
|
+
# 2. Sole provider — only one active listing for this product
|
|
30
|
+
# 3. Cheapest price — lowest price among active listings for the same product
|
|
31
|
+
scope :visible, -> {
|
|
32
|
+
active.where(<<~SQL.squish)
|
|
33
|
+
orchestrator_promoted = true
|
|
34
|
+
OR (
|
|
35
|
+
SELECT COUNT(*) FROM dscf_marketplace_listings l2
|
|
36
|
+
JOIN dscf_marketplace_supplier_products sp2 ON l2.supplier_product_id = sp2.id
|
|
37
|
+
WHERE sp2.product_id = (
|
|
38
|
+
SELECT sp.product_id FROM dscf_marketplace_supplier_products sp
|
|
39
|
+
WHERE sp.id = dscf_marketplace_listings.supplier_product_id
|
|
40
|
+
)
|
|
41
|
+
AND l2.status = #{Listing.statuses[:active]}
|
|
42
|
+
) = 1
|
|
43
|
+
OR (
|
|
44
|
+
dscf_marketplace_listings.price = (
|
|
45
|
+
SELECT MIN(l3.price) FROM dscf_marketplace_listings l3
|
|
46
|
+
JOIN dscf_marketplace_supplier_products sp3 ON l3.supplier_product_id = sp3.id
|
|
47
|
+
WHERE sp3.product_id = (
|
|
48
|
+
SELECT sp.product_id FROM dscf_marketplace_supplier_products sp
|
|
49
|
+
WHERE sp.id = dscf_marketplace_listings.supplier_product_id
|
|
50
|
+
)
|
|
51
|
+
AND l3.status = #{Listing.statuses[:active]}
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
SQL
|
|
55
|
+
}
|
|
23
56
|
|
|
24
57
|
# Ransack configuration for secure filtering
|
|
25
58
|
def self.ransackable_attributes(_auth_object = nil)
|
|
26
|
-
%w[id business_id supplier_product_id price quantity status
|
|
59
|
+
%w[id business_id supplier_product_id price quantity status
|
|
60
|
+
orchestrator_promoted orchestrator_promoted_at promoted_by_id
|
|
61
|
+
created_at updated_at]
|
|
27
62
|
end
|
|
28
63
|
|
|
29
64
|
def self.ransackable_associations(_auth_object = nil)
|
|
30
|
-
%w[business supplier_product order_items]
|
|
65
|
+
%w[business supplier_product order_items promoted_by]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Is this listing the only active listing for its product?
|
|
69
|
+
def sole_provider?
|
|
70
|
+
self.class.active
|
|
71
|
+
.joins(:supplier_product)
|
|
72
|
+
.where(supplier_product: { product_id: supplier_product.product_id })
|
|
73
|
+
.count == 1
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Does this listing have the lowest price for its product?
|
|
77
|
+
def cheapest?
|
|
78
|
+
self.class.active
|
|
79
|
+
.joins(:supplier_product)
|
|
80
|
+
.where(supplier_product: { product_id: supplier_product.product_id })
|
|
81
|
+
.minimum(:price) == price
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Should this listing be shown to end users?
|
|
85
|
+
def visible?
|
|
86
|
+
return false unless active? && quantity > 0
|
|
87
|
+
return true if orchestrator_promoted?
|
|
88
|
+
|
|
89
|
+
product_id = supplier_product.product_id
|
|
90
|
+
active_listings = self.class.active.joins(:supplier_product)
|
|
91
|
+
.where(supplier_product: { product_id: product_id })
|
|
92
|
+
|
|
93
|
+
active_listings.count == 1 || active_listings.minimum(:price) == price
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Returns :orchestrator_promoted, :sole_provider, :cheapest, or nil
|
|
97
|
+
def visibility_reason
|
|
98
|
+
return nil unless visible?
|
|
99
|
+
return :orchestrator_promoted if orchestrator_promoted?
|
|
100
|
+
return :sole_provider if sole_provider?
|
|
101
|
+
return :cheapest if cheapest?
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Market insight: how many active listings exist for this product
|
|
105
|
+
def total_listings_for_product
|
|
106
|
+
self.class.active.joins(:supplier_product)
|
|
107
|
+
.where(supplier_product: { product_id: supplier_product.product_id })
|
|
108
|
+
.count
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Market insight: lowest price across all businesses for this product
|
|
112
|
+
def cheapest_price_for_product
|
|
113
|
+
self.class.active.joins(:supplier_product)
|
|
114
|
+
.where(supplier_product: { product_id: supplier_product.product_id })
|
|
115
|
+
.minimum(:price)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Market insight: this listing's price rank (1 = cheapest)
|
|
119
|
+
def price_rank
|
|
120
|
+
self.class.active.joins(:supplier_product)
|
|
121
|
+
.where(supplier_product: { product_id: supplier_product.product_id })
|
|
122
|
+
.where("price < ?", price)
|
|
123
|
+
.count + 1
|
|
31
124
|
end
|
|
32
125
|
|
|
33
126
|
# Custom methods
|
|
@@ -74,7 +74,7 @@ module Dscf::Marketplace
|
|
|
74
74
|
end
|
|
75
75
|
|
|
76
76
|
def self.create_from_listing(listing, user, quantity, dropoff_address = nil, payment_method = nil)
|
|
77
|
-
return nil unless listing.
|
|
77
|
+
return nil unless listing.visible? && quantity <= listing.quantity
|
|
78
78
|
|
|
79
79
|
attributes = {
|
|
80
80
|
order_type: :direct_listing,
|
|
@@ -13,6 +13,18 @@ module Dscf
|
|
|
13
13
|
user.has_permission?("listings.sold_out")
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
+
def promote?
|
|
17
|
+
user.has_permission?("listings.promote")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def unpromote?
|
|
21
|
+
user.has_permission?("listings.unpromote")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def visible?
|
|
25
|
+
true
|
|
26
|
+
end
|
|
27
|
+
|
|
16
28
|
def my_listings?
|
|
17
29
|
user.has_permission?("listings.my_listings")
|
|
18
30
|
end
|
|
@@ -3,11 +3,43 @@ module Dscf
|
|
|
3
3
|
class ListingSerializer < ActiveModel::Serializer
|
|
4
4
|
attributes :id, :business_id, :supplier_product_id, :price, :quantity,
|
|
5
5
|
:status, :created_at, :updated_at, :total_value, :margin,
|
|
6
|
-
:margin_percentage, :available
|
|
6
|
+
:margin_percentage, :available?,
|
|
7
|
+
:visible, :visibility_reason, :orchestrator_promoted,
|
|
8
|
+
:orchestrator_promoted_at, :promoted_by_id,
|
|
9
|
+
:sole_provider, :cheapest,
|
|
10
|
+
:total_listings_for_product, :cheapest_price_for_product, :price_rank
|
|
7
11
|
|
|
8
12
|
belongs_to :business
|
|
9
13
|
belongs_to :supplier_product
|
|
10
14
|
has_many :order_items
|
|
15
|
+
|
|
16
|
+
def visible
|
|
17
|
+
object.visible?
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def visibility_reason
|
|
21
|
+
object.visibility_reason
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def sole_provider
|
|
25
|
+
object.sole_provider?
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def cheapest
|
|
29
|
+
object.cheapest?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def total_listings_for_product
|
|
33
|
+
object.total_listings_for_product
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def cheapest_price_for_product
|
|
37
|
+
object.cheapest_price_for_product
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def price_rank
|
|
41
|
+
object.price_rank
|
|
42
|
+
end
|
|
11
43
|
end
|
|
12
44
|
end
|
|
13
45
|
end
|
data/config/locales/en.yml
CHANGED
|
@@ -102,6 +102,9 @@ en:
|
|
|
102
102
|
activated: "Listing activated successfully"
|
|
103
103
|
paused: "Listing paused successfully"
|
|
104
104
|
sold_out: "Listing marked as sold out successfully"
|
|
105
|
+
promoted: "Listing promoted by orchestrator successfully"
|
|
106
|
+
unpromoted: "Listing unpromoted successfully"
|
|
107
|
+
visible: "Visible listings retrieved successfully"
|
|
105
108
|
errors:
|
|
106
109
|
index: "Failed to retrieve listings"
|
|
107
110
|
show: "Failed to retrieve listing details"
|
|
@@ -109,6 +112,9 @@ en:
|
|
|
109
112
|
update: "Failed to update listing"
|
|
110
113
|
destroy: "Failed to delete listing"
|
|
111
114
|
supplier_not_found: "Supplier not found"
|
|
115
|
+
promote_failed: "Failed to promote listing"
|
|
116
|
+
unpromote_failed: "Failed to unpromote listing"
|
|
117
|
+
visible_failed: "Failed to retrieve visible listings"
|
|
112
118
|
|
|
113
119
|
request_for_quotations:
|
|
114
120
|
success:
|
data/config/routes.rb
CHANGED
|
@@ -51,9 +51,12 @@ Dscf::Marketplace::Engine.routes.draw do
|
|
|
51
51
|
post "activate"
|
|
52
52
|
post "pause"
|
|
53
53
|
post "sold_out"
|
|
54
|
+
post "promote"
|
|
55
|
+
post "unpromote"
|
|
54
56
|
end
|
|
55
57
|
collection do
|
|
56
58
|
get "my_listings"
|
|
59
|
+
get "visible"
|
|
57
60
|
get "by_supplier/:supplier_id", to: "listings#listings_by_supplier"
|
|
58
61
|
end
|
|
59
62
|
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
class AddOrchestratorFieldsToListings < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
add_column :dscf_marketplace_listings, :orchestrator_promoted, :boolean, default: false, null: false
|
|
4
|
+
add_column :dscf_marketplace_listings, :orchestrator_promoted_at, :datetime
|
|
5
|
+
|
|
6
|
+
add_reference :dscf_marketplace_listings, :promoted_by,
|
|
7
|
+
foreign_key: { to_table: :dscf_core_users },
|
|
8
|
+
index: { name: "promoted_by_on_dm_listings_indx" }
|
|
9
|
+
|
|
10
|
+
# Backfill: existing active listings remain visible after deploy
|
|
11
|
+
reversible do |dir|
|
|
12
|
+
dir.up do
|
|
13
|
+
execute <<~SQL.squish
|
|
14
|
+
UPDATE dscf_marketplace_listings
|
|
15
|
+
SET orchestrator_promoted = true,
|
|
16
|
+
orchestrator_promoted_at = NOW()
|
|
17
|
+
WHERE status = 0
|
|
18
|
+
SQL
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
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.8.
|
|
4
|
+
version: 0.8.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Asrat
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-05-
|
|
10
|
+
date: 2026-05-27 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: rails
|
|
@@ -550,6 +550,7 @@ files:
|
|
|
550
550
|
- db/migrate/20260514000004_replace_base_price_with_gross_weight_on_products.rb
|
|
551
551
|
- db/migrate/20260514000005_create_dscf_marketplace_aggregator_listings.rb
|
|
552
552
|
- db/migrate/20260514000006_add_supplier_id_to_addresses.rb
|
|
553
|
+
- db/migrate/20260527000001_add_orchestrator_fields_to_listings.rb
|
|
553
554
|
- db/seeds.rb
|
|
554
555
|
- lib/dscf/marketplace.rb
|
|
555
556
|
- lib/dscf/marketplace/engine.rb
|