Class: Order
- Inherits:
-
ApplicationRecord
- Object
- ActiveRecord::Base
- ApplicationRecord
- Order
- Includes:
- Memery, Models::Auditable, Models::CouponDateOverridable, Models::InventoryCommittable, Models::Itemizable, Models::LegacyRateRequest, Models::Lineage, Models::LiquidMethods, Models::MultiRoom, Models::Notable, Models::Pickable, Models::Profitable, Models::RmaTransmittable, Models::ShipQuotable, Models::SourceAttributable, Models::TaxableResource, Models::ValidationErrorReporting, AuthorizationSplittable, ExpressHoldAlerting, PgSearch::Model
- Defined in:
- app/models/order.rb
Overview
== Schema Information
Table name: orders
Database name: primary
id :integer not null, primary key
abandoned_cart_reminder :enum default("reminder_not_sent"), not null
actual_shipping_cost :decimal(10, 2)
allow_advance_credit_without_cc :boolean
amazon_checkout_session :string
attention_name_override :string(255)
bill_shipping_to_customer :boolean
billing_emails :string is an Array
bo_notification_sent_at :datetime
bulk_buy_request :boolean
cancellation_reason :string
cod_collection_type :string(255)
completion_date :date
credit_request_state :string
currency :string(255)
custom_order_agreement_status :enum default("custom_order_agreement_not_required"), not null
customer_reference :string(255)
disable_auto_coupon :boolean default(FALSE), not null
discount_adjustment_needed :boolean
do_not_reserve_stock :boolean default(FALSE), not null
early_label_metadata :jsonb
edi_auto_cancel_options :jsonb
edi_channel_order_support :string
edi_delayed_delivery_acknowledged_at :datetime
edi_is_pick_slip_required :boolean default(FALSE), not null
edi_orchestrator_partner :string
edi_order_date :date
edi_original_order_message :text
edi_original_ship_code :string
edi_po_number :string
edi_shipping_option_name :string
exclude_from_payment_check :boolean default(FALSE), not null
exported :boolean default(FALSE), not null
first_cart_view_date :datetime
first_review_request_processed_date :datetime
first_view_date :datetime
future_release_date :date
google_campaign_attribution :jsonb
google_conversion_meta :jsonb
hold_bo_release :boolean default(FALSE), not null
incorrectly_packaged_ups_canada_order :boolean
incorrectly_packaged_ups_canada_order_fixed :boolean
is_manual_hold :boolean default(FALSE), not null
jde_b_ab :integer
jde_s_ab :integer
label_instructions :string(255)
line_offset :decimal(10, 2)
line_total :decimal(10, 2)
ltl_freight :boolean
ltl_freight_guaranteed :boolean
manual_release_only :boolean
max_discount_override :decimal(4, 2)
min_profit_markup :integer default(0)
non_commissionable :boolean default(FALSE), not null
openai_ads_conversion_meta :jsonb not null
order_reception_type :string(255) default("CRM"), not null
order_type :string(255) default("SO")
override_coupon_date :date
override_coupon_date_without_limits :boolean default(FALSE), not null
override_line_lock :boolean default(FALSE), not null
packaged_items_md5_hash :string(255)
pickup :boolean
pinterest_conversion_meta :jsonb
precreate_rma :boolean default(FALSE), not null
price_match :decimal(10, 2)
pricing_program_description :string(255)
pricing_program_discount :integer
profit_total :decimal(10, 2)
purchase_label_early :boolean default(FALSE), not null
recalculate_discounts :boolean default(FALSE), not null
recalculate_shipping :boolean default(TRUE), not null
reference_number :string(255)
requested_deliver_by :date
requested_ship_before :date
requested_ship_on_or_after :date
retrieving_shipping_costs :boolean
return_label :boolean default(FALSE), not null
revenue_consolidated_at_time_of_checkout :decimal(10, 2)
review_request_processed :boolean default(FALSE), not null
review_snoozed_until :datetime
reviewed :boolean default(FALSE), not null
rma_reference :string(255)
sales_support_commission_date :date
saturday_delivery :boolean default(FALSE), not null
second_review_request_processed :boolean default(FALSE), not null
second_review_request_processed_date :datetime
shipment_instructions :text
shipment_reference_number :string
shipped_date :date
shipping_cost :decimal(10, 2)
shipping_cost_at_time_of_checkout :decimal(10, 2)
shipping_coupon :decimal(10, 2)
shipping_issue_alerted :boolean
shipping_method :string(255)
shipping_phone :string(255)
ships_economy :boolean default(FALSE), not null
signature_confirmation :boolean default(FALSE), not null
single_origin :boolean default(FALSE), not null
smartset_redemption_code :string(255)
sold_to_billing_address :integer
spiff_state :string(255)
state :string(255)
suggested_packaging_text :text
tax_date :date
tax_exempt :boolean default(FALSE), not null
tax_offset :decimal(10, 2)
tax_total :decimal(10, 2)
taxable_total :decimal(12, 2)
total :decimal(10, 2)
tracking_email :string(255) default([]), is an Array
transmission_email :string(255) default([]), is an Array
transmission_fax :string(255) default([]), is an Array
txid :uuid
uploads_count :integer default(0)
who_will_install :string
created_at :datetime
updated_at :datetime
buying_group_id :integer
contact_id :integer
creator_id :integer
customer_id :integer
default_credit_card_vault_id :integer
deleter_id :integer
edi_transaction_id :string
from_store_id :integer
local_sales_rep_id :integer
opportunity_id :integer
parent_id :integer
primary_sales_rep_id :integer
purchase_order_id :integer
quote_id :integer
resource_tax_rate_id :integer
rma_id :integer
sales_support_rep_id :integer
secondary_sales_rep_id :integer
shipping_account_number_id :integer
shipping_address_id :integer
source_id :integer
spiff_enrollment_id :integer
spiff_rep_id :integer
tax_exemption_id :integer
to_store_id :integer
tracking_event_id :uuid
updater_id :integer
visit_id :bigint
Indexes
idx_customer_id_order_type_created_at (customer_id,order_type,created_at)
idx_customer_id_order_type_state (customer_id,order_type,state)
idx_customer_id_review_snoozed_until (customer_id,review_snoozed_until)
idx_customer_id_state_shipped_date (customer_id,state,shipped_date)
idx_opportunity_id_state (opportunity_id,state)
idx_order_type_shipping_address_id (order_type,shipping_address_id)
idx_shipping_address_id_state (shipping_address_id,state)
index_orders_on_contact_id (contact_id) USING hash
index_orders_on_customer_id (customer_id) USING hash
index_orders_on_customer_reference (customer_reference) USING gin
index_orders_on_edi_po_number (edi_po_number) USING hash
index_orders_on_google_campaign_attribution (google_campaign_attribution) USING gin
index_orders_on_google_conversion_meta (google_conversion_meta) USING gin
index_orders_on_google_conversion_meta_result (((google_conversion_meta ->> 'result'::text)))
index_orders_on_opportunity_id (opportunity_id) USING hash
index_orders_on_packaged_items_md5_hash (packaged_items_md5_hash) USING hash
index_orders_on_parent_id (parent_id)
index_orders_on_purchase_order_id (purchase_order_id) USING hash
index_orders_on_quote_id (quote_id) USING hash
index_orders_on_reference_number (reference_number) UNIQUE
index_orders_on_rma_id (rma_id) USING hash
index_orders_on_sales_support_rep_id (sales_support_rep_id)
index_orders_on_shipped_date (shipped_date) USING brin
index_orders_on_shipping_address_id (shipping_address_id) USING hash
index_orders_on_smartset_redemption_code (smartset_redemption_code) WHERE (smartset_redemption_code IS NOT NULL) USING hash
index_orders_on_source_id (source_id) USING hash
index_orders_on_spiff_enrollment_id (spiff_enrollment_id) WHERE (spiff_enrollment_id IS NOT NULL) USING hash
index_orders_on_spiff_rep_id (spiff_rep_id) WHERE (spiff_rep_id IS NOT NULL) USING hash
index_orders_on_state (state) USING hash
index_orders_on_tax_exemption_id (tax_exemption_id) WHERE (tax_exemption_id IS NOT NULL) USING hash
index_orders_on_txid (txid) USING hash
index_orders_on_updated_at (updated_at) USING brin
index_orders_on_visit_id (visit_id) WHERE (visit_id IS NOT NULL) USING hash
orders_unique_edi_transaction_id_by_customer (customer_id,edi_transaction_id) UNIQUE
Foreign Keys
fk_rails_... (contact_id => parties.id) ON DELETE => nullify
fk_rails_... (parent_id => orders.id)
fk_rails_... (purchase_order_id => purchase_orders.id)
fk_rails_... (rma_id => rmas.id) ON DELETE => cascade
fk_rails_... (shipping_address_id => addresses.id)
fk_rails_... (source_id => sources.id)
fk_rails_... (visit_id => visits.id) ON DELETE => nullify
orders_tax_exemption_id_fk (tax_exemption_id => tax_exemptions.id)
Defined Under Namespace
Modules: AuthorizationSplittable, ExpressHoldAlerting Classes: AuthorizationSplitWorker, AuthorizationSplitter, AuthorizationTopupWorker, BackOrderClientNotification, ContactLookup, CrHoldReleaseReconciler, CreateVcProcurementOrdersFromCsv, DefaultTrackingEmailExtractor, FollowUpScheduler, FraudDetector, ItemNotFound, Mover, SendAbandonedCartEmails, SendExpressHoldAlerts, Splitter
Constant Summary collapse
- SALES_ORDER =
Sales order.
'SO'- MARKETING_ORDER =
Marketing order.
'MO'- TECH_ORDER =
Tech order.
'TO'- CREDIT_ORDER =
Credit order.
'CO'- STORE_TRANSFER =
Store transfer.
'ST'- SHIPPING_STATES =
Recognised shipping states.
%i[awaiting_deliveries processing_deliveries].freeze
- SOLD_STATES =
Recognised sold states.
%i[awaiting_deliveries processing_deliveries partially_invoiced invoiced].freeze
- OPEN_FOR_CHANGE_STATES =
Recognised open for change states.
%i[cart in_shipping_estimate pending pending_payment pending_release_authorization in_cr_hold needs_serial_number_reservation cancelled fraudulent awaiting_completed_installation_plans].freeze
- OPEN_FOR_TAX_CHANGE_STATES =
Recognised open for tax change states.
%i[cart pending pending_payment pending_release_authorization in_cr_hold needs_serial_number_reservation crm_back_order awaiting_completed_installation_plans].freeze
- CAN_CR_HOLD_STATES =
Recognised can cr hold states.
%i[pending pending_payment awaiting_deliveries processing_deliveries profit_review crm_back_order].freeze
- RELEASABLE_STATES =
Recognised releasable states.
%i[cart in_cr_hold request_carrier crm_back_order pending_release_authorization profit_review pending_payment awaiting_deliveries processing_deliveries needs_serial_number_reservation awaiting_completed_installation_plans].freeze
- CANCELABLE_STATES =
Recognised cancelable states.
%i[cart pending awaiting_return profit_review crm_back_order pending_release_authorization pending_payment in_cr_hold].freeze
- ROOM_PICKABLE_STATES =
Recognised room pickable states.
%i[crm_back_order fraudulent cancelled pending pending_release_authorization pending_payment in_cr_hold].freeze
- CLOSED_STATES =
Recognised closed states.
%i[shipped invoiced cancelled fraudulent].freeze
- LOCKED_STATES =
Recognised locked states.
%i[cancelled awaiting_deliveries processing_deliveries shipped partially_invoiced invoiced fraudulent].freeze
- CAN_REQUEST_PRE_PACK_STATES =
Recognised can request pre pack states.
%i[pending pending_payment profit_review in_cr_hold needs_serial_number_reservation].freeze
- LIVE_PAYMENT_STATES =
Payment states that still hold real money against the order — an authorization
hold, a capture, or a capture with a partial refund. A currency flip must not
orphan any of these in the outgoing currency (see #live_payments_currency_consistent). %w[authorized captured partially_refunded].freeze
- RESTRICTED_ORDER_TYPES =
Recognised restricted order types.
{ CREDIT_ORDER => 'Credit Order', STORE_TRANSFER => 'Store Transfer' }.freeze
- UNRESTRICTED_ORDER_TYPES =
Recognised unrestricted order types.
{ SALES_ORDER => 'Sales Order', MARKETING_ORDER => 'Marketing Order', TECH_ORDER => 'Tech Order' }.freeze
- ALL_ORDER_TYPES =
Recognised all order types.
RESTRICTED_ORDER_TYPES.merge(UNRESTRICTED_ORDER_TYPES)
- REFERENCE_NUMBER_PATTERN =
Regex pattern matching reference number.
Regexp.new("^(#{ALL_ORDER_TYPES.keys.join('|')})(\\d+)$", Regexp::IGNORECASE)
- ALL_STATES =
Recognised all states.
Order.state_machines[:state].states.map(&:name).freeze
- NON_CART_STATES =
Recognised non cart states.
ALL_STATES - [:cart]
- SAME_DAY_CUTOFF_HOUR =
Customer-facing same-day-shipping cutoff, in warehouse-local hours
(noon CT for the US warehouse, noon ET for the CA warehouse). The
actual carrier pickup cutoffs in Store::CARRIER_PICKUP_CUT_OFF_TIMES
run later; noon is the conservative customer promise. 12- EARLY_LABEL_TRANSIENT_ERROR_PATTERN =
Marketplace error codes/messages that are transient (server-side, self-resolving) rather
than a bad request on our end. Amazon Buy Shipping'sInternalFailureand similar 5xx-style
faults routinely clear within seconds — the normal (non-early) label purchase at ship-confirm
time succeeds without any human action if we just try again, so retrying here avoids paging
the warehouse over a blip that fixes itself. /InternalFailure|InternalServerError|ServiceUnavailable|RequestThrottled|Timeout/i- EARLY_LABEL_CREATE_LABEL_MAX_ATTEMPTS =
Total attempts (including the first) before giving up and sending the failure notification
3- EARLY_LABEL_CREATE_LABEL_RETRY_DELAY_SECONDS =
Delay between retry attempts
2- EARLY_LABEL_RAPID_VOID_THRESHOLD_MINUTES =
Minimum time (in minutes) that must pass after early label purchase before auto-void is allowed
This prevents the race condition where label is voided before PDF is fully processed 3- AMAZON_OVER_FULFILL_PATTERN =
Regex pattern matching amazon over fulfill.
/Attempting to over-fulfill item\(s\)[\s\S]*?Existing shipmentIds for order:\[([^\]]+)\]/i
Constants included from Models::InventoryCommittable
Models::InventoryCommittable::STATES_WITH_NO_EXPIRATION
Constants included from Models::Auditable
Models::Auditable::ALWAYS_IGNORED
Constants included from Models::Schedulable
Models::Schedulable::SIMPLE_FORM_OPTIONS
Instance Attribute Summary collapse
-
#customer_id ⇒ Object
readonly
Customer can only have one cart per Contact.
-
#do_not_detect_shipping ⇒ Object
Returns the value of attribute do_not_detect_shipping.
-
#do_not_set_totals ⇒ Object
Returns the value of attribute do_not_set_totals.
-
#early_label_purchase_result ⇒ Object
Returns the value of attribute early_label_purchase_result.
-
#from_store_id ⇒ void
readonly
Validates from store id, to store id, create.
-
#full_shipping_address_validation ⇒ Object
Returns the value of attribute full_shipping_address_validation.
-
#is_www ⇒ Object
Returns the value of attribute is_www.
-
#is_www_ship_by_zip ⇒ Object
Returns the value of attribute is_www_ship_by_zip.
-
#max_discount_override ⇒ void
readonly
Validates max discount override.
-
#order_type ⇒ void
readonly
Validates order type.
-
#reference_number ⇒ void
readonly
Validates reference number, cart or in shipping estimate.
-
#shipping_phone ⇒ Object
readonly
skip this validation for EDI orders, let it ride.
-
#to_store_id ⇒ void
readonly
Validates from store id, to store id, create.
-
#tracking_email ⇒ void
readonly
Validates tracking email.
-
#update_shipping_address_with_contact ⇒ Object
Returns the value of attribute update_shipping_address_with_contact.
Attributes included from Models::Profitable
Attributes included from Models::Itemizable
#account_specialist, #coupons, #discounts, #force_total_reset, #total_reset
Belongs to collapse
-
#buying_group ⇒ BuyingGroup?
The associated buying group.
-
#contact ⇒ Contact?
The associated contact.
-
#customer ⇒ Customer?
The associated customer.
-
#default_credit_card_vault ⇒ CreditCardVault?
The associated default credit card vault.
-
#from_store ⇒ Store?
The associated from store.
-
#invoiced_local_sales_rep ⇒ Party?
The associated invoiced local sales rep.
-
#invoiced_primary_sales_rep ⇒ Party?
The associated invoiced primary sales rep.
-
#invoiced_secondary_sales_rep ⇒ Party?
The associated invoiced secondary sales rep.
-
#opportunity ⇒ Opportunity?
The associated opportunity.
-
#original_source ⇒ Source?
The associated original source.
-
#purchase_order ⇒ PurchaseOrder?
The associated purchase order.
-
#quote ⇒ Quote?
The associated quote.
-
#rma ⇒ Rma?
The associated rma.
-
#sales_support_rep ⇒ Employee?
The associated sales support rep.
-
#shipping_account_number ⇒ ShippingAccountNumber
DELIVERY REFACTOR HERE.
-
#shipping_address ⇒ Address?
The associated shipping address.
-
#sold_to_billing_address ⇒ Address?
The associated sold to billing address.
-
#source ⇒ Source?
The associated source.
-
#spiff_enrollment ⇒ SpiffEnrollment?
The associated spiff enrollment.
-
#spiff_rep ⇒ Contact?
The associated spiff rep.
-
#to_store ⇒ Store?
The associated to store.
-
#visit ⇒ Visit?
The associated visit.
Methods included from Models::Auditable
Methods included from Models::TaxableResource
Has one collapse
-
#credit_memo ⇒ CreditMemo?
The associated credit memo.
-
#technical_support_rep ⇒ TechnicalSupportRep?
The associated technical support rep.
Has many collapse
-
#activities ⇒ ActiveRecord::Associations::CollectionProxy
The associated activities.
-
#communications ⇒ ActiveRecord::Associations::CollectionProxy
The associated communications.
-
#credit_memos ⇒ ActiveRecord::Associations::CollectionProxy
The associated credit memos.
-
#deliveries ⇒ ActiveRecord::Associations::CollectionProxy
The associated deliveries.
-
#delivery_activities ⇒ ActiveRecord::Associations::CollectionProxy
The associated delivery activities.
-
#direct_shipments ⇒ ActiveRecord::Associations::CollectionProxy
The associated direct shipments.
-
#drop_ship_purchase_orders ⇒ ActiveRecord::Associations::CollectionProxy
The associated drop ship purchase orders.
-
#edi_communication_logs ⇒ ActiveRecord::Associations::CollectionProxy
The associated edi communication logs.
-
#edi_documents ⇒ ActiveRecord::Associations::CollectionProxy
The associated edi documents.
-
#invoices ⇒ ActiveRecord::Associations::CollectionProxy
The associated invoices.
-
#linked_support_cases ⇒ ActiveRecord::Associations::CollectionProxy
The associated linked support cases.
-
#material_alerts ⇒ ActiveRecord::Associations::CollectionProxy
The associated material alerts.
-
#messaging_logs ⇒ ActiveRecord::Associations::CollectionProxy
The associated messaging logs.
-
#payments ⇒ ActiveRecord::Associations::CollectionProxy
The associated payments.
-
#preset_jobs ⇒ ActiveRecord::Associations::CollectionProxy
The associated preset jobs.
-
#rma_items ⇒ ActiveRecord::Associations::CollectionProxy
The associated rma items.
-
#rmas ⇒ ActiveRecord::Associations::CollectionProxy
The associated rmas.
-
#shipments ⇒ ActiveRecord::Associations::CollectionProxy
The associated shipments.
-
#uploads ⇒ ActiveRecord::Associations::CollectionProxy
The associated uploads.
-
#vouchers ⇒ ActiveRecord::Associations::CollectionProxy
The associated vouchers.
-
#warranties ⇒ ActiveRecord::Relation<Warranty>
Warranty registrations against this order (kept if the order is ever hard-deleted — the registration is the customer's record, not ours).
Methods included from Models::Pickable
Has and belongs to many collapse
-
#support_cases ⇒ ActiveRecord::Associations::CollectionProxy
The associated support cases.
Methods included from Models::MultiRoom
Delegated Instance Attributes collapse
-
#billing_address ⇒ Object
Alias for Customer#billing_address.
-
#billing_entity ⇒ Object
Alias for Customer#billing_entity.
-
#catalog ⇒ Object
Alias for Customer#catalog.
-
#company ⇒ Object
Alias for Customer#company.
-
#include_po_prefix? ⇒ Object
Alias for Customer#include_po_prefix?.
-
#is_amazon_com? ⇒ Object
Alias for Customer#is_amazon_com?.
-
#is_amazon_seller_central? ⇒ Object
Alias for Customer#is_amazon_seller_central?.
-
#is_canadian_tire? ⇒ Object
Alias for Customer#is_canadian_tire?.
-
#is_costco_ca? ⇒ Object
Alias for Customer#is_costco_ca?.
-
#is_home_depot_can? ⇒ Object
Alias for Customer#is_home_depot_can?.
-
#is_home_depot_usa? ⇒ Object
Alias for Customer#is_home_depot_usa?.
-
#is_houzz? ⇒ Object
Alias for Customer#is_houzz?.
-
#is_part_of_lowes_com? ⇒ Object
Alias for Customer#is_part_of_lowes_com?.
-
#is_part_of_rona? ⇒ Object
Alias for Customer#is_part_of_rona?.
-
#is_walmart_ca? ⇒ Object
Alias for Customer#is_walmart_ca?.
-
#is_wasn4_or_wat0f? ⇒ Object
Alias for Customer#is_wasn4_or_wat0f?.
-
#store_id ⇒ Object
Alias for Store#id.
Class Method Summary collapse
-
.active ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are active.
-
.active_spiffs ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are active spiffs.
-
.all_awaiting_deliveries ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are all awaiting deliveries.
-
.all_order_types_for_select ⇒ Array<Array(String, String)>
Every order-type code the system understands, for filter dropdowns in admin search.
-
.assigned_to_rep ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are assigned to rep.
-
.awaiting_completed_installation_plans ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are awaiting completed installation plans.
-
.awaiting_payment_spiff ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are awaiting payment spiff.
-
.back_order ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are back order.
-
.by_company_id ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are by company id.
-
.by_primary_rep_id ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are by primary rep id.
-
.by_report_grouping ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are by report grouping.
-
.by_report_grouping_all_when_nil ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are by report grouping all when nil.
-
.by_sales_rep_id ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are by sales rep id.
-
.by_store ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are by store.
-
.by_store_id ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are by store id.
-
.cancelled ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are cancelled.
-
.carts ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are carts.
-
.co_only ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are co only.
-
.contains_coupon_ids ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are contains coupon ids.
-
.contains_item_ids ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are contains item ids.
-
.contains_service_items ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are contains service items.
-
.correctly_packaged ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are correctly packaged.
-
.custom_order_agreement_statuses_for_select ⇒ Array<Array(String, String)>
[label, value]pairs of every Custom-Order-Agreement status for the CRM dropdown. -
.customer_reference_search ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are customer reference search.
-
.default_source_for(customer:, opportunity: nil) ⇒ Source?
Default source for a CRM-created order, per the attribution rule decided 2026-07-23 (see doc/tasks/202607231507_SOURCE_ATTRIBUTION_MODEL_RESTRUCTURE.md): an order converted from an opportunity inherits the opportunity's source (the closing call/email is influence, not acquisition); a standalone order stamps from evidence — the customer's recent attributable visit first, the account default last.
-
.draft_spiff ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are draft spiff.
-
.edi_orders ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are edi orders.
-
.future_release ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are future release.
-
.google_conversion_acknowledged ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are google conversion acknowledged.
-
.google_conversion_attempted ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are google conversion attempted.
-
.has_manual_preset_form ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are has manual preset form.
-
.held ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are held.
-
.held_sales_orders ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are held sales orders.
-
.in_progress ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are in progress.
-
.in_state ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are in state.
-
.incorrectly_packaged_ups_canada_order ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are incorrectly packaged ups canada order.
-
.invoiced ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are invoiced.
-
.like_lookup ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are like lookup.
-
.limit_to_fba ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are limit to fba.
-
.locked ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are locked.
-
.lookup ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are lookup.
-
.mo_only ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are mo only.
-
.most_recent_first ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are most recent first.
-
.non_carts ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are non carts.
-
.non_credit ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are non credit.
-
.not_cancelled ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are not cancelled.
-
.not_in_pre_pack ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are not in pre pack.
-
.not_open_for_change ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are not open for change.
-
.not_partially_invoiced ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are not partially invoiced.
-
.not_processing_deliveries ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are not processing deliveries.
-
.not_sold ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are not sold.
-
.open_for_change ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are open for change.
-
.open_for_tax_update ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are open for tax update.
-
.order_count(company_id = nil, where_conditions = nil, where_not_conditions = nil) ⇒ Integer
Counts orders matching the given filters.
-
.order_type_from_opportunity(opportunity) ⇒ String
Derive an order_type code from an Opportunity's type by appending
'O'(soS+O='SO'sales order,T+O='TO'tech order). -
.order_type_from_quote(quote) ⇒ String
Map a Quote type code to the matching order_type code: a tech quote (
TQ) becomes a tech order (TO), a marketing quote (MQ) becomes a marketing order (MO), etc. -
.overdue_to_ship ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are overdue to ship.
-
.paid_spiff ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are paid spiff.
-
.pending ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are pending.
-
.pending_payment ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are pending payment.
-
.pending_payment_and_unpaid_invoices ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are pending payment and unpaid invoices.
-
.po_number_barcode(po_number:, file_path: nil) ⇒ String?
Class-method form of #po_number_barcode: render an arbitrary PO number as a Code-128 barcode PNG.
-
.positive_value ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are positive value.
-
.profit_review ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are profit review.
-
.quick_stats_shipping ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are quick stats shipping.
-
.quick_stats_sold ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are quick stats sold.
-
.reception_type_for_select ⇒ Hash{String => String}
Memoised
{label => value}map for the order-reception-type filter ("Online" vs "CRM"). -
.returnable_types ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are returnable types.
-
.room_not_pickable ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are room not pickable.
-
.sales_orders ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are sales orders.
-
.selectable_order_types ⇒ Array<Array(String, String)>
Order-type codes a user is allowed to choose from in the new- order form.
-
.so_only ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are so only.
-
.sold ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are sold.
-
.st_only ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are st only.
-
.states_for_select ⇒ Array<Array(String, String)>
[human_name, machine_value]pairs of every order state, sorted alphabetically by display name. -
.with_amazon_payments ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are with amazon payments.
-
.with_associations ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are with associations.
-
.with_line_items ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are with line items.
-
.with_payments ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are with payments.
-
.with_review ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are with review.
-
.without_review ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are without review.
Instance Method Summary collapse
-
#accounting_hold_order? ⇒ Object
Whether accounting hold order.
-
#add_customer_to_campaign ⇒ Object
After-save hook: when an order's source changes to a source that's tied to a marketing campaign, enroll the customer in that campaign so subsequent campaign emails reach them.
-
#add_item(sku, qty) ⇒ Array<Hash>?
Convenience for #add_multiple_items with a single sku/qty pair.
-
#add_multiple_items(sku_array = []) ⇒ Array<Hash>
Add a batch of SKUs to the order in one save.
-
#adjusted_actual_shipping_cost ⇒ Float
Like #calculate_actual_shipping_cost but applies per-delivery accounting adjustments (recoveries, fuel surcharge corrections).
-
#all_deliveries_cancelable?(current_user = nil) ⇒ Object
Whether all deliveries cancelable.
-
#all_deliveries_invoiced? ⇒ Object
Whether all deliveries invoiced.
-
#all_funds_available?(ignore_cod = false) ⇒ Object
Whether all funds available.
-
#all_funds_not_available_and_shippable? ⇒ Object
protected
Whether all funds not available and shippable.
-
#all_items_in_stock ⇒ Boolean
True when every line item has enough on-hand inventory at its delivery's warehouse to fulfil immediately.
-
#all_participant_ids ⇒ Array<Integer>
Every party that should see this order in their CRM activity feed: all opportunity participants plus the customer and contact.
-
#all_payments_are_valid? ⇒ Object
Whether all payments are valid.
-
#all_rooms_not_orderable? ⇒ Object
Whether all rooms not orderable.
-
#all_support_cases ⇒ Array<Integer>
Convenience for
support_case_ids. -
#all_uploads ⇒ ActiveRecord::Relation<Upload>
Every Upload attached anywhere in the order tree — order itself, its deliveries, and the shipments under those deliveries.
-
#allows_edi_split? ⇒ Object
Whether allows edi split.
-
#already_has_smartinstall_request? ⇒ Object
Whether already has smartinstall request.
-
#amazon_buy_shipping_eligible? ⇒ Boolean
Check if order is eligible for Amazon Buy Shipping early label purchase.
-
#amzbs_packing_slip_included? ⇒ Boolean
Amazon Buy Shipping label PDFs include a packing slip page, so no separate upload is needed when an AMZBS shipping option is selected.
-
#any_rooms_not_orderable? ⇒ Object
Whether any rooms not orderable.
-
#applies_for_smartinstall ⇒ Boolean
Eligibility check for the SmartInstall service add-on (paid in-home installation).
-
#apply_tier2_pricing? ⇒ Boolean
Whether or not to apply the tier2 pricing (customer discount) by default.
-
#attention_name ⇒ String?
"ATTN: …" line written on shipping labels.
-
#attention_name=(value) ⇒ Object
Setter that stores the override unless the caller is sending the literal sentinel string
"inherited_attention_name"(used by the CRM form to mean "go back to the inherited value"), in which case we leave the override untouched. -
#auto_reserve_serial_numbers ⇒ Object
Walks every line item that requires serial-number reservation (heating mats, cables) and asks the line to grab the next available serial-number block from inventory.
-
#awaiting_future_deliveries? ⇒ Object
Whether awaiting future deliveries.
-
#balance(ignore_cod = false, excluding_payment: nil) ⇒ BigDecimal
Outstanding amount on the order: total of all deliveries minus whatever payments have been authorised against each delivery (capped per delivery so over-authorisation on one delivery doesn't cancel out a balance owed on another).
-
#belongs_to_smartservice_group? ⇒ Object
Whether belongs to smartservice group.
-
#build_activity ⇒ Activity
Builds (but does not save) a new Activity record attached to this order with the order's primary party already populated.
-
#build_early_label_carrier_info(label_result, delivery = nil) ⇒ Hash
Build carrier info hash for early label EDI confirm (Walmart only).
-
#build_early_label_tracking_url(carrier, tracking_number) ⇒ String?
Build tracking URL for early label.
-
#calculate_actual_shipping_cost ⇒ Float
Sum of actual carrier-billed shipping cost on every invoiced delivery (i.e. what we paid the carrier, not what the customer was charged).
-
#can_auto_reserve_serial_numbers? ⇒ Object
Whether can auto reserve serial numbers.
-
#can_be_cancelled? ⇒ Object
Whether can be cancelled.
-
#can_be_returned? ⇒ Object
Whether can be returned.
-
#can_cr_hold?(current_user = nil) ⇒ Object
Whether can cr hold.
-
#can_edit_future_release_date? ⇒ Object
Whether can edit future release date.
-
#cancel_deliveries_pre_pack ⇒ Object
Cancels the in-progress packaging-estimate work on every
pre_packdelivery. -
#cancel_or_destroy ⇒ Object
Carts get destroyed (no audit trail to keep).
-
#cancelable? ⇒ Object
Whether cancelable.
-
#cannot_delete_reason(account = nil) ⇒ String?
Human-readable explanation of why an order cannot currently be deleted, or
nilwhen #ok_to_delete? would let it through. -
#cart_identifier ⇒ String
Stable identifier suitable for URLs and abandoned-cart emails: the reference number once the order has one, otherwise a
SC<id>("Shopping Cart") fallback so brand-new carts still have a slug. -
#cart_or_in_shipping_estimate? ⇒ Object
Whether cart or in shipping estimate.
-
#check_payments_status ⇒ void
Reconcile the order's payments with the upstream gateways (Stripe, PayPal).
-
#check_sales_rep ⇒ Object
protected
Validation callback that prevents an order from listing the same rep as both primary and secondary, and from setting a secondary rep without a primary.
-
#ci_invoice_credit_order? ⇒ Object
Whether ci invoice credit order.
-
#clear_shipped_date ⇒ Object
Resets the
shipped_datecolumn when an order needs to be reverted to a pre-shipped state (e.g. cancellation after a partial ship). -
#closed_state? ⇒ Object
Whether closed state.
-
#company_review_url ⇒ Object
Reviews.io dynamic link for company review using the order reference as order_id and the customer CN number as customer_identifier (for CRM context).
-
#company_review_url_for_confirmation(email: nil) ⇒ Object
Reviews.io dynamic link for the order confirmation / thank-you page.
-
#complete? ⇒ Object
Whether complete.
-
#completed_regular_deliveries ⇒ Array<Delivery>
Active deliveries that have actually shipped (parcel or freight) vs ones that completed in some other way (warehouse pickup, service-only).
-
#completely_shipped? ⇒ Object
Whether completely shipped.
-
#consolidate_revenue ⇒ Object
Snapshot the order's total and shipping cost at the moment the customer completed checkout.
-
#contact_combo ⇒ Object
Used by dynamic contact lookup on order creation allowing interaction with the tom-select input on account_managers.html.erb Contact can either be existing (single integer value for contact_id) or new contact.
-
#contact_combo=(val) ⇒ Object
Used by dynamic contact lookup on order creation allowing interaction with the tom-select input on account_managers.html.erb Contact can either be existing (single integer value for contact_id) or new contact if val is in format Customer|customer_id|full_name.
-
#contact_combo_for_select ⇒ Array<Array(String, String)>
Active contacts under this order's customer formatted for a Rails
selecthelper, with each value encoded as"Contact|<id>"so the CRM "contact" field can distinguish between picking an existing contact and typing a new name. -
#copy_customer_reps ⇒ Object
In-memory mirror of #copy_invoice_reps that pulls reps from the current customer's reps_collaboration setup.
-
#copy_invoice_reps ⇒ Object
Pulls primary / secondary / local sales-rep ids off the order's invoice and writes them onto the order itself.
-
#copy_items_from(order) ⇒ Boolean
Copy non-shipping line items from another order (typically a guest cart) into this one, then destroy the source if it is itself a cart.
-
#copy_shipping_reference_number_to_deliveries ⇒ Object
After-save callback: propagate the order's
shipment_reference_number(used by Amazon FBA shipments and by marketplace seller programs as the carrier BOL) onto every delivery. -
#country ⇒ Country?
The Country that owns this order's billing entity (resolved through
customer → catalog → store → country). -
#create_credit_memo ⇒ void
Spawn a credit-memo invoice off this order (e.g. when the order is being voided after invoicing) and run it through TaxJar to back out any tax already reported.
-
#create_smartfix_ticket ⇒ Object
Open a SmartFix service ticket (paid repair visit) and schedule the service-confirmation activity.
-
#create_smartguide_ticket ⇒ Object
Open a SmartGuide service ticket (consultation).
-
#create_smartinstall_ticket ⇒ Object
Open a SmartInstall service ticket against this order via the shared #new_ss_ticket helper and schedule the kickoff pre-installation phone activity.
-
#crm_link ⇒ String
CRM-facing path to this order's show page (relative URL, suitable for use in CRM emails, Slack notifications, and audit logs).
-
#currency_symbol ⇒ String
Currency symbol (
$,€,£,C$, …) for the order's currency, resolved through themoneygem's locale-aware Currency table. -
#custom_shipping_labels ⇒ Object
Pulls all custom ship labels from the order, deliveries and shipments.
-
#customer_qualifies_for_free_online_shipping? ⇒ Object
Whether customer qualifies for free online shipping.
-
#deep_dup ⇒ Order
Deep-clones the order (line items, discounts, EDI documents) but resets the identity / lifecycle columns so the duplicate can be saved as a fresh order.
-
#default_billing_emails ⇒ Array<String>
Default email recipients for billing notifications: every email contact point flagged as a billing notification channel for the customer (or their billing entity), plus the tracking email, contact emails, and any per-order overrides on
billing_emails. -
#deferred_payments_captured_and_ready_for_shipping? ⇒ Object
Whether deferred payments captured and ready for shipping.
-
#deletable? ⇒ Object
Whether deletable.
-
#doesnt_already_has_smartinstall? ⇒ Object
Whether doesnt already has smartinstall.
-
#download_and_store_early_label_pdf_atomic(shipper, label_result, is_amazon: false) ⇒ Upload?
SAFEGUARD A: Atomic PDF download and storage Downloads and stores the label PDF with retries, ensuring it's persisted before returning.
-
#early_label_failure_notification(error_message) ⇒ Object
Send EDI admin notification when early label purchase fails This ensures failures are not silent and the team is notified.
-
#early_label_flash_message ⇒ Hash
Generate flash messages based on early label purchase result Called by controllers after order transitions to awaiting_deliveries.
-
#early_label_purchase_enabled_for_partner? ⇒ Boolean
Checks whether the partner's orchestrator has early label purchase enabled.
-
#early_label_purchased_recently? ⇒ Boolean
Check if early label was purchased recently (within the rapid void threshold) Used by SAFEGUARD B to prevent race conditions.
-
#early_label_shipments_match?(delivery) ⇒ Hash
Check if current shipments match the early label shipments Used to detect if warehouse staff changed the packing.
-
#early_label_upload ⇒ Upload?
Get the early label upload (PDF) if it exists.
-
#echecks_requiring_authorization ⇒ Array<Payment>
eCheck payments on this order that the fraud-review pipeline has flagged as needing accounting review before the order can leave CR hold.
-
#edi_cancellation_reason_description ⇒ String
Customer-facing description of the EDI cancellation reason on the order.
-
#edi_cancellation_reasons ⇒ Array<Array(String, String)>
Allowed cancellation-reason codes for the order's EDI partner, formatted as
[[label, code], …]for a Rails select. -
#edi_force_price_match ⇒ Array<Array<String>>
Force every parent line item with an EDI unit cost to match it on both the MSRP price and the discounted price.
-
#edi_orchestrator ⇒ Edi::BaseOrchestrator?
The Edi::BaseOrchestrator subclass responsible for this order's EDI partner, if it has one.
-
#edi_price_matcheable? ⇒ Object
Whether edi price matcheable.
-
#editing_locked? ⇒ Object
Whether editing locked.
-
#effective_date_for_coupon ⇒ Date
Date used by the coupon engine to evaluate "is this coupon currently valid?" against
start_date/end_datewindows. -
#email_for_order_confirmation ⇒ String?
First non-blank email for the order's confirmation flow, checking (in order): the contact/customer's stored email, then the customer's email, then the contact's account email, then the customer's account email.
-
#email_options_for_tracking_email ⇒ Array<String>
Choices for the "tracking emails" multi-select on the order form: every email the customer has on file, plus whatever's currently set on
tracking_email(which may include free-typed addresses not yet on the customer record). -
#empty? ⇒ Object
Whether empty.
-
#encrypted_id ⇒ String
Reversibly encrypted form of the order's database id, used in public-facing URLs (payment link, cart-recovery emails) so the id doesn't appear directly in logs or share links.
-
#errors_with_deliveries_errors ⇒ Array<String>
Combined errors from the order itself and from each of its deliveries, suitable for surfacing to the CRM "save failed" banner so users see every reason at once.
-
#estimate_next_available_date_from_out_of_stock_items ⇒ Date?
Latest "next available" date across every fully out-of-stock line item (or nil if all items have stock).
-
#find_gbraid ⇒ String?
Same fallback chain as #find_gclid for the Android-app web-to-app
gbraidtoken used by Google Ads. -
#find_gclid ⇒ String?
Best-effort Google Click ID for this order.
-
#find_msclkid ⇒ String?
Microsoft Ads (Bing) click identifier, captured by the Tracker into
Visit#marketing_meta['msclkid'](JSONB-only). -
#find_oppref ⇒ String?
OpenAI Ads (ChatGPT) click identifier, captured by the Tracker into
Visit#marketing_meta['oppref'](JSONB-only — no scalar column). -
#find_wbraid ⇒ String?
Same fallback chain as #find_gclid for the iOS-app web-to-app
wbraidtoken used by Google Ads. -
#fire_early_label_edi_confirm(delivery, label_result) ⇒ Object
Fire EDI ship confirm with early label tracking info.
-
#fire_early_label_edi_confirm_amazon(delivery, label_result) ⇒ Object
Amazon early label ship confirm — mirrors Edi::Amazon::ConfirmMessageProcessor#acknowledge_order but builds the message from order data since no shipment record exists yet.
-
#fire_early_label_edi_confirm_walmart(delivery, label_result) ⇒ Object
Walmart early label ship confirm — original Walmart-specific flow.
-
#first_po_number ⇒ String?
First non-blank PO number across the order's payments.
-
#first_tracking_email ⇒ String?
Single-string variant of #tracking_email (which is an array column).
-
#fix_future_release_date ⇒ Hash
Reconciles
requested_ship_on_or_after,future_release_date, andrequested_ship_beforeso they don't contradict each other: bumpsfuture_release_dateforward to honour the ship-on-or-after date, and clearsrequested_ship_beforewhen the user explicitly sets a future-release date past it. -
#formatted_po_number ⇒ String
po_numberwith the customer's preferred prefix applied (e.g."PO# 12345"vs bare"12345"), driven bycustomer.include_po_prefix?. -
#fully_funded_by_advance_replacement? ⇒ Object
Whether fully funded by advance replacement.
-
#funded_by_advance_replacement? ⇒ Object
Whether funded by advance replacement.
-
#funded_by_cod? ⇒ Object
Whether funded by cod.
-
#funds_available_and_ready_for_warehouse? ⇒ Object
protected
Whether funds available and ready for warehouse.
-
#funds_shortfall_report(excluding_payment: nil) ⇒ String
Human-readable, per-delivery breakdown of why #all_funds_available? is false: each under-covered delivery's total vs.
-
#generate_spiff_training_activity ⇒ Activity
Schedules a 7-day-out follow-up training activity for the primary sales rep so they walk a SPIFF-eligible customer through their first WarmlyYours order.
-
#get_expected_ship_date_time ⇒ Time
Estimated delivery time = scheduled ship time + carrier-committed transit days from the selected shipping option.
-
#get_scheduled_ship_date_time ⇒ Time
When the order is expected to be picked up by the carrier.
-
#has_authorized_payment? ⇒ Object
Whether has authorized payment.
-
#has_committed_serial_number_reservations? ⇒ Object
Whether has committed serial number reservations.
-
#has_custom_packing_slip? ⇒ Object
Whether has custom packing slip.
-
#has_early_purchased_label? ⇒ Boolean
Check if order has an active (non-voided) early-purchased label.
-
#has_incomplete_reservations? ⇒ Object
Whether has incomplete reservations.
-
#has_selected_heated_items? ⇒ Object
Whether has selected heated items.
-
#has_shipping_method? ⇒ Object
Whether has shipping method.
-
#has_unreserved_line_items? ⇒ Object
Whether has unreserved line items.
-
#has_web_rooms_needing_installation_plans? ⇒ Object
Whether has web rooms needing installation plans.
-
#hold_for_early_label_mismatch!(_delivery, reason) ⇒ Object
Puts the order on CR hold with a descriptive note when early label purchase cannot proceed because the selected shipping rate doesn't match available marketplace rates.
-
#hold_order_reasons ⇒ Array<String>
Human-readable list of reasons this order is currently being held by the CR/fraud/EDI pipeline — every line item the CRM hold-page surfaces to the user.
-
#hold_orders? ⇒ Object
Whether hold orders.
-
#human_state_name ⇒ String
Human-friendly version of the state machine state.
-
#includes_schedulable_service? ⇒ Object
Whether includes schedulable service.
-
#inherited_attention_name ⇒ String?
Default attention-line value derived from the shipping address's person name, but only when the customer is a person (not a company); otherwise the company name should be used as the line 1 name and ATTN can stay blank.
-
#installation_country_iso ⇒ String?
ISO 2-letter country code of the installation site (US, CA, …).
-
#installation_country_iso3 ⇒ String?
ISO 3-letter country code of the installation site (USA, CAN, …).
-
#installation_is_within_range? ⇒ Object
Whether installation is within range.
-
#installation_postal_code ⇒ String?
Postal code where the order will be installed (from the linked opportunity's installation address, not the shipping address).
-
#installation_state_code ⇒ String?
State/province code of the installation site (see #installation_postal_code).
-
#invoice_balance ⇒ BigDecimal
Sum of Invoice#balance across every invoice on the order — the outstanding A/R amount.
-
#is_crm? ⇒ Object
Whether is crm.
-
#is_edi_order? ⇒ Object
Whether is edi order.
-
#is_fba? ⇒ Object
Whether is fba.
-
#is_from_myprojects? ⇒ Object
Whether is from myprojects.
-
#is_marketing_order? ⇒ Object
Whether is marketing order.
-
#is_online? ⇒ Object
Whether is online.
-
#is_price_editable? ⇒ Boolean
Tells whether or not the order can have its line item discounted and msrp price editable by the price editable concerns, this method is called by the ability check first.
-
#is_regular_order? ⇒ Object
Whether is regular order.
-
#is_remote_service? ⇒ Object
Whether is remote service.
-
#is_rma_replacement? ⇒ Object
Whether is rma replacement.
-
#is_rma_return? ⇒ Object
Whether is rma return.
-
#is_sales_order? ⇒ Object
Whether is sales order.
-
#is_smartfit_service? ⇒ Object
Whether is smartfit service.
-
#is_smartfix_service? ⇒ Object
Whether is smartfix service.
-
#is_smartguide_service? ⇒ Object
Whether is smartguide service.
-
#is_smartinstall_service? ⇒ Object
Whether is smartinstall service.
-
#is_store_transfer? ⇒ Object
Whether is store transfer.
-
#is_subject_to_minimum_qty_rules? ⇒ Object
Whether is subject to minimum qty rules.
-
#is_tech_order? ⇒ Object
Whether is tech order.
-
#is_warehouse_pickup? ⇒ Object
Whether is warehouse pickup.
-
#job_name ⇒ String?
Best-known job name for this order: the originating quote's opportunity name, falling back to the order's own opportunity name, or
nilif the order isn't linked to either. -
#live_payments_currency_consistent ⇒ void
protected
Blocks flipping an order's currency while it still holds a live payment (authorized OR captured) in the outgoing currency.
-
#local_sales_rep ⇒ Employee?
Local (on-site) sales rep on the order.
-
#log_early_label_event(event_type, message, details = {}) ⇒ Object
Log an event to the early label API log Used for debugging and visibility into what happened during early label purchase.
-
#mark_serial_coupons_as_used ⇒ Object
Burn any single-use coupon serial numbers that were applied to this order so they can't be redeemed again.
-
#marketplace_early_label_eligible? ⇒ Boolean
Check if order is eligible for marketplace early label purchase (Walmart SWW or Amazon Buy Shipping).
-
#merge_shipper_api_log(shipper) ⇒ Object
Merge raw HTTP API call logs from the shipper into the order's API log.
-
#minimum_order_quantity_violations ⇒ Array<Alert>
MOQ (minimum-order-quantity) alerts triggered by the current line items — e.g.
-
#move_deliveries_from_quoting! ⇒ Object
Promote any deliveries currently in
quoting(rate-shopping) state intoready_to_ship, copying the order-level shipment / label / future-release fields onto each delivery first. -
#move_deliveries_to_quoting! ⇒ Object
Force every delivery on the order back into the
quotingstate so the rate-shop can be re-run from scratch (e.g. after a shipping address change). -
#move_service_case_from_pending_service_payment ⇒ Object
When a SmartService order's payment finally clears, flip the associated SmartService support case out of
pending_service_paymentand into thecase_openstate so the service team can pick it up. -
#name ⇒ String
Human label like
"Order #SO123456 (PO# 0042)"(or withPO#sfor multi-PO orders). -
#need_to_recalculate_shipping ⇒ Object
Flag the order so the next save re-runs the carrier rate-shop and re-detects shipping options.
-
#needs_attention_issues ⇒ Array<String>
Human-readable list of mismatches between the order's state and its deliveries' states (e.g. order is
awaiting_deliveriesbut some deliveries are stillquoting). -
#needs_spiff_training? ⇒ Object
Whether needs spiff training.
-
#new_customer_online_order ⇒ Boolean
True when this is a brand-new online customer's first order — a key segmentation flag for new-customer email flows and acquisition reporting.
-
#new_customer_order ⇒ Boolean
True when this is the customer's only non-management-held order, i.e.
-
#new_ss_ticket(service, activity_type) ⇒ Object
Shared backend for #create_smartinstall_ticket, #create_smartfix_ticket, and #create_smartguide_ticket: builds the SmartService support case, attaches participants and rooms, and schedules the kickoff activity.
-
#next_six_months_with_end_dates ⇒ Array<Array(String, String)>
Six-month label/value pairs for end-of-month dates starting this month —
[["October 2026", "2026-10-31"], …]. -
#next_warehouse_ship_date(now: Time.current) ⇒ Date?
Earliest date the warehouse will hand the order to a carrier, honoring the same-day cutoff and the company holiday calendar.
-
#notify_pre_pack_cancellation ⇒ void
When an order is cancelled while it still has pre-pack deliveries (waiting on warehouse packing data), email each affected warehouse so they don't waste pack time on a dead order.
-
#ok_to_delete?(account = nil) ⇒ Object
Whether ok to delete.
-
#online_order? ⇒ Object
Whether online order.
-
#open_activities_counter ⇒ Integer
Number of open follow-up activities tied to this order, for the CRM's per-order activity badge.
-
#opportunities ⇒ Array<Opportunity>
Every opportunity tied to the order — directly via #opportunity and indirectly via each RoomConfiguration's opportunity.
-
#opportunity_name ⇒ String?
Name of the linked Opportunity, or
nilif the order isn't tied to one. -
#opportunity_name=(val) ⇒ Object
Setter that finds or creates an Opportunity on the customer with the given name and links the order to it.
-
#order_closed? ⇒ Object
Whether order closed.
-
#order_emails ⇒ Array<String>
Every email address relevant to this order: the customer + all their contacts, the transmission email, the tracking email, the bound contact's emails, and any per-order billing overrides.
-
#order_type_title ⇒ String
Human label for
order_type(e.g.'SO'→'Sales Order'). -
#overdue_to_ship? ⇒ Boolean
Whether this order is currently an overdue-to-ship slip.
-
#partially_shipped? ⇒ Object
Whether partially shipped.
-
#participants_options_for_select ⇒ Array<Array(String, Integer)>?
[name, party_id]pairs of every opportunity participant on this order, suitable for a Railsselecthelper. -
#party_for_order_confirmation ⇒ Party
The party the order-confirmation email goes to: the contact when one is bound, else the customer.
-
#pay_on_spiff? ⇒ Object
Whether pay on spiff.
-
#payment_method ⇒ String?
Category code (
Payment::CREDIT_CARD,Payment::PO, …) of the first authorised payment on the order — a quick "how was this paid?" hint. -
#payment_method_requires_authorization? ⇒ Boolean
True when any payment on the order is awaiting human authorization (e.g. unreviewed eCheck or fraud-flagged card).
-
#payment_options(source) ⇒ Array<String>
Allowed payment-method codes for this order, ordered by the canonical sort for the given UI surface.
-
#payments_requiring_authorization ⇒ Array<Payment>
Authorised payments that haven't yet been management-approved AND whose authorisation-review pipeline has flagged them as requiring sign-off (any reason — fraud risk, eCheck, large amount, etc.).
-
#payments_with_fraud_report ⇒ Array<Payment>
Non-voided payments that carry a fraud report and haven't been explicitly approved by management.
-
#paypal_invoices_paid? ⇒ Object
Whether paypal invoices paid.
-
#po_number(include_po_prefix: false, limit: nil) ⇒ String
Comma-joined PO number(s) across the order's authorised / captured payments.
-
#po_number_barcode(file_path: nil) ⇒ String
Instance-level wrapper around Order.po_number_barcode that uses this order's PO number.
-
#po_numbers ⇒ Array<String>
Distinct PO numbers across all payments on the order.
-
#potential_fraud? ⇒ Object
Whether potential fraud.
-
#potential_fraud_reasons ⇒ Array<String>
Aggregate list of fraud-report reasons across every unapproved payment on the order.
-
#precreated_rma_credit_available ⇒ BigDecimal
Remaining RMA credit pre-authorised against this order, computed as
line_total_plus_tax − already-applied RMA credit payments. -
#prevent_recalculate_shipping? ⇒ Object
Whether prevent recalculate shipping.
-
#primary_party ⇒ Party
The party who's the "face" of this order — the contact when one is bound (e.g. dealer with multiple contacts), otherwise the customer.
-
#primary_rep_name ⇒ String
Display string for the primary sales rep, falling back to the literal
"Unassigned"so list views never show a blank cell. -
#primary_sales_rep ⇒ Employee?
Primary sales rep on the order.
-
#product_review_url ⇒ String?
Reviews.io product-review landing URL pre-filled with this order's customer/email/skus.
-
#prune_cart_rooms ⇒ Object
Drop room configurations from the cart whose line items have all been removed.
-
#public_path ⇒ String?
Public-portal path to the order ("My Account" view) when the customer has a portal account, otherwise
nil. -
#public_payment_link ⇒ String
Public, tokenised URL a customer can hit to pay an order without signing in.
-
#purchase_early_label_if_requested ⇒ Hash?
Purchase shipping label early if requested and order is eligible Called from after_transition to awaiting_deliveries.
-
#quotes ⇒ Array<Quote>
Every quote tied to the order — directly via #quote and indirectly via the most recent completed quote on each linked room configuration.
-
#ready_for_pending_payment? ⇒ Object
Whether ready for pending payment.
-
#ready_for_service? ⇒ Object
Whether ready for service.
-
#ready_for_shipping? ⇒ Object
Whether ready for shipping.
-
#ready_for_warehouse? ⇒ Object
Whether ready for warehouse.
-
#recipient_name ⇒ String?
Best-known recipient name for emails and shipping labels: company name on the shipping address, then the person name on the address, finally the customer's full name.
-
#related_activities ⇒ ActiveRecord::Relation<Activity>
Activity records corresponding to #related_activities_ids.
-
#related_activities_ids ⇒ Array<Integer>
Combined ids of every Activity hung directly off this order plus those hung off its deliveries — the ids the CRM "all activity for this order" feed should display.
-
#release_order_or_hold ⇒ Object
Release the order to the warehouse unless something requires accounting/management review first (#accounting_hold_order?), in which case the order stays on hold for manual release.
-
#remove_room_configuration(rc) ⇒ Hash
Detach a RoomConfiguration from the order, switching the order's bound opportunity/quote to whichever sibling room is left (or
nilif removing the last one). -
#require_cc_for_advance_credit? ⇒ Object
Whether require cc for advance credit.
-
#requires_intervention? ⇒ Object
Whether requires intervention.
-
#reset_cart ⇒ Object
Wipe a cart back to empty: destroy all line items, quoting deliveries, and discounts, clear the shipping address, and prune any rooms that no longer have line items.
-
#restricted_order_type? ⇒ Object
Whether restricted order type.
-
#reviewer ⇒ Object
Returns the reviewer info for Reviews.io invitation Prefers contact over customer if present.
-
#rma_cancel ⇒ Object
Hard-cancel an RMA replacement order: destroy all active deliveries first (so their inventory commits release) then destroy the order itself, all in one transaction.
-
#save_early_label_api_log ⇒ Object
Save the accumulated API log to the order's early_label_metadata Called at the end of purchase_early_label_if_requested (success or failure).
-
#schedule_follow_up_activity ⇒ Boolean
Run the FollowUpScheduler service against this order.
-
#search_text ⇒ String?
protected
Indexed search text for full-text matching: order reference, PO numbers, and RMA reference.
-
#secondary_sales_rep ⇒ Employee?
Secondary sales rep on the order.
-
#selected_shipping_cost_supports_early_label?(selected_sc, delivery) ⇒ Boolean
Checks whether the delivery's selected shipping cost is compatible with the marketplace early-label flow.
-
#selection_name ⇒ String
Select-2 / Tom Select dropdown label — like #to_label but uses
"not shipped"when the order hasn't shipped yet. -
#send_back_order_notification ⇒ Object
Email the back-order team that this order moved into back-order state and needs follow-up with the customer.
-
#send_early_label_void_alert(reason:, details:) ⇒ Object
Send alert email when early label void fails or is blocked SAFEGUARD C: Ensures operations team is notified of label issues.
-
#send_online_order_confirmation ⇒ Hash{Symbol => Symbol, String}
Build and send the
ONLINE_ORDER_CONFIRMemail via CommunicationBuilder, then pin the recipient address to the order'stracking_emailarray so subsequent ship/tracking emails go to the same place. -
#send_payment_automatically_authorized_notification ⇒ Object
Notify accounting that the auto-authoriser approved a payment without human review — useful for spot-checking the rules engine.
-
#send_profit_review_notification ⇒ Object
Email the management profit-review queue when this order's margin falls below the configured threshold and needs sign-off.
-
#send_ready_for_pickup_email ⇒ Hash{Symbol => Symbol, String}
Send the warehouse-pickup-ready email (
ORDER_PICKUPtemplate) used for "your order is ready at the will-call counter" notices. -
#send_release_authorization_notification ⇒ Object
Email management when an order needs explicit release-from-CR-hold authorisation (e.g. fraud-flagged payment, large order, etc.).
-
#send_request_carrier_assignment_notification ⇒ Object
Notify dispatch when an order is waiting on a manual carrier assignment (rate-shop returned no eligible options).
-
#send_tracking_email ⇒ Hash{Symbol => Symbol, String}
Send the standard
ORDER_TRACKINGemail with the carrier's tracking link. -
#send_tracking_template_email(template_code) ⇒ Hash{Symbol => Symbol, String}
Generic helper that drives both #send_tracking_email and #send_ready_for_pickup_email: builds the communication via CommunicationBuilder for the named system template and sends it to every address in
tracking_email. -
#service_only_order? ⇒ Object
Whether service only order.
-
#set_currency ⇒ Object
protected
Before-validation callback: pin the order's currency to the customer's store currency when not already set, so currency never silently defaults to USD on non-US orders.
-
#set_custom_order_agreement ⇒ Object
Checks for the presence of custom products in excess of $2k.
-
#set_default_tracking_email(force: false) ⇒ Object
Populate
tracking_emailwith addresses extracted by DefaultTrackingEmailExtractor (customer + contact + opportunity participants). -
#set_min_profit_markup ⇒ Object
Before-validation callback: pin the order's minimum-profit markup percentage.
-
#set_opportunity_source ⇒ Object
After-save hook: when the order's source changes (and isn't the generic "unknown source"), propagate that source up to its opportunity.
-
#set_shipped_date ⇒ Object
Stamp the order's
shipped_datewith today, but only when blank — once shipped, the date is canonical and shouldn't drift on a re-save. -
#ship_from_attributes(delivery = nil) ⇒ Hash
Build the "ship from" address/phone/email/attention block for a carrier API call.
-
#ship_to_attributes ⇒ Hash
Build the "ship to" hash for carrier API calls: address, phone, email, and ATTN/name lines, with sensible cascading fallbacks (
attention_name→ address person name → literal"Customer", phone falls through customer's phones to the rep's, etc.) so the carrier never receives a blank field. -
#ship_weight ⇒ Float
Aggregate physical ship weight across all non-shipping line items, floored at 0.1 lb so carrier rate APIs that reject zero weights don't blow up.
-
#shipping_address_valid_and_not_po_box_if_present(rate_shopping = false) ⇒ Boolean
Validates the shipping address: must be present, must pass its own AR validations, and must not be a PO box for the chosen carrier (FedEx/UPS reject them; USPS accepts them).
-
#shipping_cutoff_advance_order? ⇒ Object
Whether shipping cutoff advance order.
-
#shipping_cutoff_next_day? ⇒ Object
Whether shipping cutoff next day.
-
#shipping_cutoff_same_day? ⇒ Object
Whether shipping cutoff same day.
-
#shipping_cutoff_status(now: Time.current) ⇒ Symbol
Whether the warehouse can still ship this order today.
-
#shipping_date_warnings ⇒ Object
Warnings about shipping dates that don't block order release These are displayed to users but don't prevent state transitions.
-
#should_commit_stock? ⇒ Object
Whether should commit stock.
-
#smartservice_ticket ⇒ SupportCase?
The SmartService ticket currently waiting on payment for this order, or
nilif there isn't one. -
#sms_enabled_numbers ⇒ Array<String>
SMS-capable phone numbers for every order participant (customer, contact, opportunity participants), formatted in the E.164-style our SMS provider expects.
-
#sms_messages ⇒ ActiveRecord::Relation<SmsMessage>
SMS conversation thread tied to this order — every inbound / outbound message exchanged with any participant phone returned by #sms_enabled_numbers.
-
#spiff_reward ⇒ String
Eligible SPIFF reward amount formatted as
"%.2f". -
#state_description(describe_state = nil) ⇒ String?
User-facing description for a state name, optionally for a state other than the current one.
-
#state_list ⇒ Array<Symbol>
The ordered set of states this order can be in given its type and current data.
-
#stock_status ⇒ Symbol
Combined inventory status across all line items:
:ok,:partial, or:none. -
#stop_for_pre_pack? ⇒ Object
Whether stop for pre pack.
-
#stop_for_profit_review? ⇒ Object
Whether stop for profit review.
-
#store ⇒ Store?
The Store this order is fulfilled from.
-
#store_additional_early_label_pdfs(additional_labels) ⇒ Object
Persist the secondary label PDFs returned by a multi-package marketplace label purchase.
-
#store_amazon_label_pdf_atomic(label_result, tracking_number, marketplace) ⇒ Object
Amazon: label data is returned inline — store it and verify persistence.
-
#store_early_label_pdf(label_data, tracking_number) ⇒ Upload?
Store early label PDF on the order.
-
#store_mock_early_label_pdf(tracking_number, _carrier) ⇒ Object
Store a mock label PDF for development/testing Uses a pre-generated mock PDF since the sandbox doesn't return real label PDFs.
-
#store_walmart_label_pdf_atomic(shipper, _label_result, tracking_number, carrier, marketplace) ⇒ Object
Walmart: download label via API with retries, then store and verify persistence.
-
#suggested_shipments ⇒ ActiveRecord::Relation?
Shipments still in the
suggestedstate — i.e. -
#support_case_id ⇒ Integer?
Single support-case binding for the CRM order-setup form's Tom Select picker, mirroring
Rma#support_case_id. - #support_case_id=(id) ⇒ Object
-
#support_case_ref ⇒ String
Comma-separated list of attached support case numbers.
-
#support_case_ref=(support_case_ref) ⇒ Object
Setter that resolves a comma-separated list of case numbers into the matching support-case ids and writes them to
support_case_ids. -
#suppress_edi_duplicate_warning? ⇒ Boolean
Suppresses duplicate order warnings for off-book delivery arrangements.
-
#sync_opportunity ⇒ Object
After-save hook: tell the linked opportunity to re-evaluate its state machine (the opportunity may need to advance from "active" to "sold" once the order is invoiced).
-
#terms_available? ⇒ Object
protected
Whether terms available.
-
#to_label ⇒ String
Compact label string used by autocomplete dropdowns and SMS threads —
[REF#] Customer (shipped-date). -
#to_liquid ⇒ Liquid::OrderDrop
Wrap the order in a Liquid::OrderDrop so it can be safely rendered inside customer-facing email templates without exposing internal model methods.
- #to_s ⇒ String
-
#total_cod ⇒ BigDecimal, Float
Cash-On-Delivery amount the carrier should collect from the consignee.
-
#total_money ⇒ String
Order total formatted as a
"%.2f"string for display where the template needs a fixed two-decimal value (CSV exports, EDI payloads). -
#total_payments_authorized ⇒ BigDecimal
Sum of authorised payment amounts across deliveries, capping each delivery at its own total so over-authorisation on one delivery doesn't shift coverage to another.
-
#track_profit? ⇒ Object
Whether track profit.
-
#tracking_email_address ⇒ String
Inbound-email address that uniquely identifies this order — when a tracking-email reply lands at this address, ActionMailbox decrypts the id and re-attaches the message to the order.
-
#uncommit_undelivered_line_items ⇒ Object
Release inventory commits for line items that aren't attached to a delivery (typically left over after a delivery is destroyed mid-flow).
-
#undiscounted_shipping_total ⇒ BigDecimal
Sum of the shipping ('shp') line items at their raw (pre-discount) price.
-
#unfulfilled_dropship_items ⇒ Boolean
True when any dropship line still lacks a fully-receipted supplier purchase-order item — i.e.
-
#update_customer_status ⇒ Object
protected
After-save callback: re-evaluate the customer's lifecycle state (lead → prospect → customer → returning customer) now that this order may have moved them up or down.
-
#update_linked_po_if_st ⇒ Boolean
For store-transfer orders, sync the linked PurchaseOrder record to the order's first delivery so accounting sees the same dates, quantities, and costs on both sides of the transfer.
-
#update_sales_support_rep(new_sales_support_rep_id, new_commission_date = nil) ⇒ Boolean
Reassign the sales-support rep on the order (and optionally update the support-commission date).
-
#versions_for_audit_trail(_params = {}) ⇒ ActiveRecord::Relation<RecordVersion>
Aggregate RecordVersion (PaperTrail) rows for the order's full audit trail — the order itself plus every line item, delivery, and discount that points back to it.
-
#void_credit_rma_item ⇒ Object
Void every RMA credit-item linked to this order's line items.
-
#void_early_label!(reason: 'Manual void requested', reset_flag: true) ⇒ Hash
Void the early label with a custom reason (public method for external callers) Also resets purchase_label_early flag so the next ship-label goes through normal flow.
-
#void_early_label_if_exists ⇒ Hash?
Void early-purchased label when order is pulled back from awaiting_deliveries Called from before_transition to cancelled/fraudulent/in_cr_hold.
-
#void_early_label_upload ⇒ Object
Re-categorize the early label PDF as voided so it no longer appears as an active attachment but is kept for audit trail (mirrors the ship_label_pdf -> voided_ship_label_pdf pattern in Shipment).
-
#void_payments(report_fraud = false) ⇒ Object
Void every authorised payment on the order (and refuse to act on captured ones — those need manual intervention).
-
#walmart_sww_eligible? ⇒ Boolean
Check if order is eligible for Walmart Ship with Walmart early label purchase.
Methods included from Models::SourceAttributable
#has_google_ads_attribution?, #source_event_window_respected, #source_locked?, #source_locked_reason, #visit_with_google_click_id?
Methods included from Models::Lineage
#ancestors, #ancestors_ids, #children_and_roots, #descendants, #descendants_ids, #ensure_non_recursive_lineage, #family_members, #generate_full_name, #generate_full_name_array, #lineage, #lineage_array, #lineage_simple, #root, #root_id, #self_ancestors_and_descendants, #self_ancestors_and_descendants_ids, #self_and_ancestors, #self_and_ancestors_ids, #self_and_children, #self_and_descendants, #self_and_descendants_ids, #self_and_siblings, #self_and_siblings_ids, #siblings, #siblings_ids
Methods included from Models::InventoryCommittable
#can_be_committed?, #can_be_uncommitted?, #commit_line_items, #determine_commit_expiration_date, #has_committed_line_items?, #uncommit_line_items
Methods included from Models::CouponDateOverridable
Methods included from Models::LegacyRateRequest
#last_shipping_rate_request_result, #last_shipping_rate_request_result=
Methods included from Models::RmaTransmittable
#rma_available_email_addresses, #rma_available_fax_numbers
Methods included from Models::Profitable
#default_sales_markup, #profit_margins_met?, #profitable_line_items, #profitable_status, #profitable_total_discounted, #profitable_total_estimated_cost, #profitable_total_estimated_line_cost, #profitable_total_profit, #profitable_total_profit_margin, #profitable_total_profit_markup, #validate_min_profit_markup?
Methods included from Models::Auditable
#all_skipped_columns, #audit_reference_data, #should_not_save_version, #stamp_record
Methods included from Models::MultiRoom
#add_line_items_to_all_rooms, #add_lines_for_room_configuration, #all_rooms_complete?, #all_rooms_complete_or_cancelled?, #all_rooms_complete_or_cancelled_or_draft?, #all_rooms_in_design?, #all_rooms_ppd?, #any_room_ppd?, #any_rooms_in_design?, #get_operating_costs, #get_recommended_materials, #heated_sq_ft, #insulation_sq_ft, #prioritize_room, #remove_line_items_from_all_rooms, #remove_lines_for_room_configuration, #replace_line_items_in_all_rooms, #suggested_items, #suggested_services, #synchronization_targets
Methods included from Models::ShipQuotable
#carrier, #chosen_shipping_method, #days_commitment, #determine_origin_address, #everything_in_stock?, #friendly_shipping_method, #is_drop_ship?, #is_override?, #line_items_grouped_by_deliveries_quoting, #line_items_match_deliveries_if_any, #need_to_pre_pack_reasons, #one_time_shipping_address, #one_time_shipping_address=, #qualifies_for_cod?, #refresh_deliveries_quoting, #reset_deliveries_shipping_option, #reset_shipping, #retrieve_friendly_shipping_method, #retrieve_shipping_costs, #ship_quoted, #shipping_non_db_default_but_db_customer?, #shipping_signature_confirmation_non_db_default_but_db_customer?, #shipping_via_wy_but_has_shipping_account?, #ships_economy_package?, #ships_freight?, #ships_freight_but_address_not_freight_ready?, #should_ship_freight?, #should_ship_freight_but_address_not_freight_ready?, #validate_deliveries
Methods included from Models::ShipMeasurable
#cartons_total, #crates_total, #pallets_total, #ship_freight_class_from_shipments, #ship_volume_from_shipments, #ship_volume_from_shipments_in_cubic_feet, #ship_weight_from_shipments, #shipment_set, #shipments_for_measure
Methods included from Models::Pickable
#all_my_publications, #append_suggested_items, #append_suggested_materials, #control_capacity, #determine_catalog_for_picking, #determine_skus_to_filter, #discounted_shipping_total, #electrical_heating_elements, #fix_catalog, #get_material_alerts, #has_custom_products?, #invalidate_material_alerts!, #line_items_grouped_by_room_configuration, #lines_match_pick_catalog_on_flip, #meets_custom_products_threshold?, #pricing_program_discount_factor, #purge_empty_line_items, #soft_recalc, #subtotal, #subtotal_after_trade_discount_without_shipping, #subtotal_discounted_without_shipping, #subtotal_msrp_without_shipping, #switching_pick_catalog?, #synchronize_lines, #total_amps_by_product_line, #total_coverage_by_product_line, #total_linear_feet_by_product_line, #total_spec_by_product_line, #total_watts_by_product_line, #underlayments
Methods included from Models::TaxableResource
#apply_tax_rate_to_line_items, #build_tax_params, #calculate_tax_for_all_lines, #copy_tax_rate, #effective_date, #get_rates_for_line, #get_tax_rate, #manual_rate_goods, #manual_rate_services, #manual_rate_shipping, #origin_address, #refresh_tax_rate, #resource_not_taxable?, #set_initial_tax_rate, #should_refresh_tax_rate?, #state_code, #state_code_sym, #taxes_grouped_by_rate, #taxes_grouped_by_type
Methods included from Models::Itemizable
#add_line_item, #additional_items, #assign_sequence, #breakdown_of_prices, #calculate_actual_insured_value, #calculate_discounts, #calculate_shipping_cost, #coupon_search, #customer_applied_coupons, #customer_can_apply_coupon?, #discounts_changed?, #discounts_grouped_by_coupon, #discounts_subtotal, #effective_discount, #effective_shipping_discount, #has_kits?, #has_kits_or_serial_numbers?, #has_serial_numbers?, #is_credit_order?, #line_items_requiring_serial_number, #line_items_with_counters, #line_total_plus_tax, #main_rep, #perform_db_total, #purge_empty_quoting_deliveries, #purge_shipping_when_no_other_lines, #remove_line_item, #require_total_reset?, #reset_discount, #set_for_recalc, #set_signature_confirmation_on_shipping_address_change, #set_totals, #shipping_conditions_changed?, #shipping_discounted, #shipping_method_changed?, #should_recalculate_shipping?, #smartinstall_data, #smartsupport_data, #subtotal_cogs, #sync_shipping_line, #total_cogs
Methods included from Models::Notable
Methods inherited from ApplicationRecord
ransackable_associations, ransackable_attributes, ransackable_scopes, ransortable_attributes, #to_relation
Methods included from Models::Schedulable
Methods included from Models::AfterCommittable
Methods included from Models::EventPublishable
Instance Attribute Details
#customer_id ⇒ Object (readonly)
Customer can only have one cart per Contact
Validations (if => #cart? ):
- Uniqueness ({ scope: %i[state contact_id], message: 'Only one cart is allowed per user' })
448 |
# File 'app/models/order.rb', line 448 validates :customer_id, uniqueness: { scope: %i[state contact_id], message: 'Only one cart is allowed per user' }, if: :cart? |
#do_not_detect_shipping ⇒ Object
Returns the value of attribute do_not_detect_shipping.
478 479 480 |
# File 'app/models/order.rb', line 478 def do_not_detect_shipping @do_not_detect_shipping end |
#do_not_set_totals ⇒ Object
Returns the value of attribute do_not_set_totals.
478 479 480 |
# File 'app/models/order.rb', line 478 def do_not_set_totals @do_not_set_totals end |
#early_label_purchase_result ⇒ Object
Returns the value of attribute early_label_purchase_result.
478 479 480 |
# File 'app/models/order.rb', line 478 def early_label_purchase_result @early_label_purchase_result end |
#from_store_id ⇒ void (readonly)
This method returns an undefined value.
Validates from store id, to store id, create.
458 |
# File 'app/models/order.rb', line 458 validates :from_store_id, :to_store_id, presence: { on: :create, if: proc { |o| o.order_type == STORE_TRANSFER } } |
#full_shipping_address_validation ⇒ Object
Returns the value of attribute full_shipping_address_validation.
478 479 480 |
# File 'app/models/order.rb', line 478 def full_shipping_address_validation @full_shipping_address_validation end |
#is_www ⇒ Object
Returns the value of attribute is_www.
478 479 480 |
# File 'app/models/order.rb', line 478 def is_www @is_www end |
#is_www_ship_by_zip ⇒ Object
Returns the value of attribute is_www_ship_by_zip.
478 479 480 |
# File 'app/models/order.rb', line 478 def is_www_ship_by_zip @is_www_ship_by_zip end |
#max_discount_override ⇒ void (readonly)
This method returns an undefined value.
Validates max discount override.
465 |
# File 'app/models/order.rb', line 465 validates :max_discount_override, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 100, allow_nil: true } |
#order_type ⇒ void (readonly)
This method returns an undefined value.
Validates order type.
451 |
# File 'app/models/order.rb', line 451 validates :order_type, presence: true |
#reference_number ⇒ void (readonly)
This method returns an undefined value.
Validates reference number, cart or in shipping estimate.
455 |
# File 'app/models/order.rb', line 455 validates :reference_number, presence: { unless: :cart_or_in_shipping_estimate? } |
#shipping_phone ⇒ Object (readonly)
skip this validation for EDI orders, let it ride
Validations (unless => #is_edi_order? ):
- Phone_format
459 |
# File 'app/models/order.rb', line 459 validates :shipping_phone, phone_format: true, unless: :is_edi_order? |
#to_store_id ⇒ void (readonly)
This method returns an undefined value.
Validates from store id, to store id, create.
Validations:
- Presence ({ on: :create, if: proc { |o| o.order_type == STORE_TRANSFER } })
458 |
# File 'app/models/order.rb', line 458 validates :from_store_id, :to_store_id, presence: { on: :create, if: proc { |o| o.order_type == STORE_TRANSFER } } |
#tracking_email ⇒ void (readonly)
This method returns an undefined value.
Validates tracking email.
462 |
# File 'app/models/order.rb', line 462 validates :tracking_email, email_format: true |
#update_shipping_address_with_contact ⇒ Object
Returns the value of attribute update_shipping_address_with_contact.
478 479 480 |
# File 'app/models/order.rb', line 478 def update_shipping_address_with_contact @update_shipping_address_with_contact end |
Class Method Details
.active ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are active. Active Record Scope
634 |
# File 'app/models/order.rb', line 634 scope :active, -> { where.not(state: %w[pending cancelled fraudulent cart in_shipping_estimate]) } |
.active_spiffs ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are active spiffs. Active Record Scope
668 |
# File 'app/models/order.rb', line 668 scope :active_spiffs, -> { where(spiff_state: %w[awaiting_payment paid]) } |
.all_awaiting_deliveries ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are all awaiting deliveries. Active Record Scope
585 |
# File 'app/models/order.rb', line 585 scope :all_awaiting_deliveries, -> { where(state: SHIPPING_STATES) } |
.all_order_types_for_select ⇒ Array<Array(String, String)>
Every order-type code the system understands, for filter
dropdowns in admin search.
1777 1778 1779 |
# File 'app/models/order.rb', line 1777 def self.all_order_types_for_select ALL_ORDER_TYPES.map { |code, desc| [desc, code] } end |
.assigned_to_rep ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are assigned to rep. Active Record Scope
613 614 615 616 617 618 619 620 621 622 623 624 625 |
# File 'app/models/order.rb', line 613 scope :assigned_to_rep, ->(party_or_ids) { # Guard against nil/blank input — without this, Rails turns the OR # branches into `IS NULL` predicates, which would match every order # missing any rep column. ids = Array.wrap(party_or_ids).compact next none if ids.empty? where.any_of( { primary_sales_rep_id: ids }, { secondary_sales_rep_id: ids }, { local_sales_rep_id: ids } ) } |
.awaiting_completed_installation_plans ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are awaiting completed installation plans. Active Record Scope
666 |
# File 'app/models/order.rb', line 666 scope :awaiting_completed_installation_plans, -> { where(state: :awaiting_completed_installation_plans) } |
.awaiting_payment_spiff ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are awaiting payment spiff. Active Record Scope
665 |
# File 'app/models/order.rb', line 665 scope :awaiting_payment_spiff, -> { where(spiff_state: 'awaiting_payment') } |
.back_order ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are back order. Active Record Scope
655 |
# File 'app/models/order.rb', line 655 scope :back_order, -> { where(state: :crm_back_order) } |
.by_company_id ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are by company id. Active Record Scope
590 |
# File 'app/models/order.rb', line 590 scope :by_company_id, ->(company_id) { joins(customer: { catalog: :store }).where(stores: { company_id: }) } |
.by_primary_rep_id ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are by primary rep id. Active Record Scope
591 |
# File 'app/models/order.rb', line 591 scope :by_primary_rep_id, ->(rep_id) { joins(:customer).where(parties: { primary_sales_rep_id: rep_id }) } |
.by_report_grouping ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are by report grouping. Active Record Scope
683 |
# File 'app/models/order.rb', line 683 scope :by_report_grouping, ->(report_grouping) { joins(:customer).where(parties: { report_grouping: report_grouping }) } |
.by_report_grouping_all_when_nil ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are by report grouping all when nil. Active Record Scope
682 |
# File 'app/models/order.rb', line 682 scope :by_report_grouping_all_when_nil, ->(report_grouping) { report_grouping.present? ? joins(:customer).where(parties: { report_grouping: report_grouping }) : joins(:customer) } |
.by_sales_rep_id ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are by sales rep id. Active Record Scope
685 686 687 688 689 690 691 692 693 |
# File 'app/models/order.rb', line 685 scope :by_sales_rep_id, lambda { |sales_rep_id| next none if sales_rep_id.blank? joins(:customer).where.any_of( { parties: { primary_sales_rep_id: sales_rep_id } }, { parties: { secondary_sales_rep_id: sales_rep_id } }, { parties: { local_sales_rep_id: sales_rep_id } } ) } |
.by_store ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are by store. Active Record Scope
630 |
# File 'app/models/order.rb', line 630 scope :by_store, ->(store) { where(currency: store.currency) } |
.by_store_id ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are by store id. Active Record Scope
589 |
# File 'app/models/order.rb', line 589 scope :by_store_id, ->(store_id) { joins(customer: :catalog).where(catalogs: { store_id: }) } |
.cancelled ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are cancelled. Active Record Scope
633 |
# File 'app/models/order.rb', line 633 scope :cancelled, -> { where(state: 'cancelled') } |
.carts ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are carts. Active Record Scope
640 |
# File 'app/models/order.rb', line 640 scope :carts, -> { where(state: :cart) } |
.co_only ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are co only. Active Record Scope
658 |
# File 'app/models/order.rb', line 658 scope :co_only, -> { where(order_type: CREDIT_ORDER) } |
.contains_coupon_ids ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are contains coupon ids. Active Record Scope
674 |
# File 'app/models/order.rb', line 674 scope :contains_coupon_ids, ->(coupon_ids) { where("EXISTS (select discounts.id from discounts where discounts.coupon_id IN (?) and discounts.itemizable_type = 'Order' and discounts.itemizable_id = orders.id)", coupon_ids) } |
.contains_item_ids ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are contains item ids. Active Record Scope
671 672 673 |
# File 'app/models/order.rb', line 671 scope :contains_item_ids, ->(item_ids) { where("EXISTS (select li.id from line_items li inner join catalog_items ci on ci.id= li.catalog_item_id inner join store_items si on si.id = ci.store_item_id where li.resource_id = orders.id and li.resource_type = 'Order' and si.item_id IN (?))", item_ids) } |
.contains_service_items ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are contains service items. Active Record Scope
675 |
# File 'app/models/order.rb', line 675 scope :contains_service_items, -> { contains_item_ids(Item.services.ids) } |
.correctly_packaged ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are correctly packaged. Active Record Scope
643 |
# File 'app/models/order.rb', line 643 scope :correctly_packaged, -> { where('orders.incorrectly_package_ups_canada_order IS NOT TRUE OR (orders.incorrectly_package_ups_canada_order IS TRUE and orders.incorrectly_packaged_ups_canada_order_fixed IS TRUE)') } |
.custom_order_agreement_statuses_for_select ⇒ Array<Array(String, String)>
[label, value] pairs of every Custom-Order-Agreement status
for the CRM dropdown.
1760 1761 1762 |
# File 'app/models/order.rb', line 1760 def self.custom_order_agreement_statuses_for_select Order.custom_order_agreement_statuses.keys.map { |e| [e.humanize, e] } end |
.customer_reference_search ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are customer reference search. Active Record Scope
698 |
# File 'app/models/order.rb', line 698 scope :customer_reference_search, ->(q) { where(Order[:customer_reference].matches("%#{q}%")).order([Arel.sql('orders.customer_reference <-> ?'), q]) } |
.default_source_for(customer:, opportunity: nil) ⇒ Source?
Default source for a CRM-created order, per the attribution rule
decided 2026-07-23 (see
doc/tasks/202607231507_SOURCE_ATTRIBUTION_MODEL_RESTRUCTURE.md):
an order converted from an opportunity inherits the opportunity's
source (the closing call/email is influence, not acquisition); a
standalone order stamps from evidence — the customer's recent
attributable visit first, the account default last. Web orders
bypass this via #set_source_from_visit.
1750 1751 1752 1753 1754 |
# File 'app/models/order.rb', line 1750 def self.default_source_for(customer:, opportunity: nil) opportunity&.source || customer.most_recent_attributable_visit_source || customer.source_for_opps_and_orders end |
.draft_spiff ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are draft spiff. Active Record Scope
664 |
# File 'app/models/order.rb', line 664 scope :draft_spiff, -> { where.not(spiff_enrollment_id: nil).where(spiff_state: 'draft') } |
.edi_orders ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are edi orders. Active Record Scope
697 |
# File 'app/models/order.rb', line 697 scope :edi_orders, -> { where.not(orders: { edi_transaction_id: nil }) } |
.future_release ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are future release. Active Record Scope
684 |
# File 'app/models/order.rb', line 684 scope :future_release, -> { where.not(future_release_date: nil).where.not(order_type: 'CO').where.not(state: Order::CLOSED_STATES) } |
.google_conversion_acknowledged ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are google conversion acknowledged. Active Record Scope
605 606 607 608 |
# File 'app/models/order.rb', line 605 scope :google_conversion_acknowledged, -> { jsonb_where(column_name: :google_conversion_meta, json_keys: %w[result], operator: :eq, value: 'reported') .or(jsonb_where(column_name: :google_conversion_meta, json_keys: %w[result status], operator: :eq, value: 'reported')) } |
.google_conversion_attempted ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are google conversion attempted. Active Record Scope
599 600 601 |
# File 'app/models/order.rb', line 599 scope :google_conversion_attempted, -> { jsonb_where_exists(column_name: :google_conversion_meta, key: :attempted_at) } |
.has_manual_preset_form ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are has manual preset form. Active Record Scope
676 |
# File 'app/models/order.rb', line 676 scope :has_manual_preset_form, -> { where("EXISTS(select 1 from uploads where uploads.resource_type = 'Order' and uploads.resource_id = orders.id and uploads.category = 'manual_smart_preset_form')") } |
.held ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are held. Active Record Scope
654 |
# File 'app/models/order.rb', line 654 scope :held, -> { where(state: :in_cr_hold) } |
.held_sales_orders ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are held sales orders. Active Record Scope
670 |
# File 'app/models/order.rb', line 670 scope :held_sales_orders, -> { sales_orders.where(state: %w[pending pending_payment pending_release_authorization in_cr_hold]) } |
.in_progress ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are in progress. Active Record Scope
644 |
# File 'app/models/order.rb', line 644 scope :in_progress, -> { so_only.where.not(state: %w[cart invoiced cancelled fraudulent]) } |
.in_state ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are in state. Active Record Scope
588 |
# File 'app/models/order.rb', line 588 scope :in_state, ->(state) { where(state:) } |
.incorrectly_packaged_ups_canada_order ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are incorrectly packaged ups canada order. Active Record Scope
642 |
# File 'app/models/order.rb', line 642 scope :incorrectly_packaged_ups_canada_order, -> { where('orders.incorrectly_package_ups_canada_order IS TRUE and orders.incorrectly_packaged_ups_canada_order_fixed IS NOT TRUE') } |
.invoiced ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are invoiced. Active Record Scope
637 |
# File 'app/models/order.rb', line 637 scope :invoiced, -> { where(state: 'invoiced') } |
.like_lookup ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are like lookup. Active Record Scope
680 |
# File 'app/models/order.rb', line 680 scope :like_lookup, ->(q) { where('orders.reference_number LIKE :term OR orders.edi_po_number LIKE :term', { term: "%#{q}%" }) } |
.limit_to_fba ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are limit to fba. Active Record Scope
681 |
# File 'app/models/order.rb', line 681 scope :limit_to_fba, -> { joins(customer: :billing_address).where.any_of({ parties: { id: CustomerConstants::AMAZON_COM_ID } }, { addresses: { party_id: CustomerConstants::AMAZON_COM_ID } }) } |
.locked ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are locked. Active Record Scope
694 |
# File 'app/models/order.rb', line 694 scope :locked, -> { where(state: LOCKED_STATES) } |
.lookup ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are lookup. Active Record Scope
677 |
# File 'app/models/order.rb', line 677 scope :lookup, ->(q) { where(orders: { reference_number: q }) } |
.mo_only ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are mo only. Active Record Scope
659 |
# File 'app/models/order.rb', line 659 scope :mo_only, -> { where(order_type: MARKETING_ORDER) } |
.most_recent_first ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are most recent first. Active Record Scope
653 |
# File 'app/models/order.rb', line 653 scope :most_recent_first, -> { order('orders.created_at DESC') } |
.non_carts ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are non carts. Active Record Scope
641 |
# File 'app/models/order.rb', line 641 scope :non_carts, -> { where(state: NON_CART_STATES) } |
.non_credit ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are non credit. Active Record Scope
639 |
# File 'app/models/order.rb', line 639 scope :non_credit, -> { where.not(order_type: CREDIT_ORDER) } |
.not_cancelled ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are not cancelled. Active Record Scope
635 |
# File 'app/models/order.rb', line 635 scope :not_cancelled, -> { where.not(state: 'cancelled') } |
.not_in_pre_pack ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are not in pre pack. Active Record Scope
645 |
# File 'app/models/order.rb', line 645 scope :not_in_pre_pack, -> { so_only.where.not(state: %w[pre_pack]) } |
.not_open_for_change ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are not open for change. Active Record Scope
628 |
# File 'app/models/order.rb', line 628 scope :not_open_for_change, -> { where.not(state: OPEN_FOR_CHANGE_STATES) } |
.not_partially_invoiced ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are not partially invoiced. Active Record Scope
638 |
# File 'app/models/order.rb', line 638 scope :not_partially_invoiced, -> { where.not(state: %w[invoiced partially_invoiced]) } |
.not_processing_deliveries ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are not processing deliveries. Active Record Scope
636 |
# File 'app/models/order.rb', line 636 scope :not_processing_deliveries, -> { where.not(state: 'processing_deliveries') } |
.not_sold ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are not sold. Active Record Scope
587 |
# File 'app/models/order.rb', line 587 scope :not_sold, -> { where.not(state: SOLD_STATES) } |
.open_for_change ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are open for change. Active Record Scope
626 |
# File 'app/models/order.rb', line 626 scope :open_for_change, -> { where(state: OPEN_FOR_CHANGE_STATES) } |
.open_for_tax_update ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are open for tax update. Active Record Scope
627 |
# File 'app/models/order.rb', line 627 scope :open_for_tax_update, -> { where(state: OPEN_FOR_TAX_CHANGE_STATES) } |
.order_count(company_id = nil, where_conditions = nil, where_not_conditions = nil) ⇒ Integer
Counts orders matching the given filters. The customer→catalog
→store join is mandatory because all callers want the
company-aware count, never the raw global count.
1789 1790 1791 1792 1793 1794 1795 |
# File 'app/models/order.rb', line 1789 def self.order_count(company_id = nil, where_conditions = nil, where_not_conditions = nil) o = Order.joins(customer: { catalog: :store }).order('orders.id') o = o.by_company_id(company_id) unless company_id.nil? o = o.where(where_conditions) unless where_conditions.nil? o = o.where.not(where_not_conditions) unless where_not_conditions.nil? o.count end |
.order_type_from_opportunity(opportunity) ⇒ String
Derive an order_type code from an Opportunity's type by
appending 'O' (so S + O = 'SO' sales order, T + O =
'TO' tech order).
6453 6454 6455 |
# File 'app/models/order.rb', line 6453 def self.order_type_from_opportunity(opportunity) "#{opportunity.opportunity_type}O" end |
.order_type_from_quote(quote) ⇒ String
Map a Quote type code to the matching order_type code: a
tech quote (TQ) becomes a tech order (TO), a marketing
quote (MQ) becomes a marketing order (MO), etc. Defaults to
'SO' for the standard sales-quote → sales-order path.
6443 6444 6445 |
# File 'app/models/order.rb', line 6443 def self.order_type_from_quote(quote) { TQ: 'TO', MQ: 'MO', SQ: 'SO' }[quote.quote_type.to_sym] || 'SO' end |
.overdue_to_ship ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are overdue to ship. Active Record Scope
2441 2442 2443 2444 2445 2446 |
# File 'app/models/order.rb', line 2441 scope :overdue_to_ship, lambda { where("edi_orchestrator_partner LIKE 'amazon_seller%'") .where(state: 'awaiting_deliveries') .where(requested_ship_before: ...Date.current) .where('future_release_date IS NULL OR future_release_date <= ?', Date.current) } |
.paid_spiff ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are paid spiff. Active Record Scope
667 |
# File 'app/models/order.rb', line 667 scope :paid_spiff, -> { where(spiff_state: 'paid') } |
.pending ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are pending. Active Record Scope
646 |
# File 'app/models/order.rb', line 646 scope :pending, -> { where(state: 'pending') } |
.pending_payment ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are pending payment. Active Record Scope
648 |
# File 'app/models/order.rb', line 648 scope :pending_payment, -> { where(state: 'pending_payment') } |
.pending_payment_and_unpaid_invoices ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are pending payment and unpaid invoices. Active Record Scope
649 650 651 652 |
# File 'app/models/order.rb', line 649 scope :pending_payment_and_unpaid_invoices, lambda { unpaid_invoice_order_ids = Invoice.where(state: 'unpaid').select(:order_id) where(id: unpaid_invoice_order_ids).or(pending_payment) } |
.po_number_barcode(po_number:, file_path: nil) ⇒ String?
Class-method form of #po_number_barcode: render an arbitrary
PO number as a Code-128 barcode PNG. Strips non-ASCII chars
before encoding to side-step a known barby issue with smart
quotes and other paste-in unicode (toretore/barby#61).
3396 3397 3398 3399 3400 3401 3402 3403 3404 3405 3406 3407 3408 3409 3410 3411 3412 3413 3414 3415 3416 |
# File 'app/models/order.rb', line 3396 def self.(po_number:, file_path: nil) return if po_number.blank? # we just want ASCII here, no spaces or weird cut and paste characters, see https://github.com/toretore/barby/issues/61 po_number = po_number.to_s.scan(/\S/).join.encode(Encoding::ASCII_8BIT, invalid: :replace, undef: :replace, replace: '') require 'zint' = Zint::Barcode.new(value: po_number, symbology: Zint::Constants::Symbologies::BARCODE_CODE128) .show_hrt = 0 # barby rendered bars only; keep it that way png = .to_memory_file(extension: '.png') if file_path File.open(file_path, 'wb') do |file| file.write(png) file.flush file.fsync end file_path else png end end |
.positive_value ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are positive value. Active Record Scope
661 |
# File 'app/models/order.rb', line 661 scope :positive_value, -> { where(Order[:line_total].gt(0)) } |
.profit_review ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are profit review. Active Record Scope
647 |
# File 'app/models/order.rb', line 647 scope :profit_review, -> { where(state: 'profit_review') } |
.quick_stats_shipping ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are quick stats shipping. Active Record Scope
663 |
# File 'app/models/order.rb', line 663 scope :quick_stats_shipping, -> { so_only.positive_value.in_state(SHIPPING_STATES).select('sum(line_total) as sum_line_total, currency').group(:currency) } |
.quick_stats_sold ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are quick stats sold. Active Record Scope
662 |
# File 'app/models/order.rb', line 662 scope :quick_stats_sold, -> { so_only.positive_value.in_state(SOLD_STATES).select('sum(line_total) as sum_line_total, currency').group(:currency) } |
.reception_type_for_select ⇒ Hash{String => String}
Memoised {label => value} map for the order-reception-type
filter ("Online" vs "CRM"). Each invocation returns the same
frozen-shape hash so the form caches well.
3866 3867 3868 3869 3870 3871 3872 3873 |
# File 'app/models/order.rb', line 3866 def self.reception_type_for_select unless @reception_types @reception_types = {} @reception_types['Online'] = 'Online' @reception_types['CRM'] = 'CRM' end @reception_types end |
.returnable_types ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are returnable types. Active Record Scope
660 |
# File 'app/models/order.rb', line 660 scope :returnable_types, -> { where(order_type: [SALES_ORDER, MARKETING_ORDER, TECH_ORDER]) } |
.room_not_pickable ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are room not pickable. Active Record Scope
629 |
# File 'app/models/order.rb', line 629 scope :room_not_pickable, -> { where.not(state: ROOM_PICKABLE_STATES) } |
.sales_orders ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are sales orders. Active Record Scope
669 |
# File 'app/models/order.rb', line 669 scope :sales_orders, -> { non_carts.so_only.active } |
.selectable_order_types ⇒ Array<Array(String, String)>
Order-type codes a user is allowed to choose from in the new-
order form. Hides RESTRICTED_ORDER_TYPES (CRM-only types like
credit orders that are created via dedicated flows).
1769 1770 1771 |
# File 'app/models/order.rb', line 1769 def self.selectable_order_types UNRESTRICTED_ORDER_TYPES.map { |code, desc| [desc, code] } end |
.so_only ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are so only. Active Record Scope
657 |
# File 'app/models/order.rb', line 657 scope :so_only, -> { where(order_type: SALES_ORDER) } |
.sold ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are sold. Active Record Scope
586 |
# File 'app/models/order.rb', line 586 scope :sold, -> { where(state: SOLD_STATES) } |
.st_only ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are st only. Active Record Scope
656 |
# File 'app/models/order.rb', line 656 scope :st_only, -> { where(order_type: STORE_TRANSFER) } |
.states_for_select ⇒ Array<Array(String, String)>
[human_name, machine_value] pairs of every order state, sorted
alphabetically by display name. Used to populate the state
filter on CRM list views.
1810 1811 1812 |
# File 'app/models/order.rb', line 1810 def self.states_for_select state_machine.states.sort_by(&:human_name).map { |s| [s.human_name, s.value] } end |
.with_amazon_payments ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are with amazon payments. Active Record Scope
700 |
# File 'app/models/order.rb', line 700 scope :with_amazon_payments, -> { joins(:payments).where(payments: { category: Payment::AMAZON_PAY }) } |
.with_associations ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are with associations. Active Record Scope
631 |
# File 'app/models/order.rb', line 631 scope :with_associations, -> { includes(:shipments, :shipping_account_number, :shipping_address, :creator, { customer: [:buying_group, { catalog: :store }] }) } |
.with_line_items ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are with line items. Active Record Scope
632 |
# File 'app/models/order.rb', line 632 scope :with_line_items, -> { includes(line_items: { catalog_item: { store_item: :item } }) } |
.with_payments ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are with payments. Active Record Scope
699 |
# File 'app/models/order.rb', line 699 scope :with_payments, -> { joins(:payments) } |
.with_review ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are with review. Active Record Scope
695 |
# File 'app/models/order.rb', line 695 scope :with_review, -> { where(reviewed: true) } |
.without_review ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are without review. Active Record Scope
696 |
# File 'app/models/order.rb', line 696 scope :without_review, -> { where(reviewed: false) } |
Instance Method Details
#accounting_hold_order? ⇒ Object
Whether accounting hold order.
2478 2479 2480 |
# File 'app/models/order.rb', line 2478 def accounting_hold_order? customer.on_hold || || potential_fraud? end |
#activities ⇒ ActiveRecord::Associations::CollectionProxy
The associated activities.
361 |
# File 'app/models/order.rb', line 361 has_many :activities, as: :resource, dependent: :nullify |
#add_customer_to_campaign ⇒ Object
After-save hook: when an order's source changes to a source that's
tied to a marketing campaign, enroll the customer in that campaign
so subsequent campaign emails reach them. No-op when the source
isn't campaign-linked or hasn't actually changed.
1818 1819 1820 1821 1822 1823 1824 |
# File 'app/models/order.rb', line 1818 def add_customer_to_campaign return unless saved_change_to_source_id? return unless source return unless source.linked_to_campaign? source.add_customer_to_campaign(customer) end |
#add_item(sku, qty) ⇒ Array<Hash>?
Convenience for #add_multiple_items with a single sku/qty pair.
No-op when the order is in a locked state (post-pending).
3241 3242 3243 3244 3245 3246 |
# File 'app/models/order.rb', line 3241 def add_item(sku, qty) return if editing_locked? sku_array = [{ sku:, qty: }] add_multiple_items(sku_array) end |
#add_multiple_items(sku_array = []) ⇒ Array<Hash>
Add a batch of SKUs to the order in one save. Each entry is a
Hash with :sku and optional :qty (defaulting to 1):
[{sku: 'UDG4-4999', qty: 1}, {sku: 'SS-01', qty: 2}]. Bails
without raising when the order is locked or the cart already
has 100+ lines (bot suspicion). Triggers tier-2 / auto-coupon
recalculation on save and recovers from
index_discounts_unique_per_itemizable collisions by
reloading discounts and retrying.
3199 3200 3201 3202 3203 3204 3205 3206 3207 3208 3209 3210 3211 3212 3213 3214 3215 3216 3217 3218 3219 3220 3221 3222 3223 3224 3225 3226 3227 3228 3229 3230 3231 3232 3233 |
# File 'app/models/order.rb', line 3199 def add_multiple_items(sku_array = []) return if editing_locked? return if line_items.size > 100 # Then we suspect it's a bot adding things to the cart added_items = [] sku_array.each do |hsh| sku = hsh[:sku] qty = hsh[:qty] || 1 room_configuration_id = hsh[:room_configuration_id] ci = catalog.catalog_items.public_catalog_items.by_skus(sku).first raise Order::ItemNotFound, "#{sku} not found" if ci.blank? # Persist each line_item inline. Without this, add_line_item's # `line_items.reload` at the top of the next iteration discards the # in-memory build from this one — only the final SKU in the batch # would survive the post-loop save!. li = add_line_item(catalog_item_id: ci.id, quantity: qty, room_configuration_id:) added_items << { id: li.id, sku: li.sku, name: li.name, category: li.reported_category_name, quantity: li.quantity } end if added_items.present? self.recalculate_shipping = true # doesn't hurt to set it self.recalculate_discounts = true # ensure tier2 and auto-apply discounts are calculated self.force_total_reset = true begin save! rescue ActiveRecord::RecordNotUnique => e raise unless e..include?('index_discounts_unique_per_itemizable') discounts.reload save! end end added_items end |
#adjusted_actual_shipping_cost ⇒ Float
Like #calculate_actual_shipping_cost but applies per-delivery
accounting adjustments (recoveries, fuel surcharge corrections).
6183 6184 6185 |
# File 'app/models/order.rb', line 6183 def adjusted_actual_shipping_cost deliveries.invoiced.to_a.sum { |d| d.adjusted_actual_shipping_cost.to_f } end |
#all_deliveries_cancelable?(current_user = nil) ⇒ Object
Whether all deliveries cancelable.
1301 1302 1303 |
# File 'app/models/order.rb', line 1301 def all_deliveries_cancelable?(current_user = nil) deliveries.active.empty? || deliveries.active.all? { |d| d.cancelable?(current_user) } end |
#all_deliveries_invoiced? ⇒ Object
Whether all deliveries invoiced.
2888 2889 2890 |
# File 'app/models/order.rb', line 2888 def all_deliveries_invoiced? deliveries.active.present? && deliveries.active.all?(&:invoiced?) end |
#all_funds_available?(ignore_cod = false) ⇒ Object
Whether all funds available.
2881 2882 2883 |
# File 'app/models/order.rb', line 2881 def all_funds_available?(ignore_cod = false) balance(ignore_cod) <= 0 end |
#all_funds_not_available_and_shippable? ⇒ Object (protected)
Whether all funds not available and shippable.
6403 6404 6405 |
# File 'app/models/order.rb', line 6403 def all_funds_not_available_and_shippable? !all_funds_available?(true) && shipping_address && chosen_shipping_method.present? end |
#all_items_in_stock ⇒ Boolean
True when every line item has enough on-hand inventory at its
delivery's warehouse to fulfil immediately. Computed via
LineItem.inventory_check, which considers committed inventory,
transit, and minimum-on-hand thresholds.
2918 2919 2920 |
# File 'app/models/order.rb', line 2918 def all_items_in_stock stock_status == :ok end |
#all_participant_ids ⇒ Array<Integer>
Every party that should see this order in their CRM activity feed:
all opportunity participants plus the customer and contact.
1441 1442 1443 1444 1445 1446 1447 |
# File 'app/models/order.rb', line 1441 def all_participant_ids party_ids = [] party_ids += opportunity.all_participants.ids if opportunity party_ids << customer_id party_ids << contact_id party_ids.compact.uniq end |
#all_payments_are_valid? ⇒ Object
Whether all payments are valid.
1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 |
# File 'app/models/order.rb', line 1308 def all_payments_are_valid? return true unless payments.bread_payments.any? if payments.bread_payments.where(state: 'authorized').sum(:amount) >= total true else errors.add :base, 'Financed order cannot be modified to have a higher total than the existing payment. Please create a new order for the extra items or extra cost.' false end end |
#all_rooms_not_orderable? ⇒ Object
Whether all rooms not orderable.
2398 2399 2400 |
# File 'app/models/order.rb', line 2398 def all_rooms_not_orderable? room_configurations.any? && room_configurations.all? { |rc| !rc.orderable? } end |
#all_support_cases ⇒ Array<Integer>
Convenience for support_case_ids. Kept so the CRM views can use
a single accessor whether the support cases came from a HABTM or
via a custom join.
1920 1921 1922 1923 1924 |
# File 'app/models/order.rb', line 1920 def all_support_cases support_case_ids linked_support_cases.ids SupportCase.where(id: support_case_ids).order('support_cases.case_number desc') end |
#all_uploads ⇒ ActiveRecord::Relation<Upload>
Every Upload attached anywhere in the order tree — order
itself, its deliveries, and the shipments under those deliveries.
Pre-loads the polymorphic resource so the CRM's documents tab
can render attribution without N+1.
6227 6228 6229 6230 6231 6232 6233 6234 6235 6236 6237 |
# File 'app/models/order.rb', line 6227 def all_uploads ret_uploads = if delivery_ids.present? shipment_ids = Shipment.where(delivery_id: delivery_ids).ids conditions = ["(resource_type = 'Order' and resource_id = :order_id)", "(resource_type = 'Delivery' and resource_id IN (:delivery_ids))"] conditions << "(resource_type = 'Shipment' and resource_id IN (:shipment_ids))" if shipment_ids.any? Upload.where(conditions.join(' OR '), order_id: id, delivery_ids:, shipment_ids:) else uploads end ret_uploads.includes(:resource).order(Upload[:created_at].desc) end |
#allows_edi_split? ⇒ Object
Whether allows edi split.
5798 5799 5800 |
# File 'app/models/order.rb', line 5798 def allows_edi_split? edi_channel_order_support.present? && edi_channel_order_support.in?(%w[SPLIT_ORDERS SPLIT_ORDER_LINES]) end |
#already_has_smartinstall_request? ⇒ Object
Whether already has smartinstall request.
3453 3454 3455 |
# File 'app/models/order.rb', line 3453 def already_has_smartinstall_request? activities.where(activity_type_id: ActivityTypeConstants::LEAD_SSI).any? end |
#amazon_buy_shipping_eligible? ⇒ Boolean
Check if order is eligible for Amazon Buy Shipping early label purchase
5004 5005 5006 5007 5008 5009 5010 5011 5012 5013 5014 5015 |
# File 'app/models/order.rb', line 5004 def amazon_buy_shipping_eligible? return false unless is_edi_order? return false unless edi_orchestrator_partner&.start_with?('amazon_seller') begin orchestrator = Edi::Amazon::Orchestrator.new(edi_orchestrator_partner.to_sym) orchestrator&.buy_shipping_enabled? rescue StandardError => e Rails.logger.warn("[EarlyLabel] Error checking Amazon Buy Shipping eligibility: #{e.}") false end end |
#amzbs_packing_slip_included? ⇒ Boolean
Amazon Buy Shipping label PDFs include a packing slip page, so no
separate upload is needed when an AMZBS shipping option is selected.
2666 2667 2668 |
# File 'app/models/order.rb', line 2666 def amzbs_packing_slip_included? deliveries.any? { |d| d.shipping_option&.carrier == 'AmazonSeller' } end |
#any_rooms_not_orderable? ⇒ Object
Whether any rooms not orderable.
2389 2390 2391 2392 2393 |
# File 'app/models/order.rb', line 2389 def any_rooms_not_orderable? # (self.order_reception_type == "Online" or online_order?) and self.room_configurations.any?{|rc| (rc.room_layout_attached? and !rc.complete?)} # here we are now allowing direct order of online rooms via HW so relax the test for any orders with uncompleted rooms that have layouts room_configurations.any? { |rc| !rc.orderable? } end |
#applies_for_smartinstall ⇒ Boolean
Eligibility check for the SmartInstall service add-on (paid
in-home installation). Currently hard-coded to false pending the
service refactor mentioned inline; the legacy logic checks that
the customer is a homeowner inside service range, has selected
heated items, and doesn't already carry SmartInstall.
3436 3437 3438 3439 3440 3441 |
# File 'app/models/order.rb', line 3436 def applies_for_smartinstall return false # added by Roman Aug 19th until service refactor return true if installation_is_within_range? && customer.is_homeowner? && has_selected_heated_items? && doesnt_already_has_smartinstall? false end |
#apply_tier2_pricing? ⇒ Boolean
Whether or not to apply the tier2 pricing (customer discount) by default
3574 3575 3576 |
# File 'app/models/order.rb', line 3574 def apply_tier2_pricing? is_sales_order? end |
#attention_name ⇒ String?
"ATTN: …" line written on shipping labels. Returns the explicit
attention_name_override if the user set one, otherwise falls
back to #inherited_attention_name (the shipping address's
person name when the customer is an individual).
3819 3820 3821 |
# File 'app/models/order.rb', line 3819 def attention_name attention_name_override || inherited_attention_name end |
#attention_name=(value) ⇒ Object
Setter that stores the override unless the caller is sending the
literal sentinel string "inherited_attention_name" (used by the
CRM form to mean "go back to the inherited value"), in which case
we leave the override untouched.
3829 3830 3831 3832 3833 |
# File 'app/models/order.rb', line 3829 def attention_name=(value) return if inherited_attention_name && value =~ /inherited_attention_name/i self.attention_name_override = value end |
#auto_reserve_serial_numbers ⇒ Object
Walks every line item that requires serial-number reservation
(heating mats, cables) and asks the line to grab the next available
serial-number block from inventory. No-op for credit orders, which
don't ship physical product.
1725 1726 1727 1728 1729 |
# File 'app/models/order.rb', line 1725 def auto_reserve_serial_numbers return unless can_auto_reserve_serial_numbers? line_items.select(&:require_reservation?).each(&:auto_reserve_serial_numbers) end |
#awaiting_future_deliveries? ⇒ Object
Whether awaiting future deliveries.
5837 5838 5839 |
# File 'app/models/order.rb', line 5837 def awaiting_future_deliveries? awaiting_deliveries? && deliveries.for_future_release.present? end |
#balance(ignore_cod = false, excluding_payment: nil) ⇒ BigDecimal
Outstanding amount on the order: total of all deliveries minus
whatever payments have been authorised against each delivery
(capped per delivery so over-authorisation on one delivery doesn't
cancel out a balance owed on another). Always 0 for store
transfers (no money changes hands) and for COD-funded orders
unless ignore_cod: true is passed.
2789 2790 2791 2792 2793 2794 2795 2796 2797 2798 2799 2800 |
# File 'app/models/order.rb', line 2789 def balance(ignore_cod = false, excluding_payment: nil) return BigDecimal('0.0') if is_store_transfer? || (!ignore_cod && funded_by_cod?) total = deliveries.sum(:total) || BigDecimal('0.0') bal = total deliveries.each do |dq| dq_total = dq.total || BigDecimal('0.0') dq_auth = dq.(currency, excluding_payment: excluding_payment) || BigDecimal('0.0') bal -= [dq_auth, dq_total].min end bal < 0 ? BigDecimal('0.0') : bal end |
#belongs_to_smartservice_group? ⇒ Object
Whether belongs to smartservice group.
2973 2974 2975 |
# File 'app/models/order.rb', line 2973 def belongs_to_smartservice_group? is_smartfit_service? or is_smartinstall_service? or is_smartguide_service? or is_smartfix_service? end |
#billing_address ⇒ Object
Alias for Customer#billing_address
424 |
# File 'app/models/order.rb', line 424 delegate :catalog, :billing_address, :billing_entity, :company, to: :customer |
#billing_entity ⇒ Object
Alias for Customer#billing_entity
424 |
# File 'app/models/order.rb', line 424 delegate :catalog, :billing_address, :billing_entity, :company, to: :customer |
#build_activity ⇒ Activity
Builds (but does not save) a new Activity record attached to
this order with the order's primary party already populated.
Used by the CRM activity-form helpers to seed a new activity
without round-tripping through nested form params.
1911 1912 1913 |
# File 'app/models/order.rb', line 1911 def build_activity activities.build resource: self, party: primary_party end |
#build_early_label_carrier_info(label_result, delivery = nil) ⇒ Hash
Build carrier info hash for early label EDI confirm (Walmart only)
Delegates to ShipCodeMapper for carrier name + methodCode derivation,
matching the behavior of the normal (non-early) ship confirm flow
in ConfirmMessageProcessor#build_order_line_status -> ShipCodeMapper#carrier_info.
5756 5757 5758 5759 5760 5761 5762 5763 5764 5765 5766 5767 5768 5769 5770 5771 5772 5773 |
# File 'app/models/order.rb', line 5756 def build_early_label_carrier_info(label_result, delivery = nil) actual_carrier = label_result[:carrier] || 'OTHER' orchestrator = Edi::Walmart::Orchestrator.new(edi_orchestrator_partner.to_sym) mapper = orchestrator.ship_code_mapper carrier_code = mapper.carrier_code(actual_carrier) carrier_name = if carrier_code { carrier: carrier_code } else { otherCarrier: actual_carrier.to_s } end description = delivery&.selected_shipping_cost&.description method = mapper.extract_method_from_delivery(description, carrier_code || actual_carrier) { carrierName: carrier_name, methodCode: method } end |
#build_early_label_tracking_url(carrier, tracking_number) ⇒ String?
Build tracking URL for early label
5780 5781 5782 5783 5784 5785 5786 5787 5788 5789 5790 5791 5792 5793 |
# File 'app/models/order.rb', line 5780 def build_early_label_tracking_url(carrier, tracking_number) return nil if tracking_number.blank? case carrier&.downcase when /fedex/ "https://www.fedex.com/fedextrack/?trknbr=#{tracking_number}" when /ups/ "https://www.ups.com/track?tracknum=#{tracking_number}" when /usps/ "https://tools.usps.com/go/TrackConfirmAction?tLabels=#{tracking_number}" when /ontrac/ "https://www.ontrac.com/trackingdetail.asp?tracking=#{tracking_number}" end end |
#buying_group ⇒ BuyingGroup?
The associated buying group.
310 |
# File 'app/models/order.rb', line 310 belongs_to :buying_group, optional: true |
#calculate_actual_shipping_cost ⇒ Float
Sum of actual carrier-billed shipping cost on every invoiced
delivery (i.e. what we paid the carrier, not what the customer
was charged). Used by accounting reports to track shipping margin.
6175 6176 6177 |
# File 'app/models/order.rb', line 6175 def calculate_actual_shipping_cost deliveries.invoiced.to_a.sum { |d| d.actual_shipping_cost.to_f } end |
#can_auto_reserve_serial_numbers? ⇒ Object
Whether can auto reserve serial numbers.
1717 1718 1719 |
# File 'app/models/order.rb', line 1717 def can_auto_reserve_serial_numbers? order_type != 'CO' end |
#can_be_cancelled? ⇒ Object
Whether can be cancelled.
4238 4239 4240 4241 4242 4243 |
# File 'app/models/order.rb', line 4238 def can_be_cancelled? return false if editing_locked? return false if is_edi_order? && cancellation_reason.blank? cancelable? end |
#can_be_returned? ⇒ Object
Whether can be returned.
2150 2151 2152 |
# File 'app/models/order.rb', line 2150 def can_be_returned? is_regular_order? end |
#can_cr_hold?(current_user = nil) ⇒ Object
Whether can cr hold.
2457 2458 2459 |
# File 'app/models/order.rb', line 2457 def can_cr_hold?(current_user = nil) CAN_CR_HOLD_STATES.include?(state.to_sym) && all_deliveries_cancelable?(current_user) end |
#can_edit_future_release_date? ⇒ Object
Whether can edit future release date.
1354 1355 1356 |
# File 'app/models/order.rb', line 1354 def can_edit_future_release_date? awaiting_deliveries? && deliveries.for_future_release.any? end |
#cancel_deliveries_pre_pack ⇒ Object
Cancels the in-progress packaging-estimate work on every
pre_pack delivery. Called when the order is being held or
cancelled so the warehouse stops trying to compute box dimensions
for an order that won't ship.
5900 5901 5902 5903 5904 |
# File 'app/models/order.rb', line 5900 def cancel_deliveries_pre_pack deliveries.reload.each do |delivery| delivery.cancel_estimated_packaging if delivery.pre_pack? end end |
#cancel_or_destroy ⇒ Object
Carts get destroyed (no audit trail to keep). Real orders go
through the state-machine cancel event so cancellations are
logged and reversed properly.
4082 4083 4084 |
# File 'app/models/order.rb', line 4082 def cancel_or_destroy cart? ? destroy : cancel end |
#cancelable? ⇒ Object
Whether cancelable.
2464 2465 2466 |
# File 'app/models/order.rb', line 2464 def cancelable? CANCELABLE_STATES.include?(state.to_sym) && deliveries.non_quoting.empty? end |
#cannot_delete_reason(account = nil) ⇒ String?
Human-readable explanation of why an order cannot currently be
deleted, or nil when #ok_to_delete? would let it through.
Used by the CRM delete-button confirmation dialog.
3306 3307 3308 3309 3310 3311 3312 3313 3314 3315 3316 |
# File 'app/models/order.rb', line 3306 def cannot_delete_reason(account = nil) if editing_locked? 'Order cannot be deleted because it is in pending processing or beyond or referenced by authorizations.' elsif payments.any?(&:captured?) 'Order cannot be deleted because it is referenced by authorizations.' elsif line_items.any?(&:has_linked_unvoided_rma_item?) 'Order cannot be deleted until all linked RMA items have been voided.' elsif pending? || account&.is_admin? 'Orders that are non-draft can only be deleted by an admin' end end |
#cart_identifier ⇒ String
Stable identifier suitable for URLs and abandoned-cart emails:
the reference number once the order has one, otherwise a SC<id>
("Shopping Cart") fallback so brand-new carts still have a slug.
4250 4251 4252 |
# File 'app/models/order.rb', line 4250 def cart_identifier reference_number || "SC#{id}" end |
#cart_or_in_shipping_estimate? ⇒ Object
Whether cart or in shipping estimate.
4257 4258 4259 |
# File 'app/models/order.rb', line 4257 def cart_or_in_shipping_estimate? cart? || in_shipping_estimate? end |
#catalog ⇒ Object
Alias for Customer#catalog
424 |
# File 'app/models/order.rb', line 424 delegate :catalog, :billing_address, :billing_entity, :company, to: :customer |
#check_payments_status ⇒ void
This method returns an undefined value.
Reconcile the order's payments with the upstream gateways
(Stripe, PayPal). Captures funds we know were taken outside
Heatwave, kicks expired authorizations back to pending_payment,
and re-syncs prepayments to deliveries. No-op for orders whose
only payments are non-gateway types (PO, store credit, check,
echeck, etc.) because those don't have anything to reconcile.
1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 |
# File 'app/models/order.rb', line 1573 def check_payments_status # Sometimes we manually capture the funds in paypal or stripe and HW doesn't know about it. Or the authorization is expired, etc. # This method checks that and puts the payment in the right status return if payments.blank? return if exclude_from_payment_check # Skip payment verification for orders that only have non-gateway payment types # These payment types don't require reauthorization or gateway status checks: # - PO/VPO: Terms-based, pre-approved credit # - Advance Replacement & RMA Credit: Internal credits from RMA process # - Store Credit: Internal customer credit balance # - CHECK/CASH/WIRE: Manual payments processed offline # - ECHECK: Cannot be captured through gateway, requires manual processing # - BREAD/PLAID: No check methods implemented yet non_gateway_payment_types = [ Payment::PO, Payment::VPO, Payment::ADV_REPL, Payment::RMA_CREDIT, Payment::STORE_CREDIT, Payment::CHECK, Payment::CASH, Payment::WIRE, Payment::ECHECK, Payment::BREAD, Payment::PLAID ] return if payments.all? { |p| p.category.in?(non_gateway_payment_types) } # First credit cards payments.credit_cards.each(&:check_cc_payment_status) # Next Paypal payments payments.paypal_payments.each(&:check_paypal_payment_status) # Next paypal invoice payments.paypal_invoices.each(&:check_paypal_invoice_payment_status) # re-sync the prepayments to deliveries deliveries.each(&:relink_payments) # Reload to ensure we have fresh data after payment status checks # Payment status checks may have changed payment states, and we need # fresh delivery totals and payment associations reload deliveries.reload # after re-authorizing, check there are enough funds to cover the order total if all_funds_available? # nothing to do as all funds are available. This means the total of the order is either authorized or captured already. But not only captured. elsif can_pending_payment? && deliveries.none?(&:pre_pack?) && deliveries.none?(&:pending_manifest_completion?) OrdersMailer.order_with_insuficient_payment(self).deliver_later unless pending_payment? pending_payment! end end |
#check_sales_rep ⇒ Object (protected)
Validation callback that prevents an order from listing the same
rep as both primary and secondary, and from setting a secondary
rep without a primary. Skipped for store transfers (which have
no customer-side rep concept).
6390 6391 6392 6393 6394 6395 6396 6397 6398 |
# File 'app/models/order.rb', line 6390 def check_sales_rep # Skip sales rep validation for orders without customers (e.g., Store Transfers use from_store/to_store instead) return if customer.nil? errors.add('Sales rep', 'can only be primary or secondary sales rep for a given customer at a time') if (primary_sales_rep == secondary_sales_rep) && primary_sales_rep return unless secondary_sales_rep && !primary_sales_rep errors.add('Order', 'must first have a primary sales rep to have have a secondary sales rep') end |
#ci_invoice_credit_order? ⇒ Object
Whether ci invoice credit order.
3520 3521 3522 |
# File 'app/models/order.rb', line 3520 def ci_invoice_credit_order? order_type == CREDIT_ORDER && rma&.original_invoice&.invoice_type == Invoice::CI end |
#clear_shipped_date ⇒ Object
Resets the shipped_date column when an order needs to be
reverted to a pre-shipped state (e.g. cancellation after a partial
ship). No-op when nothing was set.
3353 3354 3355 |
# File 'app/models/order.rb', line 3353 def clear_shipped_date update_attribute(:shipped_date, nil) if shipped_date.present? end |
#closed_state? ⇒ Object
Whether closed state.
2428 2429 2430 |
# File 'app/models/order.rb', line 2428 def closed_state? CLOSED_STATES.include?(state.to_sym) end |
#communications ⇒ ActiveRecord::Associations::CollectionProxy
The associated communications.
391 |
# File 'app/models/order.rb', line 391 has_many :communications, as: :resource, dependent: :nullify |
#company ⇒ Object
Alias for Customer#company
424 |
# File 'app/models/order.rb', line 424 delegate :catalog, :billing_address, :billing_entity, :company, to: :customer |
#company_review_url ⇒ Object
Reviews.io dynamic link for company review using the order reference as order_id
and the customer CN number as customer_identifier (for CRM context).
1379 1380 1381 |
# File 'app/models/order.rb', line 1379 def company_review_url Api::ReviewsIo::DynamicLinkBuilder.for_order(self) end |
#company_review_url_for_confirmation(email: nil) ⇒ Object
Reviews.io dynamic link for the order confirmation / thank-you page.
Uses the customer email instead of the CN identifier.
1386 1387 1388 |
# File 'app/models/order.rb', line 1386 def company_review_url_for_confirmation(email: nil) Api::ReviewsIo::DynamicLinkBuilder.for_order_confirmation(self, email: email) end |
#complete? ⇒ Object
Whether complete.
2711 2712 2713 |
# File 'app/models/order.rb', line 2711 def complete? invoiced? end |
#completed_regular_deliveries ⇒ Array<Delivery>
Active deliveries that have actually shipped (parcel or freight)
vs ones that completed in some other way (warehouse pickup,
service-only). Used to decide partial-vs-full ship status.
6085 6086 6087 |
# File 'app/models/order.rb', line 6085 def completed_regular_deliveries deliveries.active.select(&:completed_regular_delivery?) end |
#completely_shipped? ⇒ Object
Whether completely shipped.
6099 6100 6101 |
# File 'app/models/order.rb', line 6099 def completely_shipped? deliveries.active.all?(&:completed_regular_delivery?) end |
#consolidate_revenue ⇒ Object
Snapshot the order's total and shipping cost at the moment the
customer completed checkout. The Google Ads conversion ping later
sends these values, and any discrepancy with the live total
(which may be re-tier'd by promotions) is investigated.
6152 6153 6154 6155 6156 6157 6158 6159 |
# File 'app/models/order.rb', line 6152 def consolidate_revenue # Method to save the order total that the customer paid at the time of checkout and compare this value to the # value we send as a conversion to google ads. return unless online_order? update_column(:revenue_consolidated_at_time_of_checkout, total) update_column(:shipping_cost_at_time_of_checkout, shipping_cost || 0.0) end |
#contact ⇒ Contact?
The associated contact.
298 |
# File 'app/models/order.rb', line 298 belongs_to :contact, inverse_of: :orders, optional: true |
#contact_combo ⇒ Object
Used by dynamic contact lookup on order creation allowing interaction with the tom-select input on account_managers.html.erb
Contact can either be existing (single integer value for contact_id) or new contact
1321 1322 1323 1324 1325 |
# File 'app/models/order.rb', line 1321 def contact_combo return unless contact_id "Contact|#{contact_id}" end |
#contact_combo=(val) ⇒ Object
Used by dynamic contact lookup on order creation allowing interaction with the tom-select input on account_managers.html.erb
Contact can either be existing (single integer value for contact_id) or new contact if val is in format Customer|customer_id|full_name
1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 |
# File 'app/models/order.rb', line 1330 def contact_combo=(val) if val.blank? self.contact = nil return end contact_name, self.contact_id = val.split('|') return unless contact_id.blank? && name.present? self.contact = Contact.create(name: contact_name.squish.titleize, customer_id:) end |
#contact_combo_for_select ⇒ Array<Array(String, String)>
Active contacts under this order's customer formatted for a Rails
select helper, with each value encoded as "Contact|<id>" so the
CRM "contact" field can distinguish between picking an existing
contact and typing a new name.
1347 1348 1349 |
# File 'app/models/order.rb', line 1347 def contact_combo_for_select customer.contacts.where(inactive: false).map { |cnt| [cnt.to_s, "Contact|#{cnt.id}"] } end |
#copy_customer_reps ⇒ Object
In-memory mirror of #copy_invoice_reps that pulls reps from the
current customer's reps_collaboration setup. Called during order
construction so a fresh order inherits whoever owns the customer.
1431 1432 1433 1434 1435 |
# File 'app/models/order.rb', line 1431 def copy_customer_reps self.primary_sales_rep_id = primary_sales_rep.try(:id) self.secondary_sales_rep_id = secondary_sales_rep.try(:id) self.local_sales_rep_id = local_sales_rep.try(:id) end |
#copy_invoice_reps ⇒ Object
Pulls primary / secondary / local sales-rep ids off the order's
invoice and writes them onto the order itself. Used when an order
is invoiced after rep reassignment so the order's commission split
matches the invoice that actually shipped.
1418 1419 1420 1421 1422 1423 1424 1425 1426 |
# File 'app/models/order.rb', line 1418 def copy_invoice_reps return unless (invoice = invoices.first) update( primary_sales_rep_id: invoice.primary_sales_rep_id, secondary_sales_rep_id: invoice.secondary_sales_rep_id, local_sales_rep_id: invoice.local_sales_rep_id ) end |
#copy_items_from(order) ⇒ Boolean
Copy non-shipping line items from another order (typically a guest cart)
into this one, then destroy the source if it is itself a cart.
Atomicity matters: this is the guest -> account cart transfer path that
runs silently on login (Authenticable#handle_cart_transfer). The previous
implementation called add_multiple_items and then unconditionally
order.destroy if order.cart? — so when add_multiple_items raised
Order::ItemNotFound, returned early on editing_locked? / 100-item guard,
or hit a save failure, the source cart was destroyed while the items
never made it into the target. The customer's items vanished at checkout
with no error to the user and (often) no exception report.
Now: bail out early on no-op conditions, wrap copy + destroy in a single
transaction, and on any failure leave both carts untouched and report the
error so we can see it in AppSignal.
1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 |
# File 'app/models/order.rb', line 1500 def copy_items_from(order) return false if order.nil? # Preserve room_configuration_id so the cart UI keeps grouping merged items # under their original room after a guest→account cart transfer. sku_array = order.line_items.non_shipping.parents_only.map do |li| { sku: li.sku, qty: li.quantity, room_configuration_id: li.room_configuration_id } end if sku_array.empty? order.destroy! if order.cart? return true end if editing_locked? ErrorReporting.warning( 'Order#copy_items_from aborted: target cart is editing_locked', source: :web, source_cart_id: order.id, target_cart_id: id, target_state: state ) return false end if line_items.size + sku_array.size > 100 ErrorReporting.warning( 'Order#copy_items_from aborted: would exceed 100 line items', source: :web, source_cart_id: order.id, target_cart_id: id, existing: line_items.size, incoming: sku_array.size ) return false end copied = false ActiveRecord::Base.transaction do copied = add_multiple_items(sku_array).present? unless copied ErrorReporting.warning( 'Order#copy_items_from aborted: add_multiple_items returned no items (race with editing_locked? or 100-item guard)', source: :web, source_cart_id: order.id, target_cart_id: id, target_state: state, target_line_item_count: line_items.size ) raise ActiveRecord::Rollback end order.destroy! if order.cart? end copied rescue Order::ItemNotFound, ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved, ActiveRecord::RecordNotDestroyed => e ErrorReporting.error( e, source: :web, source_cart_id: order&.id, target_cart_id: id, skus: sku_array.pluck(:sku) ) false end |
#copy_shipping_reference_number_to_deliveries ⇒ Object
After-save callback: propagate the order's
shipment_reference_number (used by Amazon FBA shipments and by
marketplace seller programs as the carrier BOL) onto every
delivery. Skipped unless the column actually changed AND has a
value.
1844 1845 1846 1847 1848 1849 1850 |
# File 'app/models/order.rb', line 1844 def copy_shipping_reference_number_to_deliveries # The shipment reference (or FBA ID) can be used as carrier_bol number when present # Only run when shipment_reference_number actually changed and is present return unless saved_change_to_shipment_reference_number? && shipment_reference_number.present? deliveries.update_all(carrier_bol: shipment_reference_number) end |
#country ⇒ Country?
The Country that owns this order's billing entity (resolved
through customer → catalog → store → country). Used by tax,
shipping, and locale-aware code paths. Returns nil if any link
in the chain is missing rather than raising.
2702 2703 2704 2705 2706 |
# File 'app/models/order.rb', line 2702 def country customer.catalog.store.country rescue StandardError nil end |
#create_credit_memo ⇒ void
This method returns an undefined value.
Spawn a credit-memo invoice off this order (e.g. when the order is
being voided after invoicing) and run it through TaxJar to back
out any tax already reported.
3505 3506 3507 3508 |
# File 'app/models/order.rb', line 3505 def create_credit_memo CreditMemo.new_credit_memo_from_order(self) credit_memo.evaluate_taxjar_submission end |
#create_smartfix_ticket ⇒ Object
Open a SmartFix service ticket (paid repair visit) and schedule
the service-confirmation activity.
3632 3633 3634 |
# File 'app/models/order.rb', line 3632 def create_smartfix_ticket new_ss_ticket('SmartFix', ActivityTypeConstants::SSFIX_SERVICE_CONFIRM) end |
#create_smartguide_ticket ⇒ Object
Open a SmartGuide service ticket (consultation). Picks the
on-site vs remote variant based on whether the order contains the
SGS_ONSITE_FIXRATE SKU.
3639 3640 3641 3642 3643 3644 3645 |
# File 'app/models/order.rb', line 3639 def create_smartguide_ticket if line_items.joins(:item).merge(Item.where(sku: 'SGS_ONSITE_FIXRATE')).any? new_ss_ticket('SmartGuide', ActivityTypeConstants::SGS_ONSITE_PREPLAN_MEET) else new_ss_ticket('SmartGuide', ActivityTypeConstants::SGS_REMOTE_PREPLAN_MEET) end end |
#create_smartinstall_ticket ⇒ Object
Open a SmartInstall service ticket against this order via the
shared #new_ss_ticket helper and schedule the kickoff
pre-installation phone activity.
3626 3627 3628 |
# File 'app/models/order.rb', line 3626 def create_smartinstall_ticket new_ss_ticket('SmartInstall', ActivityTypeConstants::SSI_PREPLAN_MEET) end |
#credit_memo ⇒ CreditMemo?
The associated credit memo.
351 |
# File 'app/models/order.rb', line 351 has_one :credit_memo, foreign_key: :credit_order_id |
#credit_memos ⇒ ActiveRecord::Associations::CollectionProxy
The associated credit memos.
373 |
# File 'app/models/order.rb', line 373 has_many :credit_memos, foreign_key: :original_order_id |
#crm_link ⇒ String
CRM-facing path to this order's show page (relative URL, suitable
for use in CRM emails, Slack notifications, and audit logs).
3276 3277 3278 |
# File 'app/models/order.rb', line 3276 def crm_link UrlHelper.instance.order_path(self) end |
#currency_symbol ⇒ String
Currency symbol ($, €, £, C$, …) for the order's currency,
resolved through the money gem's locale-aware Currency table.
2534 2535 2536 |
# File 'app/models/order.rb', line 2534 def currency_symbol Money::Currency.new(currency).symbol end |
#custom_shipping_labels ⇒ Object
Pulls all custom ship labels from the order, deliveries and shipments
4095 4096 4097 4098 4099 4100 4101 4102 4103 4104 4105 |
# File 'app/models/order.rb', line 4095 def custom_shipping_labels label_array = [] label_array += uploads.custom_ship_labels deliveries.each do |d| label_array += d.uploads.custom_ship_labels d.shipments.each do |shipment| label_array += shipment.uploads.custom_ship_labels end end label_array.compact.uniq end |
#customer ⇒ Customer?
The associated customer.
286 |
# File 'app/models/order.rb', line 286 belongs_to :customer, inverse_of: :orders, optional: true |
#customer_qualifies_for_free_online_shipping? ⇒ Object
Whether customer qualifies for free online shipping.
2170 2171 2172 |
# File 'app/models/order.rb', line 2170 def customer_qualifies_for_free_online_shipping? discounts.free_online_shipping.present? end |
#deep_dup ⇒ Order
Deep-clones the order (line items, discounts, EDI documents) but
resets the identity / lifecycle columns so the duplicate can be
saved as a fresh order. Records the source order's id on the copy
via #parent_id, marks the copy pending, and suppresses
shipping auto-detect so the caller can reconfigure shipping
explicitly. Used by "Duplicate Order" CRM action and by order
splits.
719 720 721 722 723 724 725 726 727 728 729 730 |
# File 'app/models/order.rb', line 719 def deep_dup deep_clone( include: %i[discounts edi_documents], except: %i[txid reference_number created_at creator_id edi_transaction_id edi_po_number] ) do |original, copy| if copy.is_a?(Order) copy.state = 'pending' copy.do_not_detect_shipping = true copy.parent_id = original.id end end end |
#default_billing_emails ⇒ Array<String>
Default email recipients for billing notifications: every email
contact point flagged as a billing notification channel for the
customer (or their billing entity), plus the tracking email,
contact emails, and any per-order overrides on billing_emails.
De-duplicates and sorts so the form renders deterministically.
3153 3154 3155 3156 3157 3158 3159 3160 |
# File 'app/models/order.rb', line 3153 def default_billing_emails emails = [] billing_customer = customer.billing_entity emails += NotificationChannel.joins(:contact_point).merge(ContactPoint.emails).where(customer_id: [customer_id, billing_customer&.id].compact.uniq).pluck(ContactPoint[:detail]) emails << billing_customer.contact_points.transmittable.emails.pick(:detail) if billing_customer.profile && (billing_customer.profile.homeowner? || billing_customer.profile.direct_pro?) emails.compact.uniq.sort end |
#default_credit_card_vault ⇒ CreditCardVault?
The associated default credit card vault.
323 |
# File 'app/models/order.rb', line 323 belongs_to :default_credit_card_vault, class_name: 'CreditCardVault', optional: true |
#deferred_payments_captured_and_ready_for_shipping? ⇒ Object
Whether deferred payments captured and ready for shipping.
2133 2134 2135 |
# File 'app/models/order.rb', line 2133 def deferred_payments_captured_and_ready_for_shipping? paypal_invoices_paid? && ready_for_shipping? end |
#deletable? ⇒ Object
Whether deletable.
2526 2527 2528 |
# File 'app/models/order.rb', line 2526 def deletable? %w[pending pending_payment crm_back_order in_cr_hold].include? state end |
#deliveries ⇒ ActiveRecord::Associations::CollectionProxy
The associated deliveries.
412 |
# File 'app/models/order.rb', line 412 has_many :deliveries, -> { order(:origin_address_id) }, dependent: :destroy, autosave: true |
#delivery_activities ⇒ ActiveRecord::Associations::CollectionProxy
The associated delivery activities.
364 |
# File 'app/models/order.rb', line 364 has_many :delivery_activities, class_name: 'Activity', through: :deliveries, source: :activities |
#direct_shipments ⇒ ActiveRecord::Associations::CollectionProxy
The associated direct shipments.
379 |
# File 'app/models/order.rb', line 379 has_many :direct_shipments, class_name: 'Shipment' |
#doesnt_already_has_smartinstall? ⇒ Object
Whether doesnt already has smartinstall.
3446 3447 3448 |
# File 'app/models/order.rb', line 3446 def doesnt_already_has_smartinstall? line_items.smartinstall_items.empty? end |
#download_and_store_early_label_pdf_atomic(shipper, label_result, is_amazon: false) ⇒ Upload?
SAFEGUARD A: Atomic PDF download and storage
Downloads and stores the label PDF with retries, ensuring it's persisted before returning.
This prevents the race condition where void happens before PDF is saved.
Amazon: label data is returned inline from create_label — store directly.
Walmart: requires a separate download_label API call with retries.
5424 5425 5426 5427 5428 5429 5430 5431 5432 5433 5434 5435 5436 5437 |
# File 'app/models/order.rb', line 5424 def download_and_store_early_label_pdf_atomic(shipper, label_result, is_amazon: false) tracking_number = label_result[:tracking_number] carrier = label_result[:carrier] marketplace = is_amazon ? 'Amazon' : 'Walmart' if Rails.env.development? Rails.logger.info('[EarlyLabel] Development mode - creating mock label PDF') return store_mock_early_label_pdf(tracking_number, carrier) end return store_amazon_label_pdf_atomic(label_result, tracking_number, marketplace) if is_amazon store_walmart_label_pdf_atomic(shipper, label_result, tracking_number, carrier, marketplace) end |
#drop_ship_purchase_orders ⇒ ActiveRecord::Associations::CollectionProxy
The associated drop ship purchase orders.
418 |
# File 'app/models/order.rb', line 418 has_many :drop_ship_purchase_orders, -> { order(:id) }, through: :deliveries |
#early_label_failure_notification(error_message) ⇒ Object
Send EDI admin notification when early label purchase fails
This ensures failures are not silent and the team is notified
4898 4899 4900 4901 4902 4903 4904 4905 4906 4907 4908 4909 4910 4911 4912 4913 4914 4915 4916 4917 4918 4919 4920 4921 4922 4923 4924 4925 4926 4927 |
# File 'app/models/order.rb', line 4898 def early_label_failure_notification() marketplace = edi_orchestrator_partner&.start_with?('amazon_seller') ? 'Amazon' : 'Walmart' portal = marketplace == 'Amazon' ? 'Amazon Seller Central' : 'Walmart Seller Portal' subject = "Early Label Purchase FAILED - Order #{reference_number}" = <<~MSG Early label purchase failed for #{marketplace} order #{reference_number}. ORDER DETAILS: - Order: #{reference_number} - PO Number: #{edi_po_number} - EDI Partner: #{edi_orchestrator_partner} - Order Link: https://#{CRM_HOSTNAME}/en-US/orders/#{id} ERROR: #{} ACTION REQUIRED: The early label was NOT purchased automatically. The warehouse will need to: 1. Purchase the label manually at ship-label time, OR 2. Purchase via #{portal} and upload as manual_ship_label This order will proceed through normal warehouse flow without early tracking. MSG EdiMailer.notify_edi_admin_of_warning(subject, ).deliver_later Rails.logger.warn("[EarlyLabel] Sent failure notification for order #{reference_number}") rescue StandardError => e Rails.logger.error("[EarlyLabel] Failed to send failure notification: #{e.}") end |
#early_label_flash_message ⇒ Hash
Generate flash messages based on early label purchase result
Called by controllers after order transitions to awaiting_deliveries
5100 5101 5102 5103 5104 5105 5106 5107 5108 5109 5110 5111 5112 5113 5114 5115 5116 5117 5118 5119 5120 5121 5122 5123 5124 5125 |
# File 'app/models/order.rb', line 5100 def return nil if early_label_purchase_result.blank? result = early_label_purchase_result marketplace = edi_orchestrator_partner&.start_with?('amazon_seller') ? 'Amazon' : 'Walmart' if result[:success] if result[:already_purchased] { type: :info, message: "Early shipping label already purchased - Tracking: #{result[:tracking_number]} (#{result[:carrier]})" } else { type: :success, message: "Early shipping label purchased successfully! Tracking: #{result[:tracking_number]} (#{result[:carrier]}) - Tracking has been sent to #{marketplace}." } end else { type: :error, message: "Failed to purchase early shipping label: #{result[:error]}. The order has been released but no tracking was sent to #{marketplace}." } end end |
#early_label_purchase_enabled_for_partner? ⇒ Boolean
Checks whether the partner's orchestrator has early label purchase enabled.
Walmart uses early_label_purchase_enabled?, Amazon uses buy_shipping_enabled?.
5019 5020 5021 5022 5023 5024 5025 5026 5027 5028 5029 5030 |
# File 'app/models/order.rb', line 5019 def early_label_purchase_enabled_for_partner? if edi_orchestrator_partner&.start_with?('walmart_seller') Edi::Walmart::Orchestrator.new(edi_orchestrator_partner.to_sym).early_label_purchase_enabled? elsif edi_orchestrator_partner&.start_with?('amazon_seller') Edi::Amazon::Orchestrator.new(edi_orchestrator_partner.to_sym).buy_shipping_enabled? else false end rescue StandardError => e Rails.logger.warn("[EarlyLabel] Error checking early label purchase enabled: #{e.}") false end |
#early_label_purchased_recently? ⇒ Boolean
Check if early label was purchased recently (within the rapid void threshold)
Used by SAFEGUARD B to prevent race conditions
4769 4770 4771 4772 4773 4774 |
# File 'app/models/order.rb', line 4769 def early_label_purchased_recently? return false if early_label_purchased_at.blank? minutes_since_purchase = (Time.current - early_label_purchased_at) / 60 minutes_since_purchase < EARLY_LABEL_RAPID_VOID_THRESHOLD_MINUTES end |
#early_label_shipments_match?(delivery) ⇒ Hash
Check if current shipments match the early label shipments
Used to detect if warehouse staff changed the packing
5363 5364 5365 5366 5367 5368 5369 5370 5371 5372 5373 5374 5375 5376 5377 5378 5379 5380 5381 5382 5383 5384 5385 5386 5387 5388 5389 5390 5391 5392 5393 5394 5395 5396 5397 5398 5399 5400 5401 5402 5403 5404 5405 5406 5407 5408 5409 5410 5411 |
# File 'app/models/order.rb', line 5363 def early_label_shipments_match?(delivery) return { match: true } unless has_early_purchased_label? current_shipments = delivery.shipments.non_voided.to_a early_count = early_label_shipments_count || 0 early_data = early_label_shipments_data || [] # Check shipment count if current_shipments.size != early_count return { match: false, reason: "Shipment count changed: early label was for #{early_count} shipment(s), now #{current_shipments.size}" } end # Check dimensions/weights for significant changes current_shipments.each_with_index do |shipment, idx| early_shipment = early_data[idx] next unless early_shipment # Check for significant dimension changes (more than 20% or 2 inches) %i[length width height].each do |dim| early_val = early_shipment[dim.to_s]&.to_f || 0 current_val = shipment.send(dim)&.to_f || 0 diff = (early_val - current_val).abs if diff > 2 && diff > (early_val * 0.2) return { match: false, reason: "Shipment #{idx + 1} #{dim} changed significantly: was #{early_val.round(1)}in, now #{current_val.round(1)}in" } end end # Check for significant weight changes (more than 20% or 1 lb) early_weight = early_shipment['weight']&.to_f || 0 current_weight = shipment.weight&.to_f || 0 weight_diff = (early_weight - current_weight).abs if weight_diff > 1 && weight_diff > (early_weight * 0.2) return { match: false, reason: "Shipment #{idx + 1} weight changed significantly: was #{early_weight.round(1)}lbs, now #{current_weight.round(1)}lbs" } end end { match: true } end |
#early_label_upload ⇒ Upload?
Get the early label upload (PDF) if it exists
5081 5082 5083 |
# File 'app/models/order.rb', line 5081 def early_label_upload uploads.in_category('early_ship_label').first end |
#echecks_requiring_authorization ⇒ Array<Payment>
eCheck payments on this order that the fraud-review pipeline has
flagged as needing accounting review before the order can leave
CR hold. Driven by payment.authorization_review[:required].
2495 2496 2497 |
# File 'app/models/order.rb', line 2495 def payments..where(category: Payment::ECHECK).select { |pp| pp.[:required] == true } end |
#edi_cancellation_reason_description ⇒ String
Customer-facing description of the EDI cancellation reason on the
order. Canadian Tire wants the raw 3-char code on the wire;
everywhere else we render "<code>: human-readable reason" so
warehouse staff can read it at a glance.
3881 3882 3883 3884 3885 3886 3887 3888 3889 |
# File 'app/models/order.rb', line 3881 def edi_cancellation_reason_description if is_canadian_tire? # just return 3 character reason code for Canadian Tire cancellation_reason else # here we only want to return a description for when we use a code, fallback to humanize "#{cancellation_reason}: #{(edi_cancellation_reasons.find { |arr| arr.last == cancellation_reason }&.first || cancellation_reason).to_s.humanize.downcase}" end end |
#edi_cancellation_reasons ⇒ Array<Array(String, String)>
Allowed cancellation-reason codes for the order's EDI partner,
formatted as [[label, code], …] for a Rails select. Each EDI
partner has its own enumeration; falls through to a generic set
for partners that don't pin codes.
3897 3898 3899 3900 3901 3902 3903 3904 3905 3906 3907 3908 3909 3910 3911 3912 3913 3914 3915 3916 3917 3918 3919 3920 3921 3922 3923 3924 3925 3926 3927 3928 3929 3930 3931 3932 3933 3934 3935 3936 3937 3938 3939 3940 3941 3942 3943 3944 3945 3946 3947 3948 3949 3950 3951 3952 3953 3954 3955 3956 3957 3958 3959 3960 3961 3962 3963 3964 3965 3966 3967 3968 3969 3970 3971 3972 3973 3974 3975 3976 3977 3978 3979 3980 3981 3982 3983 3984 3985 3986 3987 3988 3989 3990 3991 3992 3993 3994 3995 3996 3997 3998 3999 4000 4001 4002 4003 4004 4005 4006 4007 4008 4009 4010 4011 4012 4013 4014 4015 4016 4017 4018 4019 4020 4021 4022 4023 4024 4025 4026 4027 4028 4029 4030 4031 4032 4033 4034 4035 4036 4037 4038 4039 4040 4041 4042 4043 4044 4045 4046 4047 4048 4049 4050 4051 4052 |
# File 'app/models/order.rb', line 3897 def edi_cancellation_reasons if is_amazon_com? [ ['Shipping 100 percent of ordered product', '00'], ['Canceled due to missing/invalid SKU', '02'], ['Canceled out of stock', '03'], ['Buyer request', '99'], ['Canceled due to duplicate Amazon Ship ID', '04'], ['Canceled due to missing/invalid Bill To Location Code', '05'], ['Canceled due to missing/invalid Ship From Location Code', '06'], ['Canceled due to missing/invalid Customer Ship to Name', '07'], ['Canceled due to missing/invalid Customer Ship to Address Line 1 ', '08'], ['Canceled due to missing/invalid Customer Ship to City', '09'], ['Canceled due to missing/invalid Customer Ship to State', '10'], ['Canceled due to missing/invalid Customer Ship to Postal Code ', '11'], ['Canceled due to missing/invalid Customer Ship to Country Code ', '12'], ['Canceled due to missing/invalid Shipping Carrier/Shipping Method ', '13'], ['Canceled due to missing/invalid Unit Price', '20'], ['Canceled due to missing/invalid Ship to Address Line 2', '21'], ['Canceled due to missing/invalid Ship to Address Line 3', '22'], ['Canceled due to Tax Nexus Issue', '50'], ['Canceled due to Restricted SKU/Qty', '51'], ['Canceled due to USPS >$400', '53'], ['Canceled due to Missing AmazonShipID', '54'], ['Canceled due to Missing AmazonOrderID', '55'], ['Canceled due to Missing LineItemId', '56'], ['Canceled due to discontinued item', '71'] ] elsif is_home_depot_usa? [ ['Bad SKU', 'bad_sku'], ["Cancelled at Merchant's Request", 'merchant_request'], ['Invalid Item Cost', 'invalid_item_cost'], ['Invalid method of shipment', 'invalid_ship_method'], ['Merchant detected fraud', 'merchant_detected_fraud'], ['Order Info Missing', 'info_missing'], ['Out of Stock', 'out_of_stock'], ['Product Has Been Discontinued', 'discontinued'], ['Supplier detected fraud', 'supplier_detected_fraud'] ] elsif is_home_depot_can? [ ['Backorder Cancellation', 'backorder_cancel'], ['Bad Address', 'bad_address'], ['Bad SKU', 'bad_sku'], ["Cancelled at Merchant's Request", 'merchant_request'], ['Cannot fulfill the order in time', 'fulfill_time_expired'], ['Cannot Ship as Ordered', 'cannot_meet_all_reqs'], ['Cannot ship to Country', 'cant_shipto_country'], ['Cannot ship to PO Box', 'cannot_shipto_POBOX'], ['Customer Refused Delivery', 'customer_refused'], ['Duplicate Order', 'duplicate_order'], ['Order Entry Error', 'order_entry_error'], ['Order Info Missing', 'info_missing'], ['Other', 'other'], ['Product Has Been Discontinued', 'discontinued'] ] elsif is_costco_ca? [ ['Bad Address', 'bad_address'], ['Bad SKU', 'bad_sku'], ["Cancelled at Merchant's Request", 'merchant_request'], ['Carrier does not service delivery location', 'carrier_does_not_service_area'], ['Customer Changed Mind', 'customer_request'], ['Duplicate Order', 'duplicate_order'], ['Minimum Order Not Met', 'min_order_not_met'], ['Order Entry Error', 'order_entry_error'], ['Order Info Missing', 'info_missing'], ['Out of Stock', 'out_of_stock'], ['Product Has Been Discontinued', 'discontinued'], ['To close order and allow reissue', 'close_and_reissue'], ['Unable to contact recipient', 'unable_to_contact_recipient'] ] elsif is_part_of_rona? [ ['Bad Address', 'bad_address'], ['Bad SKU', 'bad_sku'], ["Cancelled at Merchant's Request", 'merchant_request'], ['Cannot fulfill the order in time', 'fulfill_time_expired'], ['Cannot Ship as Ordered', 'cannot_meet_all_reqs'], ['Customer Changed Mind', 'customer_request'], ['Invalid Item Cost', 'invalid_item_cost'], ['Order Info Missing', 'info_missing'], ['Out of Stock', 'out_of_stock'], ['Product Has Been Discontinued', 'discontinued'] ] elsif is_part_of_lowes_com? [ ['Bad SKU', 'bad_sku'], ["Cancelled at Merchant's Request", 'merchant_request'], ['Minimum Order Not Met', 'min_order_not_met'], ['Other', 'other'], ['Invalid Item Cost', 'invalid_item_cost'], ['Out of Stock', 'out_of_stock'], ['Product Has Been Discontinued', 'discontinued'] ] elsif is_walmart_ca? [ ['Backorder Cancellation', 'backorder_cancel'], ['Bad SKU', 'bad_sku'], ["Cancelled at Merchant's Request", 'merchant_request'], ['Customer Changed Mind', 'customer_request'], ['Out of Stock', 'out_of_stock'], ['Product Has Been Discontinued', 'discontinued'] ] elsif edi_orchestrator_partner == 'walmart_seller_us' [ ['Backorder Cancellation', 'backorder_cancel'], ['Bad SKU', 'bad_sku'], ["Cancelled at Merchant's Request", 'merchant_request'], ['Customer Changed Mind', 'customer_request'], ['Out of Stock', 'out_of_stock'], ['Product Has Been Discontinued', 'discontinued'] ] elsif is_canadian_tire? [ ['Out of Stock', 'W01'], ['Not Enough Stock', 'W13'], ['Discontinued Item', 'A83'], ['Incorrect Address', 'A03'], ['Invalid Ship Instructions', '051'], ["Can't Ship on Time", 'D50'], ['Cancelled at Retailer Request', 'ABN'], ['Other', 'A13'], ['Bad Sku', 'A80'], ['Cannot ship to country', 'A05'], ['Cannot ship to PO box', 'A06'], ['Cannot ship USPS', 'A82'], ['Carrier does not service delivery location', 'D01'], ['Duplicate order', 'A07'], ['Invalid UOM', 'SOW'], ['Item Recall', 'IV1'], ['Minimum order not met', 'MIN'], ['Order entry error', 'W05'], ['Order info missing', 'B14'], ['Preorder cancellation', 'POA'], ['Fraud', '030'], ['To close order and allow reissue', 'RUN'], ['Unable to contact recipient', 'A58'] ] else %w[ bad_address bad_sku merchant_request carrier_wont_svc_loc customer_request customer_refused duplicate_order info_missing out_of_stock discontinued close_and_reissue ] end end |
#edi_communication_logs ⇒ ActiveRecord::Associations::CollectionProxy
The associated edi communication logs.
409 |
# File 'app/models/order.rb', line 409 has_many :edi_communication_logs, through: :edi_documents |
#edi_documents ⇒ ActiveRecord::Associations::CollectionProxy
The associated edi documents.
406 |
# File 'app/models/order.rb', line 406 has_many :edi_documents, dependent: :destroy |
#edi_force_price_match ⇒ Array<Array<String>>
Force every parent line item with an EDI unit cost to match it on
both the MSRP price and the discounted price. Used to align an
EDI-imported order back to the partner's quoted price after the
CRM has re-sourced or repriced items.
2293 2294 2295 2296 2297 2298 2299 |
# File 'app/models/order.rb', line 2293 def edi_force_price_match errs = [] line_items.parents_only.where.not(edi_unit_cost: nil).find_each do |li| errs << li.errors. unless li.update(price: li.edi_unit_cost) && li.update(discounted_price: li.edi_unit_cost) end errs end |
#edi_orchestrator ⇒ Edi::BaseOrchestrator?
The Edi::BaseOrchestrator subclass responsible for this order's
EDI partner, if it has one. Returns nil for non-EDI orders.
6326 6327 6328 6329 |
# File 'app/models/order.rb', line 6326 def edi_orchestrator # simple order to orchestrator mapping, nil otherwise Edi::BaseOrchestrator.orchestrator_for_customer_id(customer_id) if edi_transaction_id end |
#edi_price_matcheable? ⇒ Object
Whether edi price matcheable.
2276 2277 2278 2279 2280 2281 2282 2283 2284 |
# File 'app/models/order.rb', line 2276 def edi_price_matcheable? return false unless is_edi_order? target_lines = line_items.goods.parents_only return false if target_lines.blank? return false unless target_lines.all?(&:edi_unit_cost) target_lines.any? { |li| li.price != li.edi_unit_cost || li.discounted_price != li.edi_unit_cost } end |
#editing_locked? ⇒ Object
Whether editing locked.
2421 2422 2423 |
# File 'app/models/order.rb', line 2421 def editing_locked? (LOCKED_STATES.include?(state.to_sym) || purchase_order&.should_lock_linked_order?).to_b end |
#effective_date_for_coupon ⇒ Date
Date used by the coupon engine to evaluate "is this coupon
currently valid?" against start_date/end_date windows. Carts
use today; existing orders use the order's override_coupon_date
(if any) or the created_at so the coupon evaluation is stable
after the order is placed even if the coupon's window later moves.
1672 1673 1674 1675 1676 |
# File 'app/models/order.rb', line 1672 def effective_date_for_coupon return Date.current if cart? override_coupon_date.presence || created_at.try(:to_date) || Date.current end |
#email_for_order_confirmation ⇒ String?
First non-blank email for the order's confirmation flow,
checking (in order): the contact/customer's stored email, then
the customer's email, then the contact's account email, then the
customer's account email. Returns nil if every lookup fails.
3801 3802 3803 3804 3805 3806 3807 3808 3809 3810 3811 |
# File 'app/models/order.rb', line 3801 def email_for_order_confirmation party_for_order_confirmation.email || customer.email || (begin party_for_order_confirmation.account.email rescue StandardError nil end) || (begin customer.account.email rescue StandardError nil end) end |
#email_options_for_tracking_email ⇒ Array<String>
Choices for the "tracking emails" multi-select on the order form:
every email the customer has on file, plus whatever's currently
set on tracking_email (which may include free-typed addresses
not yet on the customer record).
4167 4168 4169 |
# File 'app/models/order.rb', line 4167 def [customer.all_emails, tracking_email].flatten.compact.uniq end |
#empty? ⇒ Object
Whether empty.
2519 2520 2521 |
# File 'app/models/order.rb', line 2519 def empty? line_items.non_shipping.empty? end |
#encrypted_id ⇒ String
Reversibly encrypted form of the order's database id, used in
public-facing URLs (payment link, cart-recovery emails) so the id
doesn't appear directly in logs or share links.
6029 6030 6031 |
# File 'app/models/order.rb', line 6029 def encrypted_id Encryption.encrypt_string(id.to_s) end |
#errors_with_deliveries_errors ⇒ Array<String>
Combined errors from the order itself and from each of its
deliveries, suitable for surfacing to the CRM "save failed"
banner so users see every reason at once.
6192 6193 6194 |
# File 'app/models/order.rb', line 6192 def errors_with_deliveries_errors (errors. + deliveries.map { |d| d.errors. }).flatten end |
#estimate_next_available_date_from_out_of_stock_items ⇒ Date?
Latest "next available" date across every fully out-of-stock
line item (or nil if all items have stock). Used to estimate when
a back-ordered order can ship. Bounded recursion (max_depth: 10)
protects against pathological substitution chains in the catalog.
5946 5947 5948 5949 5950 5951 5952 5953 5954 |
# File 'app/models/order.rb', line 5946 def estimate_next_available_date_from_out_of_stock_items # this returns nil if no items are out of stock, or latest available store item's next available date line_items.goods.select do |li| li.stock_status == :none end.filter_map do |li| # Use depth-limited version to prevent infinite recursion li.catalog_item.store_item.next_available_with_depth_limit(max_depth: 10)&.next_available_date end.max end |
#find_gbraid ⇒ String?
Same fallback chain as #find_gclid for the Android-app
web-to-app gbraid token used by Google Ads.
6270 6271 6272 6273 6274 6275 6276 |
# File 'app/models/order.rb', line 6270 def find_gbraid visit&. || quote&.visit&. || opportunity&.visit&. || customer&.visit&. || customer&.visits&.last_marketing_value(:gbraid) end |
#find_gclid ⇒ String?
Best-effort Google Click ID for this order. Walks the order's
visit, then the quote's visit, then the opportunity's visit,
then the customer-level captures so a conversion ping always has
an attribution token if any was ever recorded.
6245 6246 6247 6248 6249 6250 6251 6252 |
# File 'app/models/order.rb', line 6245 def find_gclid visit&. || quote&.visit&. || opportunity&.visit&. || customer&.gclid || customer&.visit&. || customer&.visits&.last_marketing_value(:gclid) end |
#find_msclkid ⇒ String?
Microsoft Ads (Bing) click identifier, captured by the Tracker into
Visit#marketing_meta['msclkid'] (JSONB-only). Keys the offline-conversion
upload in MicrosoftAds::ConversionReporter.
Mirrors #find_oppref but adds a landing-page fallback: visits created
before the msclkid-capture deploy carry the id in ?msclkid= on
landing_page (not yet in marketing_meta), so parse it back out across
the ~90-day offline-conversion window.
6308 6309 6310 6311 6312 6313 6314 6315 6316 6317 6318 6319 6320 |
# File 'app/models/order.rb', line 6308 def find_msclkid visit_msclkid = lambda do |v| next nil unless v (v. || {})['msclkid'].presence || v.landing_page_params['msclkid'].presence end visit_msclkid.call(visit) || visit_msclkid.call(quote&.visit) || visit_msclkid.call(opportunity&.visit) || visit_msclkid.call(customer&.visit) || customer&.visits&.last_marketing_value(:msclkid) end |
#find_oppref ⇒ String?
OpenAI Ads (ChatGPT) click identifier, captured by the Tracker into
Visit#marketing_meta['oppref'] (JSONB-only — no scalar column). Used
by the CAPI reporter to populate the event's top-level oppref field,
which OpenAI requires us to forward ourselves on server-side events.
Fallback chain mirrors #find_gclid but queries the JSONB blob instead
of a scalar column; the last-resort SQL lookup uses GIN-indexed
marketing_meta @> '{"oppref": ...}' containment.
6288 6289 6290 6291 6292 6293 6294 6295 6296 |
# File 'app/models/order.rb', line 6288 def find_oppref visit_oppref = ->(v) { (v&. || {})['oppref'].presence } visit_oppref.call(visit) || visit_oppref.call(quote&.visit) || visit_oppref.call(opportunity&.visit) || visit_oppref.call(customer&.visit) || customer&.visits&.last_marketing_value(:oppref) end |
#find_wbraid ⇒ String?
Same fallback chain as #find_gclid for the iOS-app web-to-app
wbraid token used by Google Ads.
6258 6259 6260 6261 6262 6263 6264 |
# File 'app/models/order.rb', line 6258 def find_wbraid visit&. || quote&.visit&. || opportunity&.visit&. || customer&.visit&. || customer&.visits&.last_marketing_value(:wbraid) end |
#fire_early_label_edi_confirm(delivery, label_result) ⇒ Object
Fire EDI ship confirm with early label tracking info.
This sends tracking to the marketplace immediately, without waiting for warehouse processing.
Branches for Amazon (packageDetail format) vs Walmart (orderShipment format).
5601 5602 5603 5604 5605 5606 5607 5608 5609 5610 5611 5612 5613 5614 5615 5616 5617 5618 5619 5620 |
# File 'app/models/order.rb', line 5601 def fire_early_label_edi_confirm(delivery, label_result) return unless is_edi_order? Rails.logger.info("[EarlyLabel] Firing EDI ship confirm for order #{reference_number}") begin if edi_orchestrator_partner&.start_with?('amazon_seller') # Buy Shipping V2 purchaseShipment automatically notifies Amazon about the # shipment — a separate confirmShipment via the Orders API is redundant and # fails with "PackageToUpdateNotFound" because Buy Shipping manages packages # in a different subsystem than the Orders API expects. Rails.logger.info("[EarlyLabel] Skipping EDI ship confirm for Amazon Buy Shipping order #{reference_number} — Buy Shipping V2 handles notification automatically") else fire_early_label_edi_confirm_walmart(delivery, label_result) end rescue StandardError => e Rails.logger.error("[EarlyLabel] Failed to fire EDI ship confirm for order #{reference_number}: #{e.}") Rails.logger.error(e.backtrace.first(5).join("\n")) end end |
#fire_early_label_edi_confirm_amazon(delivery, label_result) ⇒ Object
Amazon early label ship confirm — mirrors Edi::Amazon::ConfirmMessageProcessor#acknowledge_order
but builds the message from order data since no shipment record exists yet.
5626 5627 5628 5629 5630 5631 5632 5633 5634 5635 5636 5637 5638 5639 5640 5641 5642 5643 5644 5645 5646 5647 5648 5649 5650 5651 5652 5653 5654 5655 5656 5657 5658 5659 5660 5661 5662 5663 5664 5665 5666 5667 5668 5669 5670 5671 5672 5673 5674 5675 5676 5677 |
# File 'app/models/order.rb', line 5626 def fire_early_label_edi_confirm_amazon(delivery, label_result) orchestrator = Edi::Amazon::Orchestrator.new(edi_orchestrator_partner.to_sym) label_data = label_result.respond_to?(:to_h) ? label_result.to_h : label_result tracking_number = label_data[:tracking_number] effective_carrier = label_data[:carrier] carrier_code = Edi::Amazon::ConfirmMessageProcessor::CARRIER_TO_CARRIER_CODE_MAP_HASH[effective_carrier.to_sym] || 'Other' = raise 'edi_original_order_message is blank — cannot build ship confirm' if .blank? order_hash = JSON.parse().with_indifferent_access order_items = (order_hash[:OrderItems] || []).map do |item| { orderItemId: item[:OrderItemId], quantity: (item[:QuantityOrdered] || 1).to_i } end = { packageDetail: { packageReferenceId: '1', carrierCode: carrier_code, carrierName: effective_carrier, shippingMethod: delivery.shipping_method_friendly, trackingNumber: tracking_number, shipDate: Time.current.iso8601, orderItems: order_items }, codCollectionMethod: 'DirectPayment', marketplaceId: orchestrator.marketplace } ecl = EdiCommunicationLog.create!( partner: orchestrator.partner, category: 'order_confirm', data: .to_json, data_type: 'json', file_info: { order_id: id, reference_number: reference_number, lines_confirmed: order_items.size, early_label: true }, transaction_id: edi_transaction_id, transmit_datetime: Time.current ) ecl.edi_documents.create!(order: self) Rails.logger.info("[EarlyLabel] Created Amazon EDI ship confirm ECL #{ecl.id} for order #{reference_number}") # Immediately send via ConfirmMessageSender (POSTs to orders/v0/orders/{orderId}/shipmentConfirmation) orchestrator..process(ecl) end |
#fire_early_label_edi_confirm_walmart(delivery, label_result) ⇒ Object
Walmart early label ship confirm — original Walmart-specific flow
5682 5683 5684 5685 5686 5687 5688 5689 5690 5691 5692 5693 5694 5695 5696 5697 5698 5699 5700 5701 5702 5703 5704 5705 5706 5707 5708 5709 5710 5711 5712 5713 5714 5715 5716 5717 5718 5719 5720 5721 5722 5723 5724 5725 5726 5727 5728 5729 5730 5731 5732 5733 5734 5735 5736 5737 5738 5739 5740 5741 5742 5743 5744 5745 |
# File 'app/models/order.rb', line 5682 def fire_early_label_edi_confirm_walmart(delivery, label_result) orchestrator = Edi::Walmart::Orchestrator.new(edi_orchestrator_partner.to_sym) = raise 'edi_original_order_message is blank — cannot build Walmart ship confirm' if .blank? order_hash = JSON.parse().with_indifferent_access order_lines = order_hash.dig(:orderLines, :orderLine) || [] label_data = label_result.respond_to?(:to_h) ? label_result.to_h : label_result carrier = label_data[:carrier] tracking_number = label_data[:tracking_number] ship_datetime = Time.current.iso8601 carrier_info = build_early_label_carrier_info(label_data, delivery) tracking_url = build_early_label_tracking_url(carrier, tracking_number) shipped_order_lines = order_lines.map do |line| line_number = line[:lineNumber] quantity = line.dig(:orderLineQuantity, :amount) || '1' { lineNumber: line_number, orderLineStatuses: { orderLineStatus: [ { status: 'Shipped', statusQuantity: { unitOfMeasurement: 'EACH', amount: quantity.to_s }, trackingInfo: { shipDateTime: ship_datetime, carrierName: carrier_info[:carrierName], methodCode: carrier_info[:methodCode], trackingNumber: tracking_number, trackingURL: tracking_url }.compact } ] } } end = { orderShipment: { orderLines: { orderLine: shipped_order_lines } } } ecl = EdiCommunicationLog.create!( partner: orchestrator.partner, category: 'order_confirm', data: .to_json, data_type: 'json', file_info: { order_id: id, reference_number: reference_number, lines_confirmed: shipped_order_lines.size, early_label: true }, transaction_id: edi_transaction_id, transmit_datetime: Time.current ) ecl.edi_documents.create!(order: self) Rails.logger.info("[EarlyLabel] Created Walmart EDI ship confirm ECL #{ecl.id} for order #{reference_number}") ecl.process end |
#first_po_number ⇒ String?
First non-blank PO number across the order's payments. Used in
places that need just one PO for display where #po_number would
otherwise return a comma-separated list.
2839 2840 2841 |
# File 'app/models/order.rb', line 2839 def first_po_number payments.find(&:po_number)&.po_number end |
#first_tracking_email ⇒ String?
Single-string variant of #tracking_email (which is an array
column). Used in carrier API payloads that accept exactly one
notification email per shipment.
6108 6109 6110 |
# File 'app/models/order.rb', line 6108 def first_tracking_email tracking_email&.first end |
#fix_future_release_date ⇒ Hash
Reconciles requested_ship_on_or_after, future_release_date,
and requested_ship_before so they don't contradict each other:
bumps future_release_date forward to honour the
ship-on-or-after date, and clears requested_ship_before when
the user explicitly sets a future-release date past it.
Returns a { message: "…" } hash describing any change so the
CRM can flash the user.
6205 6206 6207 6208 6209 6210 6211 6212 6213 6214 6215 6216 6217 6218 6219 |
# File 'app/models/order.rb', line 6205 def fix_future_release_date res = {} if requested_ship_on_or_after && (requested_ship_on_or_after > Date.current) && (future_release_date.nil? || (future_release_date < requested_ship_on_or_after)) # check if we are shipping too soon and don't have a future date set or have one set too soon res[:message] = "Order ship on or after date was set past today (#{requested_ship_on_or_after}) and order future release date is set to #{future_release_date || 'none'}, so order future release date was updated to match." update(future_release_date: requested_ship_on_or_after) end # If future_release_date is after requested_ship_before, the user explicitly set a future hold. # Clear requested_ship_before since it conflicts with the intentional future release. # (Previously this would override the user's future_release_date, causing unexpected immediate release) if requested_ship_before && future_release_date && (future_release_date > requested_ship_before) res[:message] = "Order future release date (#{future_release_date}) is after the ship before date (#{requested_ship_before}). Clearing ship before date to respect the future release hold." update(requested_ship_before: nil) end res end |
#formatted_po_number ⇒ String
po_number with the customer's preferred prefix applied (e.g.
"PO# 12345" vs bare "12345"), driven by
customer.include_po_prefix?. Used in CRM list views and emails.
3383 3384 3385 |
# File 'app/models/order.rb', line 3383 def formatted_po_number po_number(include_po_prefix: include_po_prefix?) end |
#from_store ⇒ Store?
The associated from store.
335 |
# File 'app/models/order.rb', line 335 belongs_to :from_store, class_name: 'Store', optional: true |
#fully_funded_by_advance_replacement? ⇒ Object
Whether fully funded by advance replacement.
2866 2867 2868 |
# File 'app/models/order.rb', line 2866 def fully_funded_by_advance_replacement? deliveries.any? && deliveries.all? { |dq| dq.payments.any? && dq.payments.all? { |pp| pp.category == Payment::ADV_REPL } } end |
#funded_by_advance_replacement? ⇒ Object
Whether funded by advance replacement.
2873 2874 2875 |
# File 'app/models/order.rb', line 2873 def funded_by_advance_replacement? deliveries.any? { |_dq| payments.any? { |pp| pp.category == Payment::ADV_REPL } } end |
#funded_by_cod? ⇒ Object
Whether funded by cod.
2859 2860 2861 |
# File 'app/models/order.rb', line 2859 def funded_by_cod? deliveries.all?(&:funded_by_cod?) && deliveries.any? end |
#funds_available_and_ready_for_warehouse? ⇒ Object (protected)
Whether funds available and ready for warehouse.
6410 6411 6412 |
# File 'app/models/order.rb', line 6410 def funds_available_and_ready_for_warehouse? all_funds_available? && ready_for_warehouse? end |
#funds_shortfall_report(excluding_payment: nil) ⇒ String
Human-readable, per-delivery breakdown of why #all_funds_available? is
false: each under-covered delivery's total vs. authorized funds and the
shortfall. Lets ops distinguish pricing drift (a total inflated past the
authorization) from a genuine underpayment, instead of chasing a bare
"insufficient funds" error (SO728077, defect H).
2810 2811 2812 2813 2814 2815 2816 2817 2818 2819 |
# File 'app/models/order.rb', line 2810 def funds_shortfall_report(excluding_payment: nil) deliveries.filter_map do |dq| dq_total = dq.total || BigDecimal('0.0') dq_auth = dq.(currency, excluding_payment:) || BigDecimal('0.0') short = dq_total - dq_auth next if short <= 0 format('delivery #%s short $%.2f (total $%.2f vs authorized $%.2f)', dq.id, short, dq_total, dq_auth) end.join('; ') end |
#generate_spiff_training_activity ⇒ Activity
Schedules a 7-day-out follow-up training activity for the primary
sales rep so they walk a SPIFF-eligible customer through their
first WarmlyYours order. Called automatically when
#needs_spiff_training? returns true.
1981 1982 1983 |
# File 'app/models/order.rb', line 1981 def generate_spiff_training_activity Activity.create(activity_type_id: ActivityTypeConstants::SPIFFACTTRAIN, target_datetime: 7.days.from_now, assigned_resource: primary_sales_rep, party: customer, resource: self) end |
#get_expected_ship_date_time ⇒ Time
Estimated delivery time = scheduled ship time +
carrier-committed transit days from the selected shipping
option. Falls back to a 4-working-day commitment when no rate
has been picked yet.
5989 5990 5991 5992 |
# File 'app/models/order.rb', line 5989 def get_expected_ship_date_time days_committment = (deliveries.first&.selected_shipping_cost&.days_commitment || 4.0).ceil # put in some fallback of 4 days days_committment.working.days.since(get_scheduled_ship_date_time) end |
#get_scheduled_ship_date_time ⇒ Time
When the order is expected to be picked up by the carrier. Honors
future_release_date when set, falls back to today's 15:55 cutoff
in the store's local timezone, and rolls forward to the next
working day at 10:00 if we're already past cutoff or on a
weekend. Back-orders use the latest stock-available date.
5963 5964 5965 5966 5967 5968 5969 5970 5971 5972 5973 5974 5975 5976 5977 5978 5979 5980 5981 |
# File 'app/models/order.rb', line 5963 def get_scheduled_ship_date_time order_ship_date_time = future_release_date if future_release_date.present? && (future_release_date > Time.current) if crm_back_order? order_ship_date_time = estimate_next_available_date_from_out_of_stock_items || 10.working.days.since(Time.current) # is 10 days a good fallback? elsif order_ship_date_time.nil? Time.use_zone(store.time_zone_string) do # WY Canada is Eastern Time, Wy US is Central today_cut_off_time = Time.zone.parse('15:55:00') order_ship_date = (future_release_date || today_cut_off_time).strftime('%Y-%m-%d') # use future release date if present or today_cut_off_time order_ship_time = today_cut_off_time.strftime('%R:%S') if Time.current > today_cut_off_time || today_cut_off_time.on_weekend? early_pickup_time = Time.zone.parse('10:00:00') order_ship_date = 1.working.day.since(early_pickup_time).strftime('%Y-%m-%d') order_ship_time = early_pickup_time.strftime('%R:%S') end order_ship_date_time = Time.zone.parse("#{order_ship_date} #{order_ship_time}") end end order_ship_date_time.to_time end |
#has_authorized_payment? ⇒ Object
Whether has authorized payment.
1561 1562 1563 |
# File 'app/models/order.rb', line 1561 def payments..any? end |
#has_committed_serial_number_reservations? ⇒ Object
Whether has committed serial number reservations.
3127 3128 3129 |
# File 'app/models/order.rb', line 3127 def has_committed_serial_number_reservations? line_items.any? { |li| li.require_reservation? && !li.all_reserved_serial_numbers_available? } end |
#has_custom_packing_slip? ⇒ Object
Whether has custom packing slip.
2660 2661 2662 |
# File 'app/models/order.rb', line 2660 def has_custom_packing_slip? uploads.in_category('custom_packing_slip_pdf').present? || amzbs_packing_slip_included? end |
#has_early_purchased_label? ⇒ Boolean
Check if order has an active (non-voided) early-purchased label
4932 4933 4934 |
# File 'app/models/order.rb', line 4932 def has_early_purchased_label? early_label_tracking_number.present? && early_label_voided_at.nil? end |
#has_incomplete_reservations? ⇒ Object
Whether has incomplete reservations.
3113 3114 3115 |
# File 'app/models/order.rb', line 3113 def has_incomplete_reservations? has_unreserved_line_items? || has_committed_serial_number_reservations? end |
#has_selected_heated_items? ⇒ Object
Whether has selected heated items.
3460 3461 3462 |
# File 'app/models/order.rb', line 3460 def has_selected_heated_items? smartinstall_data.present? end |
#has_shipping_method? ⇒ Object
Whether has shipping method.
2673 2674 2675 |
# File 'app/models/order.rb', line 2673 def has_shipping_method? chosen_shipping_method.present? end |
#has_unreserved_line_items? ⇒ Object
Whether has unreserved line items.
3120 3121 3122 |
# File 'app/models/order.rb', line 3120 def has_unreserved_line_items? line_items.any? { |li| li.require_reservation? && !li.fully_reserved? } end |
#has_web_rooms_needing_installation_plans? ⇒ Object
Whether has web rooms needing installation plans.
3134 3135 3136 |
# File 'app/models/order.rb', line 3134 def has_web_rooms_needing_installation_plans? room_configurations.any? { |rc| rc.web_room? && rc.room_layout_attached? && !rc.complete? && !rc.installation_plans_attached? } end |
#hold_for_early_label_mismatch!(_delivery, reason) ⇒ Object
Puts the order on CR hold with a descriptive note when early label
purchase cannot proceed because the selected shipping rate doesn't
match available marketplace rates.
5060 5061 5062 5063 5064 5065 5066 5067 5068 5069 5070 5071 5072 5073 5074 5075 5076 |
# File 'app/models/order.rb', line 5060 def hold_for_early_label_mismatch!(_delivery, reason) note_text = <<~NOTE.strip ⚠ Early label purchase BLOCKED: #{reason} Please select a valid marketplace shipping rate (e.g. an AMZBS rate for Amazon orders) on the Shipping tab, then release the order. NOTE if can_cr_hold? cr_hold quick_note(note_text) if respond_to?(:quick_note) else quick_note(note_text) if respond_to?(:quick_note) Rails.logger.warn("[EarlyLabel] Cannot CR-hold order #{reference_number} (state=#{state}) — note added instead") end early_label_failure_notification(reason) end |
#hold_order_reasons ⇒ Array<String>
Human-readable list of reasons this order is currently being held
by the CR/fraud/EDI pipeline — every line item the CRM hold-page
surfaces to the user. An empty array means the order is releasable.
Combines validation across customer state, billing/shipping data,
EDI partner constraints (price match, shipping option, packing
slip), and freight readiness.
2560 2561 2562 2563 2564 2565 2566 2567 2568 2569 2570 2571 2572 2573 2574 2575 2576 2577 2578 2579 2580 2581 2582 2583 2584 2585 2586 2587 2588 2589 2590 2591 2592 2593 2594 2595 2596 2597 2598 2599 2600 2601 2602 2603 2604 2605 2606 2607 2608 2609 2610 2611 2612 2613 2614 2615 2616 2617 2618 2619 2620 2621 2622 2623 2624 2625 2626 2627 2628 2629 2630 2631 2632 2633 2634 2635 2636 2637 2638 |
# File 'app/models/order.rb', line 2560 def hold_order_reasons res = [] res << 'Order was manually held and so can only be manually released.' if is_manual_hold? res << 'Customer is in a state requiring orders to be held (lead_qualify, guest, bankrupt or closed)' if customer.lead_qualify? || customer.guest? || customer.bankrupt? || customer.closed? res << 'Logged in customer placing order has qc orders flag set' if is_online? && customer.qc_orders? && !CurrentScope.employee_logged_in? res << 'Customer has no billing address' if billing_address.nil? res << 'Order has no shipping method' unless has_shipping_method? if all_rooms_not_orderable? # we should let empty rooms through as long as other rooms in the order are orderable, case in point a tiny corner/closet room that we could not fit heating elements into res << 'Rooms/Heated Spaces are not orderable, empty line items' end res << 'Order ships via freight but shipping address is not freight ready, please set shipping address freight fields and recalculate shipping' if ships_freight_but_address_not_freight_ready? res << 'Deliveries are awaiting packaging estimates' if deliveries.any?(&:pre_pack?) if is_edi_order? res << "EDI order price match discrepancy, expects line total to be #{price_match}" if price_match.present? && price_match != line_total res << 'EDI order missing line number, all non shipping lines must have an edi line number' if line_items.parents_only.non_shipping.where(edi_line_number: nil).present? # Skip shipping option validation when order is being cancelled - shipping method doesn't matter for cancellations if cancellation_reason.blank? if edi_original_ship_code.to_s.upcase.exclude?('UNSP') && edi_shipping_option_name.present? && deliveries.any? { |d| !d.shipping_option_matches?(edi_shipping_option_name) } # unless carrier is unspecified ie UNSP, force match on mapped edi_shipping_option_name res << "EDI order must use original shipping option: #{edi_shipping_option_name}" end # For Walmart SWW orders, allow any WalmartSeller shipping option even if edi_shipping_option_name wasn't set # This handles orders created before the 'sww' shipping option name was added walmart_sww_ok = edi_orchestrator_partner&.start_with?('walmart_seller') && deliveries.all? { |d| d.shipping_option&.carrier == 'WalmartSeller' } # For Amazon Buy Shipping orders, allow any AmazonSeller shipping option amazon_amzbs_ok = edi_orchestrator_partner&.start_with?('amazon_seller') && deliveries.all? { |d| d.shipping_option&.carrier == 'AmazonSeller' } marketplace_ok = walmart_sww_ok || amazon_amzbs_ok res << "EDI order must use original shipping option matched from: #{edi_original_ship_code}" if edi_original_ship_code.present? && edi_shipping_option_name.blank? && !marketplace_ok end if deliveries.any? { |d| (!d.signature_confirmation && signature_confirmation?) || (d.signature_confirmation && !signature_confirmation?) } res << "EDI order deliveries must use EDI order signature_confirmation option: #{signature_confirmation}" end if edi_is_pick_slip_required? && !has_custom_packing_slip? link_snippet = '' encoded_po = ERB::Util.url_encode(edi_po_number.to_s) packing_slip_url = if customer&.is_houzz? "https://www.houzz.com//printBuyerOrder//orderId=#{encoded_po}" elsif customer&.is_amazon_seller_central? "https://sellercentral.amazon.com/orders/packing-slip?orderId=#{encoded_po}" elsif customer&.is_amazon_vendor_central? "https://vendorcentral.amazon.com/hz/vendor/members/df/orders?id=#{encoded_po}" end if packing_slip_url escaped_url = ERB::Util.html_escape(packing_slip_url) link_snippet = ", get it manually here: <a href=\"#{escaped_url}\" target=\"_blank\" rel=\"noopener noreferrer\"><i class=\"fa-sharp fa-solid fa-arrow-up-right-from-square\"></i> #{escaped_url}</a>" end res << "This EDI order requires a custom packing slip, but it failed to get attached#{link_snippet}" end # Home depot canada rule + item ERT240-1.5x35, remove trap when order has been found. BYPASS in notes for false positive if customer.id == 10_358 && line_items.any? { |li| li.item_id == 1910 } && !activities.notes_only.where(Activity[:notes].matches('%BYPASS%')).exists? res << 'EDI order for HDC contains item: ERT240-1.5x35, contact india to ensure not a duplicate of PO 0088974632' end if .present? && cancellation_reason.present? res << "EDI order is pending cancellation via EDI for reason: #{cancellation_reason}, if it is canceled, Heatwave will send a rejection acknowledgement message due to #{cancellation_reason}." bad_vendor_skus_msg = nil bad_merchant_skus_msg = nil bad_vendor_skus_msg = "bad vendor SKUs: #{[:bad_vendor_skus].join(', ')}" if [:bad_vendor_skus]&.any? bad_merchant_skus_msg = "bad merchant SKUs: #{[:bad_merchant_skus].join(', ')}" if [:bad_merchant_skus]&.any? res << "The following bad SKUs need to be addressed because order line items could not be created for them: #{[bad_vendor_skus_msg, bad_merchant_skus_msg].compact.join(', ')}" if bad_vendor_skus_msg || bad_merchant_skus_msg end if shipping_cost.to_f > 0.0 && customer&.bill_shipping_to_customer? res << "EDI order should have shipping cost of $0.00, since EDI customer is set to bill shipping to customer, but order shipping cost is: $#{format('%.2f', shipping_cost.to_f)}. Please ensure to use the EDI customer shipping account or, worst case, have an admin or manager add either a EDI_SHIPPING_ADJ or FS-A coupon." end end # NOTE: "Shipping too late" is now a warning, not a blocking hold reason. # It's displayed via shipping_date_warnings but doesn't prevent order release. # Users can still ship late orders after acknowledging the warning. res << 'Cannot combine service items with goods in the same order. Please split this order.' if line_items.services.with_positive_qty.any? && line_items.goods.with_positive_qty.any? if (moqv = minimum_order_quantity_violations).present? res += moqv.map(&:name) end res += errors. unless ready_for_shipping? res.uniq end |
#hold_orders? ⇒ Object
Whether hold orders.
2541 2542 2543 |
# File 'app/models/order.rb', line 2541 def hold_orders? hold_order_reasons.present? end |
#human_state_name ⇒ String
Human-friendly version of the state machine state. Mostly defers
to AASM/state_machines super, but rewrites the bland
"awaiting deliveries" into "awaiting service delivery" for orders
whose only outstanding deliveries are services (warranty visits,
SmartInstall) so the CRM list distinguishes them at a glance.
Raises ArgumentError on a state the machine doesn't declare, which is
deliberate: the order_state enum makes such a value unstorable, so seeing
one means a programmer assigned garbage in memory and the loud failure is the
point. AppSignal #3274 was the persisted case, and the enum is its fix.
2742 2743 2744 2745 2746 |
# File 'app/models/order.rb', line 2742 def human_state_name return 'awaiting service delivery' if deliveries.any?(&:service_ready_to_fulfill?) && awaiting_deliveries? super end |
#include_po_prefix? ⇒ Object
Alias for Customer#include_po_prefix?
702 703 |
# File 'app/models/order.rb', line 702 delegate :include_po_prefix?, :is_home_depot_usa?, :is_home_depot_can?, :is_walmart_ca?, :is_amazon_com?, :is_costco_ca?, :is_part_of_rona?, :is_part_of_lowes_com?, :is_houzz?, :is_wasn4_or_wat0f?, :is_canadian_tire?, :is_amazon_seller_central?, to: :customer |
#includes_schedulable_service? ⇒ Object
Whether includes schedulable service.
2980 2981 2982 |
# File 'app/models/order.rb', line 2980 def includes_schedulable_service? belongs_to_smartservice_group? end |
#inherited_attention_name ⇒ String?
Default attention-line value derived from the shipping address's
person name, but only when the customer is a person (not a
company); otherwise the company name should be used as the line 1
name and ATTN can stay blank.
3841 3842 3843 3844 3845 |
# File 'app/models/order.rb', line 3841 def inherited_attention_name n = nil n = shipping_address.person_name if shipping_address && customer&.is_person? n end |
#installation_country_iso ⇒ String?
ISO 2-letter country code of the installation site (US, CA, …).
4157 4158 4159 |
# File 'app/models/order.rb', line 4157 def installation_country_iso opportunity.presence&.installation_country_iso end |
#installation_country_iso3 ⇒ String?
ISO 3-letter country code of the installation site (USA, CAN, …).
4150 4151 4152 |
# File 'app/models/order.rb', line 4150 def installation_country_iso3 opportunity.presence&.installation_country_iso3 end |
#installation_is_within_range? ⇒ Object
Whether installation is within range.
3467 3468 3469 3470 3471 3472 3473 |
# File 'app/models/order.rb', line 3467 def installation_is_within_range? zip_code = shipping_address&.zip distance = SmartServicesController.helpers.calculate_distance_from_lz(zip_code) return true if distance.present? && (distance <= 100) # within 100 miles from office false end |
#installation_postal_code ⇒ String?
Postal code where the order will be installed (from the linked
opportunity's installation address, not the shipping address).
Used by service-area lookups for SmartInstall eligibility.
4135 4136 4137 |
# File 'app/models/order.rb', line 4135 def installation_postal_code opportunity.presence&.installation_postal_code end |
#installation_state_code ⇒ String?
State/province code of the installation site (see
#installation_postal_code).
4143 4144 4145 |
# File 'app/models/order.rb', line 4143 def installation_state_code opportunity.presence&.installation_state_code end |
#invoice_balance ⇒ BigDecimal
Sum of Invoice#balance across every invoice on the order — the
outstanding A/R amount. Differs from #balance in that this
looks at invoices once they exist; balance looks at deliveries.
2826 2827 2828 2829 2830 2831 2832 |
# File 'app/models/order.rb', line 2826 def invoice_balance bal = BigDecimal('0.0') invoices.to_a.each do |i| bal += i.balance end bal end |
#invoiced_local_sales_rep ⇒ Party?
The associated invoiced local sales rep.
332 |
# File 'app/models/order.rb', line 332 belongs_to :invoiced_local_sales_rep, class_name: 'Party', foreign_key: :local_sales_rep_id, optional: true |
#invoiced_primary_sales_rep ⇒ Party?
The associated invoiced primary sales rep.
326 |
# File 'app/models/order.rb', line 326 belongs_to :invoiced_primary_sales_rep, class_name: 'Party', foreign_key: :primary_sales_rep_id, optional: true |
#invoiced_secondary_sales_rep ⇒ Party?
The associated invoiced secondary sales rep.
329 |
# File 'app/models/order.rb', line 329 belongs_to :invoiced_secondary_sales_rep, class_name: 'Party', foreign_key: :secondary_sales_rep_id, optional: true |
#invoices ⇒ ActiveRecord::Associations::CollectionProxy
The associated invoices.
382 |
# File 'app/models/order.rb', line 382 has_many :invoices |
#is_amazon_com? ⇒ Object
Alias for Customer#is_amazon_com?
702 703 |
# File 'app/models/order.rb', line 702 delegate :include_po_prefix?, :is_home_depot_usa?, :is_home_depot_can?, :is_walmart_ca?, :is_amazon_com?, :is_costco_ca?, :is_part_of_rona?, :is_part_of_lowes_com?, :is_houzz?, :is_wasn4_or_wat0f?, :is_canadian_tire?, :is_amazon_seller_central?, to: :customer |
#is_amazon_seller_central? ⇒ Object
Alias for Customer#is_amazon_seller_central?
702 703 |
# File 'app/models/order.rb', line 702 delegate :include_po_prefix?, :is_home_depot_usa?, :is_home_depot_can?, :is_walmart_ca?, :is_amazon_com?, :is_costco_ca?, :is_part_of_rona?, :is_part_of_lowes_com?, :is_houzz?, :is_wasn4_or_wat0f?, :is_canadian_tire?, :is_amazon_seller_central?, to: :customer |
#is_canadian_tire? ⇒ Object
Alias for Customer#is_canadian_tire?
702 703 |
# File 'app/models/order.rb', line 702 delegate :include_po_prefix?, :is_home_depot_usa?, :is_home_depot_can?, :is_walmart_ca?, :is_amazon_com?, :is_costco_ca?, :is_part_of_rona?, :is_part_of_lowes_com?, :is_houzz?, :is_wasn4_or_wat0f?, :is_canadian_tire?, :is_amazon_seller_central?, to: :customer |
#is_costco_ca? ⇒ Object
Alias for Customer#is_costco_ca?
702 703 |
# File 'app/models/order.rb', line 702 delegate :include_po_prefix?, :is_home_depot_usa?, :is_home_depot_can?, :is_walmart_ca?, :is_amazon_com?, :is_costco_ca?, :is_part_of_rona?, :is_part_of_lowes_com?, :is_houzz?, :is_wasn4_or_wat0f?, :is_canadian_tire?, :is_amazon_seller_central?, to: :customer |
#is_crm? ⇒ Object
Whether is crm.
3850 3851 3852 |
# File 'app/models/order.rb', line 3850 def is_crm? order_reception_type == 'CRM' end |
#is_edi_order? ⇒ Object
Whether is edi order.
2260 2261 2262 |
# File 'app/models/order.rb', line 2260 def is_edi_order? edi_transaction_id.present? end |
#is_fba? ⇒ Object
Whether is fba.
2314 2315 2316 |
# File 'app/models/order.rb', line 2314 def is_fba? is_store_transfer? && customer&.is_amazon_com? end |
#is_from_myprojects? ⇒ Object
Whether is from myprojects.
2692 2693 2694 |
# File 'app/models/order.rb', line 2692 def is_from_myprojects? !customer_reference.to_s.strip.empty? end |
#is_home_depot_can? ⇒ Object
Alias for Customer#is_home_depot_can?
702 703 |
# File 'app/models/order.rb', line 702 delegate :include_po_prefix?, :is_home_depot_usa?, :is_home_depot_can?, :is_walmart_ca?, :is_amazon_com?, :is_costco_ca?, :is_part_of_rona?, :is_part_of_lowes_com?, :is_houzz?, :is_wasn4_or_wat0f?, :is_canadian_tire?, :is_amazon_seller_central?, to: :customer |
#is_home_depot_usa? ⇒ Object
Alias for Customer#is_home_depot_usa?
702 703 |
# File 'app/models/order.rb', line 702 delegate :include_po_prefix?, :is_home_depot_usa?, :is_home_depot_can?, :is_walmart_ca?, :is_amazon_com?, :is_costco_ca?, :is_part_of_rona?, :is_part_of_lowes_com?, :is_houzz?, :is_wasn4_or_wat0f?, :is_canadian_tire?, :is_amazon_seller_central?, to: :customer |
#is_houzz? ⇒ Object
Alias for Customer#is_houzz?
702 703 |
# File 'app/models/order.rb', line 702 delegate :include_po_prefix?, :is_home_depot_usa?, :is_home_depot_can?, :is_walmart_ca?, :is_amazon_com?, :is_costco_ca?, :is_part_of_rona?, :is_part_of_lowes_com?, :is_houzz?, :is_wasn4_or_wat0f?, :is_canadian_tire?, :is_amazon_seller_central?, to: :customer |
#is_marketing_order? ⇒ Object
Whether is marketing order.
3555 3556 3557 |
# File 'app/models/order.rb', line 3555 def is_marketing_order? order_type == MARKETING_ORDER end |
#is_online? ⇒ Object
Whether is online.
3857 3858 3859 |
# File 'app/models/order.rb', line 3857 def is_online? order_reception_type == 'Online' end |
#is_part_of_lowes_com? ⇒ Object
Alias for Customer#is_part_of_lowes_com?
702 703 |
# File 'app/models/order.rb', line 702 delegate :include_po_prefix?, :is_home_depot_usa?, :is_home_depot_can?, :is_walmart_ca?, :is_amazon_com?, :is_costco_ca?, :is_part_of_rona?, :is_part_of_lowes_com?, :is_houzz?, :is_wasn4_or_wat0f?, :is_canadian_tire?, :is_amazon_seller_central?, to: :customer |
#is_part_of_rona? ⇒ Object
Alias for Customer#is_part_of_rona?
702 703 |
# File 'app/models/order.rb', line 702 delegate :include_po_prefix?, :is_home_depot_usa?, :is_home_depot_can?, :is_walmart_ca?, :is_amazon_com?, :is_costco_ca?, :is_part_of_rona?, :is_part_of_lowes_com?, :is_houzz?, :is_wasn4_or_wat0f?, :is_canadian_tire?, :is_amazon_seller_central?, to: :customer |
#is_price_editable? ⇒ Boolean
Tells whether or not the order can have its line item discounted and msrp price editable by the
price editable concerns, this method is called by the ability check first.
2156 2157 2158 2159 2160 2161 2162 2163 2164 2165 |
# File 'app/models/order.rb', line 2156 def is_price_editable? case order_type when Order::STORE_TRANSFER !editing_locked? when Order::CREDIT_ORDER !(ready_for_printing? || printed?) else false end end |
#is_regular_order? ⇒ Object
Whether is regular order.
3562 3563 3564 |
# File 'app/models/order.rb', line 3562 def is_regular_order? [SALES_ORDER, MARKETING_ORDER, TECH_ORDER].include?(order_type) end |
#is_remote_service? ⇒ Object
Whether is remote service.
3024 3025 3026 |
# File 'app/models/order.rb', line 3024 def is_remote_service? belongs_to_smartservice_group? && line_items.any? { |a| a.sku.include?('REMOTE') } end |
#is_rma_replacement? ⇒ Object
Whether is rma replacement.
3534 3535 3536 |
# File 'app/models/order.rb', line 3534 def is_rma_replacement? rma && [SALES_ORDER, MARKETING_ORDER, TECH_ORDER].include?(order_type) end |
#is_rma_return? ⇒ Object
Whether is rma return.
3513 3514 3515 |
# File 'app/models/order.rb', line 3513 def is_rma_return? order_type == CREDIT_ORDER end |
#is_sales_order? ⇒ Object
Whether is sales order.
3541 3542 3543 |
# File 'app/models/order.rb', line 3541 def is_sales_order? order_type == SALES_ORDER end |
#is_smartfit_service? ⇒ Object
Whether is smartfit service.
2987 2988 2989 |
# File 'app/models/order.rb', line 2987 def is_smartfit_service? line_items.smartfit_items.any? end |
#is_smartfix_service? ⇒ Object
Whether is smartfix service.
3001 3002 3003 |
# File 'app/models/order.rb', line 3001 def is_smartfix_service? line_items.smartfix_items.any? end |
#is_smartguide_service? ⇒ Object
Whether is smartguide service.
3008 3009 3010 |
# File 'app/models/order.rb', line 3008 def is_smartguide_service? line_items.smartguide_items.any? end |
#is_smartinstall_service? ⇒ Object
Whether is smartinstall service.
2994 2995 2996 |
# File 'app/models/order.rb', line 2994 def is_smartinstall_service? line_items.smartinstall_items.any? end |
#is_store_transfer? ⇒ Object
Whether is store transfer.
3527 3528 3529 |
# File 'app/models/order.rb', line 3527 def is_store_transfer? order_type == STORE_TRANSFER end |
#is_subject_to_minimum_qty_rules? ⇒ Object
Whether is subject to minimum qty rules.
3569 3570 3571 |
# File 'app/models/order.rb', line 3569 def is_subject_to_minimum_qty_rules? [SALES_ORDER].include?(order_type) end |
#is_tech_order? ⇒ Object
Whether is tech order.
3548 3549 3550 |
# File 'app/models/order.rb', line 3548 def is_tech_order? order_type == TECH_ORDER end |
#is_walmart_ca? ⇒ Object
Alias for Customer#is_walmart_ca?
702 703 |
# File 'app/models/order.rb', line 702 delegate :include_po_prefix?, :is_home_depot_usa?, :is_home_depot_can?, :is_walmart_ca?, :is_amazon_com?, :is_costco_ca?, :is_part_of_rona?, :is_part_of_lowes_com?, :is_houzz?, :is_wasn4_or_wat0f?, :is_canadian_tire?, :is_amazon_seller_central?, to: :customer |
#is_warehouse_pickup? ⇒ Object
Whether is warehouse pickup.
3106 3107 3108 |
# File 'app/models/order.rb', line 3106 def is_warehouse_pickup? shipping_address&.is_warehouse end |
#is_wasn4_or_wat0f? ⇒ Object
Alias for Customer#is_wasn4_or_wat0f?
702 703 |
# File 'app/models/order.rb', line 702 delegate :include_po_prefix?, :is_home_depot_usa?, :is_home_depot_can?, :is_walmart_ca?, :is_amazon_com?, :is_costco_ca?, :is_part_of_rona?, :is_part_of_lowes_com?, :is_houzz?, :is_wasn4_or_wat0f?, :is_canadian_tire?, :is_amazon_seller_central?, to: :customer |
#job_name ⇒ String?
Best-known job name for this order: the originating quote's
opportunity name, falling back to the order's own opportunity
name, or nil if the order isn't linked to either. Used by
warehouse pack-list headings and job-folder titles.
2122 2123 2124 2125 2126 2127 2128 |
# File 'app/models/order.rb', line 2122 def job_name if quote quote.opportunity.name elsif opportunity opportunity.name end end |
#linked_support_cases ⇒ ActiveRecord::Associations::CollectionProxy
The associated linked support cases.
394 |
# File 'app/models/order.rb', line 394 has_many :linked_support_cases, through: :room_configurations, source: :support_cases |
#live_payments_currency_consistent ⇒ void (protected)
This method returns an undefined value.
Blocks flipping an order's currency while it still holds a live payment
(authorized OR captured) in the outgoing currency. A USD guest cart that
becomes a Canadian order (SO727885, 2026-07) must not silently keep a
USD/CAD-mismatched payment — the money has to be voided or refunded and
re-taken in the new currency first. Mirrors
Catalog::AssignCatalogToCustomer#error_check. Only fires on a currency
change, so pre-existing cross-currency payments are undisturbed until
someone actively re-currencies them.
6376 6377 6378 6379 6380 6381 6382 6383 6384 |
# File 'app/models/order.rb', line 6376 def live_payments_currency_consistent return unless persisted? stale = payments.where(state: LIVE_PAYMENT_STATES).where.not(currency:).where.not(currency: nil) return if stale.none? errors.add(:currency, "cannot change to #{currency} while a live payment in #{stale.distinct.pluck(:currency).join(', ')} exists — void or refund it first") end |
#local_sales_rep ⇒ Employee?
Local (on-site) sales rep on the order. Same closed-vs-open
semantics as #primary_sales_rep.
1887 1888 1889 1890 1891 |
# File 'app/models/order.rb', line 1887 def local_sales_rep return invoiced_local_sales_rep if order_closed? invoiced_local_sales_rep || customer&.local_sales_rep end |
#log_early_label_event(event_type, message, details = {}) ⇒ Object
Log an event to the early label API log
Used for debugging and visibility into what happened during early label purchase
4826 4827 4828 4829 4830 4831 4832 4833 4834 4835 |
# File 'app/models/order.rb', line 4826 def log_early_label_event(event_type, , details = {}) @early_label_api_log ||= [] @early_label_api_log << { timestamp: Time.current.iso8601, event: event_type, message: , details: details } Rails.logger.info("[EarlyLabel] #{event_type}: #{} #{details.presence&.to_json}") end |
#mark_serial_coupons_as_used ⇒ Object
Burn any single-use coupon serial numbers that were applied to
this order so they can't be redeemed again. Called when the
order transitions out of pending-payment.
6011 6012 6013 |
# File 'app/models/order.rb', line 6011 def mark_serial_coupons_as_used discounts.includes(:coupon_serial_number).joins(:coupon_serial_number).find_each { |d| d.coupon_serial_number.update_attribute!(:used, true) } end |
#marketplace_early_label_eligible? ⇒ Boolean
Check if order is eligible for marketplace early label purchase (Walmart SWW or Amazon Buy Shipping)
4981 4982 4983 |
# File 'app/models/order.rb', line 4981 def marketplace_early_label_eligible? walmart_sww_eligible? || amazon_buy_shipping_eligible? end |
#material_alerts ⇒ ActiveRecord::Associations::CollectionProxy
The associated material alerts.
403 |
# File 'app/models/order.rb', line 403 has_many :material_alerts, dependent: :destroy |
#merge_shipper_api_log(shipper) ⇒ Object
Merge raw HTTP API call logs from the shipper into the order's API log.
Works for both Walmart SWW and Amazon Buy Shipping carriers.
4840 4841 4842 4843 4844 4845 4846 4847 4848 4849 4850 4851 4852 4853 4854 4855 4856 4857 4858 4859 4860 4861 4862 4863 4864 4865 4866 4867 4868 4869 4870 4871 |
# File 'app/models/order.rb', line 4840 def merge_shipper_api_log(shipper) return unless shipper.respond_to?(:api_log) && shipper.api_log.present? prefix = case shipper when Shipping::AmazonSeller then 'amzbs_http' when Shipping::WalmartSeller then 'sww_http' else 'marketplace_http' end @early_label_api_log ||= [] @early_label_api_log_offset ||= 0 new_entries = shipper.api_log[@early_label_api_log_offset..] @early_label_api_log_offset = shipper.api_log.length (new_entries || []).each do |entry| @early_label_api_log << { timestamp: entry[:timestamp], event: "#{prefix}_#{entry[:operation]}", message: "HTTP #{entry[:method]} #{entry[:url]}", details: { operation: entry[:operation], method: entry[:method], url: entry[:url], request_payload: entry[:request_payload], response_status: entry[:response_status], response_body: entry[:response_body], success: entry[:success], error: entry[:error] } } end end |
#messaging_logs ⇒ ActiveRecord::Associations::CollectionProxy
The associated messaging logs.
400 |
# File 'app/models/order.rb', line 400 has_many :messaging_logs, dependent: :destroy, as: :resource |
#minimum_order_quantity_violations ⇒ Array<Alert>
MOQ (minimum-order-quantity) alerts triggered by the current
line items — e.g. "this control requires a minimum of 5 units".
Surfaced in #hold_order_reasons.
2653 2654 2655 |
# File 'app/models/order.rb', line 2653 def minimum_order_quantity_violations Item::Materials::Checks::Moq.new.process(container: self).alerts end |
#move_deliveries_from_quoting! ⇒ Object
Promote any deliveries currently in quoting (rate-shopping)
state into ready_to_ship, copying the order-level shipment /
label / future-release fields onto each delivery first. Skips
pre-pack deliveries (which still need warehouse packing data
before they can be released).
5807 5808 5809 5810 5811 5812 5813 5814 5815 5816 5817 5818 5819 5820 5821 5822 5823 |
# File 'app/models/order.rb', line 5807 def move_deliveries_from_quoting! deliveries.quoting.each do |delivery| next if delivery.pre_pack? delivery.shipment_instructions = shipment_instructions delivery.label_instructions = label_instructions delivery.future_release_date = future_release_date delivery.manual_release_only = manual_release_only delivery.do_not_reserve_stock = do_not_reserve_stock # Save before ready_to_ship! because ready_to_ship! calls reload inside an advisory lock, # which would discard unsaved changes (including future_release_date). # Without this save, orders with future release dates incorrectly go to at_warehouse # instead of future_release state. delivery.save! delivery.ready_to_ship! end end |
#move_deliveries_to_quoting! ⇒ Object
Force every delivery on the order back into the quoting state
so the rate-shop can be re-run from scratch (e.g. after a
shipping address change). Cancels deliveries first if they're
past the cancelable threshold. Wrapped in a transaction so a
mid-loop failure rolls everything back.
5872 5873 5874 5875 5876 5877 5878 5879 5880 5881 5882 5883 5884 5885 5886 5887 5888 5889 5890 5891 5892 5893 5894 |
# File 'app/models/order.rb', line 5872 def move_deliveries_to_quoting! deliveries.reload.each do |delivery| Rails.logger.info("[Order] Moving delivery #{delivery.id} from #{delivery.state} to quoting") begin if delivery.can_back_to_quoting? delivery.back_to_quoting! Rails.logger.info("[Order] Successfully moved delivery #{delivery.id} to quoting") else Rails.logger.warn("[Order] Cannot move delivery #{delivery.id} to quoting - transition not allowed from state: #{delivery.state}") # Try to force it by calling cancel first if delivery.cancelable? delivery.cancel delivery.back_to_quoting! if delivery.can_back_to_quoting? Rails.logger.info("[Order] Successfully moved delivery #{delivery.id} to quoting after cancel") end end rescue StandardError => e Rails.logger.error("[Order] Error moving delivery #{delivery.id} to quoting: #{e.}") Rails.logger.error(e.backtrace.first(5).join("\n")) raise # Re-raise to rollback the transaction end end end |
#move_service_case_from_pending_service_payment ⇒ Object
When a SmartService order's payment finally clears, flip the
associated SmartService support case out of
pending_service_payment and into the case_open state so the
service team can pick it up.
5829 5830 5831 5832 |
# File 'app/models/order.rb', line 5829 def move_service_case_from_pending_service_payment service_case = support_cases&.services&.pending_service_payment&.first service_case.presence&.case_open end |
#name ⇒ String
Human label like "Order #SO123456 (PO# 0042)" (or with PO#s
for multi-PO orders). Used in mailer subjects, CRM headers, and
support-case titles.
3253 3254 3255 3256 3257 3258 3259 3260 3261 |
# File 'app/models/order.rb', line 3253 def name po_numbers_text = '' po_numbers = po_number if po_numbers.present? po_numbers_text = " (PO# #{po_numbers})" unless po_numbers.index(',') po_numbers_text = " (PO#s #{po_numbers})" if po_numbers.index(',') end "Order ##{reference_number}#{po_numbers_text}" end |
#need_to_recalculate_shipping ⇒ Object
Flag the order so the next save re-runs the carrier rate-shop and
re-detects shipping options. Clears the suppression flag set by
earlier flows that wanted to bypass detection.
6164 6165 6166 6167 6168 |
# File 'app/models/order.rb', line 6164 def need_to_recalculate_shipping self.recalculate_shipping = true self.do_not_detect_shipping = false logger.debug "Deliverable, need_to_recalculate_shipping ID: #{id}, self.recalculate_shipping: #{recalculate_shipping}, self.do_not_detect_shipping: #{do_not_detect_shipping}" end |
#needs_attention_issues ⇒ Array<String>
Human-readable list of mismatches between the order's state and
its deliveries' states (e.g. order is awaiting_deliveries but
some deliveries are still quoting). Surfaces in the CRM
"Needs Attention" panel so dispatch can hand-fix them.
6000 6001 6002 6003 6004 6005 6006 |
# File 'app/models/order.rb', line 6000 def needs_attention_issues issues = [] issues << "order is #{state.humanize.downcase} but deliveries are still in quoting, need to hold the order and recalculate shipping" if SHIPPING_STATES.include?(state.to_sym) && deliveries.any?(&:quoting?) issues << "order is #{state.humanize.downcase} and deliveries in state Awaiting PO fulfillment, need to ensure PO fulfillment process proceeds" if deliveries.any?(&:awaiting_po_fulfillment?) issues << "order is #{state.humanize.downcase} and deliveries in state Processing PO fulfillment, need to ensure PO fulfillment process completed" if deliveries.any?(&:processing_po_fulfillment?) issues end |
#needs_spiff_training? ⇒ Object
Whether needs spiff training.
1971 1972 1973 |
# File 'app/models/order.rb', line 1971 def needs_spiff_training? spiff_enrollment.present? && spiff_enrollment.spiff.is_active? && customer.orders.one? && customer.activities.where(activity_type_id: ActivityTypeConstants::SPIFFACTTRAIN).empty? end |
#new_customer_online_order ⇒ Boolean
True when this is a brand-new online customer's first order —
a key segmentation flag for new-customer email flows and
acquisition reporting.
2323 2324 2325 2326 2327 2328 2329 2330 2331 2332 2333 2334 |
# File 'app/models/order.rb', line 2323 def new_customer_online_order # puts "!!!new_customer_online_order, new_customer_order: #{new_customer_order}, self.customer.reception_type.downcase: #{self.customer.reception_type.downcase}, self.customer.account.present?: #{self.customer.account.present?}" # Check that this is a new customer/order and an online order Rails.logger.info "new_customer_online_order: id: #{id}, reference_number: #{reference_number}" if new_customer_order && online_order? Rails.logger.info "new_customer_online_order: TRUE id: #{id}, reference_number: #{reference_number}" true else Rails.logger.info "new_customer_online_order: FALSE id: #{id}, reference_number: #{reference_number}" false end end |
#new_customer_order ⇒ Boolean
True when this is the customer's only non-management-held order,
i.e. their first real purchase. Used by the welcome email flow
and by SmartInstall enrollment to skip "first-time customer"
bonuses for repeat customers.
2342 2343 2344 2345 2346 2347 2348 2349 2350 2351 2352 2353 |
# File 'app/models/order.rb', line 2342 def new_customer_order # puts "!!!new_customer_order, self.customer.orders.length: #{self.customer.orders.length}, !in_management_hold?: #{!in_management_hold?}" Rails.logger.info "new_customer_order: id: #{id}, reference_number: #{reference_number}" # Check that this is a new customer/order and not in manager hold (in the case of Canada situation above) if (customer.orders.length == 1) && !in_management_hold? Rails.logger.info "new_customer_order: TRUE id: #{id}, reference_number: #{reference_number}" true else Rails.logger.info "new_customer_order: FALSE id: #{id}, reference_number: #{reference_number}" false end end |
#new_ss_ticket(service, activity_type) ⇒ Object
Shared backend for #create_smartinstall_ticket,
#create_smartfix_ticket, and #create_smartguide_ticket:
builds the SmartService support case, attaches participants and
rooms, and schedules the kickoff activity. No-op if a service
ticket already exists on the order.
3656 3657 3658 3659 3660 3661 3662 3663 3664 3665 3666 3667 3668 3669 3670 3671 3672 3673 3674 3675 3676 3677 3678 3679 |
# File 'app/models/order.rb', line 3656 def new_ss_ticket(service, activity_type) return if support_cases.services.any? ticket = support_cases.new( case_type: 'SmartService', assigned_to_id: Employee::SCOTT, state: 'new', service_address_id: shipping_address.id, description: "#{service}: Order id #{id}. #{service} request for a project.", priority: 'High', service: ) ticket.build_participants_and_rooms(order_id: id) return unless ticket.save! # ticket.order_ids = [self.id] this is already being done in the build_participants_and_rooms method Activity.create(lock_target_datetime: false, activity_type_id: activity_type, target_datetime: 1.working.days.from_now, assigned_resource: Employee.find(Employee::SCOTT), # Assigned by default to JL party: customer, resource: ticket, notes: "Call Customer to confirm date and time of the #{service} service.") end |
#next_six_months_with_end_dates ⇒ Array<Array(String, String)>
Six-month label/value pairs for end-of-month dates starting
this month — [["October 2026", "2026-10-31"], …]. Powers the
ship-by month dropdown on EDI orders.
2720 2721 2722 2723 2724 2725 2726 2727 2728 |
# File 'app/models/order.rb', line 2720 def next_six_months_with_end_dates today = Time.zone.today (0..5).map do |i| date = today >> i month_name = date.strftime('%B %Y') end_of_month = date.end_of_month.strftime('%Y-%m-%d') [month_name, end_of_month] end end |
#next_warehouse_ship_date(now: Time.current) ⇒ Date?
Earliest date the warehouse will hand the order to a carrier,
honoring the same-day cutoff and the company holiday calendar.
nil when the warehouse company can't be resolved.
2245 2246 2247 2248 2249 2250 2251 2252 2253 2254 2255 |
# File 'app/models/order.rb', line 2245 def next_warehouse_ship_date(now: Time.current) company = store&.company return nil unless company local_today = now.in_time_zone(company.working_hours_timezone).to_date case shipping_cutoff_status(now: now) when :same_day then local_today when :after_cutoff then company.next_business_day(local_today + 1) when :non_working_day then company.next_business_day(local_today) end end |
#notify_pre_pack_cancellation ⇒ void
This method returns an undefined value.
When an order is cancelled while it still has pre-pack
deliveries (waiting on warehouse packing data), email each
affected warehouse so they don't waste pack time on a dead order.
Publishes one Events::DeliveryPrePackCancelled per affected delivery
from after_all_transactions_commit; the async audience_member re-queries
by id, so a delivery destroyed later in the same transaction
(purge_empty_quoting_deliveries) is a clean no-op (AppSignal #4958).
5850 5851 5852 5853 5854 5855 5856 5857 5858 5859 5860 5861 5862 5863 5864 5865 |
# File 'app/models/order.rb', line 5850 def notify_pre_pack_cancellation pre_pack_delivery_ids = deliveries.reload.where(state: "pre_pack").ids return unless pre_pack_delivery_ids.any? cancelled_by_id = PaperTrail.request.whodunnit ActiveRecord.after_all_transactions_commit do pre_pack_delivery_ids.each do |delivery_id| Rails.configuration.event_store.publish( Events::DeliveryPrePackCancelled.new(data: { delivery_id:, cancelled_by_id: }), stream_name: "Delivery-#{delivery_id}" ) rescue StandardError => e ErrorReporting.error(e) end end end |
#ok_to_delete?(account = nil) ⇒ Object
Whether ok to delete.
3294 3295 3296 3297 |
# File 'app/models/order.rb', line 3294 def ok_to_delete?(account = nil) account&.is_admin? || (cancelled? && (line_items.empty? || reference_number.blank?)) end |
#online_order? ⇒ Object
Whether online order.
2381 2382 2383 2384 |
# File 'app/models/order.rb', line 2381 def online_order? (res = creator.blank? || !creator.is_employee?) && customer.account res end |
#open_activities_counter ⇒ Integer
Number of open follow-up activities tied to this order, for the
CRM's per-order activity badge. Only counts activities that
default-visible filters surface.
6057 6058 6059 |
# File 'app/models/order.rb', line 6057 def open_activities_counter activities.open_activities.visible_by_default.size end |
#opportunities ⇒ Array<Opportunity>
Every opportunity tied to the order — directly via
#opportunity and indirectly via each RoomConfiguration's
opportunity. Used by the CRM activity feed to roll the order up
under each linked job.
2040 2041 2042 2043 2044 2045 |
# File 'app/models/order.rb', line 2040 def opportunities opps = [] opps << opportunity opps += room_configurations.map(&:opportunity) opps.uniq.compact end |
#opportunity ⇒ Opportunity?
The associated opportunity.
295 |
# File 'app/models/order.rb', line 295 belongs_to :opportunity, inverse_of: :orders, optional: true |
#opportunity_name ⇒ String?
Name of the linked Opportunity, or nil if the order isn't
tied to one. Free-typed in the CRM order form; setting it
auto-finds-or-creates the opportunity.
2360 2361 2362 |
# File 'app/models/order.rb', line 2360 def opportunity_name opportunity.try(:name) end |
#opportunity_name=(val) ⇒ Object
Setter that finds or creates an Opportunity on the customer
with the given name and links the order to it. Passing nil or
blank detaches the opportunity entirely.
2369 2370 2371 2372 2373 2374 2375 2376 |
# File 'app/models/order.rb', line 2369 def opportunity_name=(val) if val.present? opp = customer.opportunities.find_or_create_by(name: val.strip) self.opportunity = opp else self.opportunity = nil end end |
#order_closed? ⇒ Object
Whether order closed.
1855 1856 1857 |
# File 'app/models/order.rb', line 1855 def order_closed? invoiced? || cancelled? || fraudulent? || printed? end |
#order_emails ⇒ Array<String>
Every email address relevant to this order: the customer + all
their contacts, the transmission email, the tracking email, the
bound contact's emails, and any per-order billing overrides.
De-duplicated and sorted.
3168 3169 3170 3171 3172 3173 3174 3175 3176 |
# File 'app/models/order.rb', line 3168 def order_emails emails = [] emails += customer.contacts_and_self_contact_points_by_category('email').pluck(:detail) emails += transmission_email emails += tracking_email emails += contact.contact_points.emails.pluck(:detail) if contact emails += billing_emails || [] emails.compact.uniq.sort end |
#order_type_title ⇒ String
Human label for order_type (e.g. 'SO' → 'Sales Order').
Falls back to 'Unknown' for codes not in ALL_ORDER_TYPES.
1801 1802 1803 |
# File 'app/models/order.rb', line 1801 def order_type_title ALL_ORDER_TYPES[order_type] || 'Unknown' end |
#original_source ⇒ Source?
The associated original source.
320 |
# File 'app/models/order.rb', line 320 belongs_to :original_source, class_name: 'Source', optional: true |
#overdue_to_ship? ⇒ Boolean
Returns whether this order is currently an overdue-to-ship slip.
2449 2450 2451 |
# File 'app/models/order.rb', line 2449 def overdue_to_ship? self.class.overdue_to_ship.exists?(id) end |
#partially_shipped? ⇒ Object
Whether partially shipped.
6092 6093 6094 |
# File 'app/models/order.rb', line 6092 def partially_shipped? deliveries.active.any?(&:completed_regular_delivery?) end |
#participants_options_for_select ⇒ Array<Array(String, Integer)>?
[name, party_id] pairs of every opportunity participant on
this order, suitable for a Rails select helper. nil when no
opportunity is linked.
4224 4225 4226 |
# File 'app/models/order.rb', line 4224 def opportunity&.opportunity_participants&.map { |scp| [scp.party.full_name, scp.party_id] } end |
#party_for_order_confirmation ⇒ Party
The party the order-confirmation email goes to: the contact when
one is bound, else the customer. Mirrors #primary_party but
carries different intent — the confirmation flow specifically
wants whoever placed the order, even if it's a contact under a
company account.
3791 3792 3793 |
# File 'app/models/order.rb', line 3791 def party_for_order_confirmation contact || customer end |
#pay_on_spiff? ⇒ Object
Whether pay on spiff.
1710 1711 1712 |
# File 'app/models/order.rb', line 1710 def pay_on_spiff? spiff_enrollment.present? && (spiff_enrollment.spiff.eligible_reward(self) > 0) end |
#payment_method ⇒ String?
Category code (Payment::CREDIT_CARD, Payment::PO, …) of the
first authorised payment on the order — a quick "how was this
paid?" hint. Returns nil if there are no authorised payments.
2753 2754 2755 2756 2757 |
# File 'app/models/order.rb', line 2753 def payment_method payments..first.category rescue StandardError nil end |
#payment_method_requires_authorization? ⇒ Boolean
True when any payment on the order is awaiting human
authorization (e.g. unreviewed eCheck or fraud-flagged card).
2486 2487 2488 |
# File 'app/models/order.rb', line 2486 def .any? end |
#payment_options(source) ⇒ Array<String>
Allowed payment-method codes for this order, ordered by the
canonical sort for the given UI surface. The source selects
which surface is asking:
'cart'/'online_order_payment'— public checkout'online_order_invoices'— pay-an-invoice flow'crm'— internal CRM order entry
2013 2014 2015 2016 2017 2018 2019 2020 2021 2022 2023 2024 2025 2026 2027 2028 2029 2030 2031 2032 |
# File 'app/models/order.rb', line 2013 def (source) = Payment.(billing_entity, self) .uniq! = [] if %w[cart online_order_payment].include?(source) = if includes_schedulable_service? [Payment::CREDIT_CARD] else [Payment::CREDIT_CARD, Payment::PAYPAL, Payment::PO, Payment::VPO, Payment::CHECK, Payment::WIRE] end elsif source == 'online_order_invoices' = [Payment::CREDIT_CARD] elsif source == 'crm' = [Payment::CREDIT_CARD, Payment::PO, Payment::VPO, Payment::RMA_CREDIT, Payment::STORE_CREDIT, Payment::PAYPAL_INVOICE, Payment::ECHECK, Payment::ADV_REPL, Payment::CHECK, Payment::WIRE, Payment::CASH] end .each do |po| << po if .include?(po) end end |
#payments ⇒ ActiveRecord::Associations::CollectionProxy
The associated payments.
358 |
# File 'app/models/order.rb', line 358 has_many :payments, dependent: :nullify, inverse_of: :order |
#payments_requiring_authorization ⇒ Array<Payment>
Authorised payments that haven't yet been management-approved
AND whose authorisation-review pipeline has flagged them as
requiring sign-off (any reason — fraud risk, eCheck, large
amount, etc.). Used by #payment_method_requires_authorization?.
2505 2506 2507 |
# File 'app/models/order.rb', line 2505 def payments..select { |pp| !pp.payment_approved? && (pp.[:required] == true) } end |
#payments_with_fraud_report ⇒ Array<Payment>
Non-voided payments that carry a fraud report and haven't been
explicitly approved by management. Subset of #payments used by
#potential_fraud_reasons and the fraud-review UI.
1703 1704 1705 |
# File 'app/models/order.rb', line 1703 def payments_with_fraud_report payments.non_voided.select { |a| a.fraud_report.present? && !a.payment_approved? } end |
#paypal_invoices_paid? ⇒ Object
Whether paypal invoices paid.
2140 2141 2142 2143 2144 2145 |
# File 'app/models/order.rb', line 2140 def paypal_invoices_paid? active_paypal_invoices = payments.paypal_invoices.where.not(state: %w[declined voided expired cancelled]) return true if active_paypal_invoices.empty? active_paypal_invoices.all?(&:captured?) end |
#po_number(include_po_prefix: false, limit: nil) ⇒ String
Comma-joined PO number(s) across the order's authorised /
captured payments. Store transfers use the linked
PurchaseOrder reference number instead. Pass
include_po_prefix: true to prepend "PO# ", or limit: N to
cap the number of payment rows scanned (avoids scanning
thousands of payments on a heavily-amended order).
3367 3368 3369 3370 3371 3372 3373 3374 3375 3376 |
# File 'app/models/order.rb', line 3367 def po_number(include_po_prefix: false, limit: nil) query = payments.where(state: %w[authorized captured]).where.not(po_number: [nil, '']) query = query.limit(limit) if limit po_numbers = query.distinct.pluck(:po_number) po_numbers = po_numbers.map(&:strip).uniq res = po_numbers.join(', ') res.insert(0, 'PO# ') if res.present? && include_po_prefix res = purchase_order.reference_number if is_store_transfer? res end |
#po_number_barcode(file_path: nil) ⇒ String
Instance-level wrapper around po_number_barcode that uses
this order's PO number. Used by warehouse pack-list PDFs to
render a scannable Code-128 barcode.
3425 3426 3427 |
# File 'app/models/order.rb', line 3425 def (file_path: nil) self.class.(po_number:, file_path:) end |
#po_numbers ⇒ Array<String>
Distinct PO numbers across all payments on the order.
3266 3267 3268 |
# File 'app/models/order.rb', line 3266 def po_numbers payments.where.not(po_number: nil).distinct.pluck(:po_number) end |
#potential_fraud? ⇒ Object
Whether potential fraud.
1681 1682 1683 |
# File 'app/models/order.rb', line 1681 def potential_fraud? payments..any? { |pp| pp.try(:fraud_report).try(:potential_fraud?) && !pp.payment_approved? } end |
#potential_fraud_reasons ⇒ Array<String>
Aggregate list of fraud-report reasons across every unapproved
payment on the order. Surfaced in the CR-hold UI so reviewers
see at a glance why the order tripped the fraud filter.
1690 1691 1692 1693 1694 1695 1696 |
# File 'app/models/order.rb', line 1690 def potential_fraud_reasons reasons = [] payments_with_fraud_report.each do |payment| reasons += payment.fraud_report.potential_fraud_reasons end reasons.uniq end |
#precreated_rma_credit_available ⇒ BigDecimal
Remaining RMA credit pre-authorised against this order, computed
as line_total_plus_tax − already-applied RMA credit payments.
Floored at 0 so the figure never goes negative when an RMA credit
exceeds the new order's total.
2849 2850 2851 2852 2853 2854 |
# File 'app/models/order.rb', line 2849 def precreated_rma_credit_available total_available = line_total_plus_tax spent = payments..where(category: Payment::RMA_CREDIT).sum(:amount) available = total_available - spent available < 0 ? BigDecimal('0.0') : available end |
#preset_jobs ⇒ ActiveRecord::Associations::CollectionProxy
The associated preset jobs.
397 |
# File 'app/models/order.rb', line 397 has_many :preset_jobs, inverse_of: :order |
#prevent_recalculate_shipping? ⇒ Object
Whether prevent recalculate shipping.
4174 4175 4176 |
# File 'app/models/order.rb', line 4174 def prevent_recalculate_shipping? is_credit_order? || SOLD_STATES.include?(state.to_sym) || retrieving_shipping_costs? end |
#primary_party ⇒ Party
The party who's the "face" of this order — the contact when one
is bound (e.g. dealer with multiple contacts), otherwise the
customer. Used everywhere the CRM needs a single party for
activity attribution and recipient resolution.
2307 2308 2309 |
# File 'app/models/order.rb', line 2307 def primary_party contact || customer end |
#primary_rep_name ⇒ String
Display string for the primary sales rep, falling back to the
literal "Unassigned" so list views never show a blank cell.
3142 3143 3144 |
# File 'app/models/order.rb', line 3142 def primary_rep_name primary_sales_rep.try(:full_name) || 'Unassigned' end |
#primary_sales_rep ⇒ Employee?
Primary sales rep on the order. For closed orders we return
whatever was stamped on the invoice at close time (even if
blank) so commissions match what was reported. For open orders
we fall back to the customer's current primary if the order
itself doesn't override.
1866 1867 1868 1869 1870 1871 |
# File 'app/models/order.rb', line 1866 def primary_sales_rep # if it's closed always return what's stored (even if blank) return invoiced_primary_sales_rep if order_closed? invoiced_primary_sales_rep || customer&.primary_sales_rep end |
#product_review_url ⇒ String?
Reviews.io product-review landing URL pre-filled with this
order's customer/email/skus. Returns nil when the order has no
customer email or no product SKUs (no review surface to render).
1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 |
# File 'app/models/order.rb', line 1395 def product_review_url return nil if customer&.email.blank? product_skus = line_items.non_shipping.joins(:item).pluck('items.sku').compact.uniq return nil unless product_skus.any? store_key = 'warmlyyours-com' params = { store: store_key, user: ERB::Util.url_encode(customer.full_name.presence || 'Customer'), order_id: ERB::Util.url_encode(reference_number), email: ERB::Util.url_encode(customer.email), products: ERB::Util.url_encode(product_skus.join(';')), type: 'product_review', rating: 5 } "https://www.reviews.io/store/landing_new_review?#{params.map { |k, v| "#{k}=#{v}" }.join('&')}" end |
#prune_cart_rooms ⇒ Object
Drop room configurations from the cart whose line items have
all been removed. Cart-only — orders keep their room
associations even when room contents move around.
3491 3492 3493 3494 3495 3496 3497 3498 |
# File 'app/models/order.rb', line 3491 def prune_cart_rooms # this method clears out associated rooms from the cart if none of the room line items remain # this is only for carts since we want to keep room association when linking rooms to quotes and orders in the crm before the rooms have been designed rcs = room_configurations rcs.each do |rc| remove_room_configuration(rc) if line_items.where(room_configuration_id: rc.id).empty? end end |
#public_path ⇒ String?
Public-portal path to the order ("My Account" view) when the
customer has a portal account, otherwise nil.
3284 3285 3286 3287 3288 |
# File 'app/models/order.rb', line 3284 def public_path return nil if customer&.account.blank? UrlHelper.instance.my_order_path(self) end |
#public_payment_link ⇒ String
Public, tokenised URL a customer can hit to pay an order without
signing in. The id is encrypted via Encryption.encrypt_string
so the URL doesn't leak sequential ids.
6020 6021 6022 |
# File 'app/models/order.rb', line 6020 def public_payment_link "https://#{WEB_HOSTNAME}/pay-order/#{encrypted_id}" end |
#purchase_early_label_if_requested ⇒ Hash?
Purchase shipping label early if requested and order is eligible
Called from after_transition to awaiting_deliveries
Stores result in early_label_purchase_result for controller to display flash messages
Logs all API calls to early_label_api_log for debugging/visibility
Sends EDI admin notification on any failure
4286 4287 4288 4289 4290 4291 4292 4293 4294 4295 4296 |
# File 'app/models/order.rb', line 4286 def purchase_early_label_if_requested return unless purchase_label_early? # Unpersisted records (tests) skip the lock — no DB row to lock against return purchase_early_label_locked unless persisted? Order.with_advisory_lock("early_label_purchase_#{id}", timeout_seconds: 0, disable_query_cache: true) do reload purchase_early_label_locked end || nil end |
#purchase_order ⇒ PurchaseOrder?
The associated purchase order.
314 |
# File 'app/models/order.rb', line 314 belongs_to :purchase_order, optional: true |
#quote ⇒ Quote?
The associated quote.
292 |
# File 'app/models/order.rb', line 292 belongs_to :quote, inverse_of: :orders, optional: true |
#quotes ⇒ Array<Quote>
Every quote tied to the order — directly via #quote and
indirectly via the most recent completed quote on each linked
room configuration.
2052 2053 2054 2055 2056 2057 |
# File 'app/models/order.rb', line 2052 def quotes qs = [] qs << quote qs += room_configurations.map { |rc| rc.quotes.completed_quotes.last } qs.uniq.compact end |
#ready_for_pending_payment? ⇒ Object
Whether ready for pending payment.
2966 2967 2968 |
# File 'app/models/order.rb', line 2966 def ready_for_pending_payment? ready_for_shipping? end |
#ready_for_service? ⇒ Object
Whether ready for service.
3031 3032 3033 |
# File 'app/models/order.rb', line 3031 def ready_for_service? belongs_to_smartservice_group? end |
#ready_for_shipping? ⇒ Object
Whether ready for shipping.
3038 3039 3040 3041 3042 3043 3044 3045 3046 3047 3048 3049 3050 3051 3052 3053 3054 3055 3056 3057 3058 3059 3060 3061 3062 3063 3064 3065 3066 3067 3068 3069 3070 3071 3072 3073 3074 3075 3076 3077 3078 3079 3080 3081 3082 3083 3084 3085 3086 3087 3088 3089 3090 3091 3092 3093 3094 3095 3096 3097 3098 3099 3100 3101 |
# File 'app/models/order.rb', line 3038 def ready_for_shipping? return false if shipping_address.nil? if has_unreserved_line_items? errors.add :base, "One or more line items require a serial number reservation. Line items: #{line_items.select { |li| li.require_reservation? && !li.fully_reserved? }.map(&:sku).join(', ')}" return false end if has_committed_serial_number_reservations? errors.add :base, 'Not all reserved serial numbers are available' return false end if is_sales_order? && price_match && price_match != line_total errors.add :base, "EDI order price match discrepancy, expects line total to be #{price_match}" return false end if is_sales_order? && customer&.customer_record&.always_custom_packing_list? && edi_is_pick_slip_required? && uploads.in_category('custom_packing_slip_pdf').blank? houzz_snippet = +'' if customer&.is_houzz? && edi_po_number.present? encoded_po = ERB::Util.url_encode(edi_po_number.to_s) escaped_url = ERB::Util.html_escape("https://www.houzz.com//printBuyerOrder//orderId=#{encoded_po}") houzz_snippet = ", get it here: <a href=\"#{escaped_url}\" target=\"_blank\" rel=\"noopener noreferrer\"><i class=\"fa-sharp fa-solid fa-arrow-up-right-from-square\"></i> #{escaped_url}</a>" end errors.add :base, "Customer requires a custom packing slip pdf for all orders#{houzz_snippet}".html_safe return false end # enforce that all Canadian Tire orders must ship to a store and use the store name format if is_sales_order? && customer&.billing_entity&.is_canadian_tire? && !((shipping_address.company_name || shipping_address.person_name) =~ CustomerConstants::CANADIAN_TIRE_STORE_NAME_REGEX).nil? != true errors.add :base, CustomerConstants::CANADIAN_TIRE_STORE_NAME_ERROR_MESSAGE return false end unless shipping_address_valid_and_not_po_box_if_present errors.add :base, 'Shipping address invalid' return false end if deliveries.any?(&:incurs_oversized_penalty?) errors.add :base, 'Shipping packages exceed oversized limits or include a pallet and would incur big penalties, please review packaging and consider shipping via LTL freight.' return false end # if shipping_address.is_warehouse # errors.add :base, 'Warehouse pickup, manager must release from management hold' # return false # end if shipping_address.is_warehouse || (order_type == Order::STORE_TRANSFER) || %w[USA CAN].exclude?(shipping_address.country_iso3) || shipping_address.verified_for_shipping || shipping_address.disable_address_correction? || shipping_address.override_all_address_validation? return true end shipping_address.require_carrier_validation = true carrier 'Purolator' if shipping_address.country_iso3 == 'CAN' shipping_address.validate_with_carrier = carrier shipping_address.do_not_accept_legacy_verified = true res = shipping_address.external_validation_with_carrier(true) unless res || is_www || errors.to_a.any? { |e| e.index('Customer must be contacted to obtain a valid shipping address') } # don't show this validation error for www errors.add :base, "Order carrier validation#{if carrier.present? " with #{carrier}" end} must pass for order to be released. Carrier validation can't be skipped by disabling address correction, but in exceptional circumstances can be skipped by a system administrator. In worst cases, customer may need to be contacted to obtain a valid shipping address. " end res end |
#ready_for_warehouse? ⇒ Object
Whether ready for warehouse.
2959 2960 2961 |
# File 'app/models/order.rb', line 2959 def ready_for_warehouse? all_items_in_stock && ready_for_shipping? end |
#recipient_name ⇒ String?
Best-known recipient name for emails and shipping labels:
company name on the shipping address, then the person name on
the address, finally the customer's full name.
1363 1364 1365 |
# File 'app/models/order.rb', line 1363 def recipient_name shipping_address&.company_name || shipping_address&.person_name || customer&.full_name end |
#related_activities ⇒ ActiveRecord::Relation<Activity>
Activity records corresponding to #related_activities_ids.
2414 2415 2416 |
# File 'app/models/order.rb', line 2414 def Activity.where(id: ) end |
#related_activities_ids ⇒ Array<Integer>
Combined ids of every Activity hung directly off this order
plus those hung off its deliveries — the ids the CRM "all
activity for this order" feed should display.
2407 2408 2409 |
# File 'app/models/order.rb', line 2407 def [activities.ids, delivery_activities.ids].flatten end |
#release_order_or_hold ⇒ Object
Release the order to the warehouse unless something requires
accounting/management review first (#accounting_hold_order?),
in which case the order stays on hold for manual release.
2471 2472 2473 |
# File 'app/models/order.rb', line 2471 def release_order_or_hold release_order unless accounting_hold_order? end |
#remove_room_configuration(rc) ⇒ Hash
Detach a RoomConfiguration from the order, switching the
order's bound opportunity/quote to whichever sibling room is
left (or nil if removing the last one). Recomputes discounts
and saves. Returns {status: Boolean, message: String} for the
CRM flash-message stack.
2067 2068 2069 2070 2071 2072 2073 2074 2075 2076 2077 2078 2079 2080 2081 2082 2083 2084 2085 2086 2087 2088 2089 |
# File 'app/models/order.rb', line 2067 def remove_room_configuration(rc) order_label = cart? ? 'cart' : 'order' if rc && room_configuration_ids.include?(rc.id) room_configurations.delete rc if opportunity_id == rc.opportunity_id update(opportunity: opportunities[1]) # this will be nil if no room_configurations, as desired. end if rc.quote_ids.include?(quote_id) update(quote: quotes[1]) # this will be nil if no room_configurations, as desired. end reset_discount(reset_item_pricing: false) res = save msg = if res "Room/Heated Space #{rc.name} has been removed from #{order_label}." else "Room/Heated Space #{rc.name} has been removed from #{order_label}." end else res = false msg = "Room/Heated Space #{rc.name} is not in the #{order_label}." end { status: res, message: msg } end |
#require_cc_for_advance_credit? ⇒ Object
Whether require cc for advance credit.
1470 1471 1472 1473 1474 1475 1476 1477 1478 |
# File 'app/models/order.rb', line 1470 def require_cc_for_advance_credit? if allow_advance_credit_without_cc? false elsif rma && rma.rma_items.active.all?(&:will_not_be_returned?) false else customer.credit_card_vaults.valid.visible.empty? && rma.rma_items.any?(&:is_customer_fault?) end end |
#requires_intervention? ⇒ Object
Whether requires intervention.
2548 2549 2550 |
# File 'app/models/order.rb', line 2548 def requires_intervention? crm_back_order? || in_cr_hold? || pending_review? || || pending_payment? end |
#reset_cart ⇒ Object
Wipe a cart back to empty: destroy all line items, quoting
deliveries, and discounts, clear the shipping address, and prune
any rooms that no longer have line items. Used by the public
site's "empty cart" button.
3479 3480 3481 3482 3483 3484 3485 3486 |
# File 'app/models/order.rb', line 3479 def reset_cart line_items.destroy_all deliveries.quoting.destroy_all discounts.destroy_all self.shipping_address = nil save prune_cart_rooms end |
#restricted_order_type? ⇒ Object
Whether restricted order type.
1734 1735 1736 |
# File 'app/models/order.rb', line 1734 def restricted_order_type? RESTRICTED_ORDER_TYPES.key?(order_type) end |
#reviewer ⇒ Object
Returns the reviewer info for Reviews.io invitation
Prefers contact over customer if present
1369 1370 1371 1372 1373 1374 1375 |
# File 'app/models/order.rb', line 1369 def reviewer reviewer_party = contact.presence || customer { email: reviewer_party&.email, name: reviewer_party&.full_name } end |
#rma ⇒ Rma?
The associated rma.
301 |
# File 'app/models/order.rb', line 301 belongs_to :rma, inverse_of: :orders, optional: true |
#rma_cancel ⇒ Object
Hard-cancel an RMA replacement order: destroy all active
deliveries first (so their inventory commits release) then
destroy the order itself, all in one transaction.
4123 4124 4125 4126 4127 4128 |
# File 'app/models/order.rb', line 4123 def rma_cancel Order.transaction do deliveries.active.each(&:destroy) destroy end end |
#rma_items ⇒ ActiveRecord::Associations::CollectionProxy
The associated rma items.
385 |
# File 'app/models/order.rb', line 385 has_many :rma_items, inverse_of: :replacement_order |
#rmas ⇒ ActiveRecord::Associations::CollectionProxy
The associated rmas.
370 |
# File 'app/models/order.rb', line 370 has_many :rmas, foreign_key: :original_order_id |
#sales_support_rep ⇒ Employee?
The associated sales support rep.
289 |
# File 'app/models/order.rb', line 289 belongs_to :sales_support_rep, class_name: 'Employee', optional: true |
#save_early_label_api_log ⇒ Object
Save the accumulated API log to the order's early_label_metadata
Called at the end of purchase_early_label_if_requested (success or failure)
4875 4876 4877 4878 4879 4880 4881 4882 4883 4884 4885 4886 4887 4888 4889 4890 4891 4892 |
# File 'app/models/order.rb', line 4875 def save_early_label_api_log return if @early_label_api_log.blank? # Append to existing log if present, or create new existing_log = early_label_api_log || [] new_log = existing_log + @early_label_api_log # Keep only the last 50 entries to prevent unbounded growth new_log = new_log.last(50) if new_log.length > 50 # Ensure early_label_metadata is a hash before merging = || {} update_column(:early_label_metadata, .merge('api_log' => new_log)) @early_label_api_log = nil rescue StandardError => e Rails.logger.error("[EarlyLabel] Failed to save API log: #{e.}") Rails.logger.error(e.backtrace.first(3).join("\n")) end |
#schedule_follow_up_activity ⇒ Boolean
Run the FollowUpScheduler service against this order.
The scheduler decides whether the customer is due for a
follow-up call/email and creates the activity if so.
3323 3324 3325 3326 |
# File 'app/models/order.rb', line 3323 def schedule_follow_up_activity result = Order::FollowUpScheduler.new.process(self) result.activity_scheduled? end |
#search_text ⇒ String? (protected)
Indexed search text for full-text matching: order reference,
PO numbers, and RMA reference. Carts return nil so they don't
pollute the search index.
6426 6427 6428 6429 6430 6431 6432 6433 6434 |
# File 'app/models/order.rb', line 6426 def search_text return nil if cart? st = [] st << reference_number st += payments.pluck(:po_number) st << rma_reference st.join(' ') end |
#secondary_sales_rep ⇒ Employee?
Secondary sales rep on the order. Same closed-vs-open semantics
as #primary_sales_rep.
1877 1878 1879 1880 1881 |
# File 'app/models/order.rb', line 1877 def secondary_sales_rep return invoiced_secondary_sales_rep if order_closed? invoiced_secondary_sales_rep || customer&.secondary_sales_rep end |
#selected_shipping_cost_supports_early_label?(selected_sc, delivery) ⇒ Boolean
Checks whether the delivery's selected shipping cost is compatible with
the marketplace early-label flow. Today this means AMZBS or SWW; long-term
it may also include Heatwave native ship-labels.
5039 5040 5041 5042 5043 5044 5045 5046 5047 5048 5049 5050 5051 5052 |
# File 'app/models/order.rb', line 5039 def selected_shipping_cost_supports_early_label?(selected_sc, delivery) return false unless delivery.present? && selected_sc.present? is_amazon = edi_orchestrator_partner&.start_with?('amazon_seller') is_walmart = edi_orchestrator_partner&.start_with?('walmart_seller') if is_amazon selected_sc.is_amzbs? elsif is_walmart selected_sc.is_sww? else false end end |
#selection_name ⇒ String
Select-2 / Tom Select dropdown label — like #to_label but
uses "not shipped" when the order hasn't shipped yet.
4116 4117 4118 |
# File 'app/models/order.rb', line 4116 def selection_name "#{reference_number} #{customer.full_name} (#{shipped_date.present? ? shipped_date.to_fs(:crm_default) : 'not shipped'})" end |
#send_back_order_notification ⇒ Object
Email the back-order team that this order moved into back-order
state and needs follow-up with the customer.
3723 3724 3725 |
# File 'app/models/order.rb', line 3723 def send_back_order_notification OrdersMailer.back_order_notification(self).deliver_later end |
#send_early_label_void_alert(reason:, details:) ⇒ Object
Send alert email when early label void fails or is blocked
SAFEGUARD C: Ensures operations team is notified of label issues
4781 4782 4783 4784 4785 4786 4787 4788 4789 4790 4791 4792 4793 4794 4795 4796 4797 4798 4799 4800 4801 4802 4803 4804 4805 4806 4807 4808 4809 4810 4811 4812 4813 4814 4815 4816 4817 4818 |
# File 'app/models/order.rb', line 4781 def send_early_label_void_alert(reason:, details:) marketplace = edi_orchestrator_partner&.start_with?('amazon_seller') ? 'Amazon' : 'Walmart' portal = marketplace == 'Amazon' ? 'Amazon Seller Central' : 'Walmart Seller Portal' Rails.logger.warn("[EarlyLabel] Sending void alert for order #{reference_number}: #{reason}") SystemMailer.generic_mailer( subject: "[URGENT] Early Label Void Issue - Order #{reference_number}", body: <<~BODY, An early label void issue occurred that requires attention. ORDER DETAILS: - Order: #{reference_number} - Tracking: #{early_label_tracking_number} - Carrier: #{early_label_carrier} - Label Purchased: #{early_label_purchased_at&.strftime('%Y-%m-%d %H:%M:%S %Z')} - EDI Partner: #{edi_orchestrator_partner} - PO Number: #{edi_po_number} - Marketplace: #{marketplace} ISSUE: - Reason: #{reason} - Details: #{details} ACTION REQUIRED: 1. Check the order in Heatwave: https://#{CRM_HOSTNAME}/en-US/orders/#{id} 2. Check the label status in #{portal} 3. If the label exists in #{marketplace} but not in Heatwave, manually void it in #{portal} 4. Update the order status as needed This is an automated alert from the Early Label Purchase system. BODY to: ORDERS_EMAIL, from: ADMINISTRATOR_EMAIL ).deliver_later rescue StandardError => e Rails.logger.error("[EarlyLabel] Failed to send void alert email: #{e.}") end |
#send_online_order_confirmation ⇒ Hash{Symbol => Symbol, String}
Build and send the ONLINE_ORDER_CONFIRM email via
CommunicationBuilder, then pin the recipient address to the
order's tracking_email array so subsequent ship/tracking
emails go to the same place. Returns a {status_code:, status_message:} hash for the controller's flash stack.
3758 3759 3760 3761 3762 3763 3764 3765 3766 3767 3768 3769 3770 3771 3772 3773 3774 3775 3776 3777 3778 3779 3780 3781 3782 |
# File 'app/models/order.rb', line 3758 def send_online_order_confirmation if email_for_order_confirmation sender = customer.primary_sales_rep co = CommunicationBuilder.new( resource: self, sender_party: sender, sender: sender || INFO_EMAIL, recipient_party: party_for_order_confirmation, emails: email_for_order_confirmation, recipient_name: party_for_order_confirmation.name, template_system_code: 'ONLINE_ORDER_CONFIRM', bcc: sender&.email, merge_options: { order: to_liquid } ).create if co.draft? { status_code: :error, status_message: co.errors_to_s } else # IMPORTANT!!! THIS MUST BE SET HERE update_column(:tracking_email, [email_for_order_confirmation]) { status_code: :ok, status_message: "Order confirmation e-mail sent to #{party_for_order_confirmation.name} #{email_for_order_confirmation}." } end else { status_code: :error, status_message: "Can't send: order/customer e-mail is blank!" } end end |
#send_payment_automatically_authorized_notification ⇒ Object
Notify accounting that the auto-authoriser approved a payment
without human review — useful for spot-checking the rules engine.
3747 3748 3749 |
# File 'app/models/order.rb', line 3747 def OrdersMailer.(self).deliver_later end |
#send_profit_review_notification ⇒ Object
Email the management profit-review queue when this order's
margin falls below the configured threshold and needs sign-off.
3735 3736 3737 |
# File 'app/models/order.rb', line 3735 def send_profit_review_notification OrdersMailer.order_profit_review_notification(self).deliver_later end |
#send_ready_for_pickup_email ⇒ Hash{Symbol => Symbol, String}
Send the warehouse-pickup-ready email (ORDER_PICKUP template)
used for "your order is ready at the will-call counter" notices.
6124 6125 6126 |
# File 'app/models/order.rb', line 6124 def send_ready_for_pickup_email send_tracking_template_email 'ORDER_PICKUP' end |
#send_release_authorization_notification ⇒ Object
Email management when an order needs explicit release-from-CR-hold
authorisation (e.g. fraud-flagged payment, large order, etc.).
3741 3742 3743 |
# File 'app/models/order.rb', line 3741 def OrdersMailer.(self).deliver_later end |
#send_request_carrier_assignment_notification ⇒ Object
Notify dispatch when an order is waiting on a manual carrier
assignment (rate-shop returned no eligible options).
3729 3730 3731 |
# File 'app/models/order.rb', line 3729 def send_request_carrier_assignment_notification DeliveryMailer.request_carrier_assignment_notification(self).deliver_later end |
#send_tracking_email ⇒ Hash{Symbol => Symbol, String}
Send the standard ORDER_TRACKING email with the carrier's
tracking link.
6116 6117 6118 |
# File 'app/models/order.rb', line 6116 def send_tracking_email send_tracking_template_email 'ORDER_TRACKING' end |
#send_tracking_template_email(template_code) ⇒ Hash{Symbol => Symbol, String}
Generic helper that drives both #send_tracking_email and
#send_ready_for_pickup_email: builds the communication via
CommunicationBuilder for the named system template and sends
it to every address in tracking_email. Returns a controller-
friendly {status_code:, status_message:} hash.
6136 6137 6138 6139 6140 6141 6142 6143 6144 6145 6146 |
# File 'app/models/order.rb', line 6136 def send_tracking_template_email(template_code) return { status_code: :error, status_message: "Can't send #{template_code}: no tracking e-mail present!" } if tracking_email.blank? sender = customer.primary_sales_rep co = CommunicationBuilder.new(resource: self, sender_party: sender, sender: (sender.nil? ? INFO_EMAIL : nil), recipient_party: customer, emails: tracking_email.join(','), template_system_code: template_code).create if co.draft? { status_code: :error, status_message: co.errors_to_s } else { status_code: :ok, status_message: "#{template_code} e-mail sent." } end end |
#service_only_order? ⇒ Object
Whether service only order.
2512 2513 2514 |
# File 'app/models/order.rb', line 2512 def service_only_order? line_items.non_shipping.any? && line_items.non_shipping.all?(&:is_service?) end |
#set_currency ⇒ Object (protected)
Before-validation callback: pin the order's currency to the
customer's store currency when not already set, so currency
never silently defaults to USD on non-US orders.
6362 6363 6364 |
# File 'app/models/order.rb', line 6362 def set_currency self.currency ||= customer&.store&.currency end |
#set_custom_order_agreement ⇒ Object
Checks for the presence of custom products in excess of $2k
5907 5908 5909 5910 5911 5912 5913 |
# File 'app/models/order.rb', line 5907 def set_custom_order_agreement return unless is_sales_order? return if custom_order_agreement_bypass? return unless meets_custom_products_threshold? update_column(:custom_order_agreement_status, Order.custom_order_agreement_statuses['custom_order_agreement_required']) end |
#set_default_tracking_email(force: false) ⇒ Object
Populate tracking_email with addresses extracted by
DefaultTrackingEmailExtractor (customer + contact +
opportunity participants). No-op when an address is already set
unless force: true is passed.
4073 4074 4075 4076 4077 |
# File 'app/models/order.rb', line 4073 def set_default_tracking_email(force: false) return unless force || tracking_email.empty? self.tracking_email = Order::DefaultTrackingEmailExtractor.new(self).emails end |
#set_min_profit_markup ⇒ Object
Before-validation callback: pin the order's minimum-profit
markup percentage. Sales orders default to the company-wide
default_sales_markup; everything else (RMA replacements,
marketing, tech orders) gets 0 so they don't trip the
profit-review hold.
4059 4060 4061 4062 4063 4064 4065 |
# File 'app/models/order.rb', line 4059 def set_min_profit_markup self.min_profit_markup = if is_sales_order? default_sales_markup else 0 end end |
#set_opportunity_source ⇒ Object
After-save hook: when the order's source changes (and isn't the
generic "unknown source"), propagate that source up to its
opportunity. Keeps opportunity-level acquisition reporting
consistent with whatever source the order ended up with.
1830 1831 1832 1833 1834 1835 1836 1837 |
# File 'app/models/order.rb', line 1830 def set_opportunity_source return unless source && saved_change_to_source_id? return if source&.unknown_source? return unless opportunity # If you made it this far, then you can update the opportunity source opportunity.update_column(:source_id, source.id) end |
#set_shipped_date ⇒ Object
Stamp the order's shipped_date with today, but only when blank
— once shipped, the date is canonical and shouldn't drift on a
re-save. Called by the after-ship state-machine transition.
3346 3347 3348 |
# File 'app/models/order.rb', line 3346 def set_shipped_date update_attribute(:shipped_date, Date.current) if shipped_date.blank? end |
#ship_from_attributes(delivery = nil) ⇒ Hash
Build the "ship from" address/phone/email/attention block for a
carrier API call. Defaults to the order's store warehouse, but
when delivery is supplied it uses the delivery's origin
address (which differs for dropship deliveries) and applies any
shipping-account-number override (used when the customer is
billing shipping to their own carrier account).
3690 3691 3692 3693 3694 3695 3696 3697 3698 3699 3700 3701 3702 3703 3704 3705 3706 3707 3708 3709 3710 3711 3712 3713 3714 3715 3716 3717 3718 3719 |
# File 'app/models/order.rb', line 3690 def ship_from_attributes(delivery = nil) res = {} # Here we use delivery, if present, to extract the origin address for the ship from, as well as the contact info. This is for drop shippers, but should also cover when shipper is WY US or Canada res[:address] = delivery&.origin_address || store.warehouse_address # sort of a kludge but for now use shipping configuration's sender phone for legacy matching res[:phone] = delivery&.origin_address&.party&.phone || SHIPPING_SHIPPER_CONFIGURATION[country.iso3.to_sym][:shipper_phone] res[:email] = delivery&.origin_address&.party&.email || SHIPPING_SHIPPER_CONFIGURATION[country.iso3.to_sym][:shipper_email] # post rb_any_ship_from, we actually want the ship_from to be the physical ship from and handle the shipper separately if bill_shipping_to_customer && delivery&.chosen_shipping_method&.shipping_account_number if delivery.chosen_shipping_method.shipping_account_number.address # here, we implement override of ship from address to be the address linked in the shipping_account_number, if any. res[:address] = delivery.chosen_shipping_method.shipping_account_number.address end if begin delivery.chosen_shipping_method.shipping_account_number.phone rescue StandardError false end # here, we implement override of ship from phone to be the phone linked in the shipping_account_number, if any. res[:phone] = delivery.chosen_shipping_method.shipping_account_number.phone.detail end end # attention_name cannot be blank!!!! res[:attention_name] = res[:address].person_name res[:attention_name] = 'Shipping Department' if res[:attention_name].blank? res[:name] = (res[:address].company_name || res[:address].person_name) res[:phone] = res[:phone].scan(/[0-9]/).join if res[:phone] res end |
#ship_to_attributes ⇒ Hash
Build the "ship to" hash for carrier API calls: address, phone,
email, and ATTN/name lines, with sensible cascading fallbacks
(attention_name → address person name → literal "Customer",
phone falls through customer's phones to the rep's, etc.) so the
carrier never receives a blank field.
3585 3586 3587 3588 3589 3590 3591 3592 3593 3594 3595 3596 3597 3598 3599 3600 3601 3602 3603 3604 3605 3606 3607 3608 3609 3610 3611 3612 |
# File 'app/models/order.rb', line 3585 def ship_to_attributes res = {} res[:address] = shipping_address # attention_name cannot be blank!!!! res[:attention_name] = attention_name res[:attention_name] = res[:address].person_name if res[:attention_name].blank? res[:attention_name] = 'Customer' if res[:attention_name].blank? if res[:address].company_name res[:name] = res[:address].company_name res[:name] = 'Customer' if res[:name].blank? else res[:name] = attention_name res[:name] = res[:address].person_name if res[:name].blank? res[:name] = 'Customer' if res[:name].blank? end res[:phone] = shipping_phone.presence || customer.phone || customer.cell_phone || customer.primary_sales_rep&.direct_phone || customer.store.contact_number || SHIPPING_SHIPPER_CONFIGURATION.dig(country.iso3.to_sym, :shipper_phone) res[:email] = first_tracking_email&.strip.presence || customer.primary_sales_rep&.email || customer.store.contact_email || SHIPPING_SHIPPER_CONFIGURATION.dig(country.iso3.to_sym, :shipper_email) # need something here res end |
#ship_weight ⇒ Float
Aggregate physical ship weight across all non-shipping line
items, floored at 0.1 lb so carrier rate APIs that reject zero
weights don't blow up. Memoised so freight quote calls don't
re-walk the line items.
6076 6077 6078 |
# File 'app/models/order.rb', line 6076 def ship_weight @ship_weight ||= [0.1, line_items.includes(:item).non_shipping.parents_only.sum(&:total_shipping_weight).round(1)].max end |
#shipments ⇒ ActiveRecord::Associations::CollectionProxy
The associated shipments.
415 |
# File 'app/models/order.rb', line 415 has_many :shipments, -> { order(:id) }, through: :deliveries |
#shipping_account_number ⇒ ShippingAccountNumber
DELIVERY REFACTOR HERE
311 |
# File 'app/models/order.rb', line 311 belongs_to :shipping_account_number, optional: true |
#shipping_address ⇒ Address?
The associated shipping address.
341 |
# File 'app/models/order.rb', line 341 belongs_to :shipping_address, class_name: 'Address', validate: true, optional: true |
#shipping_address_valid_and_not_po_box_if_present(rate_shopping = false) ⇒ Boolean
Validates the shipping address: must be present, must pass its
own AR validations, and must not be a PO box for the chosen
carrier (FedEx/UPS reject them; USPS accepts them). Pass
rate_shopping: true to skip the carrier check during the
initial shop-rates pass — the carrier isn't picked yet so we
can't enforce its specific rules.
2939 2940 2941 2942 2943 2944 2945 2946 2947 2948 2949 2950 2951 2952 2953 2954 |
# File 'app/models/order.rb', line 2939 def shipping_address_valid_and_not_po_box_if_present(rate_shopping = false) res = false if shipping_address.nil? errors.add(:shipping_address, 'must be selected') elsif shipping_address.present? carrer_to_use = carrier carrer_to_use = nil if rate_shopping if shipping_address.valid? && shipping_address.not_a_po_box?(carrer_to_use) res = true else res = false shipping_address.errors..each { |msg| errors.add(:shipping_address, msg) } end end res end |
#shipping_cutoff_advance_order? ⇒ Object
Whether shipping cutoff advance order.
2202 2203 2204 |
# File 'app/models/order.rb', line 2202 def shipping_cutoff_advance_order? line_items.goods.parents_only.any? { |a| !a.in_stock? } end |
#shipping_cutoff_next_day? ⇒ Object
Whether shipping cutoff next day.
2195 2196 2197 |
# File 'app/models/order.rb', line 2195 def shipping_cutoff_next_day? line_items.goods.parents_only.any? { |a| a.in_stock? && a.ships_via_freight? } # next day end |
#shipping_cutoff_same_day? ⇒ Object
Whether shipping cutoff same day.
2188 2189 2190 |
# File 'app/models/order.rb', line 2188 def shipping_cutoff_same_day? line_items.goods.parents_only.all? { |a| a.in_stock? && !a.ships_via_freight? } # same day end |
#shipping_cutoff_status(now: Time.current) ⇒ Symbol
Whether the warehouse can still ship this order today.
Branches on the warehouse company's working hours + holiday
calendar (USA/CAN), not the visitor's local clock.
2223 2224 2225 2226 2227 2228 2229 2230 2231 2232 2233 2234 2235 2236 2237 |
# File 'app/models/order.rb', line 2223 def shipping_cutoff_status(now: Time.current) company = store&.company return :unknown unless company local_now = now.in_time_zone(company.working_hours_timezone) company.with_working_hours_config do if !WorkingHours.working_day?(local_now.to_date) :non_working_day elsif local_now.hour >= SAME_DAY_CUTOFF_HOUR :after_cutoff else :same_day end end end |
#shipping_date_warnings ⇒ Object
requested_ship_before is automatically advanced when the user revisits the shipping form,
so no warning is needed when it's in the past.
Warnings about shipping dates that don't block order release
These are displayed to users but don't prevent state transitions
2644 2645 2646 |
# File 'app/models/order.rb', line 2644 def shipping_date_warnings [] end |
#should_commit_stock? ⇒ Object
Whether should commit stock.
1293 1294 1295 |
# File 'app/models/order.rb', line 1293 def should_commit_stock? (future_release_date.present? && !do_not_reserve_stock?) || all_funds_available? end |
#smartservice_ticket ⇒ SupportCase?
The SmartService ticket currently waiting on payment for this
order, or nil if there isn't one. Used by the post-payment
hook that flips the ticket open (#move_service_case_from_pending_service_payment).
3017 3018 3019 |
# File 'app/models/order.rb', line 3017 def smartservice_ticket support_cases&.services&.pending_service_payment&.first end |
#sms_enabled_numbers ⇒ Array<String>
SMS-capable phone numbers for every order participant
(customer, contact, opportunity participants), formatted in the
E.164-style our SMS provider expects.
1454 1455 1456 |
# File 'app/models/order.rb', line 1454 def sms_enabled_numbers ContactPoint.where(party_id: all_participant_ids).sms_numbers.order(:detail).map(&:formatted_for_sms).uniq end |
#sms_messages ⇒ ActiveRecord::Relation<SmsMessage>
SMS conversation thread tied to this order — every inbound /
outbound message exchanged with any participant phone returned
by #sms_enabled_numbers.
1463 1464 1465 |
# File 'app/models/order.rb', line 1463 def SmsMessage.for_numbers(sms_enabled_numbers) end |
#sold_to_billing_address ⇒ Address?
The associated sold to billing address.
344 |
# File 'app/models/order.rb', line 344 belongs_to :sold_to_billing_address, class_name: 'Address', foreign_key: :sold_to_billing_address, optional: true |
#source ⇒ Source?
The associated source.
317 |
# File 'app/models/order.rb', line 317 belongs_to :source, optional: true |
#spiff_enrollment ⇒ SpiffEnrollment?
The associated spiff enrollment.
307 |
# File 'app/models/order.rb', line 307 belongs_to :spiff_enrollment, optional: true |
#spiff_rep ⇒ Contact?
The associated spiff rep.
304 |
# File 'app/models/order.rb', line 304 belongs_to :spiff_rep, class_name: 'Contact', optional: true, inverse_of: :spiff_orders |
#spiff_reward ⇒ String
Eligible SPIFF reward amount formatted as "%.2f". Assumes the
order is enrolled in a SPIFF; raises if spiff_enrollment is
nil.
1999 2000 2001 |
# File 'app/models/order.rb', line 1999 def spiff_reward '%.2f' % spiff_enrollment.spiff.eligible_reward(self) end |
#state_description(describe_state = nil) ⇒ String?
User-facing description for a state name, optionally for a
state other than the current one. Pulled from
OrderConstants::STATE_DESCRIPTION so descriptions are managed
in one place.
1901 1902 1903 |
# File 'app/models/order.rb', line 1901 def state_description(describe_state = nil) OrderConstants::STATE_DESCRIPTION[describe_state || state] end |
#state_list ⇒ Array<Symbol>
The ordered set of states this order can be in given its type
and current data. Credit orders follow a fixed sequence; sales /
store-transfer / etc. orders compute the state list dynamically
based on which states they've actually visited (so the CRM
progress bar only shows applicable steps).
1933 1934 1935 1936 1937 1938 1939 1940 1941 1942 1943 1944 1945 1946 1947 1948 1949 1950 1951 1952 1953 1954 1955 1956 1957 1958 1959 1960 1961 1962 1963 1964 1965 1966 |
# File 'app/models/order.rb', line 1933 def state_list if order_type == CREDIT_ORDER transition %i[cart pending pending_payment pending_release_authorization crm_back_order awaiting_deliveries in_cr_hold created awaiting_return] => :fraudulent, if: :can_be_cancelled? %i[created awaiting_return pending_review ready_for_printing printed cancelled] else states = [] states << :cart if cart? states << :pending states << :pending_payment states << :crm_back_order if crm_back_order? states << :pending_release_authorization if states << :in_cr_hold if in_cr_hold? states << :needs_serial_number_reservation states << :awaiting_deliveries states << :processing_deliveries states << :partially_invoiced states << :invoiced if begin cancelled? rescue StandardError false end states << :cancelled end if begin fraudulent? rescue StandardError false end states << :fraudulent end states end end |
#stock_status ⇒ Symbol
Combined inventory status across all line items: :ok,
:partial, or :none. Computed by LineItem.inventory_check.
2926 2927 2928 |
# File 'app/models/order.rb', line 2926 def stock_status LineItem.inventory_check(self) end |
#stop_for_pre_pack? ⇒ Object
Whether stop for pre pack.
5928 5929 5930 5931 5932 5933 5934 5935 5936 5937 5938 |
# File 'app/models/order.rb', line 5928 def stop_for_pre_pack? return false unless can_request_estimated_packaging? return false if customer.is_e_commerce_misc? return false if room_configurations.present? && !room_configurations.all?(&:transmittable_and_settled?) return false if deliveries.all? { |d| d.shipments.packed.any? && d.all_lines_allocated_to_shipments? } res = need_to_pre_pack_reasons return res if res&.any? false end |
#stop_for_profit_review? ⇒ Object
Whether stop for profit review.
5918 5919 5920 5921 5922 5923 |
# File 'app/models/order.rb', line 5918 def stop_for_profit_review? # Etailer orders are immune return false if customer.is_e_commerce_misc? !profit_margins_met? end |
#store ⇒ Store?
The Store this order is fulfilled from. Store transfers carry
an explicit from_store; everything else inherits the
customer's store.
1286 1287 1288 |
# File 'app/models/order.rb', line 1286 def store from_store || customer&.store end |
#store_additional_early_label_pdfs(additional_labels) ⇒ Object
Persist the secondary label PDFs returned by a multi-package
marketplace label purchase. Each entry is {label_data:, tracking_number:}; missing or blank entries are skipped, and a
single-PDF failure doesn't abort the rest of the loop.
5550 5551 5552 5553 5554 5555 5556 5557 5558 5559 |
# File 'app/models/order.rb', line 5550 def store_additional_early_label_pdfs(additional_labels) additional_labels.each_with_index do |label, idx| next unless label[:label_data].present? && label[:tracking_number].present? store_early_label_pdf(label[:label_data], label[:tracking_number]) Rails.logger.info("[EarlyLabel] Stored additional package #{idx + 2} label PDF (tracking: #{label[:tracking_number]})") rescue StandardError => e Rails.logger.warn("[EarlyLabel] Failed to store additional package #{idx + 2} label PDF: #{e.}") end end |
#store_amazon_label_pdf_atomic(label_result, tracking_number, marketplace) ⇒ Object
Amazon: label data is returned inline — store it and verify persistence.
For multi-package orders, also stores additional package labels.
5444 5445 5446 5447 5448 5449 5450 5451 5452 5453 5454 5455 5456 5457 5458 5459 5460 5461 5462 5463 5464 5465 5466 5467 5468 5469 5470 5471 5472 |
# File 'app/models/order.rb', line 5444 def store_amazon_label_pdf_atomic(label_result, tracking_number, marketplace) label_data = label_result[:label_data] if label_data.blank? Rails.logger.warn('[EarlyLabel] Amazon label data not returned inline; label PDF will be attached at ship-label time') send_early_label_void_alert( reason: 'Amazon label data missing from purchase response', details: "Label was purchased but no inline label data was returned. " \ "The label exists in #{marketplace}'s system but PDF is not stored locally." ) return nil end upload = store_early_label_pdf(label_data, tracking_number) if upload&.persisted? Rails.logger.info("[EarlyLabel] Amazon label PDF stored as upload #{upload.id}") store_additional_early_label_pdfs(label_result[:additional_labels]) if label_result[:additional_labels].present? return upload end Rails.logger.error("[EarlyLabel] Amazon label PDF storage failed for order #{reference_number}") send_early_label_void_alert( reason: 'Amazon label PDF storage failed', details: "Label data was returned but could not be persisted. " \ "The label exists in #{marketplace}'s system but is not stored locally." ) nil end |
#store_early_label_pdf(label_data, tracking_number) ⇒ Upload?
Store early label PDF on the order
5523 5524 5525 5526 5527 5528 5529 5530 5531 5532 5533 5534 5535 5536 5537 5538 5539 5540 5541 5542 |
# File 'app/models/order.rb', line 5523 def store_early_label_pdf(label_data, tracking_number) filename = "early_ship_label_#{tracking_number}_#{Time.current.to_i}.pdf" upload = Upload.uploadify_from_data( file_name: filename, data: label_data, category: 'early_ship_label', resource: self ) if upload&.persisted? uploads << upload Rails.logger.info("[EarlyLabel] Stored early label PDF as upload #{upload.id}") end upload rescue StandardError => e Rails.logger.error("[EarlyLabel] Failed to store early label PDF: #{e.}") nil end |
#store_id ⇒ Object
Alias for Store#id
3270 |
# File 'app/models/order.rb', line 3270 delegate :id, to: :store, prefix: true |
#store_mock_early_label_pdf(tracking_number, _carrier) ⇒ Object
Store a mock label PDF for development/testing
Uses a pre-generated mock PDF since the sandbox doesn't return real label PDFs
5566 5567 5568 5569 5570 5571 5572 5573 5574 5575 5576 5577 5578 5579 5580 5581 5582 5583 5584 5585 5586 5587 5588 5589 5590 5591 5592 5593 |
# File 'app/models/order.rb', line 5566 def store_mock_early_label_pdf(tracking_number, _carrier) mock_pdf_path = Rails.root.join('test/sww_label_mock_pdf.pdf') unless File.exist?(mock_pdf_path) Rails.logger.error("[EarlyLabel] Mock PDF not found at #{mock_pdf_path}") return nil end mock_pdf_content = File.binread(mock_pdf_path) filename = "early_ship_label_#{tracking_number}_#{Time.current.to_i}.pdf" upload = Upload.uploadify_from_data( file_name: filename, data: mock_pdf_content, category: 'early_ship_label', resource: self ) if upload&.persisted? uploads << upload Rails.logger.info("[EarlyLabel] Stored mock early label PDF as upload #{upload.id}") end upload rescue StandardError => e Rails.logger.error("[EarlyLabel] Failed to store mock early label PDF: #{e.}") nil end |
#store_walmart_label_pdf_atomic(shipper, _label_result, tracking_number, carrier, marketplace) ⇒ Object
Walmart: download label via API with retries, then store and verify persistence
5480 5481 5482 5483 5484 5485 5486 5487 5488 5489 5490 5491 5492 5493 5494 5495 5496 5497 5498 5499 5500 5501 5502 5503 5504 5505 5506 5507 5508 5509 5510 5511 5512 5513 5514 5515 5516 |
# File 'app/models/order.rb', line 5480 def store_walmart_label_pdf_atomic(shipper, _label_result, tracking_number, carrier, marketplace) max_attempts = 3 delays = [1, 2, 3] max_attempts.times do |attempt| delay = delays[attempt] || 1 sleep(delay) Rails.logger.info("[EarlyLabel] Attempting PDF download (attempt #{attempt + 1}/#{max_attempts}) for tracking #{tracking_number}") download_result = shipper.download_label(carrier, tracking_number) merge_shipper_api_log(shipper) if download_result[:success] && download_result[:label_data].present? upload = store_early_label_pdf(download_result[:label_data], tracking_number) if upload&.persisted? Rails.logger.info("[EarlyLabel] PDF successfully stored (attempt #{attempt + 1}) as upload #{upload.id}") return upload else Rails.logger.warn("[EarlyLabel] PDF download succeeded but storage failed (attempt #{attempt + 1})") end else Rails.logger.warn("[EarlyLabel] PDF download failed (attempt #{attempt + 1}): #{download_result[:error]}") end end Rails.logger.error("[EarlyLabel] All PDF download attempts failed for order #{reference_number}, tracking #{tracking_number}") send_early_label_void_alert( reason: 'PDF download failed after all retries', details: "Label was created successfully but PDF could not be downloaded after #{max_attempts} attempts. " \ "The label exists in #{marketplace}'s system but is not stored locally." ) nil end |
#suggested_shipments ⇒ ActiveRecord::Relation?
Shipments still in the suggested state — i.e. proposed by
Shipping::DeterminePackaging but not yet confirmed by the
warehouse. Used by the early-label flow to know what to label.
6066 6067 6068 |
# File 'app/models/order.rb', line 6066 def suggested_shipments shipments.where(state: 'suggested') if respond_to? :shipments end |
#support_case_id ⇒ Integer?
Single support-case binding for the CRM order-setup form's Tom
Select picker, mirroring Rma#support_case_id. Backed by the HABTM
support_cases association so the form can reuse the shared
lookup_support_cases typeahead (which keys options by id).
1656 1657 1658 |
# File 'app/models/order.rb', line 1656 def support_case_id support_case_ids.first end |
#support_case_id=(id) ⇒ Object
1661 1662 1663 |
# File 'app/models/order.rb', line 1661 def support_case_id=(id) self.support_case_ids = Array(id.presence).map(&:to_i) end |
#support_case_ref ⇒ String
Comma-separated list of attached support case numbers. Used in
the CRM order form's free-typed support-case field so users can
paste in a list of case numbers without juggling ids.
1636 1637 1638 |
# File 'app/models/order.rb', line 1636 def support_case_ref support_cases.map(&:case_number).join(', ') end |
#support_case_ref=(support_case_ref) ⇒ Object
Setter that resolves a comma-separated list of case numbers
into the matching support-case ids and writes them to
support_case_ids. Tolerates spaces in the input.
1645 1646 1647 1648 |
# File 'app/models/order.rb', line 1645 def support_case_ref=(support_case_ref) refs = support_case_ref.delete(' ').split(',') self.support_case_ids = SupportCase.where(case_number: refs).ids end |
#support_cases ⇒ ActiveRecord::Associations::CollectionProxy
The associated support cases.
422 |
# File 'app/models/order.rb', line 422 has_and_belongs_to_many :support_cases |
#suppress_edi_duplicate_warning? ⇒ Boolean
Suppresses duplicate order warnings for off-book delivery arrangements.
Returns true if warnings should be suppressed (acknowledged date is in the future and order not shipped).
2266 2267 2268 2269 2270 2271 |
# File 'app/models/order.rb', line 2266 def suppress_edi_duplicate_warning? return false if edi_delayed_delivery_acknowledged_at.blank? return false if shipped? # Auto-clear when shipped edi_delayed_delivery_acknowledged_at.to_date >= Date.current end |
#sync_opportunity ⇒ Object
After-save hook: tell the linked opportunity to re-evaluate its
state machine (the opportunity may need to advance from
"active" to "sold" once the order is invoiced).
4181 4182 4183 |
# File 'app/models/order.rb', line 4181 def sync_opportunity opportunity&.sync_state end |
#technical_support_rep ⇒ TechnicalSupportRep?
The associated technical support rep.
354 |
# File 'app/models/order.rb', line 354 has_one :technical_support_rep, through: :opportunity |
#terms_available? ⇒ Object (protected)
Whether terms available.
6417 6418 6419 |
# File 'app/models/order.rb', line 6417 def terms_available? billing_entity.terms_credit_limit >= total end |
#to_label ⇒ String
Compact label string used by autocomplete dropdowns and SMS
threads — [REF#] Customer (shipped-date).
4090 4091 4092 |
# File 'app/models/order.rb', line 4090 def to_label "[#{reference_number}] #{customer.full_name} (#{shipped_date.to_fs(:crm_default)})" end |
#to_liquid ⇒ Liquid::OrderDrop
Wrap the order in a Liquid::OrderDrop so it can be safely
rendered inside customer-facing email templates without exposing
internal model methods.
3619 3620 3621 |
# File 'app/models/order.rb', line 3619 def to_liquid Liquid::OrderDrop.new self end |
#to_s ⇒ String
Returns #cart_identifier.
4108 4109 4110 |
# File 'app/models/order.rb', line 4108 def to_s cart_identifier end |
#to_store ⇒ Store?
The associated to store.
338 |
# File 'app/models/order.rb', line 338 belongs_to :to_store, class_name: 'Store', optional: true |
#total_cod ⇒ BigDecimal, Float
Cash-On-Delivery amount the carrier should collect from the
consignee. Returns 0 when the order isn't COD-funded; otherwise
subtracts any already-authorised pre-payments so we don't ask
the carrier to collect twice.
3334 3335 3336 3337 3338 3339 3340 3341 |
# File 'app/models/order.rb', line 3334 def total_cod # return 0 if the order is not funded by cod return 0.0 unless funded_by_cod? # else we need to deduct any pre_payments from the order total to work out what the cod amount should be # in case the balance was partially paid by another payment method total - end |
#total_money ⇒ String
Order total formatted as a "%.2f" string for display where the
template needs a fixed two-decimal value (CSV exports, EDI
payloads).
1990 1991 1992 |
# File 'app/models/order.rb', line 1990 def total_money '%.2f' % total end |
#total_payments_authorized ⇒ BigDecimal
Sum of authorised payment amounts across deliveries, capping
each delivery at its own total so over-authorisation on one
delivery doesn't shift coverage to another. The complement of
#balance.
2765 2766 2767 2768 2769 2770 2771 2772 2773 |
# File 'app/models/order.rb', line 2765 def amount = BigDecimal('0.0') deliveries.each do |dq| dq_total = dq.total || BigDecimal('0.0') dq_auth = dq. || BigDecimal('0.0') amount += [dq_auth, dq_total].min end amount < 0 ? BigDecimal('0.0') : amount end |
#track_profit? ⇒ Object
Whether track profit.
6048 6049 6050 |
# File 'app/models/order.rb', line 6048 def track_profit? profitable_line_items.present? && is_regular_order? end |
#tracking_email_address ⇒ String
Inbound-email address that uniquely identifies this order — when
a tracking-email reply lands at this address, ActionMailbox
decrypts the id and re-attaches the message to the order.
2682 2683 2684 2685 2686 2687 |
# File 'app/models/order.rb', line 2682 def tracking_email_address domain = Rails.application.config.x.email_domain prefix = 'ord' encrypted_id = Encryption.encrypt_string(id.to_s) "#{prefix}+id#{encrypted_id}@#{domain}" end |
#uncommit_undelivered_line_items ⇒ Object
Release inventory commits for line items that aren't attached to
a delivery (typically left over after a delivery is destroyed
mid-flow). Otherwise stock would stay reserved against a phantom
commit and warehouse pickers couldn't see it as available.
2896 2897 2898 2899 2900 |
# File 'app/models/order.rb', line 2896 def uncommit_undelivered_line_items orphaned = line_items.where(delivery_id: nil) .joins(:inventory_commits).distinct Item::InventoryCommitter.crm_uncommit(orphaned) if orphaned.any? end |
#undiscounted_shipping_total ⇒ BigDecimal
Sum of the shipping ('shp') line items at their raw (pre-discount) price.
Unlike the shipping_cost column — the post-discount net, polluted by any
live shipping credit (FS or the economy match itself) — this is a stable
base for computing shipping credits (see
Delivery#economy_shipping_match_amount).
2181 2182 2183 |
# File 'app/models/order.rb', line 2181 def undiscounted_shipping_total line_items.shipping_only.to_a.sum(BigDecimal('0')) { |li| (li.price || 0).to_d * li.quantity.to_i } end |
#unfulfilled_dropship_items ⇒ Boolean
True when any dropship line still lacks a fully-receipted
supplier purchase-order item — i.e. the supplier hasn't shipped
the order yet from their warehouse. Used to decide whether the
order can transition out of awaiting_po_fulfillment.
2908 2909 2910 |
# File 'app/models/order.rb', line 2908 def unfulfilled_dropship_items line_items.dropship.any? { |li| li.purchase_order_item.nil? || !li.purchase_order_item.fully_receipted? } end |
#update_customer_status ⇒ Object (protected)
After-save callback: re-evaluate the customer's lifecycle state
(lead → prospect → customer → returning customer) now that this
order may have moved them up or down.
6355 6356 6357 |
# File 'app/models/order.rb', line 6355 def update_customer_status customer&.set_status end |
#update_linked_po_if_st ⇒ Boolean
For store-transfer orders, sync the linked PurchaseOrder record
to the order's first delivery so accounting sees the same dates,
quantities, and costs on both sides of the transfer.
6038 6039 6040 6041 6042 6043 |
# File 'app/models/order.rb', line 6038 def update_linked_po_if_st return unless is_store_transfer? && deliveries.first po = PurchaseOrder.new_or_update_st_po_from_delivery(deliveries.first) po.present? && po.valid? end |
#update_sales_support_rep(new_sales_support_rep_id, new_commission_date = nil) ⇒ Boolean
Reassign the sales-support rep on the order (and optionally
update the support-commission date). For invoiced orders we
save with validate: false so a rep change after invoicing
doesn't accidentally fail validations that don't matter at
this point in the lifecycle.
2100 2101 2102 2103 2104 2105 2106 2107 2108 2109 2110 2111 2112 2113 2114 |
# File 'app/models/order.rb', line 2100 def update_sales_support_rep(new_sales_support_rep_id, new_commission_date = nil) # Allow updating sales_support_rep_id and commission date even for invoiced orders if invoiced? # For invoiced orders, update only the sales_support_rep_id and commission date without other validations self.sales_support_rep_id = new_sales_support_rep_id self.sales_support_commission_date = new_commission_date save(validate: false) else # For non-invoiced orders, use normal update process update( sales_support_rep_id: new_sales_support_rep_id, sales_support_commission_date: new_commission_date ) end end |
#uploads ⇒ ActiveRecord::Associations::CollectionProxy
The associated uploads.
367 |
# File 'app/models/order.rb', line 367 has_many :uploads, -> { order(created_at: :desc) }, as: :resource, dependent: :destroy |
#versions_for_audit_trail(_params = {}) ⇒ ActiveRecord::Relation<RecordVersion>
Aggregate RecordVersion (PaperTrail) rows for the order's
full audit trail — the order itself plus every line item,
delivery, and discount that points back to it. Surfaced in the
CRM "Audit" tab.
4192 4193 4194 4195 4196 4197 4198 4199 4200 4201 4202 4203 4204 4205 4206 4207 4208 4209 4210 4211 4212 4213 4214 4215 4216 4217 |
# File 'app/models/order.rb', line 4192 def versions_for_audit_trail(_params = {}) query_sql = %q{ (item_type = 'Order' AND item_id = :id) OR ( item_type = 'LineItem' AND reference_data @> '{"resource_type": "Order"}' AND reference_data @> :resource_id_json ) OR ( item_type = 'Delivery' AND reference_data @> :order_id_json ) OR ( item_type = 'Discount' AND reference_data @> :itemizable_id_json AND reference_data @> '{"itemizable_type": "Order"}' ) } RecordVersion.where( query_sql, id:, resource_id_json: { resource_id: id }.to_json, order_id_json: { order_id: id }.to_json, itemizable_id_json: { itemizable_id: id }.to_json ) end |
#visit ⇒ Visit?
The associated visit.
347 |
# File 'app/models/order.rb', line 347 belongs_to :visit, optional: true |
#void_credit_rma_item ⇒ Object
Void every RMA credit-item linked to this order's line items.
Used when a credit order is being cancelled — the linked RMA
items should no longer be redeemable against future orders.
4231 4232 4233 |
# File 'app/models/order.rb', line 4231 def void_credit_rma_item line_items.filter_map(&:credit_rma_item).uniq.each(&:void!) end |
#void_early_label!(reason: 'Manual void requested', reset_flag: true) ⇒ Hash
Void the early label with a custom reason (public method for external callers)
Also resets purchase_label_early flag so the next ship-label goes through normal flow
4942 4943 4944 4945 4946 4947 4948 4949 4950 4951 4952 4953 4954 4955 4956 4957 4958 4959 4960 4961 4962 4963 4964 4965 4966 4967 4968 4969 4970 4971 4972 4973 4974 4975 4976 |
# File 'app/models/order.rb', line 4942 def void_early_label!(reason: 'Manual void requested', reset_flag: true) return { success: true, message: 'No early label to void' } unless has_early_purchased_label? Rails.logger.info("[EarlyLabel] Voiding early label for order #{reference_number}: tracking=#{early_label_tracking_number}, reason=#{reason}") begin client, marketplace_type = build_early_label_void_client void_api_result = if marketplace_type == :amazon client.cancel_shipment(early_label_sww_label_id) else client.discard_label(early_label_carrier, early_label_tracking_number) end if void_api_result.success Rails.logger.info("[EarlyLabel] Successfully voided early label for order #{reference_number}") void_additional_early_label_packages(client) if marketplace_type == :amazon update_attrs = { early_label_voided_at: Time.current, early_label_void_reason: reason } update_attrs[:purchase_label_early] = false if reset_flag update!(update_attrs) void_early_label_upload Rails.logger.info('[EarlyLabel] Reset purchase_label_early flag') if reset_flag { success: true } else Rails.logger.error("[EarlyLabel] Failed to void early label for order #{reference_number}: #{void_api_result.error}") update!(early_label_void_reason: "#{reason} (API failed: #{void_api_result.error})") { success: false, error: void_api_result.error } end rescue StandardError => e Rails.logger.error("[EarlyLabel] Error voiding early label for order #{reference_number}: #{e.}") update!(early_label_void_reason: "#{reason} (Exception: #{e.})") { success: false, error: e. } end end |
#void_early_label_if_exists ⇒ Hash?
Void early-purchased label when order is pulled back from awaiting_deliveries
Called from before_transition to cancelled/fraudulent/in_cr_hold
SAFEGUARD B: Blocks rapid void if label was just purchased (within threshold)
SAFEGUARD C: Sends alert email if void fails
4604 4605 4606 4607 4608 4609 4610 4611 4612 4613 4614 4615 4616 4617 4618 4619 4620 4621 4622 4623 4624 4625 4626 4627 4628 4629 4630 4631 4632 4633 4634 4635 4636 4637 4638 4639 4640 4641 4642 4643 4644 4645 4646 4647 4648 4649 4650 4651 4652 4653 4654 4655 4656 4657 4658 4659 4660 4661 4662 4663 4664 4665 4666 4667 4668 4669 4670 4671 4672 4673 4674 4675 4676 4677 4678 4679 4680 4681 4682 4683 4684 |
# File 'app/models/order.rb', line 4604 def void_early_label_if_exists unless has_early_purchased_label? cleanup_failed_early_label_state if purchase_label_early? && early_label_tracking_number.blank? return end # SAFEGUARD B: Check for rapid state transition if early_label_purchased_recently? minutes_since = ((Time.current - early_label_purchased_at) / 60).round(1) wait_minutes = (EARLY_LABEL_RAPID_VOID_THRESHOLD_MINUTES - minutes_since).ceil Rails.logger.warn("[EarlyLabel] Rapid void blocked for order #{reference_number}: label purchased #{minutes_since} min ago (threshold: #{EARLY_LABEL_RAPID_VOID_THRESHOLD_MINUTES} min)") errors.add(:base, "Early label was purchased #{minutes_since} minutes ago. Please wait #{wait_minutes} more minute(s) before holding/canceling, or void the label manually first.") throw :halt end Rails.logger.info("[EarlyLabel] Auto-voiding early label for order #{reference_number}: tracking=#{early_label_tracking_number}") # Initialize API log for void operation @early_label_api_log = [] log_early_label_event('void_start', "Starting void for tracking #{early_label_tracking_number}") begin client, marketplace_type = build_early_label_void_client void_api_result = if marketplace_type == :amazon client.cancel_shipment(early_label_sww_label_id) else client.discard_label(early_label_carrier, early_label_tracking_number) end log_early_label_event('api_response', 'void/discard response', { success: void_api_result.success, error: void_api_result.error }) if void_api_result.success Rails.logger.info("[EarlyLabel] Successfully voided early label for order #{reference_number}") log_early_label_event('void_success', "Label voided successfully") void_additional_early_label_packages(client) if marketplace_type == :amazon save_early_label_api_log update!( early_label_voided_at: Time.current, early_label_void_reason: 'Order transitioned out of awaiting_deliveries' ) void_early_label_upload { success: true } else Rails.logger.error("[EarlyLabel] Failed to void early label for order #{reference_number}: #{void_api_result.error}") log_early_label_event('void_failed', "Void API failed: #{void_api_result.error}") save_early_label_api_log # SAFEGUARD C: Send alert for void failure marketplace = edi_orchestrator_partner&.start_with?('amazon_seller') ? 'Amazon' : 'Walmart' send_early_label_void_alert( reason: "#{marketplace} API void failed", details: void_api_result.error ) update!(early_label_void_reason: "Void attempted but API failed: #{void_api_result.error}") { success: false, error: void_api_result.error } end rescue StandardError => e Rails.logger.error("[EarlyLabel] Error voiding early label for order #{reference_number}: #{e.}") log_early_label_event('void_exception', "Exception: #{e.}") save_early_label_api_log # SAFEGUARD C: Send alert for void exception (unless it's our own rapid-void exception) unless e..include?('Cannot auto-void early label') send_early_label_void_alert( reason: 'Void exception', details: e. ) end update!(early_label_void_reason: "Void failed with exception: #{e.}") { success: false, error: e. } end end |
#void_early_label_upload ⇒ Object
Re-categorize the early label PDF as voided so it no longer appears
as an active attachment but is kept for audit trail (mirrors the
ship_label_pdf -> voided_ship_label_pdf pattern in Shipment)
5088 5089 5090 5091 5092 5093 5094 |
# File 'app/models/order.rb', line 5088 def void_early_label_upload uploads.in_category('early_ship_label').update_all(category: 'voided_early_ship_label') Rails.logger.info("[EarlyLabel] Re-categorized early label PDF to voided_early_ship_label on order #{reference_number}") rescue StandardError => e Rails.logger.warn("[EarlyLabel] Failed to re-categorize early label PDF: #{e.}") # Non-fatal — metadata void is what matters end |
#void_payments(report_fraud = false) ⇒ Object
Void every authorised payment on the order (and refuse to act on
captured ones — those need manual intervention). Used when an
order is cancelled or marked fraudulent; pass report_fraud: true
to flag the void to the gateway as a chargeback risk.
6338 6339 6340 6341 6342 6343 6344 6345 6346 6347 6348 |
# File 'app/models/order.rb', line 6338 def void_payments(report_fraud = false) payments.each do |pp| if pp. res = pp.gateway_class.new(pp).void(report_fraud) raise StandardError, "Unable to void authorized payment #{pp.id} for order #{reference_number}" unless res.success elsif pp.captured? # TODO: Handle captured payments. raise StandardError, "Unable to void captured payments #{pp.id} for order #{reference_number}" end end end |
#vouchers ⇒ ActiveRecord::Associations::CollectionProxy
The associated vouchers.
388 |
# File 'app/models/order.rb', line 388 has_many :vouchers |
#walmart_sww_eligible? ⇒ Boolean
Check if order is eligible for Walmart Ship with Walmart early label purchase
4988 4989 4990 4991 4992 4993 4994 4995 4996 4997 4998 4999 |
# File 'app/models/order.rb', line 4988 def walmart_sww_eligible? return false unless is_edi_order? return false unless edi_orchestrator_partner&.start_with?('walmart_seller') begin orchestrator = Edi::Walmart::Orchestrator.new(edi_orchestrator_partner.to_sym) orchestrator&.ship_with_walmart_enabled? rescue StandardError => e Rails.logger.warn("[EarlyLabel] Error checking SWW eligibility: #{e.}") false end end |
#warranties ⇒ ActiveRecord::Relation<Warranty>
Warranty registrations against this order (kept if the order is ever
hard-deleted — the registration is the customer's record, not ours).
376 |
# File 'app/models/order.rb', line 376 has_many :warranties, dependent: :nullify, inverse_of: :order |