dscf-marketplace 0.9.9 → 0.11.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '09ac0d7d913260fb2bbf266a66b72820f7f5514795df0408941d3ab5ee1520d5'
4
- data.tar.gz: 22f50cc572af09ce48a9cce8efae3feba6999c405034d8652700647793018ea7
3
+ metadata.gz: f0a4b240effc7edee8fd220eb4eca3ed1d9856b81bf1b52ae0d9825dc5457e11
4
+ data.tar.gz: 8d86a1061e2e9712836c589f4d1cda3b94b36b8d7c1a84a7e64f01705b69884f
5
5
  SHA512:
6
- metadata.gz: 79ea2b6a8236df42e17451ba5e0c3b7446a9c937bc325010995583c6440a82da0dece86e61c975d9e42c46a1c287ad479a9edb269cb59477df683a41037d3a2b
7
- data.tar.gz: fdd0bd5e5bd5ee0bcc7732cd8326f9c07b5701faf5fa3815c9923b7cb65b852504b0e0b205be9a92e3dd7ea319b38477fb67cbf85739d65e61eb6232ff1296bc
6
+ metadata.gz: 0d79fd32300f658a1b29f8dd38d921461d4fa1c33c1a9501c03edd2c25e144260c45ba4fa7f1ce4e609a2a23e230787eec1c929ca6f5d8df271623648f1f489e
7
+ data.tar.gz: ccb0a00956391215272d0a403cd536468d7d56aab7f5b69cfc0854d0f3d869c7479823f17bb4396d99d92721460b10ffb165918312f25e9606c60235c53f6152
@@ -5,7 +5,7 @@ module Dscf
5
5
 
6
6
  def feed
7
7
  authorize @clazz.new, :index?
8
- listings = @clazz.active.includes(supplier_product: :product)
8
+ listings = @clazz.active.includes(:product, {supplier_product: :product}, {sub_supplier_product: :product})
9
9
 
10
10
  options = {
11
11
  include: default_serializer_includes[:index] || [],
@@ -18,7 +18,7 @@ module Dscf
18
18
  def my_listings
19
19
  authorize @clazz.new, :my_listings?
20
20
  businesses = current_user.businesses.pluck(:id)
21
- listings = @clazz.where(aggregator_id: businesses).includes(supplier_product: :product)
21
+ listings = @clazz.where(aggregator_id: businesses).includes(:product, {supplier_product: :product}, {sub_supplier_product: :product})
22
22
 
23
23
  options = {
24
24
  include: default_serializer_includes[:index] || [],
@@ -32,7 +32,8 @@ module Dscf
32
32
 
33
33
  def model_params
34
34
  params.require(:aggregator_listing).permit(
35
- :aggregator_id, :supplier_product_id, :price, :quantity, :status
35
+ :aggregator_id, :source_kind, :supplier_product_id, :product_id,
36
+ :sub_supplier_product_id, :price, :quantity, :status
36
37
  )
37
38
  end
38
39
  end
@@ -194,6 +194,47 @@ module Dscf
194
194
  render_error(errors: e.message, status: :unprocessable_entity)
195
195
  end
196
196
 
197
+ def adjust_item
198
+ obj = find_record
199
+ authorize obj, :supplier_confirm?
200
+
201
+ item = obj.order_items.find(params[:order_item_id])
202
+ Dscf::Marketplace::OrderSplittingService.adjust_item(
203
+ obj, item,
204
+ unit_price: params[:unit_price],
205
+ quantity: params[:quantity],
206
+ note: params[:note]
207
+ )
208
+ create_notification_for_order(obj, :supplier_adjusted, reason: params[:note])
209
+ render_success(data: obj.reload, serializer_options: { include: default_serializer_includes[:show] || [] })
210
+ rescue => e
211
+ render_error(errors: e.message, status: :unprocessable_entity)
212
+ end
213
+
214
+ def accept_adjustment
215
+ obj = find_record
216
+ authorize obj, :split?
217
+
218
+ item = obj.order_items.find(params[:order_item_id])
219
+ Dscf::Marketplace::OrderSplittingService.accept_item_adjustment(obj, item)
220
+ create_notification_for_order(obj, :adjustment_accepted)
221
+ render_success(data: obj.reload, serializer_options: { include: default_serializer_includes[:show] || [] })
222
+ rescue => e
223
+ render_error(errors: e.message, status: :unprocessable_entity)
224
+ end
225
+
226
+ def reject_adjustment
227
+ obj = find_record
228
+ authorize obj, :split?
229
+
230
+ item = obj.order_items.find(params[:order_item_id])
231
+ Dscf::Marketplace::OrderSplittingService.reject_item_adjustment(obj, item, reason: params[:reason])
232
+ create_notification_for_order(obj, :adjustment_rejected, reason: params[:reason])
233
+ render_success(data: obj.reload, serializer_options: { include: default_serializer_includes[:show] || [] })
234
+ rescue => e
235
+ render_error(errors: e.message, status: :unprocessable_entity)
236
+ end
237
+
197
238
  def retailer_confirm
198
239
  obj = find_record
199
240
  authorize obj, :confirm? # reuse existing or add specific
@@ -227,13 +268,17 @@ module Dscf
227
268
  when :split_ready then "Order ##{order.id} ready for supplier confirmation"
228
269
  when :supplier_confirmed then "Supplier confirmed order ##{order.id}"
229
270
  when :supplier_rejected then "Supplier rejected item in order ##{order.id}"
271
+ when :supplier_adjusted then "Supplier proposed an adjustment on order ##{order.id}"
272
+ when :adjustment_accepted then "Adjustment accepted on order ##{order.id}"
273
+ when :adjustment_rejected then "Adjustment rejected on order ##{order.id}"
230
274
  when :retailer_confirmed then "Retailer confirmed order ##{order.id}"
231
275
  when :retailer_rejected then "Retailer cancelled order ##{order.id}"
232
276
  else "Order ##{order.id} update"
233
277
  end
234
278
 
235
279
  body = case action
236
- when :supplier_rejected, :retailer_rejected then "Reason: #{reason}" if reason
280
+ when :supplier_rejected, :retailer_rejected, :supplier_adjusted, :adjustment_rejected
281
+ "Reason: #{reason}" if reason
237
282
  else "Status: #{order.status}"
238
283
  end
239
284
 
@@ -314,7 +359,12 @@ module Dscf
314
359
  order.status = :pending
315
360
  order.ordered_to = aggregator_listing.aggregator
316
361
 
317
- product = aggregator_listing.supplier_product.product
362
+ product = aggregator_listing.product
363
+ # Order lines from the aggregator feed are auto-sourced to the originating
364
+ # AggregatorListing (regardless of source_kind) — the aggregator already
365
+ # knows the source, so there's no manual pick at split time. The listing's
366
+ # source_kind carries who ultimately fulfils it (self / sub-supplier /
367
+ # supplier), surfaced via source_name for display and confirmation routing.
318
368
  order.order_items.build(
319
369
  source: aggregator_listing,
320
370
  product: product,
@@ -0,0 +1,29 @@
1
+ module Dscf
2
+ module Marketplace
3
+ class SubSupplierProductsController < ApplicationController
4
+ include Dscf::Core::Common
5
+
6
+ private
7
+
8
+ def model_params
9
+ params.require(:sub_supplier_product).permit(
10
+ :sub_supplier_id, :product_id, :sub_supplier_price,
11
+ :aggregator_price, :quantity, :listed
12
+ )
13
+ end
14
+
15
+ def eager_loaded_associations
16
+ [ :sub_supplier, :product ]
17
+ end
18
+
19
+ def default_serializer_includes
20
+ {
21
+ index: [ :sub_supplier, :product ],
22
+ show: [ :sub_supplier, :product ],
23
+ create: [ :sub_supplier, :product ],
24
+ update: [ :sub_supplier, :product ]
25
+ }
26
+ end
27
+ end
28
+ end
29
+ end
@@ -1,32 +1,74 @@
1
1
  module Dscf::Marketplace
2
2
  class AggregatorListing < ApplicationRecord
3
3
  belongs_to :aggregator, class_name: "Dscf::Core::Business"
4
- belongs_to :supplier_product, class_name: "Dscf::Marketplace::SupplierProduct"
4
+ belongs_to :supplier_product, class_name: "Dscf::Marketplace::SupplierProduct", optional: true
5
+ belongs_to :product, class_name: "Dscf::Marketplace::Product", optional: true
6
+ belongs_to :sub_supplier_product, class_name: "Dscf::Marketplace::SubSupplierProduct", optional: true
5
7
 
6
8
  has_many :order_items, class_name: "Dscf::Marketplace::OrderItem", as: :source, dependent: :nullify
7
9
 
8
10
  delegate :name, :description, :thumbnail_url, :images_urls, to: :product, allow_nil: true
9
11
 
10
12
  enum :status, {active: 0, draft: 1, paused: 2, sold_out: 3}, default: :draft
13
+ # How the aggregator sourced this listing. Drives which reference is required,
14
+ # how the product/cost resolve, and who fulfils an order line placed against it.
15
+ enum :source_kind, {catalog: 0, supplier: 1, sub_supplier: 2}, default: :catalog
11
16
 
12
17
  validates :price, numericality: {greater_than: 0}, presence: true
13
18
  validates :quantity, numericality: {greater_than: 0}, presence: true
14
19
  validates :aggregator, presence: true
15
- validates :supplier_product_id, presence: true, uniqueness: {scope: :aggregator_id}
20
+
21
+ # Each kind requires (and is unique on) exactly one reference.
22
+ validates :supplier_product_id, presence: true, uniqueness: {scope: :aggregator_id}, if: :supplier?
23
+ validates :product_id, presence: true, uniqueness: {scope: :aggregator_id}, if: :catalog?
24
+ validates :sub_supplier_product_id, presence: true, uniqueness: {scope: :aggregator_id}, if: :sub_supplier?
16
25
 
17
26
  scope :active, -> { where(status: :active) }
18
27
  scope :by_aggregator, ->(aggregator_id) { where(aggregator_id: aggregator_id) }
19
28
 
20
29
  def self.ransackable_attributes(_auth_object = nil)
21
- %w[id aggregator_id supplier_product_id price quantity status created_at updated_at]
30
+ %w[id aggregator_id supplier_product_id product_id sub_supplier_product_id source_kind price quantity status created_at updated_at]
22
31
  end
23
32
 
24
33
  def self.ransackable_associations(_auth_object = nil)
25
- %w[aggregator supplier_product]
34
+ %w[aggregator supplier_product product sub_supplier_product]
26
35
  end
27
36
 
37
+ # Resolves the catalogue product regardless of how the listing was sourced.
28
38
  def product
29
- supplier_product&.product
39
+ case source_kind
40
+ when "supplier" then supplier_product&.product
41
+ when "sub_supplier" then sub_supplier_product&.product
42
+ else super
43
+ end
44
+ end
45
+
46
+ # The aggregator's cost basis for this listing (nil for catalog — the aggregator
47
+ # sets a selling price directly with no upstream cost to compare against).
48
+ def cost_price
49
+ case source_kind
50
+ when "supplier" then supplier_product&.supplier_price
51
+ when "sub_supplier" then sub_supplier_product&.sub_supplier_price
52
+ end
53
+ end
54
+
55
+ def margin
56
+ return nil unless cost_price
57
+ price - cost_price
58
+ end
59
+
60
+ # Human-readable name of who ultimately fulfils a line placed against this
61
+ # listing. An order line is auto-sourced to the AggregatorListing itself, so
62
+ # this drives the "Product source name" the requirements ask for:
63
+ # catalog -> the aggregator (self-fulfilled)
64
+ # supplier -> the supplier's business name
65
+ # sub_supplier -> the sub-supplier's business name
66
+ def source_label
67
+ case source_kind
68
+ when "supplier" then supplier_product&.business&.name
69
+ when "sub_supplier" then sub_supplier_product&.sub_supplier&.business_name
70
+ else aggregator&.name
71
+ end
30
72
  end
31
73
 
32
74
  def available?
@@ -1,6 +1,9 @@
1
1
  module Dscf::Marketplace
2
2
  class OrderItem < ApplicationRecord
3
- enum :status, {pending: 0, confirmed: 1, processing: 2, fulfilled: 3, cancelled: 4}
3
+ # :adjusted the supplier proposed a revised price/quantity that is awaiting
4
+ # the aggregator's accept/reject follow-up. Distinct from confirmed/cancelled so
5
+ # supplier_confirmation_complete? holds the order until the aggregator responds.
6
+ enum :status, {pending: 0, confirmed: 1, processing: 2, fulfilled: 3, cancelled: 4, adjusted: 5}
4
7
  enum :validation_status, {
5
8
  validated: 0,
6
9
  no_longer_listed: 1,
@@ -22,11 +25,11 @@ module Dscf::Marketplace
22
25
  validates :status, presence: true
23
26
  validates :product, presence: true
24
27
  validates :unit, presence: true
25
- validate :quotation_item_or_listing_present
28
+ validate :orderable_source_present
26
29
 
27
30
  # Ransack configuration for secure filtering
28
31
  def self.ransackable_attributes(_auth_object = nil)
29
- %w[id order_id quotation_item_id listing_id product_id unit_id quantity unit_price status validation_status validation_note resolved_unit_price resolved_quantity source_type source_id created_at updated_at]
32
+ %w[id order_id quotation_item_id listing_id product_id unit_id quantity unit_price status validation_status validation_note resolved_unit_price resolved_quantity source_type source_id supplier_adjusted_unit_price supplier_adjusted_quantity supplier_adjustment_note created_at updated_at]
30
33
  end
31
34
 
32
35
  def self.ransackable_associations(_auth_object = nil)
@@ -55,10 +58,15 @@ module Dscf::Marketplace
55
58
 
56
59
  private
57
60
 
58
- def quotation_item_or_listing_present
59
- unless quotation_item.present? || listing.present? || (source_type == "Dscf::Marketplace::AggregatorListing" && source_id.present?)
60
- errors.add(:base, "Either quotation_item, listing, or aggregator listing source must be present")
61
- end
61
+ # A line must trace back to something orderable: an RFQ quotation item, a
62
+ # supplier listing, or any assigned polymorphic source (aggregator listing,
63
+ # sub-supplier, other supplier/business). The last case is what lets the
64
+ # aggregator reassign a line to another source during splitting.
65
+ def orderable_source_present
66
+ return if quotation_item.present? || listing.present?
67
+ return if source_type.present? && source_id.present?
68
+
69
+ errors.add(:base, "Either a quotation item, a listing, or an assigned source must be present")
62
70
  end
63
71
  end
64
72
  end
@@ -1,9 +1,11 @@
1
1
  module Dscf
2
2
  module Marketplace
3
3
  class AggregatorListingSerializer < ActiveModel::Serializer
4
- attributes :id, :aggregator_id, :supplier_product_id, :price, :quantity,
4
+ attributes :id, :aggregator_id, :source_kind, :supplier_product_id, :product_id,
5
+ :sub_supplier_product_id, :price, :quantity, :cost_price, :margin,
5
6
  :status, :created_at, :updated_at, :total_value, :available?,
6
- :product_name, :product_description, :thumbnail_url, :images_urls
7
+ :product_name, :product_description, :thumbnail_url, :images_urls,
8
+ :source_label
7
9
 
8
10
  belongs_to :aggregator
9
11
 
@@ -5,7 +5,8 @@ module Dscf
5
5
  :product_id, :unit_id, :quantity, :unit_price,
6
6
  :status, :validation_status, :validation_note, :resolved_unit_price, :resolved_quantity,
7
7
  :source_type, :source_id, :source_name, :created_at, :updated_at,
8
- :subtotal, :product_name, :unit_name, :thumbnail_url, :images_urls
8
+ :subtotal, :product_name, :unit_name, :thumbnail_url, :images_urls,
9
+ :supplier_adjusted_unit_price, :supplier_adjusted_quantity, :supplier_adjustment_note
9
10
 
10
11
  belongs_to :order
11
12
  belongs_to :quotation_item
@@ -16,6 +17,8 @@ module Dscf
16
17
  def source_name
17
18
  return nil unless object.source
18
19
  case object.source_type
20
+ when 'Dscf::Marketplace::AggregatorListing'
21
+ object.source.try(:source_label)
19
22
  when 'Dscf::Marketplace::SubSupplier'
20
23
  object.source.try(:business_name) || object.source.try(:name)
21
24
  when 'Dscf::Marketplace::Supplier'
@@ -0,0 +1,34 @@
1
+ module Dscf
2
+ module Marketplace
3
+ class SubSupplierProductSerializer < ActiveModel::Serializer
4
+ attributes :id, :sub_supplier_id, :product_id,
5
+ :sub_supplier_price, :aggregator_price, :quantity, :listed,
6
+ :created_at, :updated_at,
7
+ :product_name, :product_sku, :thumbnail_url, :images_urls,
8
+ :sub_supplier_name
9
+
10
+ belongs_to :sub_supplier
11
+ belongs_to :product
12
+
13
+ def product_name
14
+ object.product&.name
15
+ end
16
+
17
+ def product_sku
18
+ object.product&.sku
19
+ end
20
+
21
+ def thumbnail_url
22
+ object.product&.thumbnail_url
23
+ end
24
+
25
+ def images_urls
26
+ object.product&.images_urls
27
+ end
28
+
29
+ def sub_supplier_name
30
+ object.sub_supplier&.business_name
31
+ end
32
+ end
33
+ end
34
+ end
@@ -32,7 +32,8 @@ module Dscf
32
32
 
33
33
  def self.supplier_confirm(order, confirmed: true, reason: nil)
34
34
  if confirmed
35
- order.order_items.where(status: :processing).update_all(status: OrderItem.statuses[:confirmed])
35
+ # In full impl, mark specific allocation as confirmed
36
+ # For now advance whole if appropriate
36
37
  else
37
38
  order.order_items.where(status: :processing).update_all(status: OrderItem.statuses[:cancelled])
38
39
  end
@@ -57,6 +58,48 @@ module Dscf
57
58
  order.mark_waiting_retailer_confirmation! if order.supplier_confirmation_complete?
58
59
  order_item
59
60
  end
61
+
62
+ # Supplier proposes revised terms instead of confirming/rejecting outright.
63
+ # The item goes to :adjusted holding the proposal; because :adjusted is neither
64
+ # confirmed nor cancelled, supplier_confirmation_complete? keeps the order in
65
+ # waiting_supplier_confirmation until the aggregator follows up (accept/reject).
66
+ def self.adjust_item(order, order_item, unit_price: nil, quantity: nil, note: nil)
67
+ order_item.supplier_adjusted_unit_price = unit_price if unit_price.present?
68
+ order_item.supplier_adjusted_quantity = quantity if quantity.present?
69
+ order_item.supplier_adjustment_note = note
70
+ order_item.status = :adjusted
71
+ order_item.save!
72
+ order_item
73
+ end
74
+
75
+ # Aggregator accepts the supplier's proposal: the adjusted price/quantity become
76
+ # the line's actual terms, the item is confirmed, and the order total recomputes
77
+ # (Order#calculate_total_amount runs on save). Advances once all items responded.
78
+ def self.accept_item_adjustment(order, order_item)
79
+ order_item.unit_price = order_item.supplier_adjusted_unit_price if order_item.supplier_adjusted_unit_price.present?
80
+ order_item.quantity = order_item.supplier_adjusted_quantity if order_item.supplier_adjusted_quantity.present?
81
+ order_item.supplier_adjusted_unit_price = nil
82
+ order_item.supplier_adjusted_quantity = nil
83
+ order_item.status = :confirmed
84
+ order_item.save!
85
+
86
+ order.reload
87
+ order.save! # recompute total_amount from the revised line
88
+ order.mark_waiting_retailer_confirmation! if order.supplier_confirmation_complete?
89
+ order_item
90
+ end
91
+
92
+ # Aggregator rejects the proposal — the line can't be fulfilled on agreeable
93
+ # terms, so it is cancelled (mirrors a not-confirm). Advances if all responded.
94
+ def self.reject_item_adjustment(order, order_item, reason: nil)
95
+ order_item.supplier_adjustment_note = reason if reason.present?
96
+ order_item.status = :cancelled
97
+ order_item.save!
98
+
99
+ order.reload
100
+ order.mark_waiting_retailer_confirmation! if order.supplier_confirmation_complete?
101
+ order_item
102
+ end
60
103
  end
61
104
  end
62
105
  end
@@ -51,8 +51,17 @@ module Dscf
51
51
  item
52
52
  end
53
53
 
54
+ # Returns the current price/quantity reference for an item. This is duck-typed:
55
+ # any object responding to #price and #quantity works (Listing or
56
+ # AggregatorListing), since validate_item only reads those two fields.
54
57
  private_class_method def self.find_current_active_listing(item)
55
- # Prefer the original listing if still active
58
+ # Lines placed from the aggregator feed are sourced to their AggregatorListing —
59
+ # validate against that listing's live price/quantity, not a supplier listing.
60
+ if item.source.is_a?(AggregatorListing)
61
+ return AggregatorListing.active.find_by(id: item.source_id)
62
+ end
63
+
64
+ # Prefer the original supplier listing if still active
56
65
  if item.listing && Listing.active.exists?(id: item.listing.id)
57
66
  return item.listing
58
67
  end
data/config/routes.rb CHANGED
@@ -33,6 +33,7 @@ Dscf::Marketplace::Engine.routes.draw do
33
33
  end
34
34
 
35
35
  resources :sub_suppliers
36
+ resources :sub_supplier_products
36
37
 
37
38
  # Aggregator Feed
38
39
  resources :aggregator_listings do
@@ -131,6 +132,9 @@ Dscf::Marketplace::Engine.routes.draw do
131
132
  post "split"
132
133
  post "supplier_confirm"
133
134
  post "confirm_item"
135
+ post "adjust_item"
136
+ post "accept_adjustment"
137
+ post "reject_adjustment"
134
138
  post "retailer_confirm"
135
139
  end
136
140
  collection do
@@ -0,0 +1,58 @@
1
+ class UnifyAggregatorListingSource < ActiveRecord::Migration[8.0]
2
+ def up
3
+ # New polymorphic-style source discriminator.
4
+ # 0 = catalog (aggregator lists a master-catalogue product directly)
5
+ # 1 = supplier (aggregator resells a supplier_product) <- the only kind that existed before
6
+ # 2 = sub_supplier (aggregator lists a sub-supplier product)
7
+ add_column :dscf_marketplace_aggregator_listings, :source_kind, :integer, default: 0, null: false
8
+
9
+ # Catalog / sub-supplier sourced listings don't have a supplier_product.
10
+ change_column_null :dscf_marketplace_aggregator_listings, :supplier_product_id, true
11
+
12
+ add_column :dscf_marketplace_aggregator_listings, :product_id, :bigint
13
+ add_column :dscf_marketplace_aggregator_listings, :sub_supplier_product_id, :bigint
14
+
15
+ add_index :dscf_marketplace_aggregator_listings, :product_id, name: "product_on_dm_al_indx"
16
+ add_index :dscf_marketplace_aggregator_listings, :sub_supplier_product_id, name: "ssp_on_dm_al_indx"
17
+
18
+ # Existing rows are all supplier-sourced; backfill kind + denormalised product_id.
19
+ execute(<<~SQL.squish)
20
+ UPDATE dscf_marketplace_aggregator_listings al
21
+ SET source_kind = 1,
22
+ product_id = sp.product_id
23
+ FROM dscf_marketplace_supplier_products sp
24
+ WHERE al.supplier_product_id = sp.id
25
+ AND al.supplier_product_id IS NOT NULL
26
+ SQL
27
+
28
+ # The old unique index assumed supplier_product_id is always present. Replace it
29
+ # with one partial unique index per kind so an aggregator can't double-list the
30
+ # same catalogue product / supplier_product / sub_supplier_product.
31
+ remove_index :dscf_marketplace_aggregator_listings, name: "unique_aggregator_sp_indx"
32
+
33
+ add_index :dscf_marketplace_aggregator_listings, [:aggregator_id, :supplier_product_id],
34
+ unique: true, where: "source_kind = 1", name: "uniq_agg_supplier_product_indx"
35
+ add_index :dscf_marketplace_aggregator_listings, [:aggregator_id, :product_id],
36
+ unique: true, where: "source_kind = 0", name: "uniq_agg_catalog_product_indx"
37
+ add_index :dscf_marketplace_aggregator_listings, [:aggregator_id, :sub_supplier_product_id],
38
+ unique: true, where: "source_kind = 2", name: "uniq_agg_sub_supplier_product_indx"
39
+ end
40
+
41
+ def down
42
+ remove_index :dscf_marketplace_aggregator_listings, name: "uniq_agg_supplier_product_indx"
43
+ remove_index :dscf_marketplace_aggregator_listings, name: "uniq_agg_catalog_product_indx"
44
+ remove_index :dscf_marketplace_aggregator_listings, name: "uniq_agg_sub_supplier_product_indx"
45
+
46
+ remove_index :dscf_marketplace_aggregator_listings, name: "product_on_dm_al_indx"
47
+ remove_index :dscf_marketplace_aggregator_listings, name: "ssp_on_dm_al_indx"
48
+
49
+ remove_column :dscf_marketplace_aggregator_listings, :product_id
50
+ remove_column :dscf_marketplace_aggregator_listings, :sub_supplier_product_id
51
+ remove_column :dscf_marketplace_aggregator_listings, :source_kind
52
+
53
+ # Restore the original unique index. Rows must be supplier-sourced for this to hold.
54
+ change_column_null :dscf_marketplace_aggregator_listings, :supplier_product_id, false
55
+ add_index :dscf_marketplace_aggregator_listings, [:aggregator_id, :supplier_product_id],
56
+ unique: true, name: "unique_aggregator_sp_indx"
57
+ end
58
+ end
@@ -0,0 +1,10 @@
1
+ class AddSupplierAdjustmentToDscfMarketplaceOrderItems < ActiveRecord::Migration[8.0]
2
+ def change
3
+ # When a supplier can't fulfil an allocation at the aggregator's terms, they
4
+ # propose a revised price/quantity instead of outright rejecting. The proposal
5
+ # is held here (item status -> :adjusted) until the aggregator accepts or rejects.
6
+ add_column :dscf_marketplace_order_items, :supplier_adjusted_unit_price, :decimal, precision: 15, scale: 6
7
+ add_column :dscf_marketplace_order_items, :supplier_adjusted_quantity, :decimal, precision: 15, scale: 6
8
+ add_column :dscf_marketplace_order_items, :supplier_adjustment_note, :text
9
+ end
10
+ end
@@ -28,12 +28,15 @@ module Dscf
28
28
  resource :units, actions: %i[index show create update destroy]
29
29
  resource :unit_conversions, actions: %i[index show create update destroy]
30
30
  resource :supplier_products, actions: %i[index show create update destroy my_products]
31
+ resource :sub_suppliers, actions: %i[index show create update destroy]
32
+ resource :sub_supplier_products, actions: %i[index show create update destroy]
33
+ resource :aggregator_listings, actions: %i[index show create update destroy feed my_listings]
31
34
  resource :listings, actions: %i[index show create update destroy activate pause sold_out my_listings listings_by_supplier]
32
35
  resource :request_for_quotations, actions: %i[index show create update destroy send_rfq close respond my_rfqs filter]
33
36
  resource :rfq_items, actions: %i[index show create update destroy]
34
37
  resource :quotations, actions: %i[index show create update destroy accept reject send_quotation my_quotes filter]
35
38
  resource :quotation_items, actions: %i[index show create update destroy]
36
- resource :orders, actions: %i[index show create update destroy confirm cancel complete my_orders filter validate resolve_item assign_source split supplier_confirm retailer_confirm]
39
+ resource :orders, actions: %i[index show create update destroy confirm cancel complete my_orders filter validate resolve_item assign_source split supplier_confirm confirm_item adjust_item accept_adjustment reject_adjustment retailer_confirm]
37
40
  resource :order_items, actions: %i[index show create update destroy]
38
41
  resource :delivery_orders, actions: %i[
39
42
  index show create update destroy pickup start_delivery complete_delivery mark_failed accept summary
@@ -1,5 +1,5 @@
1
1
  module Dscf
2
2
  module Marketplace
3
- VERSION = "0.9.9".freeze
3
+ VERSION = "0.11.0".freeze
4
4
  end
5
5
  end
@@ -1,9 +1,24 @@
1
1
  FactoryBot.define do
2
2
  factory :dscf_marketplace_aggregator_listing, class: "Dscf::Marketplace::AggregatorListing" do
3
3
  association :aggregator, factory: :dscf_core_business
4
- association :supplier_product, factory: :dscf_marketplace_supplier_product
5
4
  price { 250.00 }
6
5
  quantity { 50.0 }
7
6
  status { :active }
7
+
8
+ # Default: supplier-sourced (resells a supplier_product), matching pre-existing data.
9
+ source_kind { :supplier }
10
+ association :supplier_product, factory: :dscf_marketplace_supplier_product
11
+
12
+ trait :catalog do
13
+ source_kind { :catalog }
14
+ supplier_product { nil }
15
+ association :product, factory: :dscf_marketplace_product
16
+ end
17
+
18
+ trait :sub_supplier do
19
+ source_kind { :sub_supplier }
20
+ supplier_product { nil }
21
+ association :sub_supplier_product, factory: :dscf_marketplace_sub_supplier_product
22
+ end
8
23
  end
9
24
  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.9.9
4
+ version: 0.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Asrat
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-06-30 00:00:00.000000000 Z
10
+ date: 2026-07-03 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: rails
@@ -438,6 +438,7 @@ files:
438
438
  - app/controllers/dscf/marketplace/request_for_quotations_controller.rb
439
439
  - app/controllers/dscf/marketplace/retailers_controller.rb
440
440
  - app/controllers/dscf/marketplace/rfq_items_controller.rb
441
+ - app/controllers/dscf/marketplace/sub_supplier_products_controller.rb
441
442
  - app/controllers/dscf/marketplace/sub_suppliers_controller.rb
442
443
  - app/controllers/dscf/marketplace/supplier_products_controller.rb
443
444
  - app/controllers/dscf/marketplace/suppliers_controller.rb
@@ -514,6 +515,7 @@ files:
514
515
  - app/serializers/dscf/marketplace/request_for_quotation_serializer.rb
515
516
  - app/serializers/dscf/marketplace/retailer_serializer.rb
516
517
  - app/serializers/dscf/marketplace/rfq_item_serializer.rb
518
+ - app/serializers/dscf/marketplace/sub_supplier_product_serializer.rb
517
519
  - app/serializers/dscf/marketplace/sub_supplier_serializer.rb
518
520
  - app/serializers/dscf/marketplace/supplier_product_serializer.rb
519
521
  - app/serializers/dscf/marketplace/supplier_serializer.rb
@@ -590,6 +592,8 @@ files:
590
592
  - db/migrate/20260619210001_add_user_id_to_dscf_marketplace_agents.rb
591
593
  - db/migrate/20260619210002_add_user_id_to_dscf_marketplace_retailers.rb
592
594
  - db/migrate/20260622000001_add_validation_and_source_fields_to_dscf_marketplace_order_items.rb
595
+ - db/migrate/20260702000001_unify_aggregator_listing_source.rb
596
+ - db/migrate/20260703000001_add_supplier_adjustment_to_order_items.rb
593
597
  - db/seeds.rb
594
598
  - lib/dscf/marketplace.rb
595
599
  - lib/dscf/marketplace/engine.rb