dscf-marketplace 0.12.0 → 0.13.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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 16b43241cc7e4a81527b02646b620a5d89ff4836aa1f14877d80a8c20e8b8b69
|
|
4
|
+
data.tar.gz: 9cf97a1241386f95e9af9088d740cbbf4a69734b40d1f29218d50e002775f20f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 906229d9cc272a399446d8d997ee880b7b66a1ca404eca3ea0ee6f23ab51ade9c45dfe517a04129cf09c6f1660a3d241637d8ba22405ac4d4ea952bdd959674e
|
|
7
|
+
data.tar.gz: 64e5345ccf614806d09b410be755e16e8a33ec58793443715f669bdcc934f034da085f78642e2b70638e75c2002df322bc099ce516a53ca6b0a2a470a3be656b
|
|
@@ -319,71 +319,124 @@ module Dscf
|
|
|
319
319
|
|
|
320
320
|
private
|
|
321
321
|
|
|
322
|
+
# Builds one or more direct-listing orders from a cart of one or more
|
|
323
|
+
# listings. Order.ordered_to is a single business, so items are grouped
|
|
324
|
+
# by the business that fulfils them (a supplier Listing's business, or
|
|
325
|
+
# an AggregatorListing's aggregator); each group becomes one Order with
|
|
326
|
+
# one line per item, all created/decremented atomically in one
|
|
327
|
+
# transaction (a multi-vendor cart either checks out completely or not
|
|
328
|
+
# at all — mirrors how the single-item path already behaved).
|
|
322
329
|
def create_direct_listing_order
|
|
323
|
-
|
|
324
|
-
|
|
330
|
+
resolved_items = resolve_direct_listing_items
|
|
331
|
+
return if performed? # resolve_direct_listing_items already rendered an error
|
|
325
332
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
listing = Dscf::Marketplace::Listing.active.find_by(id: model_params[:listing_id])
|
|
331
|
-
if listing.blank? && model_params[:listing_type].blank?
|
|
332
|
-
aggregator_listing = Dscf::Marketplace::AggregatorListing.active.find_by(id: model_params[:listing_id])
|
|
333
|
+
orders = []
|
|
334
|
+
ActiveRecord::Base.transaction do
|
|
335
|
+
resolved_items.group_by { |ri| ri[:ordered_to] }.each_value do |items|
|
|
336
|
+
orders << build_and_save_direct_listing_order(items)
|
|
333
337
|
end
|
|
334
|
-
return render_error(errors: "Listing is not available", status: :unprocessable_entity) if listing.blank? && aggregator_listing.blank?
|
|
335
338
|
end
|
|
336
339
|
|
|
337
|
-
|
|
338
|
-
|
|
340
|
+
orders = orders.map { |o| @clazz.includes(eager_loaded_associations).find(o.id) } if eager_loaded_associations.present?
|
|
341
|
+
includes = default_serializer_includes[:create] || []
|
|
342
|
+
options = {include: includes} if includes.present?
|
|
339
343
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
344
|
+
if orders.size == 1
|
|
345
|
+
render_success(data: orders.first, serializer_options: options, status: :created)
|
|
346
|
+
else
|
|
347
|
+
render_success(data: orders, serializer_options: options, status: :created,
|
|
348
|
+
meta: {split_by_business: true, order_count: orders.size})
|
|
343
349
|
end
|
|
350
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
351
|
+
errors = e.record&.errors&.full_messages&.presence || [ e.message ]
|
|
352
|
+
render_error(errors: errors.join(", "), status: :unprocessable_entity)
|
|
353
|
+
rescue => e
|
|
354
|
+
render_error(error: e.message)
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
# Resolves each cart line to its Listing or AggregatorListing + quantity +
|
|
358
|
+
# the business it's ordered to. Falls back to the legacy top-level
|
|
359
|
+
# listing_id/listing_type params when a line doesn't carry its own
|
|
360
|
+
# (single-item callers built before the cart existed).
|
|
361
|
+
def resolve_direct_listing_items
|
|
362
|
+
items_params = model_params[:order_items_attributes]
|
|
363
|
+
if items_params.blank?
|
|
364
|
+
render_error(errors: "At least one item is required", status: :unprocessable_entity)
|
|
365
|
+
return []
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
items_params.map do |item_params|
|
|
369
|
+
listing_id = item_params[:listing_id] || model_params[:listing_id]
|
|
370
|
+
listing_type = item_params[:listing_type] || model_params[:listing_type]
|
|
371
|
+
quantity = item_params[:quantity].to_i
|
|
372
|
+
|
|
373
|
+
if quantity <= 0
|
|
374
|
+
render_error(errors: "Quantity must be greater than 0", status: :unprocessable_entity)
|
|
375
|
+
return []
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
listing = nil
|
|
379
|
+
aggregator_listing = nil
|
|
380
|
+
|
|
381
|
+
if listing_type == "AggregatorListing"
|
|
382
|
+
aggregator_listing = Dscf::Marketplace::AggregatorListing.active.find_by(id: listing_id)
|
|
383
|
+
else
|
|
384
|
+
listing = Dscf::Marketplace::Listing.active.find_by(id: listing_id)
|
|
385
|
+
aggregator_listing = Dscf::Marketplace::AggregatorListing.active.find_by(id: listing_id) if listing.blank? && listing_type.blank?
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
if listing.blank? && aggregator_listing.blank?
|
|
389
|
+
render_error(errors: "Listing is not available", status: :unprocessable_entity)
|
|
390
|
+
return []
|
|
391
|
+
end
|
|
344
392
|
|
|
345
|
-
order = nil
|
|
346
|
-
ActiveRecord::Base.transaction do
|
|
347
393
|
if listing
|
|
348
|
-
listing.
|
|
394
|
+
{listing: listing, quantity: quantity, ordered_to: listing.business}
|
|
395
|
+
else
|
|
396
|
+
{aggregator_listing: aggregator_listing, quantity: quantity, ordered_to: aggregator_listing.aggregator}
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
# Locks + validates stock for every item in a business group, builds one
|
|
402
|
+
# Order with one line per item, saves it, then decrements each source's
|
|
403
|
+
# quantity. Raises ActiveRecord::RecordInvalid on insufficient stock so
|
|
404
|
+
# the whole checkout (all groups) rolls back together.
|
|
405
|
+
def build_and_save_direct_listing_order(items)
|
|
406
|
+
items.each { |ri| (ri[:listing] || ri[:aggregator_listing]).lock! }
|
|
349
407
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
end
|
|
408
|
+
items.each do |ri|
|
|
409
|
+
source = ri[:listing] || ri[:aggregator_listing]
|
|
410
|
+
next unless ri[:quantity] > source.quantity
|
|
354
411
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
order.listing = listing
|
|
359
|
-
order.ordered_to = listing.business
|
|
412
|
+
source.errors.add(:base, "Requested quantity exceeds available listing quantity for #{source.product&.name}")
|
|
413
|
+
raise ActiveRecord::RecordInvalid.new(source)
|
|
414
|
+
end
|
|
360
415
|
|
|
416
|
+
order = @clazz.new(model_params.except(:order_items_attributes, :listing_id, :listing_type))
|
|
417
|
+
order.order_type = :direct_listing
|
|
418
|
+
order.status = :pending
|
|
419
|
+
order.ordered_to = items.first[:ordered_to]
|
|
420
|
+
# Order#listing is a single FK; set it to the first supplier-listing item
|
|
421
|
+
# so quotation_or_listing_present is satisfied for listing-only groups
|
|
422
|
+
# (an aggregator-listing-sourced item satisfies it on its own — see
|
|
423
|
+
# Order#quotation_or_listing_present).
|
|
424
|
+
order.listing = items.find { |ri| ri[:listing] }&.dig(:listing)
|
|
425
|
+
|
|
426
|
+
items.each do |ri|
|
|
427
|
+
if ri[:listing]
|
|
428
|
+
listing = ri[:listing]
|
|
361
429
|
product = listing.supplier_product.product
|
|
362
430
|
order.order_items.build(
|
|
363
431
|
listing: listing,
|
|
364
432
|
product: product,
|
|
365
433
|
unit: product.unit,
|
|
366
|
-
quantity: quantity,
|
|
434
|
+
quantity: ri[:quantity],
|
|
367
435
|
unit_price: listing.price,
|
|
368
436
|
status: :pending
|
|
369
437
|
)
|
|
370
|
-
order.save!
|
|
371
|
-
|
|
372
|
-
new_quantity = listing.quantity - quantity
|
|
373
|
-
listing.update!(quantity: new_quantity, status: (new_quantity.zero? ? :sold_out : listing.status))
|
|
374
438
|
else
|
|
375
|
-
aggregator_listing
|
|
376
|
-
|
|
377
|
-
if quantity > aggregator_listing.quantity
|
|
378
|
-
aggregator_listing.errors.add(:base, "Requested quantity exceeds available listing quantity")
|
|
379
|
-
raise ActiveRecord::RecordInvalid.new(aggregator_listing)
|
|
380
|
-
end
|
|
381
|
-
|
|
382
|
-
order = @clazz.new(model_params.except(:order_items_attributes, :listing_id, :listing_type))
|
|
383
|
-
order.order_type = :direct_listing
|
|
384
|
-
order.status = :pending
|
|
385
|
-
order.ordered_to = aggregator_listing.aggregator
|
|
386
|
-
|
|
439
|
+
aggregator_listing = ri[:aggregator_listing]
|
|
387
440
|
product = aggregator_listing.product
|
|
388
441
|
# Order lines from the aggregator feed are auto-sourced to the originating
|
|
389
442
|
# AggregatorListing (regardless of source_kind) — the aggregator already
|
|
@@ -394,42 +447,33 @@ module Dscf
|
|
|
394
447
|
source: aggregator_listing,
|
|
395
448
|
product: product,
|
|
396
449
|
unit: product.unit,
|
|
397
|
-
quantity: quantity,
|
|
450
|
+
quantity: ri[:quantity],
|
|
398
451
|
unit_price: aggregator_listing.price,
|
|
399
452
|
status: :pending
|
|
400
453
|
)
|
|
401
|
-
order.save!
|
|
402
|
-
|
|
403
|
-
new_quantity = aggregator_listing.quantity - quantity
|
|
404
|
-
aggregator_listing.update!(quantity: new_quantity, status: (new_quantity.zero? ? :sold_out : aggregator_listing.status))
|
|
405
454
|
end
|
|
406
455
|
end
|
|
407
456
|
|
|
408
|
-
order
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
457
|
+
order.save!
|
|
458
|
+
|
|
459
|
+
items.each do |ri|
|
|
460
|
+
source = ri[:listing] || ri[:aggregator_listing]
|
|
461
|
+
new_quantity = source.quantity - ri[:quantity]
|
|
462
|
+
source.update!(quantity: new_quantity, status: (new_quantity.zero? ? :sold_out : source.status))
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
order
|
|
417
466
|
end
|
|
418
467
|
|
|
419
468
|
def direct_listing_request?
|
|
420
469
|
%w[direct_listing 1].include?(model_params[:order_type].to_s)
|
|
421
470
|
end
|
|
422
471
|
|
|
423
|
-
def direct_listing_quantity
|
|
424
|
-
item = model_params[:order_items_attributes]&.first
|
|
425
|
-
(item&.[](:quantity) || item&.[]("quantity")).to_i
|
|
426
|
-
end
|
|
427
|
-
|
|
428
472
|
def model_params
|
|
429
473
|
@model_params ||= params.require(:order).permit(
|
|
430
474
|
:quotation_id, :listing_id, :user_id, :ordered_by_id, :ordered_to_id, :delivery_order_id, :dropoff_address_id,
|
|
431
475
|
:order_type, :status, :fulfillment_type, :payment_method, :received_bank_name, :transaction_reference, :listing_type,
|
|
432
|
-
order_items_attributes: [ :id, :quotation_item_id, :listing_id, :product_id, :unit_id, :quantity, :unit_price, :status, :_destroy ]
|
|
476
|
+
order_items_attributes: [ :id, :quotation_item_id, :listing_id, :listing_type, :product_id, :unit_id, :quantity, :unit_price, :status, :_destroy ]
|
|
433
477
|
)
|
|
434
478
|
@model_params[:payment_method] = normalize_payment_method(@model_params[:payment_method]) if @model_params.key?(:payment_method)
|
|
435
479
|
@model_params
|
|
@@ -5,7 +5,7 @@ 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, :product_sku, :unit_name, :thumbnail_url, :images_urls,
|
|
9
9
|
:supplier_adjusted_unit_price, :supplier_adjusted_quantity, :supplier_adjustment_note
|
|
10
10
|
|
|
11
11
|
belongs_to :order
|
|
@@ -14,6 +14,10 @@ module Dscf
|
|
|
14
14
|
belongs_to :product
|
|
15
15
|
belongs_to :unit
|
|
16
16
|
|
|
17
|
+
def product_sku
|
|
18
|
+
object.product&.sku
|
|
19
|
+
end
|
|
20
|
+
|
|
17
21
|
def source_name
|
|
18
22
|
return nil unless object.source
|
|
19
23
|
case object.source_type
|