pin_payment 0.0.5 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +3 -0
- data/Gemfile.lock +3 -1
- data/README.md +89 -18
- data/lib/pin_payment/base.rb +86 -5
- data/lib/pin_payment/card.rb +43 -0
- data/lib/pin_payment/charge.rb +37 -16
- data/lib/pin_payment/customer.rb +59 -25
- data/lib/pin_payment/refund.rb +17 -14
- data/lib/pin_payment/version.rb +2 -2
- data/lib/pin_payment.rb +1 -0
- data/pin_payment.gemspec +1 -0
- data/test/fixtures/responses.yml +10 -6
- data/test/test_helper.rb +41 -0
- data/test/test_pin_card.rb +31 -0
- data/test/test_pin_charge.rb +19 -3
- data/test/test_pin_customer.rb +37 -22
- data/test/test_pin_refund.rb +13 -14
- metadata +20 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0b482e0e3054497d152ef8302a1fa9af339834cf
|
4
|
+
data.tar.gz: 7394ef6e8624ac96d1db2b1e726141c0a918e4df
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 719872f68f5a3bdcec2f6be1368bb3a11f71281508b8b05f9a1fcc527b6c520f0a9d434e04a5b3f2bf0110dd43f5735e792c13760e67bb078a4dab931a3a774d
|
7
|
+
data.tar.gz: 66826324dcb94dc9bca096c36037c23d452a64068f6545dc414edbed98d98aa053e25103185fdba09db8187e781c7f449facc90111b65fbded2111fda85d00f1
|
data/.gitignore
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,13 +1,14 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
pin_payment (0.0
|
4
|
+
pin_payment (0.1.0)
|
5
5
|
|
6
6
|
GEM
|
7
7
|
remote: https://rubygems.org/
|
8
8
|
specs:
|
9
9
|
fakeweb (1.3.0)
|
10
10
|
rake (10.0.4)
|
11
|
+
yard (0.8.6.1)
|
11
12
|
|
12
13
|
PLATFORMS
|
13
14
|
ruby
|
@@ -16,3 +17,4 @@ DEPENDENCIES
|
|
16
17
|
fakeweb
|
17
18
|
pin_payment!
|
18
19
|
rake
|
20
|
+
yard
|
data/README.md
CHANGED
@@ -34,7 +34,7 @@ task is up to you), if you are only doing a single transaction, this step is
|
|
34
34
|
not required (but recommended).
|
35
35
|
|
36
36
|
```ruby
|
37
|
-
customer = PinPayment::Customer.create(
|
37
|
+
customer = PinPayment::Customer.create('foo@example.com', params[:card_token])
|
38
38
|
```
|
39
39
|
|
40
40
|
The important information from the returned object is `customer.token`. You
|
@@ -45,12 +45,12 @@ Now you can create charges.
|
|
45
45
|
|
46
46
|
```ruby
|
47
47
|
charge = PinPayment::Charge.create(
|
48
|
-
|
49
|
-
email:
|
50
|
-
amount:
|
51
|
-
currency:
|
52
|
-
description:
|
53
|
-
ip_address:
|
48
|
+
customer: customer, # or you can pass customer.token, either way
|
49
|
+
email: customer.email,
|
50
|
+
amount: 1000,
|
51
|
+
currency: 'USD', # only AUD and USD are supported by pin.net.au
|
52
|
+
description: 'Widgets',
|
53
|
+
ip_address: request.ip
|
54
54
|
)
|
55
55
|
|
56
56
|
if charge.success?
|
@@ -58,17 +58,81 @@ if charge.success?
|
|
58
58
|
end
|
59
59
|
```
|
60
60
|
|
61
|
+
If you don't want to create customer records and just want to create once of
|
62
|
+
charges with the credit card data, this is quite simple also. You can do this
|
63
|
+
in 2 steps if you like, or 1.
|
64
|
+
|
65
|
+
First create the card
|
66
|
+
|
67
|
+
```ruby
|
68
|
+
card = PinPayment::Card.create(
|
69
|
+
number: 5520000000000000,
|
70
|
+
expiry_month: 5,
|
71
|
+
expiry_year: 2014,
|
72
|
+
cvc: 123,
|
73
|
+
name: 'Roland Robot',
|
74
|
+
address_line1: '42 Sevenoaks St',
|
75
|
+
address_city: 'Lathlain',
|
76
|
+
address_postcode: 6454,
|
77
|
+
address_state: 'WA',
|
78
|
+
address_country: 'Australia'
|
79
|
+
)
|
80
|
+
```
|
81
|
+
|
82
|
+
Once you have created the card via the API, you will have a `PinPayment::Card`
|
83
|
+
object, and you can use the charge example code above and replace the customer
|
84
|
+
key in the has with `:card`. The same principle applies here, you can pass
|
85
|
+
`:card` into the charge creation as the card object, or as a simple string being
|
86
|
+
the card token.
|
87
|
+
|
88
|
+
Alternatively, you can skip the card creation step, and pass `:card` to the
|
89
|
+
charge creation as a hash itself, it will create the card for you as part of the
|
90
|
+
charge process. Example
|
91
|
+
|
92
|
+
```ruby
|
93
|
+
charge = PinPayment::Charge.create(
|
94
|
+
email: customer.email,
|
95
|
+
amount: 1000,
|
96
|
+
currency: 'USD', # only AUD and USD are supported by pin.net.au
|
97
|
+
description: 'Widgets',
|
98
|
+
ip_address: request.ip
|
99
|
+
card: {
|
100
|
+
number: 5520000000000000,
|
101
|
+
expiry_month: 5,
|
102
|
+
expiry_year: 2014,
|
103
|
+
cvc: 123,
|
104
|
+
name: 'Roland Robot',
|
105
|
+
address_line1: '42 Sevenoaks St',
|
106
|
+
address_city: 'Lathlain',
|
107
|
+
address_postcode: 6454,
|
108
|
+
address_state: 'WA',
|
109
|
+
address_country: 'Australia'
|
110
|
+
}
|
111
|
+
)
|
112
|
+
```
|
113
|
+
|
114
|
+
You can refund charges as well, either directly on the charge object with
|
115
|
+
`charge.refund!` or you can pass a charge object or token directory into
|
116
|
+
`PinPayment::Refund.create`
|
117
|
+
|
118
|
+
Both the Charge and Customer objects have an `all` method that you can use to
|
119
|
+
iterate over your customers and charges. They simply return an array of their
|
120
|
+
respective objects.
|
121
|
+
|
122
|
+
They also both of course have a `find` method which takes a single argument,
|
123
|
+
being the token. For this reason I highly suggest storing `charge.token` as your
|
124
|
+
payment reference whenever you are creating payments. As well as storing
|
125
|
+
`customer.token` against your customers in your own customer database.
|
126
|
+
|
61
127
|
## TODO
|
62
128
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
doing so, but as above, I've not yet had the need and have always had a
|
71
|
-
`card_token` handy to pass in.
|
129
|
+
* The `all` methods need to deal with pagination.
|
130
|
+
* Implement `PinPayment::Customer.charges`
|
131
|
+
* Implement `PinPayment::Charge.search`
|
132
|
+
* There is more info about a charge in a response than what the API says.
|
133
|
+
E.g. there is info about the fees, refund status, and trasfer status. Need
|
134
|
+
to find out what this data is all about. Since it's undocumented, I am
|
135
|
+
hesitent to implement anything that relies on it as yet.
|
72
136
|
|
73
137
|
## Testing
|
74
138
|
|
@@ -77,8 +141,15 @@ responses that I know the API gives. We're not really here to test the output of
|
|
77
141
|
the API, I think we can safely assume it will always give the same output for
|
78
142
|
the same input, and I don't really want to spam their service every time someone
|
79
143
|
runs the test suite. Nor do I want to hard code my test API keys or expect every
|
80
|
-
developer to create a pin account.
|
81
|
-
|
144
|
+
developer to create a pin account.
|
145
|
+
|
146
|
+
Having said that, you can simply jump into `test/test_helper.rb` and comment out
|
147
|
+
the line that sets up fakeweb, then you can uncomment the lines below that and
|
148
|
+
put your own pin.net.au test API keys into the code and run the tests. Note
|
149
|
+
however that this will create a large amount of customers and charges in your
|
150
|
+
test dashboard on pin.net.au.
|
151
|
+
|
152
|
+
Suggestions on improvement here are welcome though.
|
82
153
|
|
83
154
|
## Contributing
|
84
155
|
|
data/lib/pin_payment/base.rb
CHANGED
@@ -4,21 +4,25 @@ require 'pin_payment/error'
|
|
4
4
|
module PinPayment
|
5
5
|
class Base
|
6
6
|
|
7
|
+
def initialize token, options = {}
|
8
|
+
self.token = token
|
9
|
+
self.class.parse_card_data(options).each{|k,v| send("#{k}=", v) if self.class.attributes.include?(k.to_sym) }
|
10
|
+
end
|
11
|
+
|
7
12
|
protected
|
8
13
|
|
9
|
-
def self.post uri, options
|
14
|
+
def self.post uri, options = {}
|
10
15
|
fetch Net::HTTP::Post, uri, options
|
11
16
|
end
|
12
17
|
|
13
|
-
def self.put uri, options
|
18
|
+
def self.put uri, options = {}
|
14
19
|
fetch Net::HTTP::Put, uri, options
|
15
20
|
end
|
16
21
|
|
17
|
-
def self.get uri, options
|
22
|
+
def self.get uri, options = {}
|
18
23
|
fetch Net::HTTP::Get, uri, options
|
19
24
|
end
|
20
25
|
|
21
|
-
# TODO: Accept card as a hash that would create the card at the same time as the charge
|
22
26
|
def self.fetch klass, uri, options
|
23
27
|
client = Net::HTTP.new(uri.host, uri.port)
|
24
28
|
client.use_ssl = true
|
@@ -36,7 +40,84 @@ module PinPayment
|
|
36
40
|
raise Error::InvalidResponse.new(e.message)
|
37
41
|
end
|
38
42
|
raise(Error.create(response['error'], response['error_description'], response['messages'])) if response['error']
|
39
|
-
response['response']
|
43
|
+
response = response['response']
|
44
|
+
response.is_a?(Hash) ? parse_object_tokens(response) : response.map{|x| parse_object_tokens(x) }
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.parse_card_data hash
|
48
|
+
hash = hash.dup
|
49
|
+
card = hash.delete('card') if hash['card']
|
50
|
+
card = hash.delete(:card) if hash[:card]
|
51
|
+
token = hash.delete('card_token') if hash['card_token']
|
52
|
+
token = hash.delete(:card_token) if hash[:card_token]
|
53
|
+
if card.is_a?(Card) and token and !card.token
|
54
|
+
card.token = token
|
55
|
+
elsif card.is_a?(Hash)
|
56
|
+
card = Card.new(token || card[:token] || card['token'], card)
|
57
|
+
elsif card.is_a?(String)
|
58
|
+
card = Card.new(card)
|
59
|
+
elsif token
|
60
|
+
card = Card.new(token)
|
61
|
+
end
|
62
|
+
hash['card'] = card if card
|
63
|
+
hash
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.parse_customer_data hash
|
67
|
+
hash = hash.dup
|
68
|
+
customer = hash.delete('customer') if hash['customer']
|
69
|
+
customer = hash.delete(:customer) if hash[:customer]
|
70
|
+
token = hash.delete('customer_token') if hash['customer_token']
|
71
|
+
token = hash.delete(:customer_token) if hash[:customer_token]
|
72
|
+
if customer.is_a?(Customer) and token and !customer.token
|
73
|
+
customer.token = token
|
74
|
+
elsif customer.is_a?(String)
|
75
|
+
customer = Customer.new(customer)
|
76
|
+
elsif token
|
77
|
+
customer = Customer.new(token)
|
78
|
+
end
|
79
|
+
hash['customer'] = customer if customer
|
80
|
+
hash
|
81
|
+
end
|
82
|
+
|
83
|
+
def self.parse_charge_data hash
|
84
|
+
hash = hash.dup
|
85
|
+
charge = hash.delete('charge') if hash['charge']
|
86
|
+
charge = hash.delete(:charge) if hash[:charge]
|
87
|
+
token = hash.delete('charge_token') if hash['charge_token']
|
88
|
+
token = hash.delete(:charge_token) if hash[:charge_token]
|
89
|
+
if charge.is_a?(Charge) and token and !charge.token
|
90
|
+
charge.token = token
|
91
|
+
elsif charge.is_a?(String)
|
92
|
+
charge = Charge.new(charge)
|
93
|
+
elsif token
|
94
|
+
charge = Charge.new(token)
|
95
|
+
end
|
96
|
+
hash['charge'] = charge if charge
|
97
|
+
hash
|
98
|
+
end
|
99
|
+
|
100
|
+
def self.parse_object_tokens hash
|
101
|
+
parse_charge_data(parse_customer_data(parse_card_data(hash)))
|
102
|
+
end
|
103
|
+
|
104
|
+
def self.parse_options_for_request attributes, options
|
105
|
+
attributes = attributes.map(&:to_s)
|
106
|
+
options = parse_object_tokens(options.select{|k| attributes.include?(k.to_s) })
|
107
|
+
|
108
|
+
if card = options.delete('card')
|
109
|
+
if card.token
|
110
|
+
options['card_token'] = card.token
|
111
|
+
else
|
112
|
+
# Ruby's Net::HTTP#set_form_data doesn't deal with nested hashes :(
|
113
|
+
card.to_hash.each{|k,v| options["card[#{k}]"] = v }
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
options['customer_token'] = options.delete('customer').token if options['customer']
|
118
|
+
options['charge_token'] = options.delete('charge').token if options['charge']
|
119
|
+
|
120
|
+
options
|
40
121
|
end
|
41
122
|
|
42
123
|
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module PinPayment
|
2
|
+
class Card < Base
|
3
|
+
attr_accessor :token, :display_number, :scheme, :address_line1, :address_line2, :address_city, :address_postcode, :address_state, :address_country
|
4
|
+
protected :token=, :display_number=, :scheme=, :address_line1=, :address_line2=, :address_city=, :address_postcode=, :address_state=, :address_country=
|
5
|
+
|
6
|
+
attr_accessor :number, :expiry_month, :expiry_year, :cvc, :name
|
7
|
+
protected :number, :expiry_month, :expiry_year, :cvc, :name
|
8
|
+
protected :number=, :expiry_month=, :expiry_year=, :cvc=, :name=
|
9
|
+
|
10
|
+
# Use the pin API to create a credit card token, usable for 1 month from creation.
|
11
|
+
#
|
12
|
+
# @param [Hash] card_data
|
13
|
+
# @option card_data [#to_s] :name
|
14
|
+
# @option card_data [#to_s] :number
|
15
|
+
# @option card_data [#to_s] :cvc
|
16
|
+
# @option card_data [#to_s] :address_line1
|
17
|
+
# @option card_data [#to_s] :address_line2 (optional)
|
18
|
+
# @option card_data [#to_s] :address_city
|
19
|
+
# @option card_data [#to_s] :address_postcode
|
20
|
+
# @option card_data [#to_s] :address_country
|
21
|
+
# @option card_data [#to_s] :expiry_month
|
22
|
+
# @option card_data [#to_s] :expiry_year (4 digits required)
|
23
|
+
# @return [PinPayment::Card]
|
24
|
+
def self.create card_data
|
25
|
+
attributes = self.attributes - [:token, :display_number, :scheme] # fix attributes allowed by POST API
|
26
|
+
options = parse_options_for_request(attributes, card_data)
|
27
|
+
response = post(URI.parse(PinPayment.api_url).tap{|uri| uri.path = '/1/cards' }, options)
|
28
|
+
new(response.delete('token'), response)
|
29
|
+
end
|
30
|
+
|
31
|
+
# @return [Hash]
|
32
|
+
def to_hash
|
33
|
+
{}.tap{|h| self.class.attributes.each{|k| v = send(k) ; h[k] = v if v }}
|
34
|
+
end
|
35
|
+
|
36
|
+
protected
|
37
|
+
|
38
|
+
def self.attributes
|
39
|
+
[:token, :display_number, :scheme, :address_line1, :address_line2, :address_city, :address_postcode, :address_state, :address_country, :number, :expiry_month, :expiry_year, :cvc, :name]
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
data/lib/pin_payment/charge.rb
CHANGED
@@ -1,30 +1,51 @@
|
|
1
1
|
module PinPayment
|
2
2
|
class Charge < Base
|
3
|
-
|
3
|
+
attr_accessor :token, :amount, :currency, :description, :email, :ip_address, :created_at, :card, :customer, :success
|
4
|
+
protected :token=, :amount=, :currency=, :description=, :email=, :ip_address=, :created_at=, :card=, :customer=, :success=
|
4
5
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
6
|
+
# Uses the pin API to create a charge.
|
7
|
+
#
|
8
|
+
# @param [Hash] charge_data
|
9
|
+
# @option charge_data [String] :amount *required*
|
10
|
+
# @option charge_data [String] :currency *required* only AUD and USD supported
|
11
|
+
# @option charge_data [String] :email *required*
|
12
|
+
# @option charge_data [String] :description *required*
|
13
|
+
# @option charge_data [String] :ip_address *required*
|
14
|
+
# @option charge_data [String, PinPayment::Customer] :customer can be a customer token or a customer object
|
15
|
+
# @option charge_data [String, PinPayment::Card, Hash] :card can be a card token, hash or a card object
|
16
|
+
# @return [PinPayment::Charge]
|
17
|
+
def self.create charge_data
|
18
|
+
attributes = self.attributes - [:token, :success, :created_at] # fix attributes allowed by POST API
|
19
|
+
options = parse_options_for_request(attributes, charge_data)
|
20
|
+
response = post(URI.parse(PinPayment.api_url).tap{|uri| uri.path = '/1/charges' }, options)
|
21
|
+
new(response.delete('token'), response)
|
16
22
|
end
|
17
23
|
|
24
|
+
# Fetches all of your charges using the pin API.
|
25
|
+
#
|
26
|
+
# @return [Array<PinPayment::Charge>]
|
27
|
+
# TODO: pagination
|
28
|
+
def self.all
|
29
|
+
response = get(URI.parse(PinPayment.api_url).tap{|uri| uri.path = '/1/charges' })
|
30
|
+
response.map{|x| new(x.delete('token'), x) }
|
31
|
+
end
|
32
|
+
|
33
|
+
# Refund a charge via the pin API.
|
34
|
+
#
|
35
|
+
# @return [PinPayment::Refund]
|
18
36
|
def refund!
|
19
|
-
Refund.create
|
37
|
+
Refund.create self
|
20
38
|
end
|
21
39
|
|
40
|
+
# @return [Boolean]
|
22
41
|
def success?
|
23
|
-
|
42
|
+
success == true
|
24
43
|
end
|
25
44
|
|
26
|
-
|
27
|
-
|
45
|
+
protected
|
46
|
+
|
47
|
+
def self.attributes
|
48
|
+
[:token, :amount, :currency, :description, :email, :ip_address, :created_at, :card, :customer, :success]
|
28
49
|
end
|
29
50
|
end
|
30
51
|
end
|
data/lib/pin_payment/customer.rb
CHANGED
@@ -1,32 +1,66 @@
|
|
1
1
|
module PinPayment
|
2
2
|
class Customer < Base
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
3
|
+
attr_accessor :token, :email, :created_at, :card
|
4
|
+
protected :token=, :email=, :created_at=, :card=
|
5
|
+
|
6
|
+
# Uses the pin API to create a customer.
|
7
|
+
#
|
8
|
+
# @param [String] email the customer's email address
|
9
|
+
# @param [String, PinPayment::Card, Hash] card_or_token the customer's credit card details
|
10
|
+
# @return [PinPayment::Customer]
|
11
|
+
def self.create email, card_or_token = nil
|
12
|
+
attributes = self.attributes - [:token, :created_at]
|
13
|
+
options = parse_options_for_request(attributes, email: email, card: card_or_token)
|
14
|
+
response = post(URI.parse(PinPayment.api_url).tap{|uri| uri.path = '/1/customers' }, options)
|
15
|
+
new(response.delete('token'), response)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Update a customer using the pin API.
|
19
|
+
#
|
20
|
+
# @param [String] token the customer token
|
21
|
+
# @param [String] email the customer's new email address
|
22
|
+
# @param [String, PinPayment::Card, Hash] card_or_token the customer's new credit card details
|
23
|
+
# @return [PinPayment::Customer]
|
24
|
+
def self.update token, email, card_or_token = nil
|
25
|
+
new(token).tap{|c| c.update(email, card_or_token) }
|
26
|
+
end
|
27
|
+
|
28
|
+
# Fetches a customer using the pin API.
|
29
|
+
#
|
30
|
+
# @param [String] token the customer token
|
31
|
+
# @return [PinPayment::Customer]
|
32
|
+
def self.find token
|
33
|
+
response = get(URI.parse(PinPayment.api_url).tap{|uri| uri.path = "/1/customers/#{token}" })
|
34
|
+
new(response.delete('token'), response)
|
16
35
|
end
|
17
36
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
)
|
24
|
-
new.
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
37
|
+
# Fetches all of your customers using the pin API.
|
38
|
+
#
|
39
|
+
# @return [Array<PinPayment::Customer>]
|
40
|
+
# TODO: pagination
|
41
|
+
def self.all
|
42
|
+
response = get(URI.parse(PinPayment.api_url).tap{|uri| uri.path = '/1/customers' })
|
43
|
+
response.map{|x| new(x.delete('token'), x) }
|
44
|
+
end
|
45
|
+
|
46
|
+
# Update a customer using the pin API.
|
47
|
+
#
|
48
|
+
# @param [String] email the customer's new email address
|
49
|
+
# @param [String, PinPayment::Card, Hash] card_or_token the customer's new credit card details
|
50
|
+
# @return [PinPayment::Customer]
|
51
|
+
def update email, card_or_token = nil
|
52
|
+
attributes = self.class.attributes - [:token, :created_at]
|
53
|
+
options = self.class.parse_options_for_request(attributes, email: email, card: card_or_token)
|
54
|
+
response = self.class.put(URI.parse(PinPayment.api_url).tap{|uri| uri.path = "/1/customers/#{token}" }, options)
|
55
|
+
self.email = response['email']
|
56
|
+
self.card = response['card']
|
57
|
+
self
|
58
|
+
end
|
59
|
+
|
60
|
+
protected
|
61
|
+
|
62
|
+
def self.attributes
|
63
|
+
[:token, :email, :created_at, :card]
|
30
64
|
end
|
31
65
|
|
32
66
|
end
|
data/lib/pin_payment/refund.rb
CHANGED
@@ -1,19 +1,19 @@
|
|
1
1
|
module PinPayment
|
2
2
|
class Refund < Base
|
3
|
-
|
3
|
+
attr_accessor :token, :amount, :currency, :charge, :created_at, :status_message
|
4
|
+
protected :token=, :amount=, :currency=, :charge=, :created_at=, :status_message=, :status_message
|
4
5
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
end
|
14
|
-
end
|
6
|
+
# Uses the pin API to create a refund.
|
7
|
+
#
|
8
|
+
# @param [String, PinPayment::Charge] charge_or_token the charge (or token of the charge) to refund
|
9
|
+
# @return [PinPayment::Refund]
|
10
|
+
def self.create charge_or_token
|
11
|
+
token = charge_or_token.is_a?(Charge) ? charge_or_token.token : charge_or_token
|
12
|
+
response = post(URI.parse(PinPayment.api_url).tap{|uri| uri.path = "/1/charges/#{token}/refunds" })
|
13
|
+
new(response.delete('token'), response)
|
15
14
|
end
|
16
15
|
|
16
|
+
# @return [Boolean]
|
17
17
|
# TODO: API documentation only shows success as being "null" in the JSON
|
18
18
|
# response, so not sure this is possible. All my refunds on the test site
|
19
19
|
# end up in a "Pending" state so not entirely sure on this one.
|
@@ -21,12 +21,15 @@ module PinPayment
|
|
21
21
|
@success == true
|
22
22
|
end
|
23
23
|
|
24
|
+
# @return [String]
|
24
25
|
def status
|
25
|
-
|
26
|
+
status_message
|
26
27
|
end
|
27
28
|
|
28
|
-
|
29
|
-
|
29
|
+
protected
|
30
|
+
|
31
|
+
def self.attributes
|
32
|
+
[:token, :amount, :currency, :charge, :created_at, :status_message]
|
30
33
|
end
|
31
34
|
end
|
32
35
|
end
|
data/lib/pin_payment/version.rb
CHANGED
data/lib/pin_payment.rb
CHANGED
data/pin_payment.gemspec
CHANGED
data/test/fixtures/responses.yml
CHANGED
@@ -3,13 +3,17 @@ charge:
|
|
3
3
|
token_already_used: '{"error":"token_already_used","error_description":"Token already used. Card tokens can only be used once, to create a charge or assign a card to a customer."}'
|
4
4
|
invalid_amount: '{"error":"invalid_resource","error_description":"One or more parameters were missing or invalid.","messages":[{"param":"amount","code":"amount_invalid","message":"Amount must be an integer"},{"param":"amount","code":"amount_invalid","message":"Amount must be more than 1 USD"}]}'
|
5
5
|
success: '{"response":{"token":"ch_BjGW-S6WUisI6mOgpDRimg","success":true,"amount":1000,"currency":"USD","description":"Widgets","email":"foo@example.com","ip_address":"127.0.0.1","created_at":"2013-06-12T23:55:38Z","status_message":"Success!","error_message":null,"card":{"token":"card_qMwnMfpG-olOhfJeyxmrcg","display_number":"XXXX-XXXX-XXXX-0000","scheme":"master","address_line1":"123 Main St","address_line2":"","address_city":"Melbourne","address_postcode":"3000","address_state":"Victoria","address_country":"Australia"},"transfer":[],"amount_refunded":0,"total_fees":null,"merchant_entitlement":null,"refund_pending":false}}'
|
6
|
-
|
6
|
+
create_with_card: '{"response":{"token":"ch_lfUYEBK14zotCTykezJkfg","success":true,"amount":400,"currency":"AUD","description":"test charge","email":"roland@pin.net.au","ip_address":"203.192.1.172","created_at":"2013-06-27T00:39:07Z","status_message":"Success","error_message":null,"card":{"token":"card_jxk5A8fjTtmz6Y81NX9G7w","display_number":"XXXX-XXXX-XXXX-0000","scheme":"master","address_line1":"42 Sevenoaks St","address_line2":null,"address_city":"Lathlain","address_postcode":"6454","address_state":"WA","address_country":"Australia"},"transfer":null}}'
|
7
|
+
all: '{"response":[{"token":"ch_lfUYEBK14zotCTykezJkfg","success":true,"amount":400,"currency":"AUD","description":"test charge","email":"roland@pin.net.au","ip_address":"203.192.1.172","created_at":"2013-06-27T00:39:07Z","status_message":"Success","error_message":null,"card":{"token":"card_nytGw7koRg23EEp9NTmz9w","display_number":"XXXX-XXXX-XXXX-0000","scheme":"master","address_line1":"42 Sevenoaks St","address_line2":null,"address_city":"Lathlain","address_postcode":"6454","address_state":"WA","address_country":"Australia"},"transfer":null},{"token":"ch_shldvbE5eqBQuyY9Fryhzw","success":true,"amount":400,"currency":"AUD","description":"test charge","email":"roland@pin.net.au","ip_address":"203.192.1.172","created_at":"2013-06-27T00:38:41Z","status_message":"Success","error_message":null,"card":{"token":"card_nytGw7koRg23EEp9NTmz9w","display_number":"XXXX-XXXX-XXXX-0000","scheme":"master","address_line1":"42 Sevenoaks St","address_line2":null,"address_city":"Lathlain","address_postcode":"6454","address_state":"WA","address_country":"Australia"},"transfer":null}],"count":2,"pagination":{"current":1,"previous":null,"next":1,"per_page":25,"pages":1,"count":2}}'
|
8
|
+
customer:
|
7
9
|
blank_email: '{"error":"invalid_resource","error_description":"One or more parameters were missing or invalid.","messages":[{"param":"email","code":"email_invalid","message":"Email can''t be blank"}]}'
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
success: '{"response":{"token":"cus__03Cn1lSk3offZ0IGkwpCg","email":"foo@example.com","created_at":"2013-06-12T10:08:30Z","card":{"token":"card_qMwnMfpG-olOhfJeyxmrcg","display_number":"XXXX-XXXX-XXXX-0000","scheme":"master","address_line1":"123 Main St","address_line2":"","address_city":"Melbourne","address_postcode":"3000","address_state":"Victoria","address_country":"Australia"}}}'
|
10
|
+
updated: '{"response":{"token":"cus__03Cn1lSk3offZ0IGkwpCg","email":"changed@example.com","created_at":"2013-06-12T10:08:30Z","card":{"token":"card_qMwnMfpG-olOhfJeyxmrcg","display_number":"XXXX-XXXX-XXXX-0000","scheme":"master","address_line1":"123 Main St","address_line2":"","address_city":"Melbourne","address_postcode":"3000","address_state":"Victoria","address_country":"Australia"}}}'
|
11
|
+
created: '{"response":{"token":"cus__03Cn1lSk3offZ0IGkwpCg","email":"foo@example.com","created_at":"2013-06-12T10:08:30Z","card":{"token":"card_qMwnMfpG-olOhfJeyxmrcg","display_number":"XXXX-XXXX-XXXX-0000","scheme":"master","address_line1":"123 Main St","address_line2":"","address_city":"Melbourne","address_postcode":"3000","address_state":"Victoria","address_country":"Australia"}}}'
|
12
|
+
all: '{"response":[{"token":"cus_OEHdj9KE_OiC9eig0h3IEA","email":"user1@example.com","created_at":"2013-06-25T04:14:35Z","card":{"token":"card_7rviJ2IZM__WC9Dnj0RHJA","display_number":"XXXX-XXXX-XXXX-0000","scheme":"visa","address_line1":"123 Main St","address_line2":"","address_city":"Melbourne","address_postcode":"3000","address_state":"Victoria","address_country":"Australia"}},{"token":"cus_cO2VtF4YzK6i1QBcLsdzOA","email":"user2@example.com","created_at":"2013-06-24T05:53:42Z","card":{"token":"card_bVou2J4hzbDIwe-sQ3ZqdQ","display_number":"XXXX-XXXX-XXXX-0000","scheme":"visa","address_line1":"123 Main St","address_line2":"","address_city":"Melbourne","address_postcode":"3000","address_state":"Victoria","address_country":"Australia"}}],"count":2,"pagination":{"current":1,"previous":null,"next":null,"per_page":25,"pages":1,"count":2}}'
|
13
|
+
create_with_card: '{"response":{"token":"cus_XZg1ULpWaROQCOT5PdwLkQ","email":"roland@pin.net.au","created_at":"2013-06-27T00:24:43Z","card":{"token":"card_nytGw7koRg23EEp9NTmz9w","display_number":"XXXX-XXXX-XXXX-0000","scheme":"master","address_line1":"42 Sevenoaks St","address_line2":null,"address_city":"Lathlain","address_postcode":"6454","address_state":"WA","address_country":"Australia"}}}'
|
13
14
|
refund:
|
14
15
|
success: '{"response":{"token":"rf_wMYx5YHKaZAwQgj5rtNuTg","success":null,"amount":1000,"currency":"USD","charge":"ch_BjGW-S6WUisI6mOgpDRimg","created_at":"2013-06-25T03:16:33Z","error_message":null,"status_message":"Pending"}}'
|
15
16
|
duplicate: '{"error":"invalid_resource","error_description":"One or more parameters were missing or invalid.","messages":{"charge":["You have tried to refund more than the original charge"]}}'
|
17
|
+
card:
|
18
|
+
success: '{"response":{"token":"card_nytGw7koRg23EEp9NTmz9w","display_number":"XXXX-XXXX-XXXX-0000","scheme":"master","address_line1":"42 Sevenoaks St","address_line2":null,"address_city":"Lathlain","address_postcode":"6454","address_state":"WA","address_country":"Australia"}}'
|
19
|
+
invalid: '{"error":"invalid_resource","error_description":"One or more parameters were missing or invalid.","messages":[{"param":"number","code":"number_invalid","message":"Number is not a valid credit card number"}]}'
|
data/test/test_helper.rb
CHANGED
@@ -9,3 +9,44 @@ def fixtures
|
|
9
9
|
f['responses'] = YAML.load(File.read File.join(File.dirname(__FILE__), 'fixtures', 'responses.yml'))
|
10
10
|
end
|
11
11
|
end
|
12
|
+
|
13
|
+
def common_setup
|
14
|
+
FakeWeb.allow_net_connect = false
|
15
|
+
|
16
|
+
# If you want to test for realsies, change the above to true, and uncomment
|
17
|
+
# below, after putting in your own test keys
|
18
|
+
|
19
|
+
#PinPayment.secret_key = 'super secret'
|
20
|
+
#PinPayment.public_key = 'not so secret'
|
21
|
+
end
|
22
|
+
|
23
|
+
def card_hash
|
24
|
+
{
|
25
|
+
number: 5520000000000000,
|
26
|
+
expiry_month: 5,
|
27
|
+
expiry_year: 2014,
|
28
|
+
cvc: 123,
|
29
|
+
name: 'Roland Robot',
|
30
|
+
address_line1: '42 Sevenoaks St',
|
31
|
+
address_city: 'Lathlain',
|
32
|
+
address_postcode: 6454,
|
33
|
+
address_state: 'WA',
|
34
|
+
address_country: 'Australia'
|
35
|
+
}
|
36
|
+
end
|
37
|
+
|
38
|
+
def charge_hash
|
39
|
+
{
|
40
|
+
amount: 400,
|
41
|
+
currency: 'AUD',
|
42
|
+
description: 'test charge',
|
43
|
+
email: 'roland@pin.net.au',
|
44
|
+
ip_address: '203.192.1.172',
|
45
|
+
card: card_hash
|
46
|
+
}
|
47
|
+
end
|
48
|
+
|
49
|
+
def created_customer
|
50
|
+
FakeWeb.register_uri(:post, 'https://test-api.pin.net.au/1/customers', body: fixtures['responses']['customer']['created'])
|
51
|
+
customer = PinPayment::Customer.create('foo@example.com', card_hash)
|
52
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class TestPinCard < MiniTest::Unit::TestCase
|
4
|
+
def setup
|
5
|
+
common_setup
|
6
|
+
end
|
7
|
+
|
8
|
+
def test_successful_card
|
9
|
+
FakeWeb.register_uri(:post, 'https://test-api.pin.net.au/1/cards', body: fixtures['responses']['card']['success'])
|
10
|
+
card = PinPayment::Card.create(card_hash)
|
11
|
+
assert_equal 'XXXX-XXXX-XXXX-0000', card.display_number
|
12
|
+
assert_equal 'master', card.scheme
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_invalid_card
|
16
|
+
FakeWeb.register_uri(:post, 'https://test-api.pin.net.au/1/cards', body: fixtures['responses']['card']['invalid'])
|
17
|
+
assert_raises PinPayment::Error::InvalidResource do
|
18
|
+
PinPayment::Card.create(card_hash.merge(number: 5520000000000099))
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_new_from_hash
|
23
|
+
card = PinPayment::Card.new(nil, card_hash)
|
24
|
+
assert_kind_of PinPayment::Card, card
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_to_hash
|
28
|
+
card = PinPayment::Card.new(nil, card_hash)
|
29
|
+
assert_kind_of Hash, card.to_hash
|
30
|
+
end
|
31
|
+
end
|
data/test/test_pin_charge.rb
CHANGED
@@ -2,19 +2,35 @@ require 'test_helper'
|
|
2
2
|
|
3
3
|
class TestPinCharge < MiniTest::Unit::TestCase
|
4
4
|
def setup
|
5
|
-
|
5
|
+
common_setup
|
6
6
|
end
|
7
7
|
|
8
8
|
def test_invalid_amount
|
9
9
|
FakeWeb.register_uri(:post, 'https://test-api.pin.net.au/1/charges', body: fixtures['responses']['charge']['invalid_amount'])
|
10
10
|
assert_raises PinPayment::Error::InvalidResource do
|
11
|
-
PinPayment::Charge.create(
|
11
|
+
PinPayment::Charge.create(charge_hash.merge(amount: 10.1))
|
12
12
|
end
|
13
13
|
end
|
14
14
|
|
15
15
|
def test_successful_charge
|
16
16
|
FakeWeb.register_uri(:post, 'https://test-api.pin.net.au/1/charges', body: fixtures['responses']['charge']['success'])
|
17
|
-
charge = PinPayment::Charge.create(
|
17
|
+
charge = PinPayment::Charge.create(charge_hash)
|
18
18
|
assert_equal true, charge.success?
|
19
19
|
end
|
20
|
+
|
21
|
+
def test_create_charge_with_card_hash
|
22
|
+
FakeWeb.register_uri(:post, 'https://test-api.pin.net.au/1/charges', body: fixtures['responses']['charge']['create_with_card'])
|
23
|
+
charge = PinPayment::Charge.create(charge_hash)
|
24
|
+
assert_equal true, charge.success?
|
25
|
+
assert_kind_of PinPayment::Card, charge.card
|
26
|
+
assert_kind_of String, charge.card.token
|
27
|
+
assert charge.card.token.length > 0
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_fetch_all_charges
|
31
|
+
FakeWeb.register_uri(:get, 'https://test-api.pin.net.au/1/charges', body: fixtures['responses']['charge']['all'])
|
32
|
+
charges = PinPayment::Charge.all
|
33
|
+
assert_kind_of Array, charges
|
34
|
+
assert_kind_of PinPayment::Charge, charges.first
|
35
|
+
end
|
20
36
|
end
|
data/test/test_pin_customer.rb
CHANGED
@@ -2,39 +2,54 @@ require 'test_helper'
|
|
2
2
|
|
3
3
|
class TestPinCustomer < MiniTest::Unit::TestCase
|
4
4
|
def setup
|
5
|
-
|
5
|
+
common_setup
|
6
6
|
end
|
7
7
|
|
8
|
-
def
|
9
|
-
FakeWeb.register_uri(:
|
8
|
+
def test_create_with_blank_email
|
9
|
+
FakeWeb.register_uri(:post, 'https://test-api.pin.net.au/1/customers', body: fixtures['responses']['customer']['blank_email'])
|
10
10
|
assert_raises PinPayment::Error::InvalidResource do
|
11
|
-
PinPayment::Customer.
|
11
|
+
PinPayment::Customer.create(email: nil, card: card_hash)
|
12
12
|
end
|
13
13
|
end
|
14
14
|
|
15
|
-
def
|
16
|
-
|
17
|
-
|
18
|
-
PinPayment::Customer.update(email: 'foo@example.com')
|
19
|
-
end
|
15
|
+
def test_create_success
|
16
|
+
customer = created_customer
|
17
|
+
assert_kind_of PinPayment::Customer, customer
|
20
18
|
end
|
21
19
|
|
22
|
-
def
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
20
|
+
def test_direct_update
|
21
|
+
customer = created_customer
|
22
|
+
FakeWeb.register_uri(:put, "https://test-api.pin.net.au/1/customers/#{customer.token}", body: fixtures['responses']['customer']['updated'])
|
23
|
+
customer = PinPayment::Customer.update(customer.token, 'changed@example.com')
|
24
|
+
assert_equal 'changed@example.com', customer.email
|
27
25
|
end
|
28
26
|
|
29
|
-
def
|
30
|
-
|
31
|
-
|
32
|
-
|
27
|
+
def test_object_update
|
28
|
+
customer = created_customer
|
29
|
+
FakeWeb.register_uri(:put, "https://test-api.pin.net.au/1/customers/#{customer.token}", body: fixtures['responses']['customer']['updated'])
|
30
|
+
customer.update('changed@example.com')
|
31
|
+
assert_equal 'changed@example.com', customer.email
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_find_customer
|
35
|
+
customer = created_customer
|
36
|
+
FakeWeb.register_uri(:get, "https://test-api.pin.net.au/1/customers/#{customer.token}", body: fixtures['responses']['customer']['created'])
|
37
|
+
customer = PinPayment::Customer.find(customer.token)
|
38
|
+
assert_kind_of PinPayment::Customer, customer
|
39
|
+
end
|
40
|
+
|
41
|
+
def test_fetch_all_customers
|
42
|
+
FakeWeb.register_uri(:get, 'https://test-api.pin.net.au/1/customers', body: fixtures['responses']['customer']['all'])
|
43
|
+
customers = PinPayment::Customer.all
|
44
|
+
assert_kind_of Array, customers
|
45
|
+
assert_kind_of PinPayment::Customer, customers.first
|
33
46
|
end
|
34
47
|
|
35
|
-
def
|
36
|
-
FakeWeb.register_uri(:
|
37
|
-
customer = PinPayment::Customer.
|
38
|
-
|
48
|
+
def test_create_customer_with_card_hash
|
49
|
+
FakeWeb.register_uri(:post, 'https://test-api.pin.net.au/1/customers', body: fixtures['responses']['customer']['create_with_card'])
|
50
|
+
customer = PinPayment::Customer.create('roland@pin.net.au', card_hash)
|
51
|
+
assert_kind_of PinPayment::Card, customer.card
|
52
|
+
assert_kind_of String, customer.card.token
|
53
|
+
assert customer.card.token.length > 0
|
39
54
|
end
|
40
55
|
end
|
data/test/test_pin_refund.rb
CHANGED
@@ -2,30 +2,29 @@ require 'test_helper'
|
|
2
2
|
|
3
3
|
class TestPinRefund < MiniTest::Unit::TestCase
|
4
4
|
def setup
|
5
|
-
|
5
|
+
common_setup
|
6
|
+
FakeWeb.register_uri(:post, 'https://test-api.pin.net.au/1/charges', body: fixtures['responses']['charge']['success'])
|
7
|
+
@charge = PinPayment::Charge.create(charge_hash)
|
6
8
|
end
|
7
9
|
|
8
10
|
def test_duplicate_refund
|
9
|
-
token
|
10
|
-
|
11
|
+
FakeWeb.register_uri(:post, "https://test-api.pin.net.au/1/charges/#{@charge.token}/refunds", body: fixtures['responses']['refund']['success'])
|
12
|
+
PinPayment::Refund.create(@charge)
|
13
|
+
FakeWeb.register_uri(:post, "https://test-api.pin.net.au/1/charges/#{@charge.token}/refunds", body: fixtures['responses']['refund']['duplicate'])
|
11
14
|
assert_raises PinPayment::Error::InvalidResource do
|
12
|
-
PinPayment::Refund.create(
|
15
|
+
PinPayment::Refund.create(@charge)
|
13
16
|
end
|
14
17
|
end
|
15
18
|
|
16
|
-
def
|
17
|
-
token
|
18
|
-
|
19
|
-
refund = PinPayment::Refund.create(charge_token: token)
|
19
|
+
def test_direct_refund
|
20
|
+
FakeWeb.register_uri(:post, "https://test-api.pin.net.au/1/charges/#{@charge.token}/refunds", body: fixtures['responses']['refund']['success'])
|
21
|
+
refund = PinPayment::Refund.create(@charge.token)
|
20
22
|
assert_equal 'Pending', refund.status
|
21
23
|
end
|
22
24
|
|
23
|
-
def
|
24
|
-
FakeWeb.register_uri(:post,
|
25
|
-
|
26
|
-
|
27
|
-
FakeWeb.register_uri(:post, "https://test-api.pin.net.au/1/charges/#{charge.token}/refunds", body: fixtures['responses']['refund']['success'])
|
28
|
-
refund = charge.refund!
|
25
|
+
def test_object_refund
|
26
|
+
FakeWeb.register_uri(:post, "https://test-api.pin.net.au/1/charges/#{@charge.token}/refunds", body: fixtures['responses']['refund']['success'])
|
27
|
+
refund = @charge.refund!
|
29
28
|
assert_equal 'Pending', refund.status
|
30
29
|
end
|
31
30
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pin_payment
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Danial Pearce
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2013-06-
|
11
|
+
date: 2013-06-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: fakeweb
|
@@ -38,6 +38,20 @@ dependencies:
|
|
38
38
|
- - '>='
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: yard
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
41
55
|
description: Pin is the easiest way to accept payments online. See https://pin.net.au/
|
42
56
|
for details.
|
43
57
|
email:
|
@@ -54,6 +68,7 @@ files:
|
|
54
68
|
- Rakefile
|
55
69
|
- lib/pin_payment.rb
|
56
70
|
- lib/pin_payment/base.rb
|
71
|
+
- lib/pin_payment/card.rb
|
57
72
|
- lib/pin_payment/charge.rb
|
58
73
|
- lib/pin_payment/customer.rb
|
59
74
|
- lib/pin_payment/error.rb
|
@@ -62,6 +77,7 @@ files:
|
|
62
77
|
- pin_payment.gemspec
|
63
78
|
- test/fixtures/responses.yml
|
64
79
|
- test/test_helper.rb
|
80
|
+
- test/test_pin_card.rb
|
65
81
|
- test/test_pin_charge.rb
|
66
82
|
- test/test_pin_customer.rb
|
67
83
|
- test/test_pin_refund.rb
|
@@ -93,7 +109,9 @@ summary: Ruby bindings for the Pin API
|
|
93
109
|
test_files:
|
94
110
|
- test/fixtures/responses.yml
|
95
111
|
- test/test_helper.rb
|
112
|
+
- test/test_pin_card.rb
|
96
113
|
- test/test_pin_charge.rb
|
97
114
|
- test/test_pin_customer.rb
|
98
115
|
- test/test_pin_refund.rb
|
99
116
|
- test/test_pin_setup.rb
|
117
|
+
has_rdoc:
|