dscf-marketplace 0.7.6 → 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/aggregator_listings_controller.rb +40 -0
- data/app/controllers/dscf/marketplace/listings_controller.rb +46 -2
- data/app/controllers/dscf/marketplace/orders_controller.rb +18 -1
- data/app/controllers/dscf/marketplace/products_controller.rb +1 -1
- data/app/controllers/dscf/marketplace/supplier_products_controller.rb +7 -8
- data/app/controllers/dscf/marketplace/suppliers_controller.rb +28 -0
- data/app/models/dscf/marketplace/aggregator_listing.rb +40 -0
- data/app/models/dscf/marketplace/listing.rb +95 -2
- data/app/models/dscf/marketplace/order.rb +2 -1
- data/app/models/dscf/marketplace/order_invoice.rb +39 -0
- data/app/models/dscf/marketplace/product.rb +4 -4
- data/app/models/dscf/marketplace/supplier.rb +42 -0
- data/app/models/dscf/marketplace/supplier_product.rb +4 -2
- data/app/policies/dscf/marketplace/aggregator_listing_policy.rb +29 -0
- data/app/policies/dscf/marketplace/listing_policy.rb +12 -0
- data/app/policies/dscf/marketplace/supplier_policy.rb +25 -0
- data/app/serializers/dscf/marketplace/aggregator_listing_serializer.rb +27 -0
- data/app/serializers/dscf/marketplace/listing_serializer.rb +33 -1
- data/app/serializers/dscf/marketplace/product_serializer.rb +1 -1
- data/app/serializers/dscf/marketplace/supplier_product_serializer.rb +3 -2
- data/app/serializers/dscf/marketplace/supplier_serializer.rb +11 -0
- data/app/services/dscf/marketplace/invoice_pdf_generator.rb +110 -0
- data/config/locales/en.yml +6 -0
- data/config/routes.rb +19 -0
- data/db/migrate/20260514000001_create_dscf_marketplace_suppliers.rb +20 -0
- data/db/migrate/20260514000002_add_supplier_id_to_supplier_products.rb +12 -0
- data/db/migrate/20260514000003_create_dscf_marketplace_order_invoices.rb +19 -0
- data/db/migrate/20260514000004_replace_base_price_with_gross_weight_on_products.rb +6 -0
- data/db/migrate/20260514000005_create_dscf_marketplace_aggregator_listings.rb +22 -0
- data/db/migrate/20260514000006_add_supplier_id_to_addresses.rb +9 -0
- data/db/migrate/20260527000001_add_orchestrator_fields_to_listings.rb +22 -0
- data/lib/dscf/marketplace/version.rb +1 -1
- data/spec/factories/dscf/marketplace/aggregator_listings.rb +9 -0
- data/spec/factories/dscf/marketplace/order_invoices.rb +11 -0
- data/spec/factories/dscf/marketplace/products.rb +1 -1
- data/spec/factories/dscf/marketplace/suppliers.rb +10 -0
- metadata +24 -4
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
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module Dscf
|
|
2
|
+
module Marketplace
|
|
3
|
+
class AggregatorListingsController < ApplicationController
|
|
4
|
+
include Dscf::Core::Common
|
|
5
|
+
|
|
6
|
+
def feed
|
|
7
|
+
authorize @clazz.new, :index?
|
|
8
|
+
listings = @clazz.active.includes(supplier_product: :product)
|
|
9
|
+
|
|
10
|
+
options = {
|
|
11
|
+
include: default_serializer_includes[:index] || [],
|
|
12
|
+
meta: { resource_type: "aggregator_feed" }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
render_success(data: listings, serializer_options: options)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def my_listings
|
|
19
|
+
authorize @clazz.new, :my_listings?
|
|
20
|
+
businesses = current_user.businesses.pluck(:id)
|
|
21
|
+
listings = @clazz.where(aggregator_id: businesses).includes(supplier_product: :product)
|
|
22
|
+
|
|
23
|
+
options = {
|
|
24
|
+
include: default_serializer_includes[:index] || [],
|
|
25
|
+
meta: { resource_type: "my_aggregator_listings" }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
render_success(data: listings, serializer_options: options)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def model_params
|
|
34
|
+
params.require(:aggregator_listing).permit(
|
|
35
|
+
:aggregator_id, :supplier_product_id, :price, :quantity, :status
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -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
|
|
@@ -68,12 +68,29 @@ module Dscf
|
|
|
68
68
|
authorize @obj, :complete?
|
|
69
69
|
if @obj.can_be_completed? && @obj.update(status: :completed)
|
|
70
70
|
@obj.order_items.update_all(status: OrderItem.statuses[:fulfilled])
|
|
71
|
-
|
|
71
|
+
begin
|
|
72
|
+
invoice = Dscf::Marketplace::InvoicePdfGenerator.new(@obj).generate!
|
|
73
|
+
render_success("orders.success.completed", data: @obj, meta: { invoice_id: invoice.id })
|
|
74
|
+
rescue => e
|
|
75
|
+
Rails.logger.error("Invoice generation failed for order #{@obj.id}: #{e.message}")
|
|
76
|
+
render_success("orders.success.completed", data: @obj)
|
|
77
|
+
end
|
|
72
78
|
else
|
|
73
79
|
render_error("orders.errors.complete_failed")
|
|
74
80
|
end
|
|
75
81
|
end
|
|
76
82
|
|
|
83
|
+
def invoice
|
|
84
|
+
@obj = find_record
|
|
85
|
+
authorize @obj, :show?
|
|
86
|
+
invoice = @obj.order_invoice
|
|
87
|
+
if invoice&.pdf_file&.attached?
|
|
88
|
+
redirect_to rails_blob_url(invoice.pdf_file, disposition: "attachment")
|
|
89
|
+
else
|
|
90
|
+
render_error("orders.errors.no_invoice")
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
77
94
|
def my_orders
|
|
78
95
|
authorize @clazz.new, :my_orders?
|
|
79
96
|
service = MyResourceService.new(current_user)
|
|
@@ -71,22 +71,21 @@ module Dscf
|
|
|
71
71
|
|
|
72
72
|
def model_params
|
|
73
73
|
params.require(:supplier_product).permit(
|
|
74
|
-
:business_id, :product_id, :
|
|
75
|
-
:minimum_order_quantity, :status
|
|
76
|
-
:supplier_name, :supplier_address, :supplier_contact_phone
|
|
74
|
+
:business_id, :product_id, :supplier_id, :supplier_price,
|
|
75
|
+
:available_quantity, :minimum_order_quantity, :status
|
|
77
76
|
)
|
|
78
77
|
end
|
|
79
78
|
|
|
80
79
|
def eager_loaded_associations
|
|
81
|
-
[ :business, :product, :listings ]
|
|
80
|
+
[ :business, :product, :supplier, :listings ]
|
|
82
81
|
end
|
|
83
82
|
|
|
84
83
|
def default_serializer_includes
|
|
85
84
|
{
|
|
86
|
-
index: [ :business, :product ],
|
|
87
|
-
show: [ :business, :product, :listings ],
|
|
88
|
-
create: [ :business, :product ],
|
|
89
|
-
update: [ :business, :product ]
|
|
85
|
+
index: [ :business, :product, :supplier ],
|
|
86
|
+
show: [ :business, :product, :supplier, :listings ],
|
|
87
|
+
create: [ :business, :product, :supplier ],
|
|
88
|
+
update: [ :business, :product, :supplier ]
|
|
90
89
|
}
|
|
91
90
|
end
|
|
92
91
|
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module Dscf
|
|
2
|
+
module Marketplace
|
|
3
|
+
class SuppliersController < ApplicationController
|
|
4
|
+
include Dscf::Core::Common
|
|
5
|
+
|
|
6
|
+
def my_suppliers
|
|
7
|
+
authorize @clazz.new, :index?
|
|
8
|
+
businesses = current_user.businesses.pluck(:id)
|
|
9
|
+
suppliers = @clazz.where(business_id: businesses)
|
|
10
|
+
|
|
11
|
+
options = {
|
|
12
|
+
include: default_serializer_includes[:index] || [],
|
|
13
|
+
meta: { resource_type: "my_suppliers" }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
render_success("suppliers.success.index", data: suppliers, serializer_options: options)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def model_params
|
|
22
|
+
params.require(:supplier).permit(
|
|
23
|
+
:name, :address, :contact_phone, :business_type, :business_id
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module Dscf::Marketplace
|
|
2
|
+
class AggregatorListing < ApplicationRecord
|
|
3
|
+
belongs_to :aggregator, class_name: "Dscf::Core::Business"
|
|
4
|
+
belongs_to :supplier_product, class_name: "Dscf::Marketplace::SupplierProduct"
|
|
5
|
+
|
|
6
|
+
has_many :order_items, class_name: "Dscf::Marketplace::OrderItem", as: :source, dependent: :nullify
|
|
7
|
+
|
|
8
|
+
delegate :name, :description, :thumbnail_url, :images_urls, to: :product, allow_nil: true
|
|
9
|
+
|
|
10
|
+
enum :status, { active: 0, draft: 1, paused: 2, sold_out: 3 }, default: :draft
|
|
11
|
+
|
|
12
|
+
validates :price, numericality: { greater_than: 0 }, presence: true
|
|
13
|
+
validates :quantity, numericality: { greater_than: 0 }, presence: true
|
|
14
|
+
validates :aggregator, presence: true
|
|
15
|
+
validates :supplier_product_id, presence: true, uniqueness: { scope: :aggregator_id }
|
|
16
|
+
|
|
17
|
+
scope :active, -> { where(status: :active) }
|
|
18
|
+
scope :by_aggregator, ->(aggregator_id) { where(aggregator_id: aggregator_id) }
|
|
19
|
+
|
|
20
|
+
def self.ransackable_attributes(_auth_object = nil)
|
|
21
|
+
%w[id aggregator_id supplier_product_id price quantity status created_at updated_at]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.ransackable_associations(_auth_object = nil)
|
|
25
|
+
%w[aggregator supplier_product]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def product
|
|
29
|
+
supplier_product&.product
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def available?
|
|
33
|
+
active? && quantity > 0
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def total_value
|
|
37
|
+
price * quantity
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -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
|
|
@@ -13,6 +13,7 @@ module Dscf::Marketplace
|
|
|
13
13
|
belongs_to :delivery_order, optional: true
|
|
14
14
|
belongs_to :dropoff_address, class_name: "Dscf::Core::Address", optional: true
|
|
15
15
|
has_many :order_items, dependent: :destroy, autosave: true
|
|
16
|
+
has_one :order_invoice, class_name: "Dscf::Marketplace::OrderInvoice", dependent: :destroy
|
|
16
17
|
accepts_nested_attributes_for :order_items, allow_destroy: true
|
|
17
18
|
|
|
18
19
|
validates :order_type, presence: true
|
|
@@ -73,7 +74,7 @@ module Dscf::Marketplace
|
|
|
73
74
|
end
|
|
74
75
|
|
|
75
76
|
def self.create_from_listing(listing, user, quantity, dropoff_address = nil, payment_method = nil)
|
|
76
|
-
return nil unless listing.
|
|
77
|
+
return nil unless listing.visible? && quantity <= listing.quantity
|
|
77
78
|
|
|
78
79
|
attributes = {
|
|
79
80
|
order_type: :direct_listing,
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module Dscf::Marketplace
|
|
2
|
+
class OrderInvoice < ApplicationRecord
|
|
3
|
+
belongs_to :order, class_name: "Dscf::Marketplace::Order"
|
|
4
|
+
|
|
5
|
+
enum :status, { generated: 0 }
|
|
6
|
+
|
|
7
|
+
before_validation :generate_invoice_number, on: :create
|
|
8
|
+
before_validation :set_issued_at, on: :create
|
|
9
|
+
|
|
10
|
+
validates :invoice_number, presence: true, uniqueness: { case_sensitive: false }
|
|
11
|
+
validates :subtotal, presence: true, numericality: { greater_than_or_equal_to: 0 }
|
|
12
|
+
validates :total, presence: true, numericality: { greater_than_or_equal_to: 0 }
|
|
13
|
+
validates :vat_amount, presence: true, numericality: { greater_than_or_equal_to: 0 }
|
|
14
|
+
validates :status, presence: true
|
|
15
|
+
validates :issued_at, presence: true
|
|
16
|
+
validates :order, presence: true
|
|
17
|
+
|
|
18
|
+
has_one_attached :pdf_file
|
|
19
|
+
|
|
20
|
+
def self.ransackable_attributes(_auth_object = nil)
|
|
21
|
+
%w[id order_id invoice_number subtotal vat_amount total status issued_at created_at updated_at]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.ransackable_associations(_auth_object = nil)
|
|
25
|
+
%w[order]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def generate_invoice_number
|
|
31
|
+
year = Date.current.year.to_s
|
|
32
|
+
self.invoice_number ||= "INV-#{year}-#{SecureRandom.hex(4).upcase}"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def set_issued_at
|
|
36
|
+
self.issued_at ||= Time.current
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -17,7 +17,7 @@ module Dscf
|
|
|
17
17
|
validates :name, presence: true, length: {maximum: 200}
|
|
18
18
|
validates :description, length: {maximum: 2000}, allow_blank: true
|
|
19
19
|
validates :category_id, presence: true
|
|
20
|
-
validates :
|
|
20
|
+
validates :gross_weight, presence: true, numericality: {greater_than: 0}
|
|
21
21
|
validates :business_only, inclusion: {in: [ true, false ]}
|
|
22
22
|
validates :unit_id, presence: true
|
|
23
23
|
validates :base_quantity, presence: true, numericality: {greater_than: 0}
|
|
@@ -29,7 +29,7 @@ module Dscf
|
|
|
29
29
|
|
|
30
30
|
# Ransack configuration for secure filtering
|
|
31
31
|
def self.ransackable_attributes(_auth_object = nil)
|
|
32
|
-
%w[id sku name description category_id unit_id
|
|
32
|
+
%w[id sku name description category_id unit_id gross_weight base_quantity business_only created_at updated_at]
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
def self.ransackable_associations(_auth_object = nil)
|
|
@@ -41,8 +41,8 @@ module Dscf
|
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
def price_per_unit
|
|
44
|
-
return nil unless
|
|
45
|
-
|
|
44
|
+
return nil unless base_quantity && base_quantity > 0
|
|
45
|
+
nil
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
def category_path
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
module Dscf::Marketplace
|
|
2
|
+
class Supplier < ApplicationRecord
|
|
3
|
+
belongs_to :business, class_name: "Dscf::Core::Business"
|
|
4
|
+
has_many :supplier_products, class_name: "Dscf::Marketplace::SupplierProduct",
|
|
5
|
+
dependent: :nullify
|
|
6
|
+
has_many :addresses, class_name: "Dscf::Core::Address", dependent: :nullify
|
|
7
|
+
|
|
8
|
+
enum :business_type, {
|
|
9
|
+
retailer: 0,
|
|
10
|
+
wholesaler: 1,
|
|
11
|
+
distributor: 2,
|
|
12
|
+
manufacturer: 3,
|
|
13
|
+
importer: 4,
|
|
14
|
+
exporter: 5,
|
|
15
|
+
service_provider: 6,
|
|
16
|
+
aggregator: 7
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
before_validation :generate_code, on: :create
|
|
20
|
+
|
|
21
|
+
validates :name, presence: true
|
|
22
|
+
validates :code, presence: true, uniqueness: true
|
|
23
|
+
validates :business_type, presence: true
|
|
24
|
+
validates :business, presence: true
|
|
25
|
+
|
|
26
|
+
scope :by_business, ->(business_id) { where(business_id: business_id) }
|
|
27
|
+
|
|
28
|
+
def self.ransackable_attributes(_auth_object = nil)
|
|
29
|
+
%w[id name code address contact_phone business_type business_id created_at updated_at]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.ransackable_associations(_auth_object = nil)
|
|
33
|
+
%w[business supplier_products addresses]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def generate_code
|
|
39
|
+
self.code ||= "SUP-#{SecureRandom.hex(4).upcase}"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -2,8 +2,10 @@ module Dscf::Marketplace
|
|
|
2
2
|
class SupplierProduct < ApplicationRecord
|
|
3
3
|
belongs_to :business, class_name: "Dscf::Core::Business"
|
|
4
4
|
belongs_to :product, class_name: "Dscf::Marketplace::Product"
|
|
5
|
+
belongs_to :supplier, class_name: "Dscf::Marketplace::Supplier", optional: true
|
|
5
6
|
|
|
6
7
|
delegate :name, :description, :thumbnail_url, :images_urls, to: :product, allow_nil: true
|
|
8
|
+
delegate :name, :address, :contact_phone, to: :supplier, prefix: true, allow_nil: true
|
|
7
9
|
|
|
8
10
|
has_many :listings, class_name: "Dscf::Marketplace::Listing"
|
|
9
11
|
has_many :order_items, class_name: "Dscf::Marketplace::OrderItem"
|
|
@@ -20,11 +22,11 @@ module Dscf::Marketplace
|
|
|
20
22
|
scope :by_product, ->(product_id) { where(product_id: product_id) }
|
|
21
23
|
|
|
22
24
|
def self.ransackable_attributes(_auth_object = nil)
|
|
23
|
-
%w[id business_id product_id supplier_price available_quantity minimum_order_quantity status created_at updated_at
|
|
25
|
+
%w[id business_id product_id supplier_id supplier_price available_quantity minimum_order_quantity status created_at updated_at]
|
|
24
26
|
end
|
|
25
27
|
|
|
26
28
|
def self.ransackable_associations(_auth_object = nil)
|
|
27
|
-
%w[business product listings order_items]
|
|
29
|
+
%w[business product supplier listings order_items]
|
|
28
30
|
end
|
|
29
31
|
|
|
30
32
|
# Custom methods
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module Dscf
|
|
2
|
+
module Marketplace
|
|
3
|
+
class AggregatorListingPolicy < Dscf::Core::ApplicationPolicy
|
|
4
|
+
def index?
|
|
5
|
+
true
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def show?
|
|
9
|
+
true
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def create?
|
|
13
|
+
user.present?
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def update?
|
|
17
|
+
user.present? && record.aggregator_id.in?(user.businesses.pluck(:id))
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def feed?
|
|
21
|
+
true
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def my_listings?
|
|
25
|
+
user.present?
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -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
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module Dscf
|
|
2
|
+
module Marketplace
|
|
3
|
+
class SupplierPolicy < Dscf::Core::ApplicationPolicy
|
|
4
|
+
def index?
|
|
5
|
+
true
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def show?
|
|
9
|
+
true
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def create?
|
|
13
|
+
user.present?
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def update?
|
|
17
|
+
user.present? && record.business_id.in?(user.businesses.pluck(:id))
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def my_suppliers?
|
|
21
|
+
true
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module Dscf
|
|
2
|
+
module Marketplace
|
|
3
|
+
class AggregatorListingSerializer < ActiveModel::Serializer
|
|
4
|
+
attributes :id, :aggregator_id, :supplier_product_id, :price, :quantity,
|
|
5
|
+
:status, :created_at, :updated_at, :total_value, :available?,
|
|
6
|
+
:product_name, :product_description, :thumbnail_url, :images_urls
|
|
7
|
+
|
|
8
|
+
belongs_to :aggregator
|
|
9
|
+
|
|
10
|
+
def product_name
|
|
11
|
+
object.product&.name
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def product_description
|
|
15
|
+
object.product&.description
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def thumbnail_url
|
|
19
|
+
object.product&.thumbnail_url
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def images_urls
|
|
23
|
+
object.product&.images_urls
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
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
|
|
@@ -2,7 +2,7 @@ module Dscf
|
|
|
2
2
|
module Marketplace
|
|
3
3
|
class ProductSerializer < ActiveModel::Serializer
|
|
4
4
|
attributes :id, :sku, :name, :description, :category_id, :unit_id,
|
|
5
|
-
:
|
|
5
|
+
:gross_weight, :base_quantity, :business_only, :created_at, :updated_at,
|
|
6
6
|
:display_name, :price_per_unit, :thumbnail_url, :image_urls
|
|
7
7
|
|
|
8
8
|
belongs_to :category
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
module Dscf
|
|
2
2
|
module Marketplace
|
|
3
3
|
class SupplierProductSerializer < ActiveModel::Serializer
|
|
4
|
-
attributes :id, :business_id, :product_id, :
|
|
5
|
-
:minimum_order_quantity, :status, :created_at, :updated_at,
|
|
4
|
+
attributes :id, :business_id, :product_id, :supplier_id, :supplier_price,
|
|
5
|
+
:available_quantity, :minimum_order_quantity, :status, :created_at, :updated_at,
|
|
6
6
|
:in_stock?, :price_per_unit, :display_name, :name, :description,
|
|
7
7
|
:thumbnail_url, :images_urls,
|
|
8
8
|
:supplier_name, :supplier_address, :supplier_contact_phone
|
|
9
9
|
|
|
10
10
|
belongs_to :business
|
|
11
11
|
belongs_to :product
|
|
12
|
+
belongs_to :supplier
|
|
12
13
|
has_many :listings
|
|
13
14
|
end
|
|
14
15
|
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module Dscf
|
|
2
|
+
module Marketplace
|
|
3
|
+
class SupplierSerializer < ActiveModel::Serializer
|
|
4
|
+
attributes :id, :name, :code, :address, :contact_phone, :business_type,
|
|
5
|
+
:business_id, :created_at, :updated_at
|
|
6
|
+
|
|
7
|
+
belongs_to :business
|
|
8
|
+
has_many :addresses
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
module Dscf
|
|
2
|
+
module Marketplace
|
|
3
|
+
class InvoicePdfGenerator
|
|
4
|
+
attr_reader :order, :order_invoice
|
|
5
|
+
|
|
6
|
+
def initialize(order)
|
|
7
|
+
@order = order
|
|
8
|
+
@business = order.ordered_to
|
|
9
|
+
@customer = order.ordered_by
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def generate!
|
|
13
|
+
@order_invoice = build_invoice
|
|
14
|
+
ActiveRecord::Base.transaction do
|
|
15
|
+
@order_invoice.save!
|
|
16
|
+
generate_and_attach_pdf
|
|
17
|
+
end
|
|
18
|
+
@order_invoice
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def build_invoice
|
|
24
|
+
Dscf::Marketplace::OrderInvoice.new(
|
|
25
|
+
order: order,
|
|
26
|
+
subtotal: calculate_subtotal,
|
|
27
|
+
vat_amount: calculate_vat,
|
|
28
|
+
total: calculate_total,
|
|
29
|
+
status: :generated
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def calculate_subtotal
|
|
34
|
+
order.order_items.sum(&:subtotal)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def calculate_vat
|
|
38
|
+
(calculate_subtotal * 0.15).round(2)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def calculate_total
|
|
42
|
+
calculate_subtotal + calculate_vat
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def generate_and_attach_pdf
|
|
46
|
+
pdf = render_pdf
|
|
47
|
+
@order_invoice.pdf_file.attach(
|
|
48
|
+
io: StringIO.new(pdf.render),
|
|
49
|
+
filename: "#{@order_invoice.invoice_number}.pdf",
|
|
50
|
+
content_type: "application/pdf"
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def render_pdf
|
|
55
|
+
invoice = @order_invoice
|
|
56
|
+
business = @business
|
|
57
|
+
customer = @customer
|
|
58
|
+
order_items = order.order_items.includes(:product, :unit)
|
|
59
|
+
|
|
60
|
+
Prawn::Document.new do |pdf|
|
|
61
|
+
pdf.text "INVOICE", size: 24, style: :bold, align: :center
|
|
62
|
+
pdf.move_down 10
|
|
63
|
+
|
|
64
|
+
pdf.text business.name, size: 16, style: :bold
|
|
65
|
+
pdf.text "TIN: #{business.tin_number}" if business.tin_number.present?
|
|
66
|
+
pdf.text business.contact_phone if business.contact_phone.present?
|
|
67
|
+
pdf.move_down 10
|
|
68
|
+
|
|
69
|
+
pdf.text "Invoice #: #{invoice.invoice_number}"
|
|
70
|
+
pdf.text "Date: #{invoice.issued_at.strftime('%B %d, %Y')}"
|
|
71
|
+
pdf.text "Order #: #{order.id}"
|
|
72
|
+
pdf.move_down 10
|
|
73
|
+
|
|
74
|
+
pdf.text "Bill To:", style: :bold
|
|
75
|
+
pdf.text customer.name
|
|
76
|
+
pdf.move_down 15
|
|
77
|
+
|
|
78
|
+
table_data = [["Item", "Qty", "Unit", "Unit Price", "Total"]]
|
|
79
|
+
order_items.each do |item|
|
|
80
|
+
table_data << [
|
|
81
|
+
item.product_name,
|
|
82
|
+
item.quantity.to_s,
|
|
83
|
+
item.unit_name,
|
|
84
|
+
format_currency(item.unit_price),
|
|
85
|
+
format_currency(item.subtotal)
|
|
86
|
+
]
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
pdf.table(table_data, header: true, width: pdf.bounds.width) do
|
|
90
|
+
row(0).font_style = :bold
|
|
91
|
+
cells.padding = 5
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
pdf.move_down 15
|
|
95
|
+
|
|
96
|
+
pdf.text "Subtotal: #{format_currency(invoice.subtotal)}", align: :right
|
|
97
|
+
pdf.text "VAT (15%): #{format_currency(invoice.vat_amount)}", align: :right
|
|
98
|
+
pdf.text "Total: #{format_currency(invoice.total)}", align: :right, size: 14, style: :bold
|
|
99
|
+
|
|
100
|
+
pdf.move_down 30
|
|
101
|
+
pdf.text "Thank you for your business!", align: :center, style: :italic
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def format_currency(amount)
|
|
106
|
+
"ETB #{format('%.2f', amount)}"
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
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
|
@@ -19,6 +19,21 @@ Dscf::Marketplace::Engine.routes.draw do
|
|
|
19
19
|
|
|
20
20
|
resources :unit_conversions
|
|
21
21
|
|
|
22
|
+
# Supplier Management
|
|
23
|
+
resources :suppliers do
|
|
24
|
+
collection do
|
|
25
|
+
get "my_suppliers"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Aggregator Feed
|
|
30
|
+
resources :aggregator_listings do
|
|
31
|
+
collection do
|
|
32
|
+
get "feed"
|
|
33
|
+
get "my_listings"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
22
37
|
# Trading Models
|
|
23
38
|
resources :supplier_products do
|
|
24
39
|
resources :listings, only: [ :index ]
|
|
@@ -36,9 +51,12 @@ Dscf::Marketplace::Engine.routes.draw do
|
|
|
36
51
|
post "activate"
|
|
37
52
|
post "pause"
|
|
38
53
|
post "sold_out"
|
|
54
|
+
post "promote"
|
|
55
|
+
post "unpromote"
|
|
39
56
|
end
|
|
40
57
|
collection do
|
|
41
58
|
get "my_listings"
|
|
59
|
+
get "visible"
|
|
42
60
|
get "by_supplier/:supplier_id", to: "listings#listings_by_supplier"
|
|
43
61
|
end
|
|
44
62
|
end
|
|
@@ -82,6 +100,7 @@ Dscf::Marketplace::Engine.routes.draw do
|
|
|
82
100
|
post "confirm"
|
|
83
101
|
post "cancel"
|
|
84
102
|
post "complete"
|
|
103
|
+
get "invoice"
|
|
85
104
|
end
|
|
86
105
|
collection do
|
|
87
106
|
get "my_orders"
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
class CreateDscfMarketplaceSuppliers < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :dscf_marketplace_suppliers do |t|
|
|
4
|
+
t.string :name, null: false
|
|
5
|
+
t.string :code, null: false
|
|
6
|
+
t.text :address
|
|
7
|
+
t.string :contact_phone
|
|
8
|
+
t.integer :business_type, null: false, default: 0
|
|
9
|
+
t.references :business,
|
|
10
|
+
null: false,
|
|
11
|
+
index: { name: "business_on_dm_suppliers_indx" },
|
|
12
|
+
foreign_key: { to_table: :dscf_core_businesses }
|
|
13
|
+
|
|
14
|
+
t.timestamps
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
add_index :dscf_marketplace_suppliers, :code, unique: true
|
|
18
|
+
add_index :dscf_marketplace_suppliers, :business_type
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
class AddSupplierIdToSupplierProducts < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
add_reference :dscf_marketplace_supplier_products, :supplier,
|
|
4
|
+
null: true,
|
|
5
|
+
index: { name: "supplier_on_dm_sp_indx" },
|
|
6
|
+
foreign_key: { to_table: :dscf_marketplace_suppliers }
|
|
7
|
+
|
|
8
|
+
remove_column :dscf_marketplace_supplier_products, :supplier_name, :string if column_exists?(:dscf_marketplace_supplier_products, :supplier_name)
|
|
9
|
+
remove_column :dscf_marketplace_supplier_products, :supplier_address, :text if column_exists?(:dscf_marketplace_supplier_products, :supplier_address)
|
|
10
|
+
remove_column :dscf_marketplace_supplier_products, :supplier_contact_phone, :string if column_exists?(:dscf_marketplace_supplier_products, :supplier_contact_phone)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
class CreateDscfMarketplaceOrderInvoices < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :dscf_marketplace_order_invoices do |t|
|
|
4
|
+
t.references :order, null: false,
|
|
5
|
+
foreign_key: { to_table: :dscf_marketplace_orders },
|
|
6
|
+
index: { name: "order_on_dm_oi_indx" }
|
|
7
|
+
t.string :invoice_number, null: false
|
|
8
|
+
t.decimal :subtotal, precision: 15, scale: 2, null: false
|
|
9
|
+
t.decimal :vat_amount, precision: 15, scale: 2, null: false, default: 0
|
|
10
|
+
t.decimal :total, precision: 15, scale: 2, null: false
|
|
11
|
+
t.integer :status, null: false, default: 0
|
|
12
|
+
t.datetime :issued_at, null: false
|
|
13
|
+
|
|
14
|
+
t.timestamps
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
add_index :dscf_marketplace_order_invoices, :invoice_number, unique: true
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
class ReplaceBasePriceWithGrossWeightOnProducts < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
remove_column :dscf_marketplace_products, :base_price, :decimal
|
|
4
|
+
add_column :dscf_marketplace_products, :gross_weight, :decimal, precision: 15, scale: 6, null: false, default: 0
|
|
5
|
+
end
|
|
6
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
class CreateDscfMarketplaceAggregatorListings < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :dscf_marketplace_aggregator_listings do |t|
|
|
4
|
+
t.references :aggregator, null: false,
|
|
5
|
+
foreign_key: { to_table: :dscf_core_businesses },
|
|
6
|
+
index: { name: "aggregator_on_dm_al_indx" }
|
|
7
|
+
t.references :supplier_product, null: false,
|
|
8
|
+
foreign_key: { to_table: :dscf_marketplace_supplier_products },
|
|
9
|
+
index: { name: "sp_on_dm_al_indx" }
|
|
10
|
+
t.decimal :price, precision: 15, scale: 2, null: false
|
|
11
|
+
t.decimal :quantity, precision: 15, scale: 6, null: false, default: 0
|
|
12
|
+
t.integer :status, null: false, default: 0
|
|
13
|
+
|
|
14
|
+
t.timestamps
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
add_index :dscf_marketplace_aggregator_listings,
|
|
18
|
+
[:aggregator_id, :supplier_product_id],
|
|
19
|
+
unique: true,
|
|
20
|
+
name: "unique_aggregator_sp_indx"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
class AddSupplierIdToAddresses < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
add_reference :dscf_core_addresses, :supplier, null: true,
|
|
4
|
+
foreign_key: { to_table: :dscf_marketplace_suppliers },
|
|
5
|
+
index: { name: "supplier_on_dca_indx" }
|
|
6
|
+
|
|
7
|
+
change_column_null :dscf_core_addresses, :user_id, true
|
|
8
|
+
end
|
|
9
|
+
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
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
FactoryBot.define do
|
|
2
|
+
factory :dscf_marketplace_aggregator_listing, class: "Dscf::Marketplace::AggregatorListing" do
|
|
3
|
+
association :aggregator, factory: :dscf_core_business
|
|
4
|
+
association :supplier_product, factory: :dscf_marketplace_supplier_product
|
|
5
|
+
price { 250.00 }
|
|
6
|
+
quantity { 50.0 }
|
|
7
|
+
status { :active }
|
|
8
|
+
end
|
|
9
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
FactoryBot.define do
|
|
2
|
+
factory :dscf_marketplace_order_invoice, class: "Dscf::Marketplace::OrderInvoice" do
|
|
3
|
+
invoice_number { "INV-#{Date.current.year}-#{SecureRandom.hex(4).upcase}" }
|
|
4
|
+
subtotal { 1000.00 }
|
|
5
|
+
vat_amount { 150.00 }
|
|
6
|
+
total { 1150.00 }
|
|
7
|
+
status { :generated }
|
|
8
|
+
issued_at { Time.current }
|
|
9
|
+
association :order, factory: :dscf_marketplace_order
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -4,7 +4,7 @@ FactoryBot.define do
|
|
|
4
4
|
sequence(:name) { |n| "Product #{n}" }
|
|
5
5
|
description { "Product description" }
|
|
6
6
|
category { create(:dscf_marketplace_category) }
|
|
7
|
-
|
|
7
|
+
gross_weight { 1.5 }
|
|
8
8
|
business_only { false }
|
|
9
9
|
unit { create(:dscf_marketplace_unit) }
|
|
10
10
|
base_quantity { 1.0 }
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
FactoryBot.define do
|
|
2
|
+
factory :dscf_marketplace_supplier, class: "Dscf::Marketplace::Supplier" do
|
|
3
|
+
name { Faker::Company.name }
|
|
4
|
+
code { "SUP-#{SecureRandom.hex(4).upcase}" }
|
|
5
|
+
address { Faker::Address.full_address }
|
|
6
|
+
contact_phone { Faker::PhoneNumber.phone_number }
|
|
7
|
+
business_type { :retailer }
|
|
8
|
+
association :business, factory: :dscf_core_business
|
|
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.
|
|
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
|
|
@@ -35,14 +35,14 @@ dependencies:
|
|
|
35
35
|
requirements:
|
|
36
36
|
- - "~>"
|
|
37
37
|
- !ruby/object:Gem::Version
|
|
38
|
-
version: 0.3.
|
|
38
|
+
version: 0.3.3
|
|
39
39
|
type: :runtime
|
|
40
40
|
prerelease: false
|
|
41
41
|
version_requirements: !ruby/object:Gem::Requirement
|
|
42
42
|
requirements:
|
|
43
43
|
- - "~>"
|
|
44
44
|
- !ruby/object:Gem::Version
|
|
45
|
-
version: 0.3.
|
|
45
|
+
version: 0.3.3
|
|
46
46
|
- !ruby/object:Gem::Dependency
|
|
47
47
|
name: active_model_serializers
|
|
48
48
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -417,6 +417,7 @@ files:
|
|
|
417
417
|
- MIT-LICENSE
|
|
418
418
|
- Rakefile
|
|
419
419
|
- app/controllers/concerns/dscf/marketplace/demo_permission_bypass.rb
|
|
420
|
+
- app/controllers/dscf/marketplace/aggregator_listings_controller.rb
|
|
420
421
|
- app/controllers/dscf/marketplace/application_controller.rb
|
|
421
422
|
- app/controllers/dscf/marketplace/categories_controller.rb
|
|
422
423
|
- app/controllers/dscf/marketplace/delivery_order_items_controller.rb
|
|
@@ -433,10 +434,12 @@ files:
|
|
|
433
434
|
- app/controllers/dscf/marketplace/request_for_quotations_controller.rb
|
|
434
435
|
- app/controllers/dscf/marketplace/rfq_items_controller.rb
|
|
435
436
|
- app/controllers/dscf/marketplace/supplier_products_controller.rb
|
|
437
|
+
- app/controllers/dscf/marketplace/suppliers_controller.rb
|
|
436
438
|
- app/controllers/dscf/marketplace/unit_conversions_controller.rb
|
|
437
439
|
- app/controllers/dscf/marketplace/units_controller.rb
|
|
438
440
|
- app/jobs/dscf/marketplace/application_job.rb
|
|
439
441
|
- app/mailers/dscf/marketplace/application_mailer.rb
|
|
442
|
+
- app/models/dscf/marketplace/aggregator_listing.rb
|
|
440
443
|
- app/models/dscf/marketplace/application_record.rb
|
|
441
444
|
- app/models/dscf/marketplace/category.rb
|
|
442
445
|
- app/models/dscf/marketplace/delivery_order.rb
|
|
@@ -445,15 +448,18 @@ files:
|
|
|
445
448
|
- app/models/dscf/marketplace/delivery_vehicle.rb
|
|
446
449
|
- app/models/dscf/marketplace/listing.rb
|
|
447
450
|
- app/models/dscf/marketplace/order.rb
|
|
451
|
+
- app/models/dscf/marketplace/order_invoice.rb
|
|
448
452
|
- app/models/dscf/marketplace/order_item.rb
|
|
449
453
|
- app/models/dscf/marketplace/product.rb
|
|
450
454
|
- app/models/dscf/marketplace/quotation.rb
|
|
451
455
|
- app/models/dscf/marketplace/quotation_item.rb
|
|
452
456
|
- app/models/dscf/marketplace/request_for_quotation.rb
|
|
453
457
|
- app/models/dscf/marketplace/rfq_item.rb
|
|
458
|
+
- app/models/dscf/marketplace/supplier.rb
|
|
454
459
|
- app/models/dscf/marketplace/supplier_product.rb
|
|
455
460
|
- app/models/dscf/marketplace/unit.rb
|
|
456
461
|
- app/models/dscf/marketplace/unit_conversion.rb
|
|
462
|
+
- app/policies/dscf/marketplace/aggregator_listing_policy.rb
|
|
457
463
|
- app/policies/dscf/marketplace/category_policy.rb
|
|
458
464
|
- app/policies/dscf/marketplace/delivery_order_item_policy.rb
|
|
459
465
|
- app/policies/dscf/marketplace/delivery_order_policy.rb
|
|
@@ -468,9 +474,11 @@ files:
|
|
|
468
474
|
- app/policies/dscf/marketplace/quotation_policy.rb
|
|
469
475
|
- app/policies/dscf/marketplace/request_for_quotation_policy.rb
|
|
470
476
|
- app/policies/dscf/marketplace/rfq_item_policy.rb
|
|
477
|
+
- app/policies/dscf/marketplace/supplier_policy.rb
|
|
471
478
|
- app/policies/dscf/marketplace/supplier_product_policy.rb
|
|
472
479
|
- app/policies/dscf/marketplace/unit_conversion_policy.rb
|
|
473
480
|
- app/policies/dscf/marketplace/unit_policy.rb
|
|
481
|
+
- app/serializers/dscf/marketplace/aggregator_listing_serializer.rb
|
|
474
482
|
- app/serializers/dscf/marketplace/category_serializer.rb
|
|
475
483
|
- app/serializers/dscf/marketplace/delivery_order_item_serializer.rb
|
|
476
484
|
- app/serializers/dscf/marketplace/delivery_order_serializer.rb
|
|
@@ -486,12 +494,14 @@ files:
|
|
|
486
494
|
- app/serializers/dscf/marketplace/request_for_quotation_serializer.rb
|
|
487
495
|
- app/serializers/dscf/marketplace/rfq_item_serializer.rb
|
|
488
496
|
- app/serializers/dscf/marketplace/supplier_product_serializer.rb
|
|
497
|
+
- app/serializers/dscf/marketplace/supplier_serializer.rb
|
|
489
498
|
- app/serializers/dscf/marketplace/unit_conversion_serializer.rb
|
|
490
499
|
- app/serializers/dscf/marketplace/unit_serializer.rb
|
|
491
500
|
- app/serializers/dscf/marketplace/user_serializer.rb
|
|
492
501
|
- app/services/dscf/marketplace/delivery_order_service.rb
|
|
493
502
|
- app/services/dscf/marketplace/dispute_service.rb
|
|
494
503
|
- app/services/dscf/marketplace/gebeta_service.rb
|
|
504
|
+
- app/services/dscf/marketplace/invoice_pdf_generator.rb
|
|
495
505
|
- app/services/dscf/marketplace/my_resource_service.rb
|
|
496
506
|
- app/services/dscf/marketplace/rfq_response_service.rb
|
|
497
507
|
- app/services/dscf/marketplace/role_service.rb
|
|
@@ -534,6 +544,13 @@ files:
|
|
|
534
544
|
- db/migrate/20260310122000_add_missing_fields_to_dscf_core_user_roles.rb
|
|
535
545
|
- db/migrate/20260501000002_add_supplier_contact_fields_to_supplier_products.rb
|
|
536
546
|
- db/migrate/20260506100000_add_payment_references_to_dscf_marketplace_orders.rb
|
|
547
|
+
- db/migrate/20260514000001_create_dscf_marketplace_suppliers.rb
|
|
548
|
+
- db/migrate/20260514000002_add_supplier_id_to_supplier_products.rb
|
|
549
|
+
- db/migrate/20260514000003_create_dscf_marketplace_order_invoices.rb
|
|
550
|
+
- db/migrate/20260514000004_replace_base_price_with_gross_weight_on_products.rb
|
|
551
|
+
- db/migrate/20260514000005_create_dscf_marketplace_aggregator_listings.rb
|
|
552
|
+
- db/migrate/20260514000006_add_supplier_id_to_addresses.rb
|
|
553
|
+
- db/migrate/20260527000001_add_orchestrator_fields_to_listings.rb
|
|
537
554
|
- db/seeds.rb
|
|
538
555
|
- lib/dscf/marketplace.rb
|
|
539
556
|
- lib/dscf/marketplace/engine.rb
|
|
@@ -545,12 +562,14 @@ files:
|
|
|
545
562
|
- spec/factories/dscf/core/roles.rb
|
|
546
563
|
- spec/factories/dscf/core/user_roles.rb
|
|
547
564
|
- spec/factories/dscf/core/users.rb
|
|
565
|
+
- spec/factories/dscf/marketplace/aggregator_listings.rb
|
|
548
566
|
- spec/factories/dscf/marketplace/categories.rb
|
|
549
567
|
- spec/factories/dscf/marketplace/delivery_order_items.rb
|
|
550
568
|
- spec/factories/dscf/marketplace/delivery_orders.rb
|
|
551
569
|
- spec/factories/dscf/marketplace/delivery_stops.rb
|
|
552
570
|
- spec/factories/dscf/marketplace/delivery_vehicles.rb
|
|
553
571
|
- spec/factories/dscf/marketplace/listings.rb
|
|
572
|
+
- spec/factories/dscf/marketplace/order_invoices.rb
|
|
554
573
|
- spec/factories/dscf/marketplace/order_items.rb
|
|
555
574
|
- spec/factories/dscf/marketplace/orders.rb
|
|
556
575
|
- spec/factories/dscf/marketplace/products.rb
|
|
@@ -559,6 +578,7 @@ files:
|
|
|
559
578
|
- spec/factories/dscf/marketplace/request_for_quotations.rb
|
|
560
579
|
- spec/factories/dscf/marketplace/rfq_items.rb
|
|
561
580
|
- spec/factories/dscf/marketplace/supplier_products.rb
|
|
581
|
+
- spec/factories/dscf/marketplace/suppliers.rb
|
|
562
582
|
- spec/factories/dscf/marketplace/unit_conversions.rb
|
|
563
583
|
- spec/factories/dscf/marketplace/units.rb
|
|
564
584
|
homepage: https://mksaddis.com/
|