dscf-marketplace 0.12.1 → 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: 16f9345522d95b1b1ed85338ca228ab81368b1834caeec4250b026c17df75356
4
- data.tar.gz: 94bd42fea833346ce56401bdaf953886e5675779e9f4c078dd884059627d734e
3
+ metadata.gz: 16b43241cc7e4a81527b02646b620a5d89ff4836aa1f14877d80a8c20e8b8b69
4
+ data.tar.gz: 9cf97a1241386f95e9af9088d740cbbf4a69734b40d1f29218d50e002775f20f
5
5
  SHA512:
6
- metadata.gz: cbc4c95a471075a7402f3ecf9b0702356f9c938050e9e5b0cf425c67cdddd55afbd6018654b841b6255269c6aca05e4928506944a09cd62c1b16115f31eedd1d
7
- data.tar.gz: 360443b7f9e82f9337cbae45e5c2344c44ba6f528c05860a5dd8316a8eea537ef9c0a72e7c49f14a0d0d8a1cbf714060215aba47cf3452a68aa31f690360b4ea
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
- listing = nil
324
- aggregator_listing = nil
330
+ resolved_items = resolve_direct_listing_items
331
+ return if performed? # resolve_direct_listing_items already rendered an error
325
332
 
326
- if model_params[:listing_type] == "AggregatorListing"
327
- aggregator_listing = Dscf::Marketplace::AggregatorListing.active.find_by(id: model_params[:listing_id])
328
- return render_error(errors: "Listing is not available", status: :unprocessable_entity) unless aggregator_listing
329
- else
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
- quantity = direct_listing_quantity
338
- return render_error(errors: "Quantity must be greater than 0", status: :unprocessable_entity) unless quantity.positive?
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
- limit_quantity = listing ? listing.quantity : aggregator_listing.quantity
341
- if quantity > limit_quantity
342
- return render_error(errors: "Requested quantity exceeds available listing quantity", status: :unprocessable_entity)
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.lock!
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
- if quantity > listing.quantity
351
- listing.errors.add(:base, "Requested quantity exceeds available listing quantity")
352
- raise ActiveRecord::RecordInvalid.new(listing)
353
- end
408
+ items.each do |ri|
409
+ source = ri[:listing] || ri[:aggregator_listing]
410
+ next unless ri[:quantity] > source.quantity
354
411
 
355
- order = @clazz.new(model_params.except(:order_items_attributes, :listing_type))
356
- order.order_type = :direct_listing
357
- order.status = :pending
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.lock!
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 = @clazz.includes(eager_loaded_associations).find(order.id) if eager_loaded_associations.present?
409
- includes = default_serializer_includes[:create] || []
410
- options = {include: includes} if includes.present?
411
- render_success(data: order, serializer_options: options, status: :created)
412
- rescue ActiveRecord::RecordInvalid => e
413
- errors = e.record&.errors&.full_messages&.presence || [ e.message ]
414
- render_error(errors: errors.join(", "), status: :unprocessable_entity)
415
- rescue => e
416
- render_error(error: e.message)
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
@@ -1,5 +1,5 @@
1
1
  module Dscf
2
2
  module Marketplace
3
- VERSION = "0.12.1".freeze
3
+ VERSION = "0.13.0".freeze
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dscf-marketplace
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.12.1
4
+ version: 0.13.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Asrat