Class: Payment::Gateways::PaypalInvoice

Inherits:
BasePaymentGateway
  • Object
show all
Defined in:
app/services/payment/gateways/paypal_invoice.rb

Overview

PayPal hosted-invoice gateway. Authorizes by sending a PayPal-hosted
invoice to the customer; capture happens when the customer pays the
invoice on PayPal and we poll back via Payment#check_paypal_invoice_payment_status.

Defined Under Namespace

Classes: ReceiptResult, Result

Instance Method Summary collapse

Constructor Details

#initialize(payment = nil, delivery = nil) ⇒ PaypalInvoice

Returns a new instance of PaypalInvoice.



14
15
16
17
# File 'app/services/payment/gateways/paypal_invoice.rb', line 14

def initialize(payment = nil, delivery = nil)
  @payment = payment
  @delivery = delivery
end

Instance Method Details

#authorizePayment::Gateways::PaypalInvoice::Result

Build a PayPal Invoicing v2 invoice (line items copied from the
delivery, or a single "Balance Due" line for partial-amount
invoices), send it to the customer's PayPal email, and mark the
local Payment authorized once PayPal accepts it.



25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'app/services/payment/gateways/paypal_invoice.rb', line 25

def authorize
  invoice_amount = @payment.amount.to_f
  delivery_total = @delivery.total.to_f
  partial_invoice = (invoice_amount - delivery_total).abs > 0.01

  req = {
    detail: {
      currency_code: @payment.order.currency,
      terms_and_conditions: 'DueOnReceipt',
      note: "Customer ID: #{@payment.order.customer_id} / Order Ref: #{@payment.order.reference_number}#{" / PO: #{@payment.po_number}" if @payment.po_number.present?}"
    },
    invoicer: {
      email_address: Payment::Apis::Paypal.merchant_email,
      name: { full_name: 'WarmlyYours.com, Inc.' },
      phones: [{
        country_code: '001',
        national_number: '8008755285',
        phone_type: 'HOME'
      }],
      website: 'www.warmlyyours.com',
      address: format_address_for_v2(@payment.order.company.address)
    },
    primary_recipients: [{
      billing_info: {
        email_address: @payment.paypal_email,
        name: { full_name: @payment.order.billing_entity.full_name },
        address: format_address_for_v2(@payment.order.billing_address)
      },
      shipping_info: {
        name: { full_name: @payment.order.shipping_address.person_name || @payment.order.shipping_address.company_name },
        address: format_address_for_v2(@payment.order.shipping_address)
      }
    }],
    configuration: {
      allow_tip: false,
      tax_calculated_after_discount: true,
      tax_inclusive: false
    }
  }

  if partial_invoice
    req[:items] = [{
      name: "Balance Due - Order #{@payment.order.reference_number}",
      description: "Remaining balance for order #{@payment.order.reference_number}",
      quantity: '1',
      unit_amount: {
        currency_code: @payment.order.currency,
        value: format('%.2f', invoice_amount)
      }
    }]
  else
    sli = @delivery.shipping_line_item
    req[:items] = @delivery.line_items.non_shipping.collect do |li|
      item = {
        name: li.sku,
        description: li.name.to_s.truncate(100),
        quantity: li.quantity.to_s,
        unit_amount: {
          currency_code: li.currency,
          value: format('%.2f', li.discounted_price.to_f)
        }
      }
      if li.has_tax?
        item[:tax] = {
          name: li.tax_type.presence || 'Tax',
          percent: format('%.4f', li.tax_rate.to_f * 100)
        }
      end
      item
    end
    if sli
      req[:amount] = {
        breakdown: {
          shipping: {
            amount: {
              currency_code: sli.currency,
              value: format('%.2f', sli.discounted_total.to_f)
            }
          }.tap do |shipping_hash|
            if sli.has_tax?
              shipping_hash[:tax] = {
                name: sli.tax_type.presence || 'Shipping Tax',
                percent: format('%.4f', sli.tax_rate.to_f * 100)
              }
            end
          end
        }
      }
    end
  end

  invoice_response = Payment::Apis::Paypal.create_invoice(req)
  invoice_href = invoice_response.dig('href')
  invoice_id = extract_invoice_id(invoice_response)

  if invoice_id.present?
    send_result = Payment::Apis::Paypal.send_invoice(invoice_id)
    invoice_success = send_result['_http_success']
  else
    invoice_success = false
  end

  total_value = invoice_response.dig('amount', 'value') ||
                invoice_response.dig('detail', 'metadata', 'recipient_view_url') ||
                '0'

  @payment.transactions << OrderTransaction.new(
    action: 'authorization',
    amount: invoice_success ? (extract_total_amount(invoice_response) * 100) : 0,
    success: invoice_success,
    reference: invoice_id,
    message: invoice_success ? 'Invoice sent' : "#{invoice_response['name']} - #{invoice_response['message']}",
    params: invoice_response
  )

  if invoice_success
    @payment.update(
      reference: @payment.paypal_email,
      amount: BigDecimal(extract_total_amount(invoice_response).to_s),
      authorization_code: invoice_id
    )
    @payment.payment_authorized!
  else
    @payment.last_response = "#{invoice_response['name']} - #{invoice_response['message']}"
    @payment.transaction_declined!
  end
  Result.new(success: invoice_success)
end

#capture(response) ⇒ Payment::Gateways::PaypalInvoice::Result

Reflect a customer's PayPal-side invoice payment locally: append
a capture transaction for the paid amount and advance the
Payment state. Called from Payment#check_paypal_invoice_payment_status.

Parameters:

  • response (Hash)

    parsed PayPal invoice details body

Returns:



186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
# File 'app/services/payment/gateways/paypal_invoice.rb', line 186

def capture(response)
  paid_amount = response.dig('payments', 'transactions', 0, 'amount', 'value') ||
                response.dig('paid_amount', 'paypal', 'value') ||
                @payment.amount
  amount_value = paid_amount.to_f

  @payment.transactions << OrderTransaction.new(
    action: 'capture',
    amount: amount_value * 100,
    success: true,
    reference: @payment.authorization_code,
    params: response
  )
  @payment.payment_captured!
  Result.new(success: true)
end

#create_receipt(invoice, amount, balance) ⇒ Payment::Gateways::PaypalInvoice::ReceiptResult

Build the Receipt that mirrors the captured PayPal invoice
payment. Notifies admin when the captured amount exceeds the
invoice balance so AR knows to refund the difference.

Parameters:

  • invoice (Invoice)
  • amount (BigDecimal, Numeric)
  • balance (BigDecimal, Numeric)

Returns:



218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
# File 'app/services/payment/gateways/paypal_invoice.rb', line 218

def create_receipt(invoice, amount, balance)
  new_receipt = @payment.receipts.new(
    company: invoice.company,
    customer: invoice.customer,
    category: 'PayPal',
    amount: amount,
    reference: @payment.reference,
    currency: invoice.currency,
    payment: @payment,
    bank_account: invoice.company.,
    gl_date: Date.current,
    receipt_date: Date.current
  )
  new_receipt.receipt_details << ReceiptDetail.new(
    category: 'Invoice', invoice: invoice, amount: amount, gl_date: Date.current
  )

  begin
    new_receipt.save!
    Rails.logger.info("#{Time.current}: Created new receipt id: #{new_receipt.id}")
    if @payment.amount > balance
      Mailer.admin_notification(
        "Paypal Invoice amount already captured greater than balance on invoice id: #{invoice.id}, ref: #{invoice.reference_number}",
        "Amount already captured for PayPal Invoice payment id: #{@payment.id} was greater than order balance. PayPal Invoice amount: #{@payment.amount}, Invoice balance: #{balance}. Inform accounting who should process a refund."
      ).deliver
    end
    success = true
  rescue StandardError => exc
    report_exception exc, payment_id: @payment.id, message: "Unable to create new receipt for PayPal Invoice payment"
    success = false
  end
  ReceiptResult.new(success: success, receipt: new_receipt)
end

#refundObject

Refunds aren't supported for hosted PayPal invoices; AR has to
process them manually. Report a warning to AppSignal so the request
is visible.



206
207
208
# File 'app/services/payment/gateways/paypal_invoice.rb', line 206

def refund
  ErrorReporting.warning("Payment 'Refund' action reached but action is not supported", { payment_id: @payment.id })
end

#void(report_fraud = false) ⇒ Payment::Gateways::PaypalInvoice::Result

Cancel the open PayPal invoice and void the local Payment.

Parameters:

  • report_fraud (Boolean) (defaults to: false)

    retained for API parity

Returns:



158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
# File 'app/services/payment/gateways/paypal_invoice.rb', line 158

def void(report_fraud = false)
  return Result.new(success: false) unless @payment.authorized?

  result = Payment::Apis::Paypal.cancel_invoice(@payment.authorization_code)
  void_success = result['_http_success']

  @payment.transactions << OrderTransaction.new(
    action: 'void',
    success: void_success,
    reference: @payment.authorization_code,
    params: result
  )

  if void_success
    Rails.logger.info("#{Time.current}: Paypal Invoice Cancel of Auth ID #{@payment.id} succeeded")
    @payment.payment_voided!
  else
    Rails.logger.info("#{Time.current}: Paypal Invoice Cancel of Auth ID #{@payment.id} failed")
  end
  Result.new(success: void_success, message: void_success ? nil : 'Something went wrong with PayPal.')
end