-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
/
credit_card.rb
197 lines (169 loc) · 7.25 KB
/
credit_card.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
# frozen_string_literal: true
module Spree
# The default `source` of a `Spree::Payment`.
#
class CreditCard < Spree::PaymentSource
belongs_to :user, class_name: Spree::UserClassHandle.new, foreign_key: 'user_id', optional: true
belongs_to :address, optional: true
before_save :set_last_digits
accepts_nested_attributes_for :address
attr_reader :number, :verification_value
attr_accessor :encrypted_data
validates :month, :year, numericality: { only_integer: true }, if: :require_card_numbers?, on: :create
validates :number, presence: true, if: :require_card_numbers?, on: :create, unless: :imported
validates :name, presence: true, if: :require_card_numbers?, on: :create
validates :verification_value, presence: true, if: :require_card_numbers?, on: :create, unless: :imported
scope :with_payment_profile, -> { where('gateway_customer_profile_id IS NOT NULL') }
def self.default
Spree::Deprecation.warn("CreditCard.default is deprecated. Please use Spree::Wallet instead.")
joins(:wallet_payment_sources).where(spree_wallet_payment_sources: { default: true })
end
# needed for some of the ActiveMerchant gateways (eg. SagePay)
alias_attribute :brand, :cc_type
# Taken from ActiveMerchant
# https://github.com/activemerchant/active_merchant/blob/2f2acd4696e8de76057b5ed670b9aa022abc1187/lib/active_merchant/billing/credit_card_methods.rb#L5
CARD_TYPES = {
'visa' => /^4\d{12}(\d{3})?(\d{3})?$/,
'master' => /^(5[1-5]\d{4}|677189|222[1-9]\d{2}|22[3-9]\d{3}|2[3-6]\d{4}|27[01]\d{3}|2720\d{2})\d{10}$/,
'discover' => /^(6011|65\d{2}|64[4-9]\d)\d{12}|(62\d{14})$/,
'american_express' => /^3[47]\d{13}$/,
'diners_club' => /^3(0[0-5]|[68]\d)\d{11}$/,
'jcb' => /^35(28|29|[3-8]\d)\d{12}$/,
'switch' => /^6759\d{12}(\d{2,3})?$/,
'solo' => /^6767\d{12}(\d{2,3})?$/,
'dankort' => /^5019\d{12}$/,
'maestro' => /^(5[06-8]|6\d)\d{10,17}$/,
'forbrugsforeningen' => /^600722\d{10}$/,
'laser' => /^(6304|6706|6709|6771(?!89))\d{8}(\d{4}|\d{6,7})?$/
}.freeze
def default
Spree::Deprecation.warn("CreditCard#default is deprecated. Please use user.wallet.default_wallet_payment_source instead.", caller)
return false if user.nil?
user.wallet.default_wallet_payment_source.try!(:payment_source) == self
end
def default=(set_as_default)
Spree::Deprecation.warn("CreditCard#default= is deprecated. Please use user.wallet.default_wallet_payment_source= instead.", caller)
if user.nil?
raise "Cannot set 'default' on a credit card without a user"
elsif set_as_default # setting this card as default
wallet_payment_source = user.wallet.add(self)
user.wallet.default_wallet_payment_source = wallet_payment_source
true
else # removing this card as default
if user.wallet.default_wallet_payment_source.try!(:payment_source) == self
user.wallet.default_wallet_payment_source = nil
end
false
end
end
def address_attributes=(attributes)
self.address = Spree::Address.immutable_merge(address, attributes)
end
# Sets the expiry date on this credit card.
#
# @param expiry [String] the desired new expiry date in one of the
# following formats: "mm/yy", "mm / yyyy", "mmyy", "mmyyyy"
def expiry=(expiry)
return unless expiry.present?
self[:month], self[:year] =
if expiry =~ /\d{2}\s?\/\s?\d{2,4}/ # will match mm/yy and mm / yyyy
expiry.delete(' ').split('/')
elsif match = expiry.match(/(\d{2})(\d{2,4})/) # will match mmyy and mmyyyy
[match[1], match[2]]
end
if self[:year]
self[:year] = "20" + self[:year] if self[:year].length == 2
self[:year] = self[:year].to_i
end
self[:month] = self[:month].to_i if self[:month]
end
# Sets the credit card number, removing any non-numeric characters.
#
# @param num [String] the desired credit card number
def number=(num)
@number =
if num.is_a?(String)
num.gsub(/[^0-9]/, '')
end
end
def verification_value=(value)
@verification_value = value.to_s.gsub(/\s/, '')
end
# Sets the credit card type, converting it to the preferred internal
# representation from jquery.payment's representation when appropriate.
#
# @param type [String] the desired credit card type
def cc_type=(type)
# cc_type is set by jquery.payment, which helpfully provides different
# types from Active Merchant. Converting them is necessary.
self[:cc_type] = case type
when 'mastercard', 'maestro' then 'master'
when 'amex' then 'american_express'
when 'dinersclub' then 'diners_club'
when '' then try_type_from_number
else type
end
end
# Sets the last digits field based on the assigned credit card number.
def set_last_digits
self.last_digits ||= number.to_s.length <= 4 ? number : number.to_s.slice(-4..-1)
end
# @return [String] the credit card type if it can be determined from the
# number, otherwise the empty string
def try_type_from_number
CARD_TYPES.each do |type, pattern|
return type if number =~ pattern
end
''
end
# @return [Boolean] true when a verification value is present
def verification_value?
verification_value.present?
end
# @return [String] the card number, with all but last 4 numbers replace
# with "X", as in "XXXX-XXXX-XXXX-4338"
def display_number
"XXXX-XXXX-XXXX-#{last_digits}"
end
def reusable?
has_payment_profile?
end
# @return [Boolean] true when there is a gateway customer or payment
# profile id present
def has_payment_profile?
gateway_customer_profile_id.present? || gateway_payment_profile_id.present?
end
# @note ActiveMerchant needs first_name/last_name because we pass it a
# Spree::CreditCard and it calls those methods on it.
# @todo We should probably be calling #to_active_merchant before passing
# the object to ActiveMerchant.
# @return [String] the first name on this credit card
def first_name
Spree::Address::Name.new(name).first_name
end
# @note ActiveMerchant needs first_name/last_name because we pass it a
# Spree::CreditCard and it calls those methods on it.
# @todo We should probably be calling #to_active_merchant before passing
# the object to ActiveMerchant.
# @return [String] the last name on this credit card
def last_name
Spree::Address::Name.new(name).last_name
end
# @return [ActiveMerchant::Billing::CreditCard] an ActiveMerchant credit
# card that represents this credit card
def to_active_merchant
ActiveMerchant::Billing::CreditCard.new(
number: number,
month: month,
year: year,
verification_value: verification_value,
first_name: first_name,
last_name: last_name
)
end
private
def require_card_numbers?
!encrypted_data.present? && !has_payment_profile?
end
end
end