Class: Payment
- Inherits:
-
ApplicationRecord
- Object
- ActiveRecord::Base
- ApplicationRecord
- Payment
- Includes:
- Models::Auditable, PgSearch::Model
- Defined in:
- app/models/payment.rb
Overview
== Schema Information
Table name: payments
Database name: primary
id :bigint not null, primary key
account_holder_type :string
account_type :string
address_line1_check :string
address_zip_check :string
amazon_pay_status :string
amount :decimal(, )
approval_notification_sent :boolean
authorization_code :string
authorization_reference :string
authorization_type :string
billing_address_city :string
billing_address_country :string
billing_address_line1 :string
billing_address_line2 :string
billing_address_state :string
billing_address_zip :string
brand :string
bread_token :string
capture_before :datetime
card_country :string
card_expires_on :date
card_identifier :string
card_type :string
category :string
currency :string
cvc_check :string
date :date
email :string
exp_month :integer
exp_year :integer
first_name :string
fraud_review_done :boolean
http_accept_language :string
http_user_agent :string
issuer_number :string
last4 :string
last_name :string
name :string
payment_approved :boolean
paypal_email :string
paypal_metadata :jsonb
paypal_token :string
plaid_expected_settlement_date :date
plaid_public_token :string
po_number :string
radar_network_status :string
radar_reason :string
radar_risk_level :string
radar_seller_message :string
radar_type :string
reference :string
remote_ip_address :string
routing_number :string
send_authorization_email :boolean
shipping_address_city :string
shipping_address_country :string
shipping_address_line1 :string
shipping_address_line2 :string
shipping_address_name :string
shipping_address_state :string
shipping_address_zip :string
skip_auto_receipt :boolean default(FALSE)
skip_minfraud :boolean
state :string default("pending")
stripe_capabilities :jsonb
test :boolean
uploads_count :integer
zip_code :string
created_at :datetime not null
updated_at :datetime not null
account_id :integer
amazon_pay_charge_id :string
amazon_pay_charge_permission_id :string
amazon_pay_checkout_session_id :string
creator_id :integer
credit_memo_id :integer
customer_id :integer
delivery_id :integer
invoice_id :integer
order_id :integer
paypal_payer_id :string
paypal_transaction_id :string
plaid_account_id :string
plaid_transfer_id :string
plaid_transfer_intent_id :string
rma_id :integer
stripe_payment_intent_id :string
transaction_id :string
updater_id :integer
vault_id :string
vpo_contact_id :integer
Indexes
by_did_ctry_st (delivery_id,category,state)
by_iid_st_at (invoice_id,state,authorization_type)
by_oid_ctry_st (order_id,category,state)
idx_state_category (state,category)
index_payments_on_account_id (account_id)
index_payments_on_authorization_type_and_authorization_code (authorization_type,authorization_code) WHERE ((authorization_type IS NOT NULL) AND (authorization_code IS NOT NULL))
index_payments_on_creator_id (creator_id)
index_payments_on_credit_memo_id (credit_memo_id)
index_payments_on_customer_id (customer_id)
index_payments_on_order_id_and_state (order_id,state)
index_payments_on_paypal_payer_id (paypal_payer_id)
index_payments_on_paypal_transaction_id (paypal_transaction_id)
index_payments_on_rma_id (rma_id)
index_payments_on_stripe_payment_intent_id (stripe_payment_intent_id) WHERE (stripe_payment_intent_id IS NOT NULL)
index_payments_on_transaction_id (transaction_id)
index_payments_on_updater_id (updater_id)
index_payments_on_vault_id (vault_id)
index_payments_on_vpo_contact_id (vpo_contact_id)
Foreign Keys
payments_credit_memo_id_fkey (credit_memo_id => credit_memos.id)
payments_customer_id_fkey (customer_id => parties.id) ON DELETE => cascade
payments_delivery_id_fk (delivery_id => deliveries.id) ON DELETE => nullify
payments_invoice_id_fkey (invoice_id => invoices.id)
payments_order_id_fk (order_id => orders.id) ON DELETE => cascade
Defined Under Namespace
Classes: DailyIssuesDigestWorker, DuplicateChargeGuard, OrderProcessor, PaypalStatusResult, StrategyResolver, StripeRefundReconciliationWorker
Constant Summary collapse
- ADV_REPL =
Advance Replacement category.
'Advance Replacement'- CHECK =
Check category.
'Check'- CREDIT_CARD =
Credit Card category.
'Credit Card'- CREDIT_CARD_TERMINAL =
Credit Card Terminal category.
'Credit Card Terminal'- BREAD =
Bread category.
'Bread'- PLAID =
Plaid category.
'Plaid'- AMAZON_PAY =
Amazon Pay category.
'Amazon Pay'- PO =
Purchase Order category.
'Purchase Order'- VPO =
Verbal Purchase Order category.
'Verbal Purchase Order'- ECHECK =
eCheck category.
'eCheck'- PAYPAL =
PayPal category.
'PayPal'- PAYPAL_INVOICE =
PayPal Invoice category.
'PayPal Invoice'- RMA_CREDIT =
RMA Credit category.
'RMA Credit'- CASH =
Cash category.
'Cash'- STORE_CREDIT =
Store Credit category.
'Store Credit'- WIRE =
Wire Transfer category.
'Wire Transfer'- ACCOUNT_HOLDER_TYPES =
Bank account holder types for eCheck payments.
%w[personal business]
- ACCOUNT_TYPES =
Bank account types for eCheck payments.
%w[checking savings]
- PAYPAL_MIN_SIGNATURE_REQUIRED =
Minimum PayPal amount requiring a signature.
750.00- ECHECK_MIN_AMOUNT_WITHOUT_SUPERVISION =
Maximum eCheck amount processable without supervision.
2000.00- CATEGORIES_REQUIRING_REVIEW =
Categories whose payments need manual review.
[PAYPAL_INVOICE, RMA_CREDIT, ECHECK, CHECK, CASH, WIRE]
- CATEGORIES_NOT_ALLOWING_CAPTURE =
Categories that cannot be captured through the gateway.
[PO, VPO, ECHECK, WIRE]
- PAYPAL_OVER_CAPTURE_PERCENT =
PayPal allows captures up to 115% of the original authorization total.
BigDecimal('1.15')
- PAYPAL_OVER_CAPTURE_MAX_INCREASE =
PayPal over-capture is additionally capped at $75 above the original total.
BigDecimal('75')
- PAYPAL_HONOR_PERIOD =
How long PayPal honors an authorization after capture_before.
3.days
- PAYPAL_REAUTH_BUFFER =
Minimum time left before expiry required to skip reauthorization.
12.hours
- PAYPAL_MAX_AUTH_DAYS =
Hard PayPal limit: an authorization cannot live past 29 days.
29- PAYPAL_REAUTH_EARLIEST_DAY =
PayPal reauthorization is only attempted from this day onward after the
original authorization. 4
Constants included from Models::Auditable
Models::Auditable::ALWAYS_IGNORED
Constants included from Models::Schedulable
Models::Schedulable::SIMPLE_FORM_OPTIONS
Belongs to collapse
-
#account ⇒ Account?
The GL account for this payment.
-
#credit_card_vault ⇒ CreditCardVault?
The vaulted card/check used.
-
#credit_memo ⇒ CreditMemo?
The credit memo backing this payment.
-
#customer ⇒ Customer?
The paying customer.
-
#delivery ⇒ Delivery?
The delivery this payment is attached to.
-
#invoice ⇒ Invoice?
The invoice this payment settles.
-
#legacy_order ⇒ Order?
Legacy alias for order.
-
#order ⇒ Order?
The order this payment applies to.
-
#rma ⇒ Rma?
The RMA this payment is linked to.
-
#vpo_contact ⇒ Contact?
The contact who gave the verbal PO.
Has one collapse
-
#fraud_report ⇒ FraudReport?
The fraud screening report for this payment.
Has many collapse
-
#receipts ⇒ ActiveRecord::Associations::CollectionProxy<Receipt>
Receipts issued for this payment.
-
#transactions ⇒ ActiveRecord::Associations::CollectionProxy<OrderTransaction>
Gateway transactions for this payment.
-
#uploads ⇒ ActiveRecord::Associations::CollectionProxy<Upload>
Files attached to this payment.
Instance Attribute Summary collapse
-
#account_holder_type ⇒ String?
Bank account holder type, required for non-vaulted checks.
-
#account_number ⇒ Object
Returns the value of attribute account_number.
-
#account_type ⇒ String?
Bank account type, required for non-vaulted checks.
-
#address_city ⇒ Object
Returns the value of attribute address_city.
-
#address_country ⇒ Object
Returns the value of attribute address_country.
-
#address_id ⇒ Object
Returns the value of attribute address_id.
-
#address_line1 ⇒ Object
Returns the value of attribute address_line1.
-
#address_line2 ⇒ Object
Returns the value of attribute address_line2.
-
#address_state ⇒ Object
Returns the value of attribute address_state.
-
#address_zip ⇒ Object
Returns the value of attribute address_zip.
-
#amount ⇒ BigDecimal
The payment amount.
-
#amount_to_capture ⇒ Object
Returns the value of attribute amount_to_capture.
-
#bread_token ⇒ Object
Returns the value of attribute bread_token.
-
#card_token ⇒ Object
Returns the value of attribute card_token.
-
#category ⇒ String
The payment category (one of the category constants).
-
#consent_channel ⇒ Object
Returns the value of attribute consent_channel.
-
#currency ⇒ String
ISO currency code.
-
#email ⇒ String?
Receipt email address.
-
#error_codes ⇒ Object
Returns the value of attribute error_codes.
-
#issuer_number ⇒ Object
Returns the value of attribute issuer_number.
-
#last_response ⇒ Object
Returns the value of attribute last_response.
-
#paypal_email ⇒ String?
Required for PayPal invoice payments.
-
#po_number ⇒ String?
Required for purchase orders.
-
#rma_id ⇒ Integer?
Required for advance replacements.
-
#state ⇒ String
The payment state machine state.
-
#store_address ⇒ Object
Returns the value of attribute store_address.
-
#store_card ⇒ Object
Returns the value of attribute store_card.
-
#store_card_name ⇒ Object
Returns the value of attribute store_card_name.
-
#vpo_contact_id ⇒ Integer?
Required for verbal purchase orders.
Class Method Summary collapse
-
.all_amazon_pay_captured ⇒ ActiveRecord::Relation<Payment>
A relation of Payments that are all amazon pay captured.
-
.all_authorized ⇒ ActiveRecord::Relation<Payment>
A relation of Payments that are all authorized.
-
.all_captured ⇒ ActiveRecord::Relation<Payment>
A relation of Payments that are all captured.
-
.all_cc_captured ⇒ ActiveRecord::Relation<Payment>
A relation of Payments that are all cc captured.
-
.all_check_captured ⇒ ActiveRecord::Relation<Payment>
A relation of Payments that are all check captured.
-
.all_collect_captured ⇒ ActiveRecord::Relation<Payment>
A relation of Payments that are all collect captured.
-
.all_paypal_invoice_captured ⇒ ActiveRecord::Relation<Payment>
A relation of Payments that are all paypal invoice captured.
-
.all_plaid_captured ⇒ ActiveRecord::Relation<Payment>
A relation of Payments that are all plaid captured.
-
.amazon_payments ⇒ ActiveRecord::Relation<Payment>
A relation of Payments that are amazon payments.
-
.bread_payments ⇒ ActiveRecord::Relation<Payment>
A relation of Payments that are bread payments.
-
.can_be_refunded ⇒ ActiveRecord::Relation<Payment>
A relation of Payments that are can be refunded.
-
.cc_paypal_bread_amazon ⇒ ActiveRecord::Relation<Payment>
A relation of Payments that are cc paypal bread amazon.
-
.check_payments ⇒ ActiveRecord::Relation<Payment>
A relation of Payments that are check payments.
-
.credit_cards ⇒ ActiveRecord::Relation<Payment>
A relation of Payments that are credit cards.
-
.echeck_payment_review(amount, customer, order) ⇒ Hash{Symbol=>Boolean,Array<String>}
Decide whether an eCheck of
amountforcustomer/orderrequires manual approval. -
.expired ⇒ ActiveRecord::Relation<Payment>
A relation of Payments that are expired.
-
.non_voided ⇒ ActiveRecord::Relation<Payment>
A relation of Payments that are non voided.
- .order ⇒ Order?
-
.partially_captured ⇒ ActiveRecord::Relation<Payment>
A relation of Payments that are partially captured.
-
.payment_options(customer, order = nil, currency = nil) ⇒ Array<String>
Categories the customer is allowed to pick from in the new-payment form.
-
.paypal_invoices ⇒ ActiveRecord::Relation<Payment>
A relation of Payments that are paypal invoices.
-
.paypal_payments ⇒ ActiveRecord::Relation<Payment>
A relation of Payments that are paypal payments.
-
.plaid_payments ⇒ ActiveRecord::Relation<Payment>
A relation of Payments that are plaid payments.
-
.po_search ⇒ ActiveRecord::Relation<Payment>
A relation of Payments that are po search.
-
.purchase_orders ⇒ ActiveRecord::Relation<Payment>
A relation of Payments that are purchase orders.
-
.wire_payments ⇒ ActiveRecord::Relation<Payment>
A relation of Payments that are wire payments.
Instance Method Summary collapse
-
#all_pi_siblings ⇒ ActiveRecord::Relation<Payment>
All other payments (any state) sharing this PI.
-
#amount_captured_on_paypal ⇒ Float, false
Amount PayPal reports captured against this authorization (dollar value, not cents).
-
#amount_captured_on_stripe ⇒ Float, false
Cents-or-dollars amount Stripe reports as captured on the upstream PI/charge.
-
#attempt_paypal_reauthorization_if_needed(auth_expiration) ⇒ Payment::PaypalStatusResult
Decide whether to reauthorize this PayPal payment now.
-
#auth_code ⇒ String?
Card-network auth code (the 6-digit code the issuer returned on capture).
-
#auth_url ⇒ String?
Deep link to the auth/PI on the upstream gateway dashboard (PayPal, Stripe live, or Stripe test).
-
#authorization_review ⇒ Hash{Symbol=>Boolean,Array<String>}
Decide whether this payment needs Accounting approval before being treated as authorized.
-
#authorization_review_required? ⇒ Boolean
True when the category requires Accounting review.
-
#automatically_authorized? ⇒ Boolean
True when review applies but no approval is actually required.
-
#available_to_refund ⇒ BigDecimal, Float
Cash still available to refund: captured minus already refunded.
-
#billing_address ⇒ Address
Build a transient Address from the captured billing fields (
billing_address_*columns). -
#can_be_voided? ⇒ Boolean
True when an authorized payment may still be voided.
-
#capture_deadline ⇒ ActiveSupport::TimeWithZone?
Hard deadline by which the auth must be captured (or reauthorized); delegates to the StrategyResolver for gateway-specific rules.
-
#captured_on_paypal? ⇒ Boolean
True when PayPal shows the authorization as CAPTURED.
-
#captured_on_stripe? ⇒ Boolean
True when Stripe shows the payment captured/succeeded.
-
#check_cc_payment_status ⇒ void
Reconcile a Stripe credit-card payment with the upstream Payment Intent: void on canceled, sync capture on succeeded, trigger reauthorization when nearing the deadline, and fall back to legacy charge handling when the object isn't a PI.
-
#check_paypal_invoice_payment_status ⇒ Boolean?
Check PayPal for whether a hosted invoice has been paid yet; if so capture locally and (when every PayPal-invoice payment on the order is captured) release the order.
-
#check_paypal_payment_status ⇒ Payment::PaypalStatusResult
Re-poll PayPal for the current state of this authorization and reconcile the local payment: capture externally, void, expire, decline, or attempt a reauthorization, depending on what PayPal reports.
-
#coherent_with_order? ⇒ Boolean
True when this payment can still be safely settled against its order: same currency AND the authorized amount still covers the order balance.
-
#communication_resource ⇒ Order?
Resource the audit/comm subsystem keys off — for payments, that's the parent Order.
-
#crm_link ⇒ String
CRM order URL for this payment, swallowing routing errors so audit exports never blow up on a bad route.
-
#currency_symbol ⇒ String
Currency symbol used in CRM/invoice displays.
-
#deep_dup ⇒ Payment
Clone the payment, including transactions/uploads via deep_clone, but drop the Delivery association so the copy can be re-attached on the target shipment without violating the unique pairing.
-
#default_cc_options(ip_address = nil, email = nil) ⇒ Hash
ActiveMerchant-style options hash used when authorizing/capturing against the card gateway from an order context.
-
#default_echeck_options(_ip_address = nil) ⇒ Hash
Forte-formatted options hash used when authorizing an eCheck.
-
#default_paypal_options ⇒ Hash
Minimal options hash for PayPal API calls (just currency for now).
-
#detect_fraud(force_new_report: false) ⇒ Order::FraudDetector::Result?
Run the fraud detector for credit card / PayPal / eCheck payments.
-
#does_not_allow_capture? ⇒ Boolean
True for categories that cannot be gateway-captured.
-
#email_collection_for_select ⇒ Array<String>
Email options for the "send receipt to" select on the payment form: every email on the customer plus the address captured on the payment itself, deduped.
-
#full_card_number ⇒ String?
Reconstruct a masked PAN for display: BIN + xxx + last4 if both are known, otherwise just the last 4 with a generic mask.
-
#full_name ⇒ String
Cardholder/payer full name from the captured first_name/last_name pair.
-
#full_name=(value) ⇒ Object
Split a free-form name string via PersonNameParser and assign
first_name/last_nameaccordingly. -
#funds_fully_refunded? ⇒ Boolean
True when everything captured has been refunded.
-
#funds_partially_refunded? ⇒ Boolean
True when some but not all captured funds were refunded.
-
#gateway_class ⇒ Class
Strategy class used to authorize/capture/void this payment.
-
#is_advanced_replacement? ⇒ Boolean
True for advance replacement payments.
-
#is_amazon_pay? ⇒ Boolean
True for Amazon Pay payments.
-
#is_crm_legacy_vault? ⇒ Boolean
True for CRM orders using a legacy vault entry without an address.
-
#is_plaid? ⇒ Boolean
True for Plaid payments.
-
#is_po? ⇒ Boolean
True for purchase order / verbal purchase order payments.
-
#is_receipt_skippable? ⇒ Boolean
True when no receipt needs to be sent for this payment.
-
#is_rma_credit? ⇒ Boolean
True for RMA credit payments.
-
#is_store_credit? ⇒ Boolean
True for store credit payments.
-
#is_www_apple_pay? ⇒ Boolean
True when the order was paid online via Apple Pay.
-
#last_authorization_message ⇒ String?
Message text on the most recent OrderTransaction (typically the gateway's last response message).
-
#live_pi_payments ⇒ Array<Payment>
Live (authorized/captured) payments making up the shared authorization on this PI, including self — the real slices, excluding any split-superseded original.
-
#matches_order_currency? ⇒ Boolean
Strict: an unknown (blank) currency on either side is NOT a match, so
coherent_with_order?never green-lights settling a payment whose currency can't be verified. -
#paypal_over_capture_headroom ⇒ BigDecimal
How much more is still capturable on this PayPal auth before hitting #paypal_over_capture_limit, considering every active sibling payment.
-
#paypal_over_capture_limit ⇒ BigDecimal?
Maximum total that can be captured against this PayPal auth: the smaller of 115% of the original total and original + $75.
-
#paypal_shared_auth_total ⇒ BigDecimal?
Original total at the time the PayPal authorization was first established (sum of every sibling's amount).
-
#pending_release_authorization? ⇒ Boolean
True when authorization review is required but not yet approved.
-
#po_upload ⇒ Upload?
First non-deleted upload tagged
purchase_order— the customer's PO PDF supporting a PO payment. -
#process_tx_results(tx) ⇒ Object
Copy card metadata, billing/shipping addresses, and Stripe Radar signals from a successful Stripe OrderTransaction (or the linked CreditCardVault) onto this payment so the CRM can show full context without re-querying Stripe.
-
#refunded_on_stripe? ⇒ Boolean
True when Stripe shows the payment refunded/canceled.
-
#resend_paypal_invoice ⇒ Hash{Symbol=>Object}
Re-send the PayPal invoice reminder email via the PayPal API.
-
#send_authorization_email_notification ⇒ Communication, String
Send the cardholder a "we authorized your card" email.
-
#send_wire_info_email ⇒ Communication, String
Send the customer the WIRE_TRANSFER_INFO email with bank details they need to wire funds.
-
#shared_paypal_auth? ⇒ Boolean
True when other payments share this PayPal authorization.
-
#shared_paypal_auth_siblings ⇒ ActiveRecord::Relation<Payment>
Other authorized PayPal payments sharing this PayPal authorization id (PayPal supports up to 115% over-capture across siblings).
-
#shared_pi? ⇒ Boolean
True when other payments share this Stripe PaymentIntent.
-
#shared_pi_authorized_total ⇒ Float
PI-wide authorized total: the sum of the live slice amounts across the shared PI — the true "shared authorization" figure.
-
#shared_pi_siblings ⇒ ActiveRecord::Relation<Payment>
Other authorized payments that share this Stripe payment intent (split orders / partial captures stack on one PI).
-
#shipping_address ⇒ Address
Build a transient Address from the captured shipping fields.
-
#stripe_api_key ⇒ String
Stripe API key appropriate for this payment's currency (the codebase routes USD through one Stripe account, CAD through another).
-
#stripe_payment_object ⇒ Stripe::PaymentIntent, ...
(also: #stripe_charge)
Fetch the upstream Stripe object (Charge or PaymentIntent) for this payment.
-
#stripe_resolver ⇒ Payment::StrategyResolver
Memoised Stripe capture/reauth strategy resolver — encapsulates the capture window math (Stripe legacy 7d, extended 30d, PayPal 29d).
-
#superseded_by_split? ⇒ Boolean
True when this payment's
voidedstate is a bookkeeping hand-off from an order split — its authorization was redistributed onto sibling payments on the same still-live Stripe PaymentIntent — rather than a real gateway void/cancel. -
#supports_extended_authorization? ⇒ Boolean
True when the gateway supports extended authorization windows.
-
#supports_incremental_authorization? ⇒ Boolean
True when the gateway supports incremental authorization.
-
#supports_multicapture? ⇒ Boolean
True when the gateway supports multiple captures per authorization.
-
#sync_capture_before_from_paypal_expiry(auth_expiration) ⇒ Object
Push
capture_beforeforward when PayPal hands back a laterexpiration_timethan we have stored — this keeps the local capture-window cache in sync with PayPal's truth. -
#sync_external_capture(pi) ⇒ Object
Reflect a Stripe-side capture (or release) of a Payment Intent into this Payment.
-
#sync_paypal_authorization_expiry(auth_expiration) ⇒ Object
Persist PayPal's authoritative authorization expiry into
paypal_metadata['authorization_expiry']so callers don't have to re-poll on every action. -
#sync_paypal_external_capture(auth_response) ⇒ Object
Record a capture that happened on the PayPal dashboard rather than through Heatwave: append a synthetic capture OrderTransaction and advance the state to captured.
-
#to_s ⇒ String
"Payment 1234 (ORD-555)".
-
#total_authorized ⇒ Float
Live authorization total in dollars.
-
#total_captured ⇒ Float
Sum of all successful capture/purchase/settle transactions in dollars (gateway records cents).
-
#total_refunded ⇒ Float
Sum of all successful refund transactions in dollars.
-
#transaction_reference(action) ⇒ String?
Reference returned by the gateway for the first successful transaction of
action(e.g. authorization, capture, refund). -
#update_authorization_code(action) ⇒ Object
Refresh
authorization_codefrom the latest successful transaction of the given action (e.g.'authorization','reauthorization').
Methods included from Models::Auditable
#all_skipped_columns, #audit_reference_data, #should_not_save_version, #stamp_record
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
#account ⇒ Account?
Returns the GL account for this payment.
205 |
# File 'app/models/payment.rb', line 205 belongs_to :account, optional: true |
#account_holder_type ⇒ String?
Returns bank account holder type, required for non-vaulted checks.
255 |
# File 'app/models/payment.rb', line 255 validates :account_type, :account_holder_type, presence: { if: proc { |pp| pp.vault_id.blank? && pp. == 'check' } } |
#account_number ⇒ Object
Returns the value of attribute account_number.
286 287 288 |
# File 'app/models/payment.rb', line 286 def account_number @account_number end |
#account_type ⇒ String?
Returns bank account type, required for non-vaulted checks.
255 |
# File 'app/models/payment.rb', line 255 validates :account_type, :account_holder_type, presence: { if: proc { |pp| pp.vault_id.blank? && pp. == 'check' } } |
#address_city ⇒ Object
Returns the value of attribute address_city.
286 287 288 |
# File 'app/models/payment.rb', line 286 def address_city @address_city end |
#address_country ⇒ Object
Returns the value of attribute address_country.
286 287 288 |
# File 'app/models/payment.rb', line 286 def address_country @address_country end |
#address_id ⇒ Object
Returns the value of attribute address_id.
286 287 288 |
# File 'app/models/payment.rb', line 286 def address_id @address_id end |
#address_line1 ⇒ Object
Returns the value of attribute address_line1.
286 287 288 |
# File 'app/models/payment.rb', line 286 def address_line1 @address_line1 end |
#address_line2 ⇒ Object
Returns the value of attribute address_line2.
286 287 288 |
# File 'app/models/payment.rb', line 286 def address_line2 @address_line2 end |
#address_state ⇒ Object
Returns the value of attribute address_state.
286 287 288 |
# File 'app/models/payment.rb', line 286 def address_state @address_state end |
#address_zip ⇒ Object
Returns the value of attribute address_zip.
286 287 288 |
# File 'app/models/payment.rb', line 286 def address_zip @address_zip end |
#amount ⇒ BigDecimal
Returns the payment amount.
Validations:
- Numericality ({ greater_than_or_equal_to: 0.0 })
241 |
# File 'app/models/payment.rb', line 241 validates :category, :amount, :currency, :state, presence: true |
#amount_to_capture ⇒ Object
Returns the value of attribute amount_to_capture.
286 287 288 |
# File 'app/models/payment.rb', line 286 def amount_to_capture @amount_to_capture end |
#bread_token ⇒ Object
Returns the value of attribute bread_token.
286 287 288 |
# File 'app/models/payment.rb', line 286 def bread_token @bread_token end |
#card_token ⇒ Object
Returns the value of attribute card_token.
286 287 288 |
# File 'app/models/payment.rb', line 286 def card_token @card_token end |
#category ⇒ String
Returns the payment category (one of the category constants).
Validations:
- Inclusion ({ in: [CHECK, WIRE, CREDIT_CARD, PO, VPO, ADV_REPL, ECHECK, AMAZON_PAY, PAYPAL, PAYPAL_INVOICE, RMA_CREDIT, CASH, STORE_CREDIT, BREAD, PLAID] })
- Exclusion ({ in: [PO, VPO], on: :create, unless: :customer_has_terms?, message: 'is invalid as billing customer does not have terms' })
241 |
# File 'app/models/payment.rb', line 241 validates :category, :amount, :currency, :state, presence: true |
#consent_channel ⇒ Object
Returns the value of attribute consent_channel.
286 287 288 |
# File 'app/models/payment.rb', line 286 def @consent_channel end |
#credit_card_vault ⇒ CreditCardVault?
Returns the vaulted card/check used.
199 |
# File 'app/models/payment.rb', line 199 belongs_to :credit_card_vault, primary_key: 'vault_id', foreign_key: 'vault_id', optional: true |
#credit_memo ⇒ CreditMemo?
Returns the credit memo backing this payment.
202 |
# File 'app/models/payment.rb', line 202 belongs_to :credit_memo, optional: true |
#currency ⇒ String
Returns ISO currency code.
241 |
# File 'app/models/payment.rb', line 241 validates :category, :amount, :currency, :state, presence: true |
#customer ⇒ Customer?
Returns the paying customer.
190 |
# File 'app/models/payment.rb', line 190 belongs_to :customer, optional: true |
#delivery ⇒ Delivery?
Returns the delivery this payment is attached to.
208 |
# File 'app/models/payment.rb', line 208 belongs_to :delivery, inverse_of: :payments, optional: true |
#email ⇒ String?
Returns receipt email address.
280 |
# File 'app/models/payment.rb', line 280 validates :email, email_format: true |
#error_codes ⇒ Object
Returns the value of attribute error_codes.
286 287 288 |
# File 'app/models/payment.rb', line 286 def error_codes @error_codes end |
#fraud_report ⇒ FraudReport?
Returns the fraud screening report for this payment.
215 |
# File 'app/models/payment.rb', line 215 has_one :fraud_report |
#invoice ⇒ Invoice?
Returns the invoice this payment settles.
196 |
# File 'app/models/payment.rb', line 196 belongs_to :invoice, optional: true |
#issuer_number ⇒ Object
Returns the value of attribute issuer_number.
286 287 288 |
# File 'app/models/payment.rb', line 286 def issuer_number @issuer_number end |
#last_response ⇒ Object
Returns the value of attribute last_response.
286 287 288 |
# File 'app/models/payment.rb', line 286 def last_response @last_response end |
#legacy_order ⇒ Order?
Returns legacy alias for order.
193 |
# File 'app/models/payment.rb', line 193 belongs_to :legacy_order, class_name: 'Order', foreign_key: 'order_id', optional: true |
#order ⇒ Order?
Returns the order this payment applies to.
184 |
# File 'app/models/payment.rb', line 184 belongs_to :order, optional: true |
#paypal_email ⇒ String?
Returns required for PayPal invoice payments.
258 |
# File 'app/models/payment.rb', line 258 validates :paypal_email, presence: { if: proc { |pp| pp. == 'paypal_invoice' } } |
#po_number ⇒ String?
Returns required for purchase orders.
247 |
# File 'app/models/payment.rb', line 247 validates :po_number, presence: { if: proc { |pp| pp.category == PO } } |
#receipts ⇒ ActiveRecord::Associations::CollectionProxy<Receipt>
Returns receipts issued for this payment.
225 |
# File 'app/models/payment.rb', line 225 has_many :receipts |
#rma ⇒ Rma?
Returns the RMA this payment is linked to.
187 |
# File 'app/models/payment.rb', line 187 belongs_to :rma, optional: true |
#rma_id ⇒ Integer?
Returns required for advance replacements.
250 |
# File 'app/models/payment.rb', line 250 validates :rma_id, presence: { if: proc { |pp| pp.category == ADV_REPL } } |
#state ⇒ String
Returns the payment state machine state.
241 |
# File 'app/models/payment.rb', line 241 validates :category, :amount, :currency, :state, presence: true |
#store_address ⇒ Object
Returns the value of attribute store_address.
286 287 288 |
# File 'app/models/payment.rb', line 286 def store_address @store_address end |
#store_card ⇒ Object
Returns the value of attribute store_card.
286 287 288 |
# File 'app/models/payment.rb', line 286 def store_card @store_card end |
#store_card_name ⇒ Object
Returns the value of attribute store_card_name.
286 287 288 |
# File 'app/models/payment.rb', line 286 def store_card_name @store_card_name end |
#transactions ⇒ ActiveRecord::Associations::CollectionProxy<OrderTransaction>
Returns gateway transactions for this payment.
219 |
# File 'app/models/payment.rb', line 219 has_many :transactions, class_name: 'OrderTransaction', dependent: :destroy |
#uploads ⇒ ActiveRecord::Associations::CollectionProxy<Upload>
Returns files attached to this payment.
222 |
# File 'app/models/payment.rb', line 222 has_many :uploads, as: :resource, dependent: :destroy |
#vpo_contact ⇒ Contact?
Returns the contact who gave the verbal PO.
211 |
# File 'app/models/payment.rb', line 211 belongs_to :vpo_contact, class_name: 'Contact', optional: true |
#vpo_contact_id ⇒ Integer?
Returns required for verbal purchase orders.
244 |
# File 'app/models/payment.rb', line 244 validates :vpo_contact_id, presence: { if: proc { |pp| pp.category == VPO } } |
Class Method Details
.all_amazon_pay_captured ⇒ ActiveRecord::Relation<Payment>
A relation of Payments that are all amazon pay captured. Active Record Scope
308 |
# File 'app/models/payment.rb', line 308 scope :all_amazon_pay_captured, -> { all_captured.where(authorization_type: 'amazon_pay') } |
.all_authorized ⇒ ActiveRecord::Relation<Payment>
A relation of Payments that are all authorized. Active Record Scope
311 |
# File 'app/models/payment.rb', line 311 scope :all_authorized, -> { where(state: 'authorized') } |
.all_captured ⇒ ActiveRecord::Relation<Payment>
A relation of Payments that are all captured. Active Record Scope
303 |
# File 'app/models/payment.rb', line 303 scope :all_captured, -> { where(state: 'captured') } |
.all_cc_captured ⇒ ActiveRecord::Relation<Payment>
A relation of Payments that are all cc captured. Active Record Scope
305 |
# File 'app/models/payment.rb', line 305 scope :all_cc_captured, -> { all_captured.where(authorization_type: %w[credit_card paypal]) } |
.all_check_captured ⇒ ActiveRecord::Relation<Payment>
A relation of Payments that are all check captured. Active Record Scope
306 |
# File 'app/models/payment.rb', line 306 scope :all_check_captured, -> { all_captured.where(authorization_type: 'check') } |
.all_collect_captured ⇒ ActiveRecord::Relation<Payment>
A relation of Payments that are all collect captured. Active Record Scope
310 |
# File 'app/models/payment.rb', line 310 scope :all_collect_captured, -> { all_captured.where(authorization_type: 'credit_card', reference: 'Collect Card Reader') } |
.all_paypal_invoice_captured ⇒ ActiveRecord::Relation<Payment>
A relation of Payments that are all paypal invoice captured. Active Record Scope
309 |
# File 'app/models/payment.rb', line 309 scope :all_paypal_invoice_captured, -> { all_captured.where(authorization_type: 'paypal_invoice') } |
.all_plaid_captured ⇒ ActiveRecord::Relation<Payment>
A relation of Payments that are all plaid captured. Active Record Scope
307 |
# File 'app/models/payment.rb', line 307 scope :all_plaid_captured, -> { all_captured.where(authorization_type: 'plaid') } |
.amazon_payments ⇒ ActiveRecord::Relation<Payment>
A relation of Payments that are amazon payments. Active Record Scope
293 |
# File 'app/models/payment.rb', line 293 scope :amazon_payments, -> { where(category: AMAZON_PAY) } |
.bread_payments ⇒ ActiveRecord::Relation<Payment>
A relation of Payments that are bread payments. Active Record Scope
292 |
# File 'app/models/payment.rb', line 292 scope :bread_payments, -> { where(category: BREAD) } |
.can_be_refunded ⇒ ActiveRecord::Relation<Payment>
A relation of Payments that are can be refunded. Active Record Scope
324 |
# File 'app/models/payment.rb', line 324 scope :can_be_refunded, -> { where(state: %w[captured partially_refunded]) } |
.cc_paypal_bread_amazon ⇒ ActiveRecord::Relation<Payment>
A relation of Payments that are cc paypal bread amazon. Active Record Scope
304 |
# File 'app/models/payment.rb', line 304 scope :cc_paypal_bread_amazon, -> { where(authorization_type: %w[credit_card paypal bread, amazon_pay]) } |
.check_payments ⇒ ActiveRecord::Relation<Payment>
A relation of Payments that are check payments. Active Record Scope
295 |
# File 'app/models/payment.rb', line 295 scope :check_payments, -> { where(category: CHECK) } |
.credit_cards ⇒ ActiveRecord::Relation<Payment>
A relation of Payments that are credit cards. Active Record Scope
299 |
# File 'app/models/payment.rb', line 299 scope :credit_cards, -> { where(category: CREDIT_CARD) } |
.echeck_payment_review(amount, customer, order) ⇒ Hash{Symbol=>Boolean,Array<String>}
Decide whether an eCheck of amount for customer/order requires
manual approval. Enforces daily company-wide and per-customer caps,
the $10k upper bound, and routes through Models::CustomerFinancials#request_credit
for credit-limit checks.
1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 |
# File 'app/models/payment.rb', line 1011 def self.echeck_payment_review(amount, customer, order) res = {} fails = 0 fail_reasons = [] pass_reasons = [] if Payment.where(category: ECHECK)..where('payments.created_at > ?', Time.current.at_midnight).count > 5 fails += 1 fail_reasons << 'Limit exceeded for number of automatic eCheck approvals in one day, company wide (5).' end if Payment.joins(:order).where(category: ECHECK, orders: { customer_id: order.customer_id })..where('payments.created_at > ?', Time.current.at_midnight).count > 2 fails += 1 fail_reasons << 'Limit exceeded for number of automatic eCheck approvals in one day, per customer (2).' end if amount < 100 pass_reasons = ['Amount less than $100'] else if amount > 10_000 fails += 1 fail_reasons << 'Amount greater than $10,000' end rc_res = customer.request_credit(amount) if rc_res[:approved] == false fails += rc_res[:fails] fail_reasons += rc_res[:fail_reasons] else pass_reasons = rc_res[:pass_reasons] end end if fails > 0 res[:required] = true res[:fail_reasons] = fail_reasons else res[:required] = false res[:pass_reasons] = pass_reasons end res end |
.expired ⇒ ActiveRecord::Relation<Payment>
A relation of Payments that are expired. Active Record Scope
301 |
# File 'app/models/payment.rb', line 301 scope :expired, -> { where(state: 'expired') } |
.non_voided ⇒ ActiveRecord::Relation<Payment>
A relation of Payments that are non voided. Active Record Scope
325 |
# File 'app/models/payment.rb', line 325 scope :non_voided, -> { where.not(state: %w[voided expired]) } |
.order ⇒ Order?
this class-level shadow is highly suspect — it would call
itself recursively. Kept here because it is referenced from older
reports; treat as effectively dead until reviewed.
1328 1329 1330 |
# File 'app/models/payment.rb', line 1328 def self.order legacy_order || order end |
.partially_captured ⇒ ActiveRecord::Relation<Payment>
A relation of Payments that are partially captured. Active Record Scope
317 318 319 320 321 322 323 |
# File 'app/models/payment.rb', line 317 scope :partially_captured, -> { .where( OrderTransaction.where(action: %w[capture purchase settle], success: true) .where(OrderTransaction.arel_table[:payment_id].eq(arel_table[:id])) .arel.exists ) } |
.payment_options(customer, order = nil, currency = nil) ⇒ Array<String>
Categories the customer is allowed to pick from in the new-payment
form. Adds PO/VPO when the customer has terms, ADV_REPL when an RMA
has credit available, RMA_CREDIT for precreated-RMA orders, ECHECK
for USD orders, and STORE_CREDIT only when the customer actually has
an applicable CreditMemo.
421 422 423 424 425 426 427 428 429 430 431 432 433 434 |
# File 'app/models/payment.rb', line 421 def self.(customer, order = nil, currency = nil) = [CHECK, WIRE, CREDIT_CARD, PAYPAL, AMAZON_PAY, PAYPAL_INVOICE, CASH] if customer.has_terms? << PO # Use exists? instead of any? for better performance (single COUNT query vs loading records) << VPO if customer.contacts.verbal_po_contacts.exists? end << ADV_REPL if order && order.rma.present? && order.rma.credit_available > 0 << RMA_CREDIT if order && order.precreate_rma? << ECHECK if (order && order.currency == 'USD') || currency == 'USD' # Use exists? and check store credit in single condition to avoid loading all credit memos << STORE_CREDIT if customer.available_store_credit.to_d > 0 && customer.credit_memos.available_to_apply.exists? .sort end |
.paypal_invoices ⇒ ActiveRecord::Relation<Payment>
A relation of Payments that are paypal invoices. Active Record Scope
300 |
# File 'app/models/payment.rb', line 300 scope :paypal_invoices, -> { where(category: PAYPAL_INVOICE) } |
.paypal_payments ⇒ ActiveRecord::Relation<Payment>
A relation of Payments that are paypal payments. Active Record Scope
297 |
# File 'app/models/payment.rb', line 297 scope :paypal_payments, -> { where(category: PAYPAL) } |
.plaid_payments ⇒ ActiveRecord::Relation<Payment>
A relation of Payments that are plaid payments. Active Record Scope
294 |
# File 'app/models/payment.rb', line 294 scope :plaid_payments, -> { where(category: PLAID) } |
.po_search ⇒ ActiveRecord::Relation<Payment>
A relation of Payments that are po search. Active Record Scope
302 |
# File 'app/models/payment.rb', line 302 scope :po_search, ->(term) { where(Payment[:po_number].matches("%#{term}%")) } |
Instance Method Details
#all_pi_siblings ⇒ ActiveRecord::Relation<Payment>
All other payments (any state) sharing this PI. Used when reconciling
external Stripe captures across split orders.
503 504 505 506 507 508 |
# File 'app/models/payment.rb', line 503 def all_pi_siblings return Payment.none unless stripe_payment_intent_id.present? Payment.where(stripe_payment_intent_id: stripe_payment_intent_id) .where.not(id: id) end |
#amount_captured_on_paypal ⇒ Float, false
Amount PayPal reports captured against this authorization (dollar
value, not cents). False for non-PayPal payments.
1316 1317 1318 1319 1320 1321 |
# File 'app/models/payment.rb', line 1316 def amount_captured_on_paypal return false if != 'paypal' auth_details = Payment::Apis::Paypal.(self).parse auth_details["amount"]["value"].to_f end |
#amount_captured_on_stripe ⇒ Float, false
Cents-or-dollars amount Stripe reports as captured on the upstream
PI/charge. Returns false for non-CC payments or on lookup failure
so callers can next unless.
1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 |
# File 'app/models/payment.rb', line 1290 def amount_captured_on_stripe return false if != 'credit_card' obj = stripe_payment_object return false unless obj if Payment::Apis::Stripe.payment_intent?() obj.amount_received.to_f else obj.try(:amount_captured).to_f rescue false end end |
#attempt_paypal_reauthorization_if_needed(auth_expiration) ⇒ Payment::PaypalStatusResult
Decide whether to reauthorize this PayPal payment now. Returns
early with :still_valid when the auth or honor period still has
capture buffer; flips to expired and notifies AR/admin if past the
29-day hard limit; otherwise calls
Payment::Gateways::Paypal#reauthorize and surfaces the outcome.
806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 |
# File 'app/models/payment.rb', line 806 def (auth_expiration) if auth_expiration.present? && auth_expiration > PAYPAL_REAUTH_BUFFER.from_now sync_capture_before_from_paypal_expiry(auth_expiration) hours_left = ((auth_expiration - Time.current) / 1.hour).round(1) return PaypalStatusResult.new(status: :still_valid, message: "PayPal authorization still valid (#{hours_left} hours remaining, expires #{auth_expiration.strftime('%b %d %H:%M %Z')})") end honor_deadline = capture_before || (created_at + PAYPAL_HONOR_PERIOD) if honor_deadline > PAYPAL_REAUTH_BUFFER.from_now hours_left = ((honor_deadline - Time.current) / 1.hour).round(1) return PaypalStatusResult.new(status: :still_valid, message: "Honor period still valid (#{hours_left} hours remaining, capture by #{honor_deadline.strftime('%b %d %H:%M %Z')})") end if created_at < PAYPAL_MAX_AUTH_DAYS.days.ago # No per-payment email — accounting picks this up via the nightly # Payment::DailyIssuesDigestWorker, which scans for PayPal # payments that transitioned to `expired` in the last 24 hours. logger.error("Payment #{id}: PayPal authorization past #{PAYPAL_MAX_AUTH_DAYS}-day limit, cannot reauthorize") order.cr_hold if order.present? payment_expired! return PaypalStatusResult.new(status: :expired, message: "Authorization past #{PAYPAL_MAX_AUTH_DAYS}-day limit") end days_since_auth = ((Time.current - created_at) / 1.day).floor if days_since_auth < PAYPAL_REAUTH_EARLIEST_DAY return PaypalStatusResult.new(status: :still_valid, message: "Too early to reauthorize (day #{days_since_auth} of #{PAYPAL_REAUTH_EARLIEST_DAY} minimum). Authorization is still capturable.") end res = Payment::Gateways::Paypal.new(self).(amount) if res.success PaypalStatusResult.new(status: :reauthorized, message: 'Successfully reauthorized') else # No per-payment email — accounting picks this up via the nightly # Payment::DailyIssuesDigestWorker, which scans for # action='reauthorization' OrderTransactions with success=false # in the last 24 hours. logger.error("#{Time.current}: PAYPAL REAUTHORIZATION ERROR: Problem reauthorizing paypal payment.") PaypalStatusResult.new(status: :reauth_failed, message: res. || 'Reauthorization failed') end end |
#auth_code ⇒ String?
Card-network auth code (the 6-digit code the issuer returned on
capture). Read from the first capture transaction's params.
1203 1204 1205 1206 |
# File 'app/models/payment.rb', line 1203 def auth_code capture = transactions.where(action: 'capture', success: true).first capture.nil? ? nil : capture&.params&.[]('auth_code') end |
#auth_url ⇒ String?
Deep link to the auth/PI on the upstream gateway dashboard
(PayPal, Stripe live, or Stripe test). Returns nil when there is
no auth yet.
1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 |
# File 'app/models/payment.rb', line 1554 def auth_url return nil if .nil? if == 'paypal' host = test? ? "www.sandbox.paypal.com" : "www.paypal.com" "https://#{host}/activity/payment/#{}" elsif test? "https://dashboard.stripe.com/test/payments/#{}" else "https://dashboard.stripe.com/payments/#{}" end end |
#authorization_review ⇒ Hash{Symbol=>Boolean,Array<String>}
Decide whether this payment needs Accounting approval before being
treated as authorized. Returns a hash with :required and lists
of :fail_reasons/:pass_reasons strings used by the CRM review
UI.
634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 |
# File 'app/models/payment.rb', line 634 def res = {} case category when PAYPAL_INVOICE, RMA_CREDIT res[:required] = true res[:fail_reasons] = ["#{category} payment requires Accounting approval"] when ECHECK # This branch gates *order release* on the order's total authorized # eChecks. A receipt-form eCheck (paying invoices directly) has no # order to release, so there is nothing to review — and dereferencing # `order` here raised `NoMethodError: undefined method 'payments' for # nil` when viewing /payments/:id/transactions (AppSignal, payment # 285034: orderless eCheck for customer 98660). if order echeck_total = order.payments..where(category: ECHECK).sum(:amount) echeck_res = Payment.echeck_payment_review(echeck_total, order.customer, order) res[:required] = echeck_res[:required] res[:fail_reasons] = echeck_res[:fail_reasons] res[:pass_reasons] = echeck_res[:pass_reasons] else res[:required] = false end when CHECK, WIRE res[:required] = true res[:fail_reasons] = ['All Check/Wire payments requires Accounting approval'] when CASH if order.nil? # Orderless cash (receipt-form payment): no order release to gate. res[:required] = false elsif order.is_warehouse_pickup? if amount > 100 # pickups where cash amount is greater than $100 require authorization res[:required] = true res[:fail_reasons] = ["Pickup order with #{category} payment more than $100 requires Accounting approval"] else res[:required] = false res[:pass_reasons] = ["Pickup order with #{category} payment more than $100 requires Accounting approval"] end else # if it's not a pickup, then cash always require authorization res[:required] = true res[:fail_reasons] = ["Non-pickup order with #{category} payment requires Accounting approval"] end else # any other payment method doesn't require authorization res[:required] = false end res end |
#authorization_review_required? ⇒ Boolean
Returns true when the category requires Accounting review.
624 625 626 |
# File 'app/models/payment.rb', line 624 def CATEGORIES_REQUIRING_REVIEW.include?(category) end |
#automatically_authorized? ⇒ Boolean
Returns true when review applies but no approval is actually required.
685 686 687 |
# File 'app/models/payment.rb', line 685 def and [:required] == false end |
#available_to_refund ⇒ BigDecimal, Float
Cash still available to refund: captured minus already refunded.
1335 1336 1337 |
# File 'app/models/payment.rb', line 1335 def available_to_refund total_captured - total_refunded end |
#billing_address ⇒ Address
Build a transient Address from the captured billing fields
(billing_address_* columns). Used for AVS displays and receipts.
1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 |
# File 'app/models/payment.rb', line 1581 def billing_address Address.new( street1: billing_address_line1, street2: billing_address_line2, city: billing_address_city, zip: billing_address_zip, state_code: State.code_for_string(billing_address_state), country_iso3: Country.iso3_for_string(billing_address_country) ) end |
#can_be_voided? ⇒ Boolean
Returns true when an authorized payment may still be voided.
483 484 485 |
# File 'app/models/payment.rb', line 483 def can_be_voided? ( and !order.editing_locked? and category != 'Credit Card Terminal') end |
#capture_deadline ⇒ ActiveSupport::TimeWithZone?
Hard deadline by which the auth must be captured (or reauthorized);
delegates to the StrategyResolver for gateway-specific
rules.
478 479 480 |
# File 'app/models/payment.rb', line 478 def capture_deadline stripe_resolver. end |
#captured_on_paypal? ⇒ Boolean
Returns true when PayPal shows the authorization as CAPTURED.
1304 1305 1306 1307 1308 1309 1310 |
# File 'app/models/payment.rb', line 1304 def captured_on_paypal? return false if != 'paypal' auth_details = Payment::Apis::Paypal.(self).parse auth_status = auth_details['status'] auth_status == 'CAPTURED' end |
#captured_on_stripe? ⇒ Boolean
Returns true when Stripe shows the payment captured/succeeded.
1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 |
# File 'app/models/payment.rb', line 1272 def captured_on_stripe? return false if != 'credit_card' obj = stripe_payment_object return false unless obj if Payment::Apis::Stripe.payment_intent?() obj.status == 'succeeded' else obj.try(:captured?) rescue false end end |
#check_cc_payment_status ⇒ void
This method returns an undefined value.
Reconcile a Stripe credit-card payment with the upstream Payment
Intent: void on canceled, sync capture on succeeded, trigger
reauthorization when nearing the deadline, and fall back to legacy
charge handling when the object isn't a PI.
865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 |
# File 'app/models/payment.rb', line 865 def check_cc_payment_status return unless return unless == 'credit_card' obj = stripe_payment_object return unless obj pi_status = Payment::Apis::Stripe.payment_intent?() ? obj.status : nil case pi_status when 'canceled' logger.info("Payment #{id}: PI #{stripe_payment_intent_id} canceled on Stripe, voiding") reason = obj.cancellation_reason == 'expired' ? 'Authorization expired on Stripe' : 'PI canceled on Stripe' transactions.create!( amount: 0, action: 'void', success: true, reference: stripe_payment_intent_id || , message: "AUTO-VOID: #{reason} (status: canceled, reason: #{obj.cancellation_reason})", test: false ) payment_voided! when 'succeeded' # PI was finalized — either captured or released externally sync_external_capture(obj) when 'requires_capture' # PI still open — check if approaching reauth deadline if stripe_resolver..reauth_needed Payment::Gateways::CreditCard.new(self). end else # Legacy charge objects or unknown status — fall back to old behavior if obj.try(:captured?) transaction = OrderTransaction.new( amount: obj.try(:amount_captured).to_f, action: 'capture', success: true, reference: , message: "THIS PAYMENT WAS MANUALLY CAPTURED ON THE PAYMENT PLATFORM", params: obj.to_hash, test: false ) transactions.push(transaction) payment_captured! elsif stripe_resolver..reauth_needed Payment::Gateways::CreditCard.new(self). end end end |
#check_paypal_invoice_payment_status ⇒ Boolean?
Check PayPal for whether a hosted invoice has been paid yet; if so
capture locally and (when every PayPal-invoice payment on the order
is captured) release the order.
969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 |
# File 'app/models/payment.rb', line 969 def check_paypal_invoice_payment_status if response = Payment::Apis::Paypal.get_invoice_details() res = JSON.parse(response.to_s) invoice_status = res['status'] if invoice_status.present? && invoice_status == 'PAID' Payment::Gateways::PaypalInvoice.new(self).capture(res) order.reload order.release_order if order.payments.paypal_invoices.all?(&:captured?) true else # Paypal invoice has not been paid yet false end elsif captured? # If the payment is captured do nothing. We could verify that the receipt has been created, etc. true end end |
#check_paypal_payment_status ⇒ Payment::PaypalStatusResult
Re-poll PayPal for the current state of this authorization and
reconcile the local payment: capture externally, void, expire,
decline, or attempt a reauthorization, depending on what PayPal
reports. Used by the daily PayPal sync worker.
713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 |
# File 'app/models/payment.rb', line 713 def check_paypal_payment_status return PaypalStatusResult.new(status: :not_authorized, message: 'Payment is not in authorized state') unless response = Payment::Apis::Paypal.() auth_status = response['status'] auth_expiration = response['expiration_time']&.to_time (auth_expiration) case auth_status when 'CAPTURED' sync_paypal_external_capture(response) PaypalStatusResult.new(status: :captured, message: 'Payment was captured externally on PayPal') when 'VOIDED' logger.info("Payment #{id}: PayPal authorization voided externally") transactions.create!( amount: 0, action: 'void', success: true, reference: , message: "AUTO-VOID: PayPal authorization voided externally", test: false ) payment_voided! PaypalStatusResult.new(status: :voided, message: 'Authorization was voided on PayPal') when 'EXPIRED' logger.info("Payment #{id}: PayPal authorization expired") transactions.create!( amount: 0, action: 'void', success: true, reference: , message: "AUTO-VOID: PayPal authorization expired", test: false ) payment_expired! PaypalStatusResult.new(status: :expired, message: 'Authorization has expired') when 'DENIED' logger.info("Payment #{id}: PayPal authorization denied") transaction_declined! PaypalStatusResult.new(status: :denied, message: 'Authorization was denied') when 'CREATED', 'PENDING' (auth_expiration) else PaypalStatusResult.new(status: :unknown, message: "Unknown PayPal status: #{auth_status}") end end |
#coherent_with_order? ⇒ Boolean
True when this payment can still be safely settled against its order:
same currency AND the authorized amount still covers the order balance.
A catalog swap that re-prices lines (see CatalogItem::Remapper /
Catalog::SwitchItemizableCatalog) or a currency flip leaves an earlier
authorization "stale" — capturing it would charge the wrong currency or
under-collect. UI/ops use this to force a re-authorization. Regression
guard for SO727885 (2026-07).
1616 1617 1618 1619 1620 |
# File 'app/models/payment.rb', line 1616 def coherent_with_order? return true if order.nil? matches_order_currency? && amount.present? && amount >= order.balance(excluding_payment: self) end |
#communication_resource ⇒ Order?
Resource the audit/comm subsystem keys off — for payments, that's
the parent Order.
409 |
# File 'app/models/payment.rb', line 409 belongs_to :order, optional: true |
#crm_link ⇒ String
CRM order URL for this payment, swallowing routing errors so audit
exports never blow up on a bad route.
1162 1163 1164 1165 1166 |
# File 'app/models/payment.rb', line 1162 def crm_link UrlHelper.instance.order_path(order) rescue StandardError '' end |
#currency_symbol ⇒ String
Returns currency symbol used in CRM/invoice displays.
1340 1341 1342 |
# File 'app/models/payment.rb', line 1340 def currency_symbol Money::Currency.new(currency).symbol end |
#deep_dup ⇒ Payment
Clone the payment, including transactions/uploads via deep_clone,
but drop the Delivery association so the copy can be re-attached
on the target shipment without violating the unique pairing.
403 404 405 |
# File 'app/models/payment.rb', line 403 def deep_dup deep_clone(except: :delivery_id) end |
#default_cc_options(ip_address = nil, email = nil) ⇒ Hash
ActiveMerchant-style options hash used when authorizing/capturing
against the card gateway from an order context. Includes order
metadata and the Stripe customer id when charging a stored vault
card.
1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 |
# File 'app/models/payment.rb', line 1504 def (ip_address = nil, email = nil) = { description: (order.reference_number.present? ? "Order #{order.reference_number}" : "Order ID #{order.id}"), statement_description: "WarmlyYours #{order.reference_number}", currency:, shipping_address: order.shipping_address.format_for_payment_gateway(true), metadata: { email:, ip: ip_address, order_id: order.id, payment_id: id } } # only pass the customer as an option if we detect a vault_id, otherwise is will just charge the first card stored on the customer account [:customer] = order.customer.stripe_customer_id if vault_id.present? end |
#default_echeck_options(_ip_address = nil) ⇒ Hash
Forte-formatted options hash used when authorizing an eCheck.
_ip_address is retained for signature compatibility with
#default_cc_options.
1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 |
# File 'app/models/payment.rb', line 1537 def (_ip_address = nil) { # customer: order.customer_id, # ip: ip_address, # email: self.authorization.email, # shipping_address: order.shipping_address.format_for_payment_gateway(true), billing_address: order.billing_address.format_for_forte(true), order_id: order.reference_number # description: "Order #{order.reference_number}" } end |
#default_paypal_options ⇒ Hash
Minimal options hash for PayPal API calls (just currency for now).
1525 1526 1527 1528 1529 |
# File 'app/models/payment.rb', line 1525 def { currency: currency } end |
#detect_fraud(force_new_report: false) ⇒ Order::FraudDetector::Result?
Run the fraud detector for credit card / PayPal / eCheck payments.
Skipped for non-eligible categories and SmartService orders.
Errors are reported to AppSignal but never raised, so authorize
transitions don't fail because the fraud report did.
1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 |
# File 'app/models/payment.rb', line 1175 def detect_fraud(force_new_report: false) return nil if order.nil? return nil unless category.in?(['Credit Card', 'PayPal', 'eCheck']) return nil if order.belongs_to_smartservice_group? begin Order::FraudDetector.new(order, self).process(force_new_report:) rescue StandardError => e ErrorReporting.error(e, "FraudDetector Error - Unable to process new FraudDetector request for Payment ID:#{id}") nil end end |
#does_not_allow_capture? ⇒ Boolean
Returns true for categories that cannot be gateway-captured.
1140 1141 1142 |
# File 'app/models/payment.rb', line 1140 def does_not_allow_capture? category.in?(CATEGORIES_NOT_ALLOWING_CAPTURE) end |
#email_collection_for_select ⇒ Array<String>
Email options for the "send receipt to" select on the payment form:
every email on the customer plus the address captured on the
payment itself, deduped.
1223 1224 1225 1226 1227 |
# File 'app/models/payment.rb', line 1223 def email_collection_for_select customer_for_collection = customer customer_for_collection ||= order&.customer [customer_for_collection&.all_emails, email].flatten.compact.uniq end |
#full_card_number ⇒ String?
Reconstruct a masked PAN for display: BIN + xxx + last4 if both are
known, otherwise just the last 4 with a generic mask.
1233 1234 1235 1236 1237 1238 1239 |
# File 'app/models/payment.rb', line 1233 def full_card_number if issuer_number and last4 "#{issuer_number}xxxxxx#{last4}" elsif last4 "xxxxxxxxxxxx#{last4}" end end |
#full_name ⇒ String
Returns cardholder/payer full name from the captured
first_name/last_name pair.
1346 1347 1348 |
# File 'app/models/payment.rb', line 1346 def full_name [first_name, last_name].compact.join(' ') end |
#full_name=(value) ⇒ Object
Split a free-form name string via PersonNameParser and assign
first_name/last_name accordingly.
1354 1355 1356 1357 1358 |
# File 'app/models/payment.rb', line 1354 def full_name=(value) pnp = PersonNameParser.new(value) self.first_name = pnp.first self.last_name = pnp.last end |
#funds_fully_refunded? ⇒ Boolean
Returns true when everything captured has been refunded.
1209 1210 1211 |
# File 'app/models/payment.rb', line 1209 def funds_fully_refunded? total_refunded == total_captured end |
#funds_partially_refunded? ⇒ Boolean
Returns true when some but not all captured funds were refunded.
1214 1215 1216 |
# File 'app/models/payment.rb', line 1214 def funds_partially_refunded? total_refunded.positive? and total_refunded < total_captured end |
#gateway_class ⇒ Class
Strategy class used to authorize/capture/void this payment. Resolves
category to a constant under Gateways; falls back to
Payment::Gateways::Default when unmatched.
441 442 443 444 445 446 |
# File 'app/models/payment.rb', line 441 def gateway_class class_name = category.parameterize(separator: '_') # Map non-conventional class names class_name = { VPO: 'VerbalPurchaseOrder', PO: 'PurchaseOrder' }[class_name] || class_name "Payment::Gateways::#{class_name.classify}".safe_constantize || Payment::Gateways::Default end |
#is_advanced_replacement? ⇒ Boolean
Returns true for advance replacement payments.
1063 1064 1065 |
# File 'app/models/payment.rb', line 1063 def is_advanced_replacement? category == ADV_REPL end |
#is_amazon_pay? ⇒ Boolean
Returns true for Amazon Pay payments.
1078 1079 1080 |
# File 'app/models/payment.rb', line 1078 def is_amazon_pay? category == AMAZON_PAY end |
#is_crm_legacy_vault? ⇒ Boolean
Returns true for CRM orders using a legacy vault entry without an address.
1492 1493 1494 |
# File 'app/models/payment.rb', line 1492 def is_crm_legacy_vault? order&.order_reception_type == 'CRM' && (vault_id && credit_card_vault&.address_line1.blank?) end |
#is_plaid? ⇒ Boolean
Returns true for Plaid payments.
1073 1074 1075 |
# File 'app/models/payment.rb', line 1073 def is_plaid? category == PLAID end |
#is_po? ⇒ Boolean
Returns true for purchase order / verbal purchase order payments.
1053 1054 1055 |
# File 'app/models/payment.rb', line 1053 def is_po? category.in?([PO, VPO]) end |
#is_receipt_skippable? ⇒ Boolean
Returns true when no receipt needs to be sent for this payment.
1487 1488 1489 |
# File 'app/models/payment.rb', line 1487 def is_receipt_skippable? .in?(%w[paypal_invoice check]) and state == 'captured' and invoice.nil? end |
#is_rma_credit? ⇒ Boolean
Returns true for RMA credit payments.
1058 1059 1060 |
# File 'app/models/payment.rb', line 1058 def is_rma_credit? category == RMA_CREDIT end |
#is_store_credit? ⇒ Boolean
Returns true for store credit payments.
1068 1069 1070 |
# File 'app/models/payment.rb', line 1068 def is_store_credit? category == STORE_CREDIT end |
#is_www_apple_pay? ⇒ Boolean
Returns true when the order was paid online via Apple Pay.
1451 1452 1453 |
# File 'app/models/payment.rb', line 1451 def is_www_apple_pay? order&.order_reception_type == 'Online' && transactions.any? { |t| t.params&.dig('source')&.dig('tokenization_method') == 'apple_pay' } end |
#last_authorization_message ⇒ String?
Returns message text on the most recent
OrderTransaction (typically the gateway's last response message).
1146 1147 1148 |
# File 'app/models/payment.rb', line 1146 def transactions&.first&. end |
#live_pi_payments ⇒ Array<Payment>
Live (authorized/captured) payments making up the shared authorization
on this PI, including self — the real slices, excluding any
split-superseded original. Falls back to just self when not a shared PI.
Memoized: the transactions view resolves this several times per render
(directly and via #shared_pi_authorized_total); without the cache each
call re-runs the sibling query.
542 543 544 545 546 547 548 |
# File 'app/models/payment.rb', line 542 def live_pi_payments @live_pi_payments ||= if shared_pi? ([self] + all_pi_siblings.to_a).select { |p| p.state.in?(%w[authorized captured]) } else [self] end end |
#matches_order_currency? ⇒ Boolean
Strict: an unknown (blank) currency on either side is NOT a match, so
coherent_with_order? never green-lights settling a payment whose currency
can't be verified. A payment with no order is trivially consistent.
1627 1628 1629 1630 1631 |
# File 'app/models/payment.rb', line 1627 def matches_order_currency? return true if order.nil? currency.present? && order.currency.present? && currency == order.currency end |
#paypal_over_capture_headroom ⇒ BigDecimal
How much more is still capturable on this PayPal auth before
hitting #paypal_over_capture_limit, considering every active
sibling payment.
613 614 615 616 617 618 619 620 621 |
# File 'app/models/payment.rb', line 613 def paypal_over_capture_headroom limit = paypal_over_capture_limit return BigDecimal('0') unless limit all_committed = Payment.where(authorization_code: , authorization_type: 'paypal') .where.not(state: %w[voided declined expired]) .sum(:amount) [limit - all_committed, BigDecimal('0')].max end |
#paypal_over_capture_limit ⇒ BigDecimal?
Maximum total that can be captured against this PayPal auth: the
smaller of 115% of the original total and original + $75. Returns
nil when the original total is unknown.
601 602 603 604 605 606 |
# File 'app/models/payment.rb', line 601 def paypal_over_capture_limit auth_total = paypal_shared_auth_total return nil unless auth_total&.positive? [auth_total * PAYPAL_OVER_CAPTURE_PERCENT, auth_total + PAYPAL_OVER_CAPTURE_MAX_INCREASE].min end |
#paypal_shared_auth_total ⇒ BigDecimal?
Original total at the time the PayPal authorization was first
established (sum of every sibling's amount). Stored on
paypal_metadata because PayPal will not let you reauthorize for
more than 115% of the original total.
590 591 592 593 594 |
# File 'app/models/payment.rb', line 590 def paypal_shared_auth_total BigDecimal(&.dig('shared_authorization_total').to_s) rescue ArgumentError nil end |
#pending_release_authorization? ⇒ Boolean
Returns true when authorization review is required but not yet approved.
1083 1084 1085 |
# File 'app/models/payment.rb', line 1083 def !payment_approved? && [:required] == true end |
#po_upload ⇒ Upload?
First non-deleted upload tagged purchase_order — the customer's
PO PDF supporting a PO payment.
1154 1155 1156 |
# File 'app/models/payment.rb', line 1154 def po_upload uploads.in_category('purchase_order').valid.first end |
#process_tx_results(tx) ⇒ Object
Copy card metadata, billing/shipping addresses, and Stripe Radar
signals from a successful Stripe OrderTransaction (or the linked
CreditCardVault) onto this payment so the CRM can show full
context without re-querying Stripe.
1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 |
# File 'app/models/payment.rb', line 1366 def process_tx_results(tx) if vault_id.present? and vault = CreditCardVault.find_by(vault_id:) self.issuer_number = vault.issuer_number self.card_type = vault.card_type self.name = vault.name self.exp_month = vault.exp_month self.exp_year = vault.exp_year self.reference = self.last4 = vault.number self.address_line1_check = vault.address_line1_check self.address_zip_check = vault.address_zip_check self.cvc_check = vault.cvc_check self.card_identifier = vault.vault_id self.card_country = nil self.billing_address_line1 = vault.address_line1 self.billing_address_line2 = vault.address_line2 self.billing_address_city = vault.address_city self.billing_address_state = vault.address_state self.billing_address_zip = vault.zip_code self.billing_address_country = vault.address_country end if (shipping_atts = order&.ship_to_attributes).present? shipping = shipping_atts[:address] self.shipping_address_name = shipping_atts[:attention_name] unless shipping_atts[:attention_name].blank? || shipping.is_placeholder unless shipping_atts[:name].blank? || shipping.is_placeholder || shipping_atts[:attention_name] == shipping_atts[:name] self.shipping_address_name = shipping_atts[:name] end self.shipping_address_line1 = shipping.street1 self.shipping_address_line2 = shipping.street2 self.shipping_address_city = shipping.city self.shipping_address_state = shipping.state&.name self.shipping_address_zip = shipping.zip self.shipping_address_country = shipping.country&.iso end if tx_source = tx.params['source'] self.card_type = tx_source['brand'] self.name = tx_source['name'] self.exp_month = tx_source['exp_month'] self.exp_year = tx_source['exp_year'] self.reference = "....#{tx_source['last4']}" self.last4 = tx_source['last4'] self.address_line1_check = tx_source['address_line1_check'] self.address_zip_check = tx_source['address_zip_check'] self.cvc_check = tx_source['cvc_check'] self.card_identifier = tx_source['id'] self.card_country = tx_source['country'] self.billing_address_line1 = tx_source['address_line1'] self.billing_address_line2 = tx_source['address_line2'] self.billing_address_city = tx_source['address_city'] self.billing_address_state = tx_source['address_state'] self.billing_address_zip = tx_source['address_zip'] self.billing_address_country = tx_source['address_country'] end # Removing this after disconnecting David's active merchant branch on July 2020 # if tx.params["shipping"] # self.shipping_address_name = tx.params["shipping"]["name"] # if tx_shipping = tx.params["shipping"]["address"] # self.shipping_address_line1 = tx_shipping["line1"] # self.shipping_address_line2 = tx_shipping["line2"] # self.shipping_address_city = tx_shipping["city"] # self.shipping_address_state = tx_shipping["state"] # self.shipping_address_zip = tx_shipping["postal_code"] # self.shipping_address_country = tx_shipping["country"] # end # end if tx_outcome = tx.params['outcome'] self.radar_network_status = tx_outcome['network_status'] self.radar_reason = tx_outcome['reason'] self.radar_risk_level = tx_outcome['risk_level'] self. = tx_outcome['seller_message'] self.radar_type = tx_outcome['type'] end return unless livemode = tx.params['livemode'] self.test = livemode == false end |
#refunded_on_stripe? ⇒ Boolean
Returns true when Stripe shows the payment refunded/canceled.
1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 |
# File 'app/models/payment.rb', line 1258 def refunded_on_stripe? return false if != 'credit_card' obj = stripe_payment_object return false unless obj if Payment::Apis::Stripe.payment_intent?() obj.status == 'canceled' else obj.try(:refunded?) rescue false end end |
#resend_paypal_invoice ⇒ Hash{Symbol=>Object}
Re-send the PayPal invoice reminder email via the PayPal API.
993 994 995 996 997 998 999 1000 |
# File 'app/models/payment.rb', line 993 def resend_paypal_invoice response = Payment::Apis::Paypal.remind_invoice() if response['_http_success'] { success: true } else { success: false, message: 'Something went wrong with Paypal reminder.' } end end |
#send_authorization_email_notification ⇒ Communication, String
Send the cardholder a "we authorized your card" email. Only fires
for CREDIT_CARD payments where send_authorization_email was opted
into. Returns a string when the message wasn't sent so the caller
can surface the reason.
1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 |
# File 'app/models/payment.rb', line 1093 def if category == CREDIT_CARD and email.present? and == true sender = order.customer.try(:primary_sales_rep) CommunicationBuilder.new( resource: self, sender_party: sender, sender: (sender.nil? ? INFO_EMAIL : nil), emails: email, bcc: (sender.nil? ? nil : sender.email) ).create else 'Unable to send authorization email' end end |
#send_wire_info_email ⇒ Communication, String
Send the customer the WIRE_TRANSFER_INFO email with bank details
they need to wire funds. Only fires for WIRE payments with an email
captured. Falls back to a string when the message can't be sent.
1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 |
# File 'app/models/payment.rb', line 1113 def send_wire_info_email if category == WIRE and email.present? sender = order.customer.try(:primary_sales_rep) comm = CommunicationBuilder.new( resource: order, sender_party: sender, sender: (sender.nil? ? INFO_EMAIL : nil), emails: email, merge_options: { order_reference: order.cart_identifier, order_total: amount, country_instructions: (order.catalog.id == 1 ? 'usa' : 'ca') }, bcc: (sender.nil? ? nil : sender.email), template: EmailTemplate.find_by(system_code: 'WIRE_TRANSFER_INFO') ).create else 'Unable to send wire info email' end end |
#shared_paypal_auth? ⇒ Boolean
Returns true when other payments share this PayPal authorization.
573 574 575 576 577 |
# File 'app/models/payment.rb', line 573 def shared_paypal_auth? == 'paypal' && .present? && Payment.where(authorization_code: , authorization_type: 'paypal') .where.not(id: id).exists? end |
#shared_paypal_auth_siblings ⇒ ActiveRecord::Relation<Payment>
Other authorized PayPal payments sharing this PayPal authorization
id (PayPal supports up to 115% over-capture across siblings).
564 565 566 567 568 569 570 |
# File 'app/models/payment.rb', line 564 def shared_paypal_auth_siblings return Payment.none unless == 'paypal' && .present? Payment.where(authorization_code: , authorization_type: 'paypal') .where.not(id: id) .where(state: 'authorized') end |
#shared_pi? ⇒ Boolean
Returns true when other payments share this Stripe PaymentIntent.
511 512 513 514 |
# File 'app/models/payment.rb', line 511 def shared_pi? stripe_payment_intent_id.present? && Payment.where(stripe_payment_intent_id: stripe_payment_intent_id).where.not(id: id).exists? end |
#shared_pi_authorized_total ⇒ Float
PI-wide authorized total: the sum of the live slice amounts across the
shared PI — the true "shared authorization" figure. Unlike
#total_authorized (this payment's own slice only), this is stable no
matter which sibling you view it from.
556 557 558 |
# File 'app/models/payment.rb', line 556 def live_pi_payments.sum { |p| p.amount.to_f }.round(2) end |
#shared_pi_siblings ⇒ ActiveRecord::Relation<Payment>
Other authorized payments that share this Stripe payment intent
(split orders / partial captures stack on one PI). Excludes self.
491 492 493 494 495 496 497 |
# File 'app/models/payment.rb', line 491 def shared_pi_siblings return Payment.none unless stripe_payment_intent_id.present? Payment.where(stripe_payment_intent_id: stripe_payment_intent_id) .where.not(id: id) .where(state: %w[authorized]) end |
#shipping_address ⇒ Address
Build a transient Address from the captured shipping fields. Used
to compare against the order's ship-to for fraud signals.
1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 |
# File 'app/models/payment.rb', line 1596 def shipping_address Address.new( street1: shipping_address_line1, street2: shipping_address_line2, city: shipping_address_city, zip: shipping_address_zip, state_code: State.code_for_string(shipping_address_state), country_iso3: Country.iso3_for_string(shipping_address_country) ) end |
#stripe_api_key ⇒ String
Stripe API key appropriate for this payment's currency (the codebase
routes USD through one Stripe account, CAD through another).
1446 1447 1448 |
# File 'app/models/payment.rb', line 1446 def stripe_api_key Payment::Apis::Stripe.api_key(currency) end |
#stripe_payment_object ⇒ Stripe::PaymentIntent, ... Also known as: stripe_charge
Fetch the upstream Stripe object (Charge or PaymentIntent) for this
payment. Returns false on missing auth or any Stripe API error so
callers can guard with a single unless.
1246 1247 1248 1249 1250 1251 1252 |
# File 'app/models/payment.rb', line 1246 def stripe_payment_object return false if .blank? Payment::Apis::Stripe.retrieve_payment_object(, currency: currency) rescue ::Stripe::StripeError false end |
#stripe_resolver ⇒ Payment::StrategyResolver
Memoised Stripe capture/reauth strategy resolver — encapsulates the
capture window math (Stripe legacy 7d, extended 30d, PayPal 29d).
452 453 454 |
# File 'app/models/payment.rb', line 452 def stripe_resolver @stripe_resolver ||= Payment::StrategyResolver.new(self) end |
#superseded_by_split? ⇒ Boolean
True when this payment's voided state is a bookkeeping hand-off from
an order split — its authorization was redistributed onto sibling
payments on the same still-live Stripe PaymentIntent — rather than a
real gateway void/cancel. The split path soft-voids the original with a
bare update_all(state: 'voided') (no Stripe call, no void
transaction) while the money stays authorized on the shared PI, so the
bare "Voided" badge misreads as a cancelled/released charge.
525 526 527 528 529 530 531 |
# File 'app/models/payment.rb', line 525 def superseded_by_split? return false unless state == 'voided' return false if stripe_payment_intent_id.blank? return false if transactions.any? { |t| t.success && t.action == 'void' } # a real gateway void all_pi_siblings.where(state: %w[authorized captured]).exists? end |
#supports_extended_authorization? ⇒ Boolean
Returns true when the gateway supports extended authorization windows.
469 470 471 |
# File 'app/models/payment.rb', line 469 def stripe_resolver. end |
#supports_incremental_authorization? ⇒ Boolean
Returns true when the gateway supports incremental authorization.
464 465 466 |
# File 'app/models/payment.rb', line 464 def stripe_resolver. end |
#supports_multicapture? ⇒ Boolean
Returns true when the gateway supports multiple captures per authorization.
457 458 459 460 461 |
# File 'app/models/payment.rb', line 457 def supports_multicapture? return true if == 'paypal' stripe_resolver.supports_multicapture? end |
#sync_capture_before_from_paypal_expiry(auth_expiration) ⇒ Object
Push capture_before forward when PayPal hands back a later
expiration_time than we have stored — this keeps the local
capture-window cache in sync with PayPal's truth.
852 853 854 855 856 857 |
# File 'app/models/payment.rb', line 852 def sync_capture_before_from_paypal_expiry(auth_expiration) effective_capture_before = auth_expiration - PAYPAL_HONOR_PERIOD if capture_before.nil? || capture_before < effective_capture_before update_column(:capture_before, effective_capture_before) end end |
#sync_external_capture(pi) ⇒ Object
Reflect a Stripe-side capture (or release) of a Payment Intent into
this Payment. Computes the share that hasn't been claimed by
sibling payments yet, mints a synthetic capture transaction for it,
and advances state — or voids the payment when the PI finalised
without funds for us.
925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 |
# File 'app/models/payment.rb', line 925 def sync_external_capture(pi) pi_received_cents = pi.amount_received.to_i sibling_captured_cents = all_pi_siblings .where.not(state: %w[declined expired]) .sum { |s| (s.total_captured * 100).to_i } unaccounted_cents = pi_received_cents - sibling_captured_cents - (total_captured * 100).to_i if unaccounted_cents.positive? capture_amount_cents = [unaccounted_cents, (amount * 100).to_i].min logger.info("Payment #{id}: PI finalized on Stripe, syncing external capture of #{capture_amount_cents} cents") transactions.create!( amount: capture_amount_cents, action: 'capture', success: true, reference: , message: "THIS PAYMENT WAS CAPTURED EXTERNALLY ON STRIPE", params: pi.to_hash, test: false ) payment_captured! else # PI finalized but no funds captured for this payment — remaining auth was released logger.info("Payment #{id}: PI finalized on Stripe with no capture for this payment, voiding") msg = total_captured.positive? ? "AUTO-VOID: PI finalized, remaining authorization released (partial capture existed)" : "AUTO-VOID: PI finalized with no capture for this payment, authorization released" transactions.create!( amount: 0, action: 'void', success: true, reference: stripe_payment_intent_id || , message: msg, test: false ) payment_voided! end end |
#sync_paypal_authorization_expiry(auth_expiration) ⇒ Object
Persist PayPal's authoritative authorization expiry into
paypal_metadata['authorization_expiry'] so callers don't have to
re-poll on every action.
766 767 768 769 770 771 772 |
# File 'app/models/payment.rb', line 766 def (auth_expiration) return unless auth_expiration.present? = || {} ['authorization_expiry'] = auth_expiration.iso8601 update_column(:paypal_metadata, ) end |
#sync_paypal_external_capture(auth_response) ⇒ Object
Record a capture that happened on the PayPal dashboard rather than
through Heatwave: append a synthetic capture OrderTransaction and
advance the state to captured.
779 780 781 782 783 784 785 786 787 788 789 790 791 792 |
# File 'app/models/payment.rb', line 779 def sync_paypal_external_capture(auth_response) captured_amount_dollars = auth_response.dig("amount", "value").to_f transaction = OrderTransaction.new( amount: (captured_amount_dollars * 100).to_i, action: 'capture', success: true, reference: auth_response['id'] || , message: "THIS PAYMENT WAS MANUALLY CAPTURED ON THE PAYMENT PLATFORM", params: auth_response.except('_http_status', '_http_success', '_raw_body'), test: false ) transactions.push(transaction) payment_captured! end |
#to_s ⇒ String
Returns "Payment 1234 (ORD-555)".
1135 1136 1137 |
# File 'app/models/payment.rb', line 1135 def to_s "Payment #{id} (#{order&.reference_number})" end |
#total_authorized ⇒ Float
Live authorization total in dollars. Prefers the latest
incremental_authorization transaction (Stripe replaces the prior
auth amount on each step-up); otherwise sums the original auths.
1460 1461 1462 1463 1464 1465 1466 1467 |
# File 'app/models/payment.rb', line 1460 def latest_increment = transactions.select { |t| t.success && t.action == 'incremental_authorization' }.max_by(&:created_at) if latest_increment (latest_increment.amount.to_f / 100).round(2) else transactions.select { |t| t.success && t.action.in?(%w[authorization authorize]) }.sum { |t| t.amount.to_f / 100 }.to_f.round(2) end end |
#total_captured ⇒ Float
Sum of all successful capture/purchase/settle transactions in
dollars (gateway records cents).
1473 1474 1475 1476 1477 |
# File 'app/models/payment.rb', line 1473 def total_captured transactions.select do |t| t.success and (t.action == 'capture' or t.action == 'purchase' or t.action == 'settle') end.sum { |t| t.amount.to_f / 100 }.to_f.round(2) end |
#total_refunded ⇒ Float
Sum of all successful refund transactions in dollars.
1482 1483 1484 |
# File 'app/models/payment.rb', line 1482 def total_refunded transactions.select { |t| t.success and t.action == 'refund' }.sum { |t| t.amount.to_f / 100 }.to_f.round(2) end |
#transaction_reference(action) ⇒ String?
Reference returned by the gateway for the first successful
transaction of action (e.g. authorization, capture, refund).
1193 1194 1195 1196 1197 |
# File 'app/models/payment.rb', line 1193 def transaction_reference(action) return unless payment = transactions.where(action:).where(success: true).order(:id).first payment.reference end |
#update_authorization_code(action) ⇒ Object
Refresh authorization_code from the latest successful
transaction of the given action (e.g. 'authorization',
'reauthorization').
1572 1573 1574 1575 |
# File 'app/models/payment.rb', line 1572 def (action) update(authorization_code: transaction_reference(action)) payment.update(amount:) end |