merchant_sidekick 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +12 -0
- data/Changelog.md +38 -0
- data/Gemfile +2 -0
- data/MIT-LICENSE +19 -0
- data/README.md +88 -0
- data/Rakefile +10 -0
- data/lib/merchant_sidekick.rb +45 -0
- data/lib/merchant_sidekick/active_merchant/credit_card_payment.rb +117 -0
- data/lib/merchant_sidekick/active_merchant/gateways/authorize_net_gateway.rb +26 -0
- data/lib/merchant_sidekick/active_merchant/gateways/base.rb +29 -0
- data/lib/merchant_sidekick/active_merchant/gateways/bogus_gateway.rb +19 -0
- data/lib/merchant_sidekick/active_merchant/gateways/paypal_gateway.rb +43 -0
- data/lib/merchant_sidekick/addressable/address.rb +400 -0
- data/lib/merchant_sidekick/addressable/addressable.rb +353 -0
- data/lib/merchant_sidekick/buyer.rb +99 -0
- data/lib/merchant_sidekick/gateway.rb +81 -0
- data/lib/merchant_sidekick/install.rb +19 -0
- data/lib/merchant_sidekick/invoice.rb +179 -0
- data/lib/merchant_sidekick/line_item.rb +128 -0
- data/lib/merchant_sidekick/migrations/addressable.rb +47 -0
- data/lib/merchant_sidekick/migrations/billing.rb +100 -0
- data/lib/merchant_sidekick/migrations/shopping_cart.rb +28 -0
- data/lib/merchant_sidekick/money.rb +38 -0
- data/lib/merchant_sidekick/order.rb +244 -0
- data/lib/merchant_sidekick/payment.rb +59 -0
- data/lib/merchant_sidekick/purchase_invoice.rb +180 -0
- data/lib/merchant_sidekick/purchase_order.rb +350 -0
- data/lib/merchant_sidekick/railtie.rb +7 -0
- data/lib/merchant_sidekick/sales_invoice.rb +56 -0
- data/lib/merchant_sidekick/sales_order.rb +122 -0
- data/lib/merchant_sidekick/sellable.rb +88 -0
- data/lib/merchant_sidekick/seller.rb +93 -0
- data/lib/merchant_sidekick/shopping_cart/cart.rb +225 -0
- data/lib/merchant_sidekick/shopping_cart/line_item.rb +152 -0
- data/lib/merchant_sidekick/version.rb +3 -0
- data/merchant_sidekick.gemspec +37 -0
- data/spec/address_spec.rb +153 -0
- data/spec/addressable_spec.rb +250 -0
- data/spec/buyer_spec.rb +203 -0
- data/spec/cart_line_item_spec.rb +58 -0
- data/spec/cart_spec.rb +213 -0
- data/spec/config/merchant_sidekick.yml +10 -0
- data/spec/credit_card_payment_spec.rb +175 -0
- data/spec/fixtures/addresses.yml +97 -0
- data/spec/fixtures/line_items.yml +18 -0
- data/spec/fixtures/orders.yml +24 -0
- data/spec/fixtures/payments.yml +17 -0
- data/spec/fixtures/products.yml +12 -0
- data/spec/fixtures/users.yml +11 -0
- data/spec/gateway_spec.rb +136 -0
- data/spec/invoice_spec.rb +79 -0
- data/spec/line_item_spec.rb +65 -0
- data/spec/order_spec.rb +85 -0
- data/spec/payment_spec.rb +14 -0
- data/spec/purchase_invoice_spec.rb +70 -0
- data/spec/purchase_order_spec.rb +191 -0
- data/spec/sales_invoice_spec.rb +58 -0
- data/spec/sales_order_spec.rb +107 -0
- data/spec/schema.rb +28 -0
- data/spec/sellable_spec.rb +34 -0
- data/spec/seller_spec.rb +201 -0
- data/spec/spec_helper.rb +255 -0
- metadata +201 -0
@@ -0,0 +1,59 @@
|
|
1
|
+
# Superclass for all payment transaction. Each purchase, authorization, etc. attempt
|
2
|
+
# will result in a new sublcass payment instance
|
3
|
+
module MerchantSidekick
|
4
|
+
class Payment < ActiveRecord::Base
|
5
|
+
self.table_name = "payments"
|
6
|
+
|
7
|
+
#--- associations
|
8
|
+
belongs_to :payable, :polymorphic => true
|
9
|
+
acts_as_list :scope => 'payable_id=#{quote_value(payable_id)} AND payable_type=#{quote_value(payable_type)}'
|
10
|
+
|
11
|
+
#--- mixins
|
12
|
+
money :amount, :cents => :cents, :currency => :currency
|
13
|
+
|
14
|
+
#--- class methods
|
15
|
+
|
16
|
+
# Determines which payment class to use based on the payment object passed.
|
17
|
+
# overriden this if other payment types must be supported, e.g. for bank
|
18
|
+
# transfer, etc.
|
19
|
+
#
|
20
|
+
# E.g.
|
21
|
+
#
|
22
|
+
# Payment.class_for(ActiveMerchant::Billing::CreditCard.new(...))
|
23
|
+
# #=> MerchantSidekick::ActiveMerchant::CreditCardPayment
|
24
|
+
|
25
|
+
def self.class_for(payment_object)
|
26
|
+
MerchantSidekick::ActiveMerchant::CreditCardPayment
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.content_column_names
|
30
|
+
content_columns.map(&:name) - %w(payable_type payable_id kind reference message action params test cents currency lock_version position type uuid created_at updated_at success)
|
31
|
+
end
|
32
|
+
|
33
|
+
#--- instance methods
|
34
|
+
|
35
|
+
# override in sublcass
|
36
|
+
# infers payment
|
37
|
+
def payment_type
|
38
|
+
:payment
|
39
|
+
end
|
40
|
+
|
41
|
+
# returns true if the payment transaction was successful
|
42
|
+
def success?
|
43
|
+
!!(self[:success] || false)
|
44
|
+
end
|
45
|
+
|
46
|
+
# return only attributes with relevant content
|
47
|
+
def content_attributes
|
48
|
+
self.attributes.reject {|k,v| !self.content_column_names.include?(k.to_s)}.symbolize_keys
|
49
|
+
end
|
50
|
+
|
51
|
+
# returns content column name strings
|
52
|
+
def content_column_names
|
53
|
+
self.class.content_column_names
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# MerchantSidekick::AuthorizationError
|
58
|
+
class AuthorizationError < StandardError; end
|
59
|
+
end
|
@@ -0,0 +1,180 @@
|
|
1
|
+
# Implements an outbound invoice.
|
2
|
+
module MerchantSidekick
|
3
|
+
class PurchaseInvoice < Invoice
|
4
|
+
belongs_to :purchase_order, :foreign_key => :order_id, :class_name => "MerchantSidekick::PurchaseOrder"
|
5
|
+
|
6
|
+
# overrides superclass
|
7
|
+
def purchase_invoice?
|
8
|
+
true
|
9
|
+
end
|
10
|
+
|
11
|
+
# authorizes the payment
|
12
|
+
# payment_object is the credit_card instance or other payment objects
|
13
|
+
def authorize(payment_object, options={})
|
14
|
+
transaction do
|
15
|
+
authorization = MerchantSidekick::Payment.class_for(payment_object).authorize(
|
16
|
+
gross_total,
|
17
|
+
payment_object,
|
18
|
+
payment_options(options)
|
19
|
+
)
|
20
|
+
|
21
|
+
self.push_payment(authorization)
|
22
|
+
|
23
|
+
if authorization.success?
|
24
|
+
save(:validate => false)
|
25
|
+
payment_authorized!
|
26
|
+
else
|
27
|
+
# we don't want to save the payment and related objects
|
28
|
+
# when the authorization fails
|
29
|
+
# transaction_declined!
|
30
|
+
end
|
31
|
+
authorization
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Captures a previously authorized payment. If the gross amount of the
|
36
|
+
# invoice has changed between authorization and capture, the difference
|
37
|
+
# in case authorization amount - capture amount > 0 will be refunded to
|
38
|
+
# the respective account, otherwise an exception thrown.
|
39
|
+
# Only payments can be captured, not deposits.
|
40
|
+
#
|
41
|
+
# E.g.
|
42
|
+
#
|
43
|
+
# @order = person.purchase(@product)
|
44
|
+
# @payment = @order.authorize(@credit_card)
|
45
|
+
# ...
|
46
|
+
# @order.capture
|
47
|
+
# @order.invoice.paid? #=> true
|
48
|
+
#
|
49
|
+
def capture(options={})
|
50
|
+
transaction do
|
51
|
+
if authorization
|
52
|
+
capture_result = authorization.class.capture(
|
53
|
+
gross_total,
|
54
|
+
authorization_reference,
|
55
|
+
payment_options(authorization.content_attributes.merge(options))
|
56
|
+
)
|
57
|
+
|
58
|
+
self.push_payment(capture_result)
|
59
|
+
|
60
|
+
save(:validate => false)
|
61
|
+
|
62
|
+
if capture_result.success?
|
63
|
+
payment_captured!
|
64
|
+
else
|
65
|
+
transaction_declined!
|
66
|
+
end
|
67
|
+
capture_result
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# overrides accessor and caches authorization
|
73
|
+
def authorization
|
74
|
+
@authorization ||= if auth = self.payments.find_by_action_and_success('authorization', true, :order => 'id ASC')
|
75
|
+
auth
|
76
|
+
elsif auth = self.payments.find_by_action_and_success('purchase', true, :order => 'id ASC')
|
77
|
+
auth
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def authorization_reference
|
82
|
+
authorization.reference if authorization
|
83
|
+
end
|
84
|
+
|
85
|
+
# void a previously authorized payment
|
86
|
+
def void(options={})
|
87
|
+
transaction do
|
88
|
+
if authorization
|
89
|
+
void_result = authorization.class.void(
|
90
|
+
gross_total,
|
91
|
+
authorization_reference,
|
92
|
+
payment_options(authorization.content_attributes.merge(options))
|
93
|
+
)
|
94
|
+
self.push_payment(void_result)
|
95
|
+
|
96
|
+
save(:validate => false)
|
97
|
+
|
98
|
+
if void_result.success?
|
99
|
+
payment_voided!
|
100
|
+
else
|
101
|
+
transaction_declined!
|
102
|
+
end
|
103
|
+
void_result
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# refunds the entire amount or the amount provided
|
109
|
+
# of the invoice
|
110
|
+
#
|
111
|
+
# Options:
|
112
|
+
# :card_number must be supplied in the options
|
113
|
+
# :amount => specify the amount to be refunded
|
114
|
+
#
|
115
|
+
def credit(options={})
|
116
|
+
transaction do
|
117
|
+
if authorization
|
118
|
+
credit_result = authorization.class.credit(
|
119
|
+
options[:amount] || gross_total,
|
120
|
+
authorization_reference,
|
121
|
+
payment_options(authorization.content_attributes.merge(options))
|
122
|
+
)
|
123
|
+
self.push_payment(credit_result)
|
124
|
+
|
125
|
+
save(:validate => false)
|
126
|
+
|
127
|
+
if credit_result.success?
|
128
|
+
payment_refunded!
|
129
|
+
else
|
130
|
+
transaction_declined!
|
131
|
+
end
|
132
|
+
capture_result
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# Purchase invoice, combines authorization and capture in one step
|
138
|
+
def purchase(payment_object, options={})
|
139
|
+
transaction do
|
140
|
+
purchase_result = MerchantSidekick::Payment.class_for(payment_object).purchase(
|
141
|
+
gross_total,
|
142
|
+
payment_object,
|
143
|
+
payment_options(options)
|
144
|
+
)
|
145
|
+
self.push_payment(purchase_result)
|
146
|
+
|
147
|
+
save(:validate => false)
|
148
|
+
|
149
|
+
if purchase_result.success?
|
150
|
+
payment_paid!
|
151
|
+
else
|
152
|
+
transaction_declined!
|
153
|
+
end
|
154
|
+
purchase_result
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
# returns a hash of additional merchant data passed to authorize
|
159
|
+
# you want to pass in the following additional options
|
160
|
+
#
|
161
|
+
# :ip => ip address of the buyer
|
162
|
+
#
|
163
|
+
def payment_options(options={})
|
164
|
+
{ # general
|
165
|
+
:buyer => self.buyer,
|
166
|
+
:seller => self.seller,
|
167
|
+
:payable => self,
|
168
|
+
# active merchant relevant
|
169
|
+
:customer => self.buyer ? "#{self.buyer.class.name} (#{self.buyer.id})" : nil,
|
170
|
+
:merchant => self.seller ? "#{self.seller.class.name} (#{self.seller.id})" : nil,
|
171
|
+
:email => self.buyer && self.buyer.respond_to?(:email) ? self.buyer.email : nil,
|
172
|
+
:invoice => self.number,
|
173
|
+
:currency => self.currency,
|
174
|
+
:billing_address => self.billing_address ? self.billing_address.to_merchant_attributes : nil,
|
175
|
+
:shipping_address => self.shipping_address ? self.shipping_address.to_merchant_attributes : nil
|
176
|
+
}.merge(options)
|
177
|
+
end
|
178
|
+
|
179
|
+
end
|
180
|
+
end
|
@@ -0,0 +1,350 @@
|
|
1
|
+
# Implements outbound orders, i.e. the merchant sells a product to a user
|
2
|
+
module MerchantSidekick
|
3
|
+
class PurchaseOrder < Order
|
4
|
+
has_many :purchase_invoices, :foreign_key => :order_id, :class_name => "MerchantSidekick::PurchaseInvoice"
|
5
|
+
|
6
|
+
# Authorizes a payment over the order gross amount
|
7
|
+
def authorize(payment_object, options={})
|
8
|
+
defaults = {:order_id => number}
|
9
|
+
options = defaults.merge(options).symbolize_keys
|
10
|
+
transaction do
|
11
|
+
buyer.send(:before_authorize_payment, self) if buyer && buyer.respond_to?(:before_authorize_payment)
|
12
|
+
self.build_addresses
|
13
|
+
self.build_invoice unless self.last_unsaved_invoice
|
14
|
+
authorization_result = self.last_unsaved_invoice.authorize(payment_object, options)
|
15
|
+
if authorization_result.success?
|
16
|
+
process_payment!
|
17
|
+
end
|
18
|
+
buyer.send(:after_authorize_payment, self) if buyer && buyer.respond_to?(:after_authorize_payment)
|
19
|
+
authorization_result
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Captures the amount of the order that was previously authorized
|
24
|
+
# If the capture amount
|
25
|
+
def capture(options={})
|
26
|
+
defaults = {:order_id => number}
|
27
|
+
options = defaults.merge(options).symbolize_keys
|
28
|
+
|
29
|
+
if invoice = self.purchase_invoices.find(:all, :created_at => "invoices.id ASC").last
|
30
|
+
buyer.send(:before_capture_payment, self) if buyer && buyer.respond_to?(:before_capture_payment)
|
31
|
+
capture_result = invoice.capture(options)
|
32
|
+
if capture_result.success?
|
33
|
+
approve_payment!
|
34
|
+
end
|
35
|
+
buyer.send(:after_capture_payment, self) if buyer && buyer.respond_to?(:after_capture_payment)
|
36
|
+
capture_result
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Pay the order and generate invoice
|
41
|
+
def pay(payment_object, options={})
|
42
|
+
defaults = { :order_id => number }
|
43
|
+
options = defaults.merge(options).symbolize_keys
|
44
|
+
|
45
|
+
# fire buyer's before_payment callback
|
46
|
+
buyer.send(:before_payment, self) if buyer && buyer.respond_to?(:before_payment)
|
47
|
+
self.build_addresses
|
48
|
+
self.build_invoice unless self.last_unsaved_invoice
|
49
|
+
payment = self.last_unsaved_invoice.purchase(payment_object, options)
|
50
|
+
if payment.success?
|
51
|
+
process_payment!
|
52
|
+
approve_payment!
|
53
|
+
end
|
54
|
+
save!
|
55
|
+
# fire buyer's after_payment callback
|
56
|
+
buyer.send(:after_payment, self ) if buyer && buyer.respond_to?(:after_payment)
|
57
|
+
payment
|
58
|
+
end
|
59
|
+
|
60
|
+
# Voids a previously authorized invoice payment and sets the status to cancel
|
61
|
+
# Usage:
|
62
|
+
# void(options = {})
|
63
|
+
#
|
64
|
+
def void(options={})
|
65
|
+
defaults = { :order_id => self[:number] }
|
66
|
+
options = defaults.merge(options).symbolize_keys
|
67
|
+
|
68
|
+
if invoice = self.purchase_invoices.find(:all, :created_at => "invoices.id ASC").last
|
69
|
+
# before_payment
|
70
|
+
buyer.send(:before_void_payment, self ) if buyer && buyer.respond_to?(:before_void_payment)
|
71
|
+
voided_result = invoice.void(options)
|
72
|
+
if voided_result.success?
|
73
|
+
cancel!
|
74
|
+
end
|
75
|
+
save!
|
76
|
+
# after_void_payment
|
77
|
+
buyer.send(:after_void_payment, self) if buyer && buyer.respond_to?(:after_void_payment)
|
78
|
+
voided_result
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# refunds a previously paid order
|
83
|
+
# Note: :card_number must be supplied
|
84
|
+
def refund(options={})
|
85
|
+
defaults = { :order_id => number }
|
86
|
+
options = defaults.merge(options).symbolize_keys
|
87
|
+
|
88
|
+
if (invoice = self.purchase_invoices.find(:all, :created_at => "invoices.id ASC").last) && invoice.paid?
|
89
|
+
# fire buyer's before_refund_payment callback
|
90
|
+
buyer.send(:before_refund_payment, self) if buyer && buyer.respond_to?(:before_refund_payment)
|
91
|
+
refunded_result = invoice.credit(options)
|
92
|
+
if refunded_result.success?
|
93
|
+
refund!
|
94
|
+
end
|
95
|
+
save!
|
96
|
+
# fire buyer's after_refund_payment callback
|
97
|
+
buyer.send(:after_refund_payment, self) if buyer && buyer.respond_to?(:after_refund_payment)
|
98
|
+
refunded_result
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# E.g.
|
103
|
+
#
|
104
|
+
# @order.recurring(@payment, :interval => {:length => 1, :unit => :month},
|
105
|
+
# :duration => {:start_date => Date.today, :occurrences => 999})
|
106
|
+
#
|
107
|
+
def recurring(payment_object, options={})
|
108
|
+
defaults = {:order_id => number}
|
109
|
+
options = defaults.merge(options).symbolize_keys
|
110
|
+
|
111
|
+
self.build_addresses
|
112
|
+
|
113
|
+
authorization = Payment.class_for(payment_object).recurring(
|
114
|
+
gross_total, payment_object, payment_options(options))
|
115
|
+
self.push_payment(authorization)
|
116
|
+
if authorization.success?
|
117
|
+
save(false)
|
118
|
+
process_payment!
|
119
|
+
else
|
120
|
+
# we don't want to save the payment and related objects
|
121
|
+
# when the authorization fails
|
122
|
+
# transaction_declined!
|
123
|
+
end
|
124
|
+
authorization
|
125
|
+
end
|
126
|
+
|
127
|
+
# E.g.
|
128
|
+
#
|
129
|
+
# @order.pay_recurring("c3s34", :add_line_items => @line_items)
|
130
|
+
#
|
131
|
+
def pay_recurring(authorization=nil, options={})
|
132
|
+
# recurring payment
|
133
|
+
recurring_payment = if authorization.nil?
|
134
|
+
self.payments.recurring.find(:all, :order => "payments.id ASC").last
|
135
|
+
elsif authorization.is_a?(Payment) && self.payments.include?(authorization)
|
136
|
+
authorization
|
137
|
+
else
|
138
|
+
self.payments.find(:first, :conditions => ["payments.id = ? OR payments.reference = ? OR payments.uuid = ?", authorization])
|
139
|
+
end
|
140
|
+
|
141
|
+
# recurring expired
|
142
|
+
if !self.pending? || self.purchase_invoices.paid.count > recurring_payment.duration_occurrences
|
143
|
+
raise MerchantSidekick::RecurringPaymentError, "Recurring order #{self.number} expired"
|
144
|
+
end
|
145
|
+
|
146
|
+
transaction do
|
147
|
+
buyer.send(:before_pay_recurring, self) if buyer && buyer.respond_to?(:before_pay_recurring)
|
148
|
+
self.additional_line_items(options[:add_line_items])
|
149
|
+
self.build_invoice unless self.last_unsaved_invoice
|
150
|
+
|
151
|
+
authorization_result = self.last_unsaved_invoice.purchase(recurring_payment, options)
|
152
|
+
if authorization_result.success?
|
153
|
+
# add next billing due date
|
154
|
+
process_payment!
|
155
|
+
end
|
156
|
+
buyer.send(:after_pay_recurring, self) if buyer && buyer.respond_to?(:before_pay_recurring)
|
157
|
+
authorization_result
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def authorize_recurring(authorization=nil, options={})
|
162
|
+
# recurring payment
|
163
|
+
recurring_payment = if authorization.nil?
|
164
|
+
self.payments.recurring.find(:all, :order => "payments.id ASC").last
|
165
|
+
elsif authorization.is_a?(Payment) && self.payments.include?(authorization)
|
166
|
+
authorization
|
167
|
+
else
|
168
|
+
self.payments.find(:first, :conditions => ["payments.id = ? OR payments.reference = ? OR payments.uuid = ?", authorization])
|
169
|
+
end
|
170
|
+
|
171
|
+
# recurring expired
|
172
|
+
if !self.pending? || self.purchase_invoices.paid.count > recurring_payment.duration_occurrences
|
173
|
+
raise MerchantSidekick::RecurringPaymentError, "Recurring order #{self.number} expired"
|
174
|
+
end
|
175
|
+
|
176
|
+
transaction do
|
177
|
+
buyer.send(:before_authorize_recurring, self) if buyer && buyer.respond_to?(:before_authorize_recurring)
|
178
|
+
self.additional_line_items(options[:add_line_items])
|
179
|
+
self.build_invoice unless self.last_unsaved_invoice
|
180
|
+
|
181
|
+
authorization_result = self.last_unsaved_invoice.authorize(recurring_payment, options)
|
182
|
+
if authorization_result.success?
|
183
|
+
process_payment!
|
184
|
+
end
|
185
|
+
buyer.send(:after_authorize_recurring, self) if buyer && buyer.respond_to?(:after_authorize_recurring)
|
186
|
+
authorization_result
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
# returns a hash of additional merchant data passed to authorize
|
191
|
+
# you want to pass in the following additional options
|
192
|
+
#
|
193
|
+
# :ip => ip address of the buyer
|
194
|
+
#
|
195
|
+
def payment_options(options={})
|
196
|
+
{ # general
|
197
|
+
:buyer => self.buyer,
|
198
|
+
:seller => self.seller,
|
199
|
+
:payable => self,
|
200
|
+
# active merchant relevant
|
201
|
+
:customer => self.buyer ? "#{self.buyer.name} (#{self.buyer.id})" : nil,
|
202
|
+
:email => self.buyer && self.buyer.respond_to?(:email) ? self.buyer.email : nil,
|
203
|
+
:order_number => self.number,
|
204
|
+
#:invoice => self.number,
|
205
|
+
:merchant => self.seller ? "#{self.seller.name} (#{self.seller.id})" : nil,
|
206
|
+
:currency => self.currency,
|
207
|
+
:billing_address => self.billing_address ? self.billing_address.to_merchant_attributes : nil,
|
208
|
+
:shipping_address => self.shipping_address ? self.shipping_address.to_merchant_attributes : nil
|
209
|
+
}.merge(options)
|
210
|
+
end
|
211
|
+
|
212
|
+
# yes, i am a purchase order!
|
213
|
+
def purchase_order?
|
214
|
+
true
|
215
|
+
end
|
216
|
+
|
217
|
+
# used in build_invoice to determine which type of invoice
|
218
|
+
def to_invoice_class_name
|
219
|
+
"MerchantSidekick::PurchaseInvoice"
|
220
|
+
end
|
221
|
+
|
222
|
+
# returns last unsaved invoice
|
223
|
+
def last_unsaved_invoice
|
224
|
+
unless self.purchase_invoices.empty?
|
225
|
+
self.purchase_invoices.last.new_record? ? self.purchase_invoices.last : nil
|
226
|
+
else
|
227
|
+
nil
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
def build_invoice
|
232
|
+
new_invoice = self.purchase_invoices.build(
|
233
|
+
:line_items => self.duplicate_line_items,
|
234
|
+
:net_amount => self.net_total,
|
235
|
+
:tax_amount => self.tax_total,
|
236
|
+
:gross_amount => self.gross_total,
|
237
|
+
:buyer => self.buyer,
|
238
|
+
:seller => self.seller,
|
239
|
+
:origin_address => self.origin_address ? self.origin_address.dup : nil,
|
240
|
+
:billing_address => self.billing_address ? self.billing_address.dup : nil,
|
241
|
+
:shipping_address => self.shipping_address ? self.shipping_address.dup : nil
|
242
|
+
)
|
243
|
+
|
244
|
+
# set new invoice's line items to invoice we just created
|
245
|
+
new_invoice.line_items.each do |li|
|
246
|
+
if li.new_record?
|
247
|
+
li.invoice = new_invoice
|
248
|
+
else
|
249
|
+
li.update_attribute(:invoice, new_invoice)
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
# duplicate addresses
|
254
|
+
new_invoice.build_origin_address(self.origin_address.content_attributes) if self.origin_address
|
255
|
+
new_invoice.build_billing_address(self.billing_address.content_attributes) if self.billing_address
|
256
|
+
new_invoice.build_shipping_address(self.shipping_address.content_attributes) if self.shipping_address
|
257
|
+
|
258
|
+
new_invoice.evaluate
|
259
|
+
@additional_line_items = nil
|
260
|
+
new_invoice
|
261
|
+
end
|
262
|
+
|
263
|
+
# make sure we get a copy of the line items
|
264
|
+
def duplicate_line_items
|
265
|
+
result = []
|
266
|
+
lis = self.line_items
|
267
|
+
lis += @additional_line_items.to_a
|
268
|
+
lis.each do |line_item|
|
269
|
+
li = line_item.dup # Note: use clone in Rails < 3.1
|
270
|
+
li.sellable = line_item.sellable
|
271
|
+
li.net_amount = line_item.net_amount
|
272
|
+
li.gross_amount = line_item.gross_amount
|
273
|
+
li.order = nil
|
274
|
+
result << li
|
275
|
+
end
|
276
|
+
result
|
277
|
+
end
|
278
|
+
|
279
|
+
# process additional line items before build invoice
|
280
|
+
def additional_line_items(items)
|
281
|
+
# add line items
|
282
|
+
if !items.blank?
|
283
|
+
if items.is_a?(Array) && items.all? {|i| i.is_a?(MerchantSidekick::ShoppingCart::LineItem)}
|
284
|
+
duped = self.buyer.dup
|
285
|
+
order = duped.purchase items
|
286
|
+
@additional_line_items = order.line_items
|
287
|
+
elsif items.is_a?(Array) && items.all? {|i| i.is_a?(LineItem)}
|
288
|
+
@additional_line_items = items
|
289
|
+
elsif items.is_a?(Array) && items.all? {|i| i.is_a?(Product)}
|
290
|
+
# array of products
|
291
|
+
duped = self.buyer.dup
|
292
|
+
cart = Cart.new(self.currency)
|
293
|
+
items.each {|product| cart.add(product)}
|
294
|
+
order = duped.purchase cart.line_items
|
295
|
+
@additional_line_items = order.line_items
|
296
|
+
end
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
# Builds billing, shipping and origin addresses
|
301
|
+
def build_addresses(options={})
|
302
|
+
raise ArgumentError.new("No address declared for buyer (#{buyer.class.name} ##{buyer.id}), use e.g. class #{buyer.class.name}; has_address; end") \
|
303
|
+
unless buyer.respond_to?(:find_default_address)
|
304
|
+
|
305
|
+
# buyer's billing or default address
|
306
|
+
unless default_billing_address
|
307
|
+
if buyer.respond_to?(:billing_address) && buyer.default_billing_address
|
308
|
+
self.build_billing_address(buyer.default_billing_address.content_attributes)
|
309
|
+
else
|
310
|
+
if buyer_default_address = buyer.find_default_address
|
311
|
+
self.build_billing_address(buyer_default_address.content_attributes)
|
312
|
+
else
|
313
|
+
raise ArgumentError.new(
|
314
|
+
"No billing or default address for buyer (#{buyer.class.name} ##{buyer.id}), use e.g. class #{buyer.class.name}; has_address :billing; end")
|
315
|
+
end
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
# buyer's shipping address
|
320
|
+
if buyer.respond_to?(:shipping_address)
|
321
|
+
self.build_shipping_address(buyer.find_shipping_address_or_clone_from(
|
322
|
+
self.billing_address
|
323
|
+
).content_attributes) unless self.default_shipping_address
|
324
|
+
end
|
325
|
+
|
326
|
+
self.billing_address.street = "#{Merchant::Sidekick::Version::NAME}" if self.billing_address && self.billing_address.street.to_s =~ /^backend$/
|
327
|
+
self.shipping_address.street = "#{Merchant::Sidekick::Version::NAME}" if self.shipping_address && self.shipping_address.street.to_s =~ /^backend$/
|
328
|
+
|
329
|
+
# seller's billing or default address
|
330
|
+
if seller
|
331
|
+
raise ArgumentError.new("No address for seller (#{seller.class.name} ##{seller.id}), use acts_as_addressable") \
|
332
|
+
unless seller.respond_to?(:find_default_address)
|
333
|
+
|
334
|
+
unless default_origin_address
|
335
|
+
if seller.respond_to?(:billing_address) && seller.find_billing_address
|
336
|
+
self.build_origin_address(seller.find_billing_address.content_attributes)
|
337
|
+
else
|
338
|
+
if seller_default_address = seller.find_default_address
|
339
|
+
self.build_origin_address(seller_default_address.content_attributes)
|
340
|
+
end
|
341
|
+
end
|
342
|
+
end
|
343
|
+
end
|
344
|
+
end
|
345
|
+
|
346
|
+
end
|
347
|
+
|
348
|
+
class ::MerchantSidekick::RecurringPaymentError < Exception
|
349
|
+
end
|
350
|
+
end
|