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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/dscf/marketplace/aggregator_listings_controller.rb +40 -0
  3. data/app/controllers/dscf/marketplace/listings_controller.rb +46 -2
  4. data/app/controllers/dscf/marketplace/orders_controller.rb +18 -1
  5. data/app/controllers/dscf/marketplace/products_controller.rb +1 -1
  6. data/app/controllers/dscf/marketplace/supplier_products_controller.rb +7 -8
  7. data/app/controllers/dscf/marketplace/suppliers_controller.rb +28 -0
  8. data/app/models/dscf/marketplace/aggregator_listing.rb +40 -0
  9. data/app/models/dscf/marketplace/listing.rb +95 -2
  10. data/app/models/dscf/marketplace/order.rb +2 -1
  11. data/app/models/dscf/marketplace/order_invoice.rb +39 -0
  12. data/app/models/dscf/marketplace/product.rb +4 -4
  13. data/app/models/dscf/marketplace/supplier.rb +42 -0
  14. data/app/models/dscf/marketplace/supplier_product.rb +4 -2
  15. data/app/policies/dscf/marketplace/aggregator_listing_policy.rb +29 -0
  16. data/app/policies/dscf/marketplace/listing_policy.rb +12 -0
  17. data/app/policies/dscf/marketplace/supplier_policy.rb +25 -0
  18. data/app/serializers/dscf/marketplace/aggregator_listing_serializer.rb +27 -0
  19. data/app/serializers/dscf/marketplace/listing_serializer.rb +33 -1
  20. data/app/serializers/dscf/marketplace/product_serializer.rb +1 -1
  21. data/app/serializers/dscf/marketplace/supplier_product_serializer.rb +3 -2
  22. data/app/serializers/dscf/marketplace/supplier_serializer.rb +11 -0
  23. data/app/services/dscf/marketplace/invoice_pdf_generator.rb +110 -0
  24. data/config/locales/en.yml +6 -0
  25. data/config/routes.rb +19 -0
  26. data/db/migrate/20260514000001_create_dscf_marketplace_suppliers.rb +20 -0
  27. data/db/migrate/20260514000002_add_supplier_id_to_supplier_products.rb +12 -0
  28. data/db/migrate/20260514000003_create_dscf_marketplace_order_invoices.rb +19 -0
  29. data/db/migrate/20260514000004_replace_base_price_with_gross_weight_on_products.rb +6 -0
  30. data/db/migrate/20260514000005_create_dscf_marketplace_aggregator_listings.rb +22 -0
  31. data/db/migrate/20260514000006_add_supplier_id_to_addresses.rb +9 -0
  32. data/db/migrate/20260527000001_add_orchestrator_fields_to_listings.rb +22 -0
  33. data/lib/dscf/marketplace/version.rb +1 -1
  34. data/spec/factories/dscf/marketplace/aggregator_listings.rb +9 -0
  35. data/spec/factories/dscf/marketplace/order_invoices.rb +11 -0
  36. data/spec/factories/dscf/marketplace/products.rb +1 -1
  37. data/spec/factories/dscf/marketplace/suppliers.rb +10 -0
  38. metadata +24 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4beaf427e69fdbc843de062d0083c55ece3d751cca2adab8cf989c20de633886
4
- data.tar.gz: 8479a97308423b04afa67d95c2cc3fde8096b01a6b24ec1c9f3519e0dfe0d35f
3
+ metadata.gz: f20dc6ec95c1bd2483126bf1f0b241ba3403999673a705b9f0e72e214ec29889
4
+ data.tar.gz: 6233b5ae039cd138f26ca51d70db6a9458e51b25d29a2ba21d37a142b103b933
5
5
  SHA512:
6
- metadata.gz: 98399d8626f0fdd09979f7be48bc34f36126d3c07ad268d57338b1de4a9575ece83a8eabf13e279dcb0ea1824dd68317c211025ff6c3eb57ebf07121d3ec261a
7
- data.tar.gz: b9e8eec981a2c12e01a06dac62e7d22d98c170d4c08760486321e6acbd61601e2380a9bf33cd1cf5fd5806f545543fdeb527795521e4c908c5eb6f394930a670
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
- render_success("orders.success.completed", data: @obj)
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)
@@ -28,7 +28,7 @@ module Dscf
28
28
  def model_params
29
29
  params.require(:product).permit(
30
30
  :sku, :name, :description, :category_id, :unit_id,
31
- :base_price, :base_quantity, :business_only,
31
+ :gross_weight, :base_quantity, :business_only,
32
32
  :thumbnail, images: []
33
33
  )
34
34
  end
@@ -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, :supplier_price, :available_quantity,
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 created_at updated_at]
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.status == "active" && quantity <= listing.quantity
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 :base_price, presence: true, numericality: {greater_than: 0}
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 base_price base_quantity business_only created_at updated_at]
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 base_price && base_quantity && base_quantity > 0
45
- base_price / base_quantity
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 supplier_name supplier_address supplier_contact_phone]
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
- :base_price, :base_quantity, :business_only, :created_at, :updated_at,
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, :supplier_price, :available_quantity,
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
@@ -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
@@ -1,5 +1,5 @@
1
1
  module Dscf
2
2
  module Marketplace
3
- VERSION = "0.7.6".freeze
3
+ VERSION = "0.8.1".freeze
4
4
  end
5
5
  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
- base_price { 99.99 }
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.7.6
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-06 00:00:00.000000000 Z
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.0
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.0
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/