Class: Spree::Fulfillments::Create

Inherits:
Object
  • Object
show all
Includes:
ServiceModule::Base
Defined in:
app/services/spree/fulfillments/create.rb

Overview

Manually creates a fulfillment (Spree::Shipment) on a completed order, bypassing order routing. Moves the requested quantities of each line item's not-yet-shipped inventory units out of their current shipments into the new fulfillment, mirroring externally-managed fulfillment (3PL, courier API, drop-shipping) back into Spree.

Stock bookkeeping follows the split/transfer semantics: when the source and target stock locations differ, moved quantities are restocked at the source and unstocked at the target. Source shipments left empty are destroyed and their cost and selected delivery method carry over to the new fulfillment, unless the caller provides its own cost / delivery_method.

The resulting cost is frozen only for fulfillments registered with status: 'shipped' (rate refresh skips shipped shipments). Pending/ready fulfillments participate in the standard rate machinery — the order updater re-prices them from the delivery method calculators on the next recalculation, exactly like shipments created via split/transfer.

Instance Method Summary collapse

Methods included from ServiceModule::Base

prepended

Instance Method Details

#call(order:, stock_location:, items: nil, tracking: nil, delivery_method: nil, cost: nil, status: nil, metadata: nil) ⇒ Spree::ServiceModule::Result

Returns the created shipment on success.

Parameters:

  • order (Spree::Order)

    completed order to fulfill

  • stock_location (Spree::StockLocation)

    location the fulfillment ships from

  • items (Array<Hash>, nil) (defaults to: nil)

    [{ line_item: Spree::LineItem, quantity: Integer }]; nil fulfills every not-yet-shipped unit on the order

  • tracking (String, nil) (defaults to: nil)

    carrier tracking number

  • delivery_method (Spree::ShippingMethod, nil) (defaults to: nil)

    carrier; stored as the selected rate. Defaults to the delivery method of the drained source fulfillment(s)

  • cost (String, Numeric, nil) (defaults to: nil)

    explicit shipping cost (e.g. the 3PL's price). Defaults to the summed cost of the drained source fulfillment(s), keeping the order total unchanged; an explicit cost changes the order total and payment state. Guaranteed to persist only with status: 'shipped' — pending fulfillments are re-priced by the rate engine (see class docs)

  • status (String, nil) (defaults to: nil)

    pass 'shipped' to register an already-shipped fulfillment

  • metadata (Hash, nil) (defaults to: nil)

    metadata stored on the fulfillment

Returns:



39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# File 'app/services/spree/fulfillments/create.rb', line 39

def call(order:, stock_location:, items: nil, tracking: nil, delivery_method: nil, cost: nil, status: nil, metadata: nil)
  return failure(nil, Spree.t('fulfillments.errors.invalid_status')) unless status.nil? || status == 'shipped'

  cost = parse_cost(cost)
  return cost if cost.is_a?(Spree::ServiceModule::Result)

  fulfillment = nil

  # The order row is locked before reading fulfillable units so
  # concurrent creations (e.g. duplicate carrier webhooks) validate and
  # move units against a serialized snapshot. The API layer already
  # serializes via with_order_lock; this covers direct service callers.
  ActiveRecord::Base.transaction do
    order.lock!

    return failure(nil, Spree.t('fulfillments.errors.order_not_completed')) unless order.completed?
    return failure(nil, Spree.t('fulfillments.errors.order_canceled')) if order.canceled?

     = fulfillable_units(order)

    requested = normalize_items(order, items, )
    return requested if requested.is_a?(Spree::ServiceModule::Result)

    fulfillment = order.shipments.new(
      stock_location: stock_location,
      address_id: order.ship_address_id,
      tracking: tracking
    )
    fulfillment. =  if .present?
    fulfillment.save!

    source_shipments = move_units(order, fulfillment, requested, )
    inherited = destroy_drained_shipments(source_shipments, capture_delivery_method: delivery_method.nil?)
    attach_cost_and_rate(fulfillment, delivery_method, cost, inherited)

    if status == 'shipped'
      mark_shipped(fulfillment)
    else
      fulfillment.update!(order)
    end

    order.reload.update_with_updater!
  end

  success(fulfillment.reload)
end