Class: Catalog::SwitchItemizableCatalog

Inherits:
BaseService show all
Defined in:
app/services/catalog/switch_itemizable_catalog.rb

Overview

The single sanctioned, atomic path for "flipping" an itemizable — an Order,
Quote, or RoomConfiguration (cart) — onto a different catalog/currency (e.g. a
US cart that becomes a Canadian order, SO727885, 2026-07).

The flip is a multi-step mutation — the customer, the currency, every line
item's catalog item, the inventory reservations behind those lines, and any
authorized payment — and it MUST be all-or-nothing. Done piecemeal (or via a
save-time callback) it can leave an order half-converted: CAD currency with a
US line, or US-warehouse stock committed under a Canadian order. This service
performs the whole thing inside a single transaction, so a failure anywhere
(an item with no equivalent in the target catalog, a live authorization in
the wrong currency, an incoherent save) rolls the itemizable back to its
original, coherent state.

Coherence is enforced declaratively by
LineItem#catalog_item_matches_resource_catalog (through
Models::Pickable#validates_associated), which keys off the customer's
catalog. So a real flip has to move the customer too — pass +target_customer+
(the incident: a cart reassigned from a guest to a Canadian customer). Pass
+target_catalog+ instead only when the customer already resolves to it.
Sibling of Catalog::AssignCatalogToCustomer (which drives the customer-keeps-
identity, catalog-changes cascade).

Examples:

Reassign an order to a Canadian customer, coherently

result = Catalog::SwitchItemizableCatalog.new.process(order, target_customer: ca_customer)
result.switched? # => true, or false with result.messages on abort

Defined Under Namespace

Classes: Result

Instance Attribute Summary

Attributes inherited from BaseService

#options

Instance Method Summary collapse

Methods inherited from BaseService

#initialize, #log_debug, #log_error, #log_info, #log_warning, #logger, #tagged_logger

Constructor Details

This class inherits a constructor from BaseService

Instance Method Details

#process(itemizable, target_catalog: nil, target_customer: nil) ⇒ Result

Atomically re-bases +itemizable+ onto a target catalog: (optionally)
reassigns the customer, sets the currency, remaps every goods line to the
equivalent item in the target catalog (moving its stock reservation), and
refuses when a live authorization in the outgoing currency would be left
mismatched. Any failure aborts the whole transaction — no half-flipped
state.

Parameters:

  • itemizable (Order, Quote, RoomConfiguration)
  • target_catalog (Catalog, nil) (defaults to: nil)

    destination catalog; defaults to
    +target_customer+'s catalog

  • target_customer (Party, nil) (defaults to: nil)

    reassign the itemizable to this customer
    within the transaction (Order only; quotes/rooms reassign via their
    opportunity)

Returns:



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
85
86
87
# File 'app/services/catalog/switch_itemizable_catalog.rb', line 55

def process(itemizable, target_catalog: nil, target_customer: nil)
  target_catalog ||= target_customer&.catalog
  raise ArgumentError, 'target_catalog or target_customer is required' if target_catalog.nil?
  # Fail fast rather than silently flip only the catalog while leaving the old
  # customer attached — Quote/RoomConfiguration reassign via their opportunity.
  raise ArgumentError, "#{itemizable.class.name} does not support target_customer reassignment" if target_customer && !itemizable.respond_to?(:customer=)

  return Result.new(messages: ['Already on the target catalog'], itemizable:) if !target_customer && on_target?(itemizable, target_catalog)

  errors = precondition_errors(itemizable, target_catalog)
  return Result.new(messages: errors, itemizable:) if errors.present?

  itemizable.transaction do
    itemizable.customer = target_customer if target_customer
    # strict_mode: raise (→ rollback) rather than silently drop a line with no
    # equivalent in the target catalog. Remapper sets the currency from the
    # target catalog and moves inventory commits to the new store items.
    CatalogItem::Remapper.new(itemizable, target_catalog:, strict_mode: true).fix_catalog
    # Remapper skips its own save! (and reload) when the itemizable is already
    # coherent — e.g. a same-catalog customer reassignment — so persist any
    # still-pending change (the customer swap) that would otherwise be lost.
    itemizable.save! if itemizable.changed?
  end

  Result.new(switched: true, messages: ["Switched #{itemizable.class.name} ##{itemizable.id} to catalog #{target_catalog.id}"], itemizable:)
rescue CatalogItem::Remapper::CatalogItemMissing, ActiveRecord::RecordInvalid => e
  log_warning("SwitchItemizableCatalog aborted for #{itemizable.class.name} ##{itemizable.id}: #{e.message}")
  # The transaction rolled back, but the in-memory object still holds the
  # attempted currency/customer/line changes — refresh it so callers see the
  # true (unchanged) state.
  itemizable.reload if itemizable.persisted?
  Result.new(switched: false, messages: Array(e.message), itemizable:)
end