plastic 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,9 @@
1
+ *.gem
2
+ *.gemspec
3
+ *.log
4
+ *.pid
5
+ *.sqlite3
6
+ *.tmproj
7
+ .DS_Store
8
+ log/*
9
+ pkg/*
@@ -0,0 +1,20 @@
1
+ === Version 0.2.1 2010-07-18
2
+
3
+ Initial public release.
4
+
5
+
6
+ === Version 0.2.0
7
+
8
+ Updated with additional validations and brand support.
9
+
10
+ * PAN validations.
11
+ * More robust expiration year / month accessors.
12
+ * Support American Express PAN format with spaces.
13
+ * Brand detection.
14
+
15
+
16
+ === Version 0.1.0
17
+
18
+ Internal preview release of Plastic.
19
+
20
+ * Basic gem + project framework.
@@ -0,0 +1,7 @@
1
+ Copyright © 2010 Square, Inc.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,21 @@
1
+ = Plastic
2
+
3
+ Credit card “plastic” library for Ruby. Parses track 1, track 2 and individual arguments. For ease of integration, Plastic duck-types an {ActiveMerchant}[http://www.activemerchant.org/] {CreditCard}[http://activemerchant.rubyforge.org/classes/ActiveMerchant/Billing/CreditCard.html] model.
4
+
5
+ == Prerequisites
6
+
7
+ * Money and a FICA score > 600.
8
+
9
+ == Testing
10
+
11
+ Testing requires the RSpec gem:
12
+
13
+ * {Rspec}[http://rspec.info/]
14
+
15
+ To test, run:
16
+ rake spec
17
+
18
+ == License
19
+
20
+ Copyright © 2010 Square, Inc.
21
+ See LICENSE.txt in this directory.
@@ -0,0 +1,45 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'spec/rake/spectask'
4
+
5
+ require File.join(File.dirname(__FILE__), 'lib', 'plastic', 'version')
6
+
7
+ begin
8
+ require 'jeweler'
9
+ Jeweler::Tasks.new do |gem|
10
+ gem.version = Plastic::VERSION::STRING
11
+ gem.name = "plastic"
12
+ gem.summary = "Credit card library for Ruby."
13
+ gem.description = "Handle credit, debit, bank and other cards."
14
+ gem.email = "github@squareup.com"
15
+ gem.homepage = "http://github.com/square/plastic"
16
+ gem.authors = [
17
+ "Randy Reddig",
18
+ "Cameron Walters",
19
+ "Chris Kampmeier",
20
+ "Erica Kwan",
21
+ "Matthew O'Connor",
22
+ "Damon McCormick",
23
+ "Brian Jenkins",
24
+ ]
25
+ end
26
+ rescue LoadError
27
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
28
+ end
29
+
30
+ desc "Run all specs"
31
+ Spec::Rake::SpecTask.new do |t|
32
+ t.spec_opts = ["--options", "spec/spec.opts"]
33
+ t.spec_files = FileList["spec/**/*_spec.rb"]
34
+ t.rcov = ENV["RCOV"]
35
+ t.rcov_opts = %w{--exclude osx\/objc,gems\/,spec\/}
36
+ t.verbose = true
37
+ end
38
+
39
+ task :spec => :check_dependencies
40
+ task :default => :spec
41
+
42
+ desc "Remove trailing whitespace"
43
+ task :whitespace do
44
+ sh %{find . -name '*.rb' -exec sed -i '' 's/ *$//g' {} \\;}
45
+ end
@@ -0,0 +1,5 @@
1
+ require 'plastic/version'
2
+ require 'plastic/core'
3
+ require 'plastic/track'
4
+ require 'plastic/validations'
5
+ require 'plastic/duck_type'
@@ -0,0 +1,80 @@
1
+ class Plastic
2
+ BRANDS = [:visa, :mastercard, :american_express, :discover]
3
+
4
+ attr_accessor :pan, :expiration
5
+ attr_accessor :track_name, :surname, :given_name, :title
6
+ attr_accessor :service_code, :cvv2
7
+ attr_accessor :track_1, :track_2
8
+
9
+ def initialize(attributes={})
10
+ if attributes.kind_of? Hash
11
+ self.update! attributes
12
+ parse_tracks!
13
+ else
14
+ parse_track! attributes
15
+ end
16
+ end
17
+
18
+ def update!(attributes={})
19
+ attributes.each do |key, value|
20
+ setter_method_name = :"#{key}="
21
+ send(setter_method_name, value) if respond_to?(setter_method_name)
22
+ end
23
+ end
24
+
25
+ def name
26
+ [title, given_name, surname].flatten.compact.join(" ").strip
27
+ end
28
+
29
+ def expiration=(yymm)
30
+ @expiration = yymm.to_s[0..3]
31
+ end
32
+
33
+ def expiration_year
34
+ DateTime.strptime(expiration_yy, "%y").year
35
+ end
36
+
37
+ def expiration_month
38
+ expiration_mm.to_i
39
+ end
40
+
41
+ def brand
42
+ case pan
43
+ when /^4/ then :visa
44
+ when /^5[1-5]/ then :mastercard
45
+ when /^677189/ then :mastercard
46
+ when /^6011/ then :discover
47
+ when /^65/ then :discover
48
+ when /^3[47]/ then :american_express
49
+ end
50
+ end
51
+
52
+ def valid?
53
+ value_is_present?(pan) && value_is_present?(expiration) && valid_pan? && valid_expiration?
54
+ end
55
+
56
+ private
57
+
58
+ def expiration_yy
59
+ @expiration.to_s[0..1]
60
+ end
61
+
62
+ def expiration_mm
63
+ @expiration.to_s[2..3]
64
+ end
65
+
66
+ def value_is_present?(value)
67
+ !value_is_blank?(value)
68
+ end
69
+
70
+ def value_is_blank?(value)
71
+ if value.respond_to?(:blank?)
72
+ value.blank?
73
+ elsif value.respond_to?(:empty?)
74
+ value.empty?
75
+ else
76
+ value.nil?
77
+ end
78
+ end
79
+
80
+ end
@@ -0,0 +1,27 @@
1
+ class Plastic
2
+ # ActiveMerchant::Billing::CreditCard
3
+ DUCK_TYPE_INTERFACE = [
4
+ [:number, :pan],
5
+ [:first_name, :given_name],
6
+ [:last_name, :surname],
7
+ [:verification_value, :cvv2],
8
+ [:security_code, :cvv2],
9
+ [:expiration_date, :expiration],
10
+ [:track1, :track_1],
11
+ [:track2, :track_2],
12
+ ]
13
+
14
+ DUCK_TYPE_INTERFACE.each do |_alias, attribute_name|
15
+ alias_method _alias, attribute_name
16
+ alias_method :"#{_alias}=", :"#{attribute_name}="
17
+ define_method :"#{_alias}?", lambda { value_is_present?(send(_alias)) }
18
+ end
19
+
20
+ def year
21
+ expiration ? DateTime.new(expiration_year).strftime("%y") : nil
22
+ end
23
+
24
+ def month
25
+ expiration ? "%02d" % expiration_month : nil
26
+ end
27
+ end
@@ -0,0 +1,86 @@
1
+ # http://en.wikipedia.org/wiki/Magnetic_stripe
2
+
3
+ class Plastic
4
+ def parse_tracks!
5
+ parse_track_2!
6
+ parse_track_1!
7
+ end
8
+
9
+ def parse_track!(value)
10
+ parse_track_2! value
11
+ parse_track_1! value
12
+ end
13
+
14
+ # Track 1, Format B
15
+ #
16
+ # Start sentinel — one character (generally '%')
17
+ # Format code="B" — one character (alpha only)
18
+ # Primary account number (PAN) — up to 19 characters. Usually, but not always, matches the credit card number printed on the front of the card.
19
+ # Field Separator — one character (generally '^')
20
+ # Name — two to 26 characters
21
+ # Field Separator — one character (generally '^')
22
+ # Expiration date — four characters in the form YYMM.
23
+ # Service code — three characters
24
+ # Discretionary data — may include Pin Verification Key Indicator (PVKI, 1 character), PIN Verification Value (PVV, 4 characters), Card Verification Value or Card Verification Code (CVV or CVK, 3 characters)
25
+ # End sentinel — one character (generally '?')
26
+
27
+ TRACK_1_PARSER = /
28
+ \A # Start of string
29
+ %? # Start sentinel
30
+ [bB] # Format code
31
+ (\d{12,19}|\d{4}\ \d{6}\ \d{5}) # PAN
32
+ \^ # Field separator
33
+ ( # Name field
34
+ (?=[^^]{2,26}) # Lookahead assertion
35
+ ([^\/]+) # Surname
36
+ (?:\/?([^.]+)(?:\.?([^^]+))?)? # Given name and title
37
+ ) #
38
+ \^ # Field separator
39
+ (\d{4}) # Expiration
40
+ (\d{3}) # Service code
41
+ ([^?]*) # Discretionary data
42
+ \?? # End sentinel
43
+ \z # End of string
44
+ /x.freeze
45
+
46
+ def self.track_1_parser
47
+ TRACK_1_PARSER
48
+ end
49
+
50
+ def parse_track_1!(value=nil)
51
+ value ||= track_1
52
+ if matched = self.class.track_1_parser.match(value.to_s)
53
+ self.pan = matched[1].delete(' ')
54
+ self.track_name = matched[2]
55
+ self.surname = matched[3]
56
+ self.given_name = matched[4]
57
+ self.title = matched[5]
58
+ self.expiration = matched[6]
59
+ self.service_code = matched[7]
60
+ end
61
+ end
62
+
63
+ # Track 2
64
+ #
65
+ # Start sentinel — one character (generally ';')
66
+ # Primary account number (PAN) — up to 19 characters. Usually, but not always, matches the credit card number printed on the front of the card.
67
+ # Separator — one char (generally '=')
68
+ # Expiration date — four characters in the form YYMM.
69
+ # Service code — three characters
70
+ # Discretionary data — as in track one
71
+ # End sentinel — one character (generally '?')
72
+
73
+ TRACK_2_PARSER = /\A;?(\d{12,19})\=(\d{4})(.{3})([^\?]*)\??\z/.freeze
74
+
75
+ def self.track_2_parser
76
+ TRACK_2_PARSER
77
+ end
78
+
79
+ def parse_track_2!(value=nil)
80
+ value ||= track_2
81
+ if matched = self.class.track_2_parser.match(value.to_s)
82
+ self.pan = matched[1]
83
+ self.expiration = matched[2]
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,43 @@
1
+ require 'date'
2
+
3
+ class Plastic
4
+ def valid_pan?
5
+ valid_pan_length? && valid_pan_checksum?
6
+ end
7
+
8
+ def valid_expiration?
9
+ return false unless valid_expiration_year? && valid_expiration_month?
10
+ this = Time.now.utc
11
+ if this.year == expiration_year
12
+ (this.month..12).include?(expiration_month)
13
+ elsif expiration_year > this.year
14
+ true
15
+ else
16
+ false
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def valid_pan_length?
23
+ pan.to_s.length >= 12
24
+ end
25
+
26
+ def valid_pan_checksum?
27
+ odd = false
28
+ sum = pan.reverse.chars.inject(0) do |checksum, d|
29
+ d = d.to_i
30
+ checksum + ((odd = !odd) ? d : (d * 2 > 9 ? d * 2 - 9 : d * 2))
31
+ end
32
+ sum % 10 == 0
33
+ end
34
+
35
+ def valid_expiration_month?
36
+ (1..12).include?(expiration_month)
37
+ end
38
+
39
+ def valid_expiration_year?
40
+ this = Time.now.utc
41
+ (this.year..this.year + 20).include?(expiration_year)
42
+ end
43
+ end
@@ -0,0 +1,9 @@
1
+ class Plastic
2
+ module VERSION #:nodoc:
3
+ MAJOR = 0
4
+ MINOR = 2
5
+ TINY = 1
6
+
7
+ STRING = [MAJOR, MINOR, TINY].join('.')
8
+ end
9
+ end
@@ -0,0 +1,221 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ class OtherHash < Hash
4
+ end
5
+
6
+ describe Plastic do
7
+ subject { described_class.new }
8
+
9
+ [
10
+ :pan, :expiration,
11
+ :track_name, :surname, :given_name, :title,
12
+ :service_code, :cvv2,
13
+ :track_1, :track_2,
14
+ ].each do |accessor|
15
+ it "has accessor :#{accessor} and :#{accessor}=" do
16
+ subject.should respond_to(:"#{accessor}")
17
+ subject.should respond_to(:"#{accessor}=")
18
+ end
19
+ end
20
+
21
+ describe "new" do
22
+ it "returns an instance of #{described_class.name}" do
23
+ subject.should be_instance_of(described_class)
24
+ end
25
+ end
26
+
27
+ describe "#initialize" do
28
+ context "with no arguments" do
29
+ it "calls update! with an empty hash" do
30
+ subject.should_receive(:update!).with({})
31
+ subject.send :initialize
32
+ end
33
+ end
34
+
35
+ context "with a nil argument" do
36
+ it "calls parse_track!" do
37
+ subject.should_receive(:parse_track!).with(nil)
38
+ subject.send :initialize, nil
39
+ end
40
+ end
41
+
42
+ context "with a hash argument" do
43
+ subject { described_class.new(:track_1 => "%B123456789012345^Dorsey/Jack.Dr^1010123?") }
44
+
45
+ it "calls update! with the hash argument" do
46
+ arg = {:foo => "bar"}
47
+ subject.should_receive(:update!).with(arg)
48
+ subject.send :initialize, arg
49
+ end
50
+
51
+ it "parses the track data if given" do
52
+ subject.track_1.should == "%B123456789012345^Dorsey/Jack.Dr^1010123?"
53
+ subject.pan.should == "123456789012345"
54
+ subject.expiration.should == "1010"
55
+ subject.name.should == "Dr Jack Dorsey"
56
+ end
57
+ end
58
+
59
+ context "with a string argument" do
60
+ it "calls parse_track! with the string argument" do
61
+ arg = "foo=bar"
62
+ subject.should_receive(:parse_track!).with(arg)
63
+ subject.send :initialize, arg
64
+ end
65
+ end
66
+ end
67
+
68
+ describe "#update!" do
69
+ context "with no arguments" do
70
+ it "calls update! with an empty hash" do
71
+ subject.should_not_receive(:send)
72
+ subject.update!
73
+ end
74
+ end
75
+
76
+ context "with a nil argument" do
77
+ it "raises an exception" do
78
+ expect { subject.update! nil }.to raise_error(NoMethodError)
79
+ end
80
+ end
81
+
82
+ context "with a hash argument" do
83
+ it "assigns the passed keys" do
84
+ arg = {:pan => "bar"}
85
+ subject.update! arg
86
+ subject.pan.should == "bar"
87
+ end
88
+
89
+ it "ignores parameters that do not correspond to a setter" do
90
+ subject.should_not respond_to(:foo=)
91
+ expect { subject.update! :foo => 97 }.to_not raise_error
92
+ end
93
+ end
94
+
95
+ context "with a subclass of hash" do
96
+ before do
97
+ @other_hash = OtherHash.new
98
+ @other_hash[:pan] = "bar"
99
+ @other_hash.should be_kind_of(Hash)
100
+ end
101
+
102
+ it "assigns the passed keys" do
103
+ subject.update! @other_hash
104
+ subject.pan.should == "bar"
105
+ end
106
+ end
107
+ end
108
+
109
+ describe "#name" do
110
+ it "returns a string" do
111
+ subject.name.should be_instance_of(String)
112
+ end
113
+
114
+ [
115
+ ["Prince", nil, nil, "Prince"],
116
+ ["Walters", "Cameron", nil, "Cameron Walters"],
117
+ ["Howser", "Doogie", "Dr", "Dr Doogie Howser"],
118
+ ].each do |surname, given_name, title, name|
119
+ it "returns #{name} for the given input" do
120
+ subject.surname = surname
121
+ subject.given_name = given_name
122
+ subject.title = title
123
+ subject.name.should == name
124
+ end
125
+ end
126
+ end
127
+
128
+ describe "#expiration_year" do
129
+ it "returns a year 2000s when two digit expiration is in the range 00-68, inclusive" do
130
+ (0..68).each do |y|
131
+ yy = "%02d" % y
132
+ Plastic.new(:expiration => "#{yy}01").expiration_year.should == 2000 + y
133
+ end
134
+ end
135
+
136
+ it "returns a year in 1900s when when two digit expiration is in the range 69-99, inclusive" do
137
+ (69..99).each do |y|
138
+ yy = "%02d" % y
139
+ Plastic.new(:expiration => "#{yy}01").expiration_year.should == 1900 + y
140
+ end
141
+ end
142
+ end
143
+
144
+ describe "expiration_month" do
145
+ it "returns an integer month" do
146
+ subject.expiration = "9901"
147
+ subject.expiration_month.should == 1
148
+ subject.expiration = "9912"
149
+ subject.expiration_month.should == 12
150
+ end
151
+ end
152
+
153
+ describe "#brand" do
154
+ it "recognizes Visa cards" do
155
+ Plastic.new(:pan => "4111111111111111").brand.should == :visa
156
+ end
157
+
158
+ it "recognizes MasterCard cards" do
159
+ Plastic.new(:pan => "5100000000000000").brand.should == :mastercard
160
+ Plastic.new(:pan => "5200000000000000").brand.should == :mastercard
161
+ Plastic.new(:pan => "5300000000000000").brand.should == :mastercard
162
+ Plastic.new(:pan => "5400000000000000").brand.should == :mastercard
163
+ Plastic.new(:pan => "5500000000000000").brand.should == :mastercard
164
+ Plastic.new(:pan => "6771890000000000").brand.should == :mastercard # TODO: confirm that 6771- is really MasterCard
165
+ end
166
+
167
+ it "returns nil for bogus pseudo-MasterCard cards" do
168
+ Plastic.new(:pan => "5000000000000000").brand.should be_nil
169
+ Plastic.new(:pan => "5600000000000000").brand.should be_nil
170
+ Plastic.new(:pan => "5700000000000000").brand.should be_nil
171
+ Plastic.new(:pan => "5800000000000000").brand.should be_nil
172
+ Plastic.new(:pan => "5900000000000000").brand.should be_nil
173
+ end
174
+
175
+ it "recognizes Discover cards" do
176
+ Plastic.new(:pan => "6011000000000000").brand.should == :discover
177
+ Plastic.new(:pan => "6500000000000000").brand.should == :discover
178
+ end
179
+
180
+ it "recognizes American Express cards" do
181
+ Plastic.new(:pan => "340000000000000").brand.should == :american_express
182
+ Plastic.new(:pan => "370000000000000").brand.should == :american_express
183
+ end
184
+ end
185
+
186
+ describe "BRANDS constant" do
187
+ it "returns a list of the brands as symbols" do
188
+ Plastic::BRANDS.should == [:visa, :mastercard, :american_express, :discover]
189
+ end
190
+ end
191
+
192
+ describe "#valid?" do
193
+ before do
194
+ @plastic = Plastic.new(:pan => "5480020605154711", :expiration => "1501")
195
+ end
196
+
197
+ it "is valid if it has both a valid pan and an expiration" do
198
+ @plastic.should be_valid
199
+ end
200
+
201
+ it "is not valid if the pan is missing" do
202
+ @plastic.pan = nil
203
+ @plastic.should_not be_valid
204
+ end
205
+
206
+ it "is not valid if the expiration is missing" do
207
+ @plastic.expiration = nil
208
+ @plastic.should_not be_valid
209
+ end
210
+
211
+ it "is not valid if the card is expired" do
212
+ @plastic.expiration = "0901"
213
+ @plastic.should_not be_valid
214
+ end
215
+
216
+ it "is not valid if the pan does not pass its checksum" do
217
+ @plastic.pan = "4001111111111"
218
+ @plastic.should_not be_valid
219
+ end
220
+ end
221
+ end
@@ -0,0 +1,62 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ describe Plastic do
4
+ subject { described_class.new }
5
+
6
+ [
7
+ [:number, :pan],
8
+ [:first_name, :given_name],
9
+ [:last_name, :surname],
10
+ [:verification_value, :cvv2],
11
+ [:security_code, :cvv2],
12
+ [:expiration_date, :expiration],
13
+ [:track1, :track_1],
14
+ [:track2, :track_2],
15
+ ].each do |_alias, method|
16
+ it "##{method} is aliased as ##{_alias}" do
17
+ subject.method(_alias).should == subject.method(method)
18
+ end
19
+
20
+ it "##{method} is aliased as ##{_alias}" do
21
+ subject.method("#{_alias}=").should == subject.method("#{method}=")
22
+ end
23
+
24
+ it "##{method}? checks blankness of ##{_alias}" do
25
+ subject.send(:"#{_alias}?").should be_false
26
+ end
27
+ end
28
+
29
+ describe "when expiration is set" do
30
+ before do
31
+ subject.expiration = "1501"
32
+ end
33
+
34
+ describe "#year" do
35
+ it "returns the two digit year as a string" do
36
+ subject.expiration_year.should == 2015
37
+ subject.year.should == "15"
38
+ end
39
+ end
40
+
41
+ describe "#month" do
42
+ it "returns the two digit month as a string" do
43
+ subject.expiration_month.should == 1
44
+ subject.month.should == "01"
45
+ end
46
+ end
47
+ end
48
+
49
+ describe "when expiration is not set" do
50
+ describe "#year" do
51
+ it "returns nil" do
52
+ subject.year.should be_nil
53
+ end
54
+ end
55
+
56
+ describe "#month" do
57
+ it "returns nil" do
58
+ subject.month.should be_nil
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,126 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ describe Plastic do
4
+ subject { described_class.new }
5
+
6
+ describe "#parse_tracks!" do
7
+ it "calls #parse_track_2, then #parse_track_1" do
8
+ subject.should_receive(:parse_track_2!).with().once
9
+ subject.should_receive(:parse_track_1!).with().once.and_raise(StandardError)
10
+ expect { subject.parse_tracks! }.to raise_error(StandardError)
11
+ end
12
+ end
13
+
14
+ describe "#parse_track!" do
15
+ it "calls #parse_track_2, then #parse_track_1" do
16
+ arg = "foo"
17
+ subject.should_receive(:parse_track_2!).with(arg).once
18
+ subject.should_receive(:parse_track_1!).with(arg).once.and_raise(StandardError)
19
+ expect { subject.parse_track! arg }.to raise_error(StandardError)
20
+ end
21
+ end
22
+
23
+ describe "self.track_1_parser" do
24
+ it "returns a regular expression" do
25
+ described_class.track_1_parser.should be_instance_of(Regexp)
26
+ end
27
+ end
28
+
29
+ describe "#parse_track_1!" do
30
+ def mock_track_1_parser
31
+ subject
32
+ parser = mock()
33
+ described_class.should_receive(:track_1_parser).once.and_return(parser)
34
+ parser
35
+ end
36
+
37
+ it "with no arguments parses the contents of #track_1" do
38
+ arg = "foo"
39
+ subject.should_receive(:track_1).once.and_return(arg)
40
+ mock_track_1_parser.should_receive(:match).with(arg).once
41
+ subject.parse_track_1!
42
+ end
43
+
44
+ it "with nil parses the contents of #track_1" do
45
+ arg = "foo"
46
+ subject.should_receive(:track_1).once.and_return(arg)
47
+ mock_track_1_parser.should_receive(:match).with(arg).once
48
+ subject.parse_track_1! nil
49
+ end
50
+
51
+ [0, 1, "foo", "bar", StandardError].each do |value|
52
+ it "with #{value} attempts to parse the string representation" do
53
+ mock_track_1_parser.should_receive(:match).with(value.to_s).once
54
+ subject.parse_track_1! value
55
+ end
56
+ end
57
+ end
58
+
59
+ describe "self.track_2_parser" do
60
+ it "returns a regular expression" do
61
+ described_class.track_2_parser.should be_instance_of(Regexp)
62
+ end
63
+ end
64
+
65
+ describe "#parse_track_2!" do
66
+ def mock_track_2_parser
67
+ subject
68
+ parser = mock()
69
+ described_class.should_receive(:track_2_parser).once.and_return(parser)
70
+ parser
71
+ end
72
+
73
+ it "with no arguments parses the contents of #track_2" do
74
+ arg = "foo"
75
+ subject.should_receive(:track_2).once.and_return(arg)
76
+ mock_track_2_parser.should_receive(:match).with(arg).once
77
+ subject.parse_track_2!
78
+ end
79
+
80
+ it "with nil parses the contents of #track_2" do
81
+ arg = "foo"
82
+ subject.should_receive(:track_2).once.and_return(arg)
83
+ mock_track_2_parser.should_receive(:match).with(arg).once
84
+ subject.parse_track_2! nil
85
+ end
86
+
87
+ [0, 1, "foo", "bar", StandardError].each do |value|
88
+ it "with #{value} attempts to parse the string representation" do
89
+ mock_track_2_parser.should_receive(:match).with(value.to_s).once
90
+ subject.parse_track_2! value
91
+ end
92
+ end
93
+ end
94
+
95
+ [
96
+ # Track 1
97
+ ["", nil, nil, ""],
98
+ ["foobar", nil, nil, ""],
99
+ ["B1^N^1230", nil, nil, ""],
100
+ ["%B1^N^1230?", nil, nil, ""],
101
+ ["B12345678901234567890^CW^1010123", nil, nil, ""],
102
+ ["B123456789012^CW^0909123", "123456789012", "0909", "CW"],
103
+ ["B123456789012345^Dorsey/Jack^1010123", "123456789012345", "1010", "Jack Dorsey"],
104
+ ["%B123456789012345^Dorsey/Jack^1010123?", "123456789012345", "1010", "Jack Dorsey"],
105
+ ["B123456789012345^Dorsey/Jack.Dr^1010123", "123456789012345", "1010", "Dr Jack Dorsey"],
106
+ ["%B123456789012345^Dorsey/Jack.Dr^1010123?", "123456789012345", "1010", "Dr Jack Dorsey"],
107
+ ["%B1234 567890 12345^Dorsey/Jack.Dr^1010123?", "123456789012345", "1010", "Dr Jack Dorsey"],
108
+
109
+ # Track 2
110
+ ["", nil, nil, ""],
111
+ ["foobar", nil, nil, ""],
112
+ ["1=1230", nil, nil, ""],
113
+ ["?1=1230?", nil, nil, ""],
114
+ ["12345678901234567890=1010123", nil, nil, ""],
115
+ ["123456789012=0909123", "123456789012", "0909", ""],
116
+ ["123456789012345=1010123", "123456789012345", "1010", ""],
117
+ [";123456789012345=1010123?", "123456789012345", "1010", ""],
118
+ ].each do |value, pan, expiration, name|
119
+ it "#parse_track!(\"#{value}\") correctly parses the track data" do
120
+ subject.parse_track! value
121
+ subject.pan.should == pan
122
+ subject.expiration.should == expiration
123
+ subject.name.should == name
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,165 @@
1
+ require 'spec_helper'
2
+
3
+ describe Plastic, "validations" do
4
+ describe "#valid_pan?" do
5
+ subject { Plastic.new(:pan => "4111111111111111") }
6
+
7
+ it "is true when #valid_pan_length? and #valid_pan_checksum? are true" do
8
+ subject.should be_valid_pan
9
+ end
10
+
11
+ it "is false when #valid_pan_length? is false" do
12
+ subject.should_receive(:valid_pan_length?).and_return(false)
13
+ subject.should_not be_valid_pan
14
+ end
15
+
16
+ it "is false when #valid_pan_checksum? is false" do
17
+ subject.should_receive(:valid_pan_checksum?).and_return(false)
18
+ subject.should_not be_valid_pan
19
+ end
20
+ end
21
+
22
+ describe "#valid_expiration?" do
23
+ subject { Plastic.new(:expiration => "1312") }
24
+
25
+ describe "when expiration_year is next year" do
26
+ it "is true for all values of expiration_month" do
27
+ next_year = (DateTime.now.year + 1).to_s[-2..-1]
28
+ (1..12).each do |month|
29
+ subject.expiration = "%02d%02d" % [next_year, month]
30
+ subject.should be_valid_expiration
31
+ end
32
+ end
33
+ end
34
+
35
+ describe "when expiration_year is this year" do
36
+ before do
37
+ @this_year = DateTime.now.year.to_s[-2..-1]
38
+ end
39
+
40
+ it "is true when expiration_month is this month and after" do
41
+ (DateTime.now.month..12).each do |month|
42
+ subject.expiration = "%02d%02d" % [@this_year, month]
43
+ subject.should be_valid_expiration
44
+ end
45
+ end
46
+
47
+ it "is false when expiration_month is earlier than this month" do
48
+ this_year = DateTime.now.year.to_s[-2..-1]
49
+ (1...DateTime.now.month).each do |month|
50
+ subject.expiration = "%02d%02d" % [@this_year, month]
51
+ subject.should_not be_valid_expiration
52
+ end
53
+ end
54
+ end
55
+
56
+ it "is false when #valid_expiration_year? is false" do
57
+ subject.should_receive(:valid_expiration_year?).and_return(false)
58
+ subject.should_not be_valid_expiration
59
+ end
60
+
61
+ it "is false when #valid_expiration_month? is false" do
62
+ subject.should_receive(:valid_expiration_month?).and_return(false)
63
+ subject.should_not be_valid_expiration
64
+ end
65
+ end
66
+
67
+ describe "valid_pan_length?" do
68
+ it "is true when card number is 12 or more digits" do
69
+ extra_numbers = %w[11 234 5678 99999999].each do |n|
70
+ Plastic.new(:pan => "0123456789#{n}").should be_valid_pan_length
71
+ end
72
+ end
73
+
74
+ it "is false when card number is less than 12 digits" do
75
+ Plastic.new(:pan => "0123456789").should_not be_valid_pan_length
76
+ Plastic.new(:pan => "01234567890").should_not be_valid_pan_length
77
+ end
78
+ end
79
+
80
+ describe "valid_pan_checksum?" do
81
+ %w[
82
+ 5454545454545454
83
+ 5480020605154711
84
+ 3566002020190001
85
+ 4111111111111111
86
+ 4005765777003
87
+ 371449635398456
88
+ 6011000995504101
89
+ 36438999910011
90
+ 4055011111111111
91
+ 5581111111111119
92
+ ].each do |pan|
93
+ it "is true with valid test PAN #{pan}" do
94
+ Plastic.new(:pan => pan).should be_valid_pan_checksum
95
+ end
96
+ end
97
+
98
+ %w[
99
+ 5451666666666666
100
+ 5480000000000000
101
+ 3566333333333333
102
+ 4222222222222222
103
+ 4001111111111
104
+ 371433333333333
105
+ 6011444444444444
106
+ 36435555555555
107
+ 4055999999999999
108
+ 5581777777777777
109
+ ].each do |pan|
110
+ it "is false with invalid PAN #{pan}" do
111
+ Plastic.new(:pan => pan).should_not be_valid_pan_checksum
112
+ end
113
+ end
114
+ end
115
+
116
+ describe "#valid_expiration_month?" do
117
+ it "is true when the month is any of 1-12" do
118
+ (1..12).each do |month|
119
+ expiration = "YY%02d" % month
120
+ expiration.should match(/^YY[01]\d$/)
121
+ Plastic.new(:expiration => expiration).should be_valid_expiration_month
122
+ end
123
+ end
124
+
125
+ it "is false when the month is outside the range 1-12" do
126
+ [0, 17, 31].each do |month|
127
+ expiration = "YY%02d" % month
128
+ expiration.should match(/^YY\d\d$/)
129
+ Plastic.new(:expiration => expiration).should_not be_valid_expiration_month
130
+ end
131
+ end
132
+ end
133
+
134
+ describe "#valid_expiration_year?" do
135
+ def format_expiration_year_as_track(year)
136
+ two_digit_year = year.to_s[-2..-1].to_i % 99
137
+ expiration = "%02dMM" % two_digit_year
138
+ expiration.should match(/^\d\dMM$/)
139
+ expiration
140
+ end
141
+
142
+ before do
143
+ @this = DateTime.now
144
+ end
145
+
146
+ it "is true when set to the current year and up to 20 years later" do
147
+ (@this.year..@this.year + 20).each do |year|
148
+ expiration = format_expiration_year_as_track(year)
149
+ Plastic.new(:expiration => expiration).should be_valid_expiration_year
150
+ end
151
+ end
152
+
153
+ it "is false when set to last year" do
154
+ invalid_year = DateTime.now.year - 1
155
+ expiration = format_expiration_year_as_track(invalid_year)
156
+ Plastic.new(:expiration => expiration).should_not be_valid_expiration_year
157
+ end
158
+
159
+ it "is false when set 21 years in the future" do
160
+ invalid_year = DateTime.now.year + 21
161
+ expiration = format_expiration_year_as_track(invalid_year)
162
+ Plastic.new(:expiration => expiration).should_not be_valid_expiration_year
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,4 @@
1
+ --colour
2
+ --format progress
3
+ --loadby mtime
4
+ --reverse
@@ -0,0 +1,6 @@
1
+ require 'rubygems'
2
+
3
+ $:.unshift(File.join(File.dirname(__FILE__), %w[.. lib]))
4
+
5
+ require 'plastic'
6
+ require 'spec/expectations'
metadata ADDED
@@ -0,0 +1,94 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: plastic
3
+ version: !ruby/object:Gem::Version
4
+ hash: 21
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 2
9
+ - 1
10
+ version: 0.2.1
11
+ platform: ruby
12
+ authors:
13
+ - Randy Reddig
14
+ - Cameron Walters
15
+ - Chris Kampmeier
16
+ - Erica Kwan
17
+ - Matthew O'Connor
18
+ - Damon McCormick
19
+ - Brian Jenkins
20
+ autorequire:
21
+ bindir: bin
22
+ cert_chain: []
23
+
24
+ date: 2010-07-18 00:00:00 -07:00
25
+ default_executable:
26
+ dependencies: []
27
+
28
+ description: Handle credit, debit, bank and other cards.
29
+ email: github@squareup.com
30
+ executables: []
31
+
32
+ extensions: []
33
+
34
+ extra_rdoc_files:
35
+ - LICENSE.txt
36
+ - README.rdoc
37
+ files:
38
+ - .gitignore
39
+ - HISTORY.rdoc
40
+ - LICENSE.txt
41
+ - README.rdoc
42
+ - Rakefile
43
+ - lib/plastic.rb
44
+ - lib/plastic/core.rb
45
+ - lib/plastic/duck_type.rb
46
+ - lib/plastic/track.rb
47
+ - lib/plastic/validations.rb
48
+ - lib/plastic/version.rb
49
+ - spec/plastic/core_spec.rb
50
+ - spec/plastic/duck_type_spec.rb
51
+ - spec/plastic/track_spec.rb
52
+ - spec/plastic/validations_spec.rb
53
+ - spec/spec.opts
54
+ - spec/spec_helper.rb
55
+ has_rdoc: true
56
+ homepage: http://github.com/square/plastic
57
+ licenses: []
58
+
59
+ post_install_message:
60
+ rdoc_options:
61
+ - --charset=UTF-8
62
+ require_paths:
63
+ - lib
64
+ required_ruby_version: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ hash: 3
70
+ segments:
71
+ - 0
72
+ version: "0"
73
+ required_rubygems_version: !ruby/object:Gem::Requirement
74
+ none: false
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ hash: 3
79
+ segments:
80
+ - 0
81
+ version: "0"
82
+ requirements: []
83
+
84
+ rubyforge_project:
85
+ rubygems_version: 1.3.7
86
+ signing_key:
87
+ specification_version: 3
88
+ summary: Credit card library for Ruby.
89
+ test_files:
90
+ - spec/plastic/core_spec.rb
91
+ - spec/plastic/duck_type_spec.rb
92
+ - spec/plastic/track_spec.rb
93
+ - spec/plastic/validations_spec.rb
94
+ - spec/spec_helper.rb