dscf-marketplace 0.7.6 → 0.8.0

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 (32) 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/orders_controller.rb +18 -1
  4. data/app/controllers/dscf/marketplace/products_controller.rb +1 -1
  5. data/app/controllers/dscf/marketplace/supplier_products_controller.rb +7 -8
  6. data/app/controllers/dscf/marketplace/suppliers_controller.rb +28 -0
  7. data/app/models/dscf/marketplace/aggregator_listing.rb +40 -0
  8. data/app/models/dscf/marketplace/order.rb +1 -0
  9. data/app/models/dscf/marketplace/order_invoice.rb +39 -0
  10. data/app/models/dscf/marketplace/product.rb +4 -4
  11. data/app/models/dscf/marketplace/supplier.rb +42 -0
  12. data/app/models/dscf/marketplace/supplier_product.rb +4 -2
  13. data/app/policies/dscf/marketplace/aggregator_listing_policy.rb +29 -0
  14. data/app/policies/dscf/marketplace/supplier_policy.rb +25 -0
  15. data/app/serializers/dscf/marketplace/aggregator_listing_serializer.rb +27 -0
  16. data/app/serializers/dscf/marketplace/product_serializer.rb +1 -1
  17. data/app/serializers/dscf/marketplace/supplier_product_serializer.rb +3 -2
  18. data/app/serializers/dscf/marketplace/supplier_serializer.rb +11 -0
  19. data/app/services/dscf/marketplace/invoice_pdf_generator.rb +110 -0
  20. data/config/routes.rb +16 -0
  21. data/db/migrate/20260514000001_create_dscf_marketplace_suppliers.rb +20 -0
  22. data/db/migrate/20260514000002_add_supplier_id_to_supplier_products.rb +12 -0
  23. data/db/migrate/20260514000003_create_dscf_marketplace_order_invoices.rb +19 -0
  24. data/db/migrate/20260514000004_replace_base_price_with_gross_weight_on_products.rb +6 -0
  25. data/db/migrate/20260514000005_create_dscf_marketplace_aggregator_listings.rb +22 -0
  26. data/db/migrate/20260514000006_add_supplier_id_to_addresses.rb +9 -0
  27. data/lib/dscf/marketplace/version.rb +1 -1
  28. data/spec/factories/dscf/marketplace/aggregator_listings.rb +9 -0
  29. data/spec/factories/dscf/marketplace/order_invoices.rb +11 -0
  30. data/spec/factories/dscf/marketplace/products.rb +1 -1
  31. data/spec/factories/dscf/marketplace/suppliers.rb +10 -0
  32. metadata +23 -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: 5f0f591983cea40d4d2f6a5c4467b7d545de79760fc7911ab344859f7511c77a
4
+ data.tar.gz: 99b10ebe989e7152e2727da4c789dc857a33bf348192f706618f041c22bddc71
5
5
  SHA512:
6
- metadata.gz: 98399d8626f0fdd09979f7be48bc34f36126d3c07ad268d57338b1de4a9575ece83a8eabf13e279dcb0ea1824dd68317c211025ff6c3eb57ebf07121d3ec261a
7
- data.tar.gz: b9e8eec981a2c12e01a06dac62e7d22d98c170d4c08760486321e6acbd61601e2380a9bf33cd1cf5fd5806f545543fdeb527795521e4c908c5eb6f394930a670
6
+ metadata.gz: 81582f57d99751874955a157a46e73588352f38e1c93ae2ac08a11f2f5b7419df0cbd16076a4d0653aa0026d219a9e183b58ee7cb432077aed6365a6bc79dfc5
7
+ data.tar.gz: fe8a10f50d36184274dceeb2d36d09406d5beda9665b064d0a45b6858a6e4088fd6ad5bcf053f56d8e3cba9f7fa37a9e860a23c5e3d71645f8a3dcf6facb281e
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
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 ]
@@ -82,6 +97,7 @@ Dscf::Marketplace::Engine.routes.draw do
82
97
  post "confirm"
83
98
  post "cancel"
84
99
  post "complete"
100
+ get "invoice"
85
101
  end
86
102
  collection do
87
103
  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
@@ -1,5 +1,5 @@
1
1
  module Dscf
2
2
  module Marketplace
3
- VERSION = "0.7.6".freeze
3
+ VERSION = "0.8.0".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.0
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-24 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,12 @@ 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
537
553
  - db/seeds.rb
538
554
  - lib/dscf/marketplace.rb
539
555
  - lib/dscf/marketplace/engine.rb
@@ -545,12 +561,14 @@ files:
545
561
  - spec/factories/dscf/core/roles.rb
546
562
  - spec/factories/dscf/core/user_roles.rb
547
563
  - spec/factories/dscf/core/users.rb
564
+ - spec/factories/dscf/marketplace/aggregator_listings.rb
548
565
  - spec/factories/dscf/marketplace/categories.rb
549
566
  - spec/factories/dscf/marketplace/delivery_order_items.rb
550
567
  - spec/factories/dscf/marketplace/delivery_orders.rb
551
568
  - spec/factories/dscf/marketplace/delivery_stops.rb
552
569
  - spec/factories/dscf/marketplace/delivery_vehicles.rb
553
570
  - spec/factories/dscf/marketplace/listings.rb
571
+ - spec/factories/dscf/marketplace/order_invoices.rb
554
572
  - spec/factories/dscf/marketplace/order_items.rb
555
573
  - spec/factories/dscf/marketplace/orders.rb
556
574
  - spec/factories/dscf/marketplace/products.rb
@@ -559,6 +577,7 @@ files:
559
577
  - spec/factories/dscf/marketplace/request_for_quotations.rb
560
578
  - spec/factories/dscf/marketplace/rfq_items.rb
561
579
  - spec/factories/dscf/marketplace/supplier_products.rb
580
+ - spec/factories/dscf/marketplace/suppliers.rb
562
581
  - spec/factories/dscf/marketplace/unit_conversions.rb
563
582
  - spec/factories/dscf/marketplace/units.rb
564
583
  homepage: https://mksaddis.com/