tappay_ruby 1.1.0 → 2.0.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9cbe85ab3afecf239323a807b1a2d55311d1bcb3ad471a85681170eb52770a06
4
- data.tar.gz: f9d027824f0691aeb7487febc2155d7e31d5a487d2fe3562b79bdb009ca401c9
3
+ metadata.gz: '079c12112d7324813f5fed82b293db20a238b0232a9b08e038189b76f523b745'
4
+ data.tar.gz: 3a980d58f677236213f6df7cecb3ef431ee2c352725b410e7279ef6e38796014
5
5
  SHA512:
6
- metadata.gz: d98f0e74412ca52ecc67c9f9cf6a24a574f5d4f409aa0c4dda9f9e6ab073d3c775aeafda4cf49140e702e0e6c1e43add2843cda020a5190b40a504e6a5ec82ae
7
- data.tar.gz: 3b13ec89b5d01fa9b92fe0c5586c89d17462aedeced858178333200c0417070cfe7285f9011da8b3dd2f3b366ac67c3fd45c6a5cb69494dc64325ed4e0aacdea
6
+ metadata.gz: 29470a1418a5bd920ae3c198b8581e1b6dfda7982bcfd7587f6a68c637a41762089d9336e2de15664b3f81618d9dd0bf2f632729df4cf65d140623c1e744338a
7
+ data.tar.gz: 8775276b65bdb9203c1c7621ed5da4147029387849fa0f758060b3339abd4868a1ffcb31110d411c166233ca955e1173ca4eda5c34c310c42660a6a8bff5778f
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025-2026 Zac
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md CHANGED
@@ -46,6 +46,49 @@ Or install it yourself as:
46
46
  $ gem install tappay_ruby
47
47
  ```
48
48
 
49
+ ## Upgrading from 1.x to 2.0
50
+
51
+ Four changes need action. Each replaces a silent wrong answer with a loud one.
52
+
53
+ **Transaction query timestamps are milliseconds.** 1.x documented seconds.
54
+ TapPay accepts a seconds-magnitude range but matches nothing against it, so
55
+ `Transaction::Query` returned an empty list for every query. Multiply by 1000:
56
+
57
+ ```ruby
58
+ # before - always returned []
59
+ time: { start_time: 30.days.ago.to_i, end_time: Time.now.to_i }
60
+ # after
61
+ time: { start_time: 30.days.ago.to_i * 1000, end_time: Time.now.to_i * 1000 }
62
+ ```
63
+
64
+ Out-of-scale values now raise `Tappay::ValidationError` rather than returning
65
+ nothing.
66
+
67
+ **`Response#success?` reflects TapPay's `status`, not the HTTP code.** TapPay
68
+ answers a declined card with HTTP 200 and a non-zero `status`, so 1.x reported
69
+ failed payments as successful. If you were already checking
70
+ `result['status'] == 0` yourself, nothing changes; you can now use `success?`
71
+ instead. See [Checking the result](#checking-the-result) for what it does and
72
+ does not promise.
73
+
74
+ **Trade records carry every field TapPay returns.** 1.x kept 12 of roughly 32.
75
+ `transaction_time` and `tsp` are gone - they matched nothing in the response
76
+ and were always `nil`. The transaction timestamp is `time`, in milliseconds.
77
+ Fields such as `refunded_amount`, `is_captured`, `original_amount`,
78
+ `bank_result_code` and `bank_result_msg` are now available.
79
+
80
+ **Some public constants are gone.** Grep before upgrading:
81
+
82
+ ```bash
83
+ rg 'Tappay::(PaymentError|RefundError|QueryError)|api_version|Endpoints::Bind|trade_history_url|cap_url'
84
+ ```
85
+
86
+ `Tappay::PaymentError`, `Tappay::RefundError` and `Tappay::QueryError` were
87
+ never raised by the gem, so they never caught anything - use `success?`.
88
+ Removing them matters because `rescue Tappay::PaymentError` now raises
89
+ `NameError` when the rescue clause is evaluated, which happens only once some
90
+ other exception is already in flight.
91
+
49
92
  ## Configuration
50
93
 
51
94
  There are several ways to configure the gem:
@@ -59,6 +102,7 @@ Tappay.configure do |config|
59
102
  config.merchant_id = 'YOUR_MERCHANT_ID'
60
103
  config.jko_pay_merchant_id = 'YOUR_JKO_PAY_MERCHANT_ID' # Optional, falls back to merchant_id if not set
61
104
  config.line_pay_merchant_id = 'YOUR_LINE_PAY_MERCHANT_ID' # Optional, falls back to merchant_id if not set
105
+ config.currency = 'TWD' # Optional, default for payments that do not pass :currency
62
106
  end
63
107
  ```
64
108
 
@@ -103,7 +147,7 @@ result = Tappay::CreditCard::Pay.by_prime(
103
147
  name: 'Test User',
104
148
  email: 'test@example.com'
105
149
  }
106
- )
150
+ ).execute
107
151
 
108
152
  # Payment with saved card token
109
153
  result = Tappay::CreditCard::Pay.by_token(
@@ -113,7 +157,7 @@ result = Tappay::CreditCard::Pay.by_token(
113
157
  currency: 'TWD',
114
158
  details: 'Order Details',
115
159
  ccv_prime: 'ccv_prime_from_tappay' # Optional: CVV verification
116
- )
160
+ ).execute
117
161
 
118
162
  # Instalment payment (3-30 months)
119
163
  result = Tappay::CreditCard::Instalment.by_prime(
@@ -126,7 +170,7 @@ result = Tappay::CreditCard::Instalment.by_prime(
126
170
  name: 'Test User',
127
171
  email: 'test@example.com'
128
172
  }
129
- )
173
+ ).execute
130
174
 
131
175
  # Instalment payment with saved card token
132
176
  result = Tappay::CreditCard::Instalment.by_token(
@@ -136,7 +180,7 @@ result = Tappay::CreditCard::Instalment.by_token(
136
180
  instalment: 12,
137
181
  details: 'Order Details',
138
182
  ccv_prime: 'ccv_prime_from_tappay' # Optional: CVV verification
139
- )
183
+ ).execute
140
184
  ```
141
185
 
142
186
  ### Line Pay
@@ -247,11 +291,11 @@ result = payment.execute
247
291
  Query transaction records with required time range:
248
292
 
249
293
  ```ruby
250
- # Query transactions within a specific time range
294
+ # time is required; TapPay caps the range at 90 days
251
295
  result = Tappay::Transaction::Query.new(
252
296
  time: {
253
- start_time: 1706198400, # Unix timestamp for start time
254
- end_time: 1706284800 # Unix timestamp for end time
297
+ start_time: 1706198400000, # Unix timestamp in MILLISECONDS
298
+ end_time: 1706284800000 # Unix timestamp in MILLISECONDS
255
299
  },
256
300
  order_number: 'ORDER123', # Optional: filter by order number
257
301
  records_per_page: 50, # Optional: default is 50
@@ -270,7 +314,65 @@ result[:trade_records].each do |record|
270
314
  end
271
315
  ```
272
316
 
273
- Note: The `time` parameter with both `start_time` and `end_time` is required for querying transactions.
317
+ Every field TapPay returns for a trade record is passed through verbatim, with
318
+ keys symbolized (recursively, so nested objects like `refund_info` and
319
+ `card_info` are symbol-keyed too). The gem does not whitelist fields, so
320
+ `refunded_amount`, `is_captured`, `bank_result_code`, `bank_result_msg`,
321
+ `original_amount`, `time` and the various `*_millis` timestamps are all
322
+ available, and fields TapPay adds later work without a gem upgrade. The exact
323
+ key set varies by payment method. See
324
+ `spec/fixtures/transaction_query_response.json` for a real response.
325
+
326
+ Two things worth knowing about the records:
327
+
328
+ - `amount` is what remains after refunds. A fully refunded transaction reports
329
+ `amount: 0` with `original_amount` and `refunded_amount` both set to the
330
+ original charge, so compare `refunded_amount` against `original_amount` to
331
+ tell a partial refund from a full one.
332
+ - The transaction timestamp is `time` (milliseconds). `transaction_complete_millis`
333
+ is `0` on records that have not completed.
334
+
335
+ `status` in the result is TapPay's, not an HTTP code, and **`2` means "End of
336
+ list", not "nothing found"** - it is what you get on the last page, records
337
+ included. Verified against the sandbox: a query returning 33 records answered
338
+ `status: 2`. Treat `0` and `2` alike and read `trade_records`; checking
339
+ `status == 0` before looking drops the final page. `Response#success?` is
340
+ `status == 0`, so it is the wrong question for a query - `Transaction::Query`
341
+ does not use it.
342
+
343
+ Note: `time` is required and its timestamps are in **milliseconds**, not
344
+ seconds. Seconds are accepted by TapPay but match nothing, so the gem rejects
345
+ them with a `ValidationError` rather than returning an empty list. TapPay caps
346
+ the range at 90 days.
347
+
348
+ ### Checking the result
349
+
350
+ `Pay.by_prime` and friends return an unexecuted payment object - call
351
+ `execute` to send the request. TapPay reports business failures (declined
352
+ card, insufficient funds, expired card) as **HTTP 200 with a non-zero
353
+ `status`**, so `success?` checks that status rather than the HTTP code:
354
+
355
+ ```ruby
356
+ result = Tappay::CreditCard::Pay.by_prime(...).execute
357
+
358
+ if result.success? # status == 0
359
+ result['rec_trade_id']
360
+ else
361
+ result['status'] # e.g. 10003
362
+ result['msg'] # e.g. 'Card is declined'
363
+ end
364
+ ```
365
+
366
+ **`success?` does not mean the money has moved.** It means TapPay accepted and
367
+ processed the request. What that implies depends on the payment method:
368
+
369
+ - **Credit card** - the transaction was authorised. Capture is asynchronous;
370
+ confirm it with `is_captured` from a `Transaction::Query`.
371
+ - **LINE Pay / JKO Pay / iPass Money** - only that a `payment_url` was created.
372
+ The customer has not paid yet. Redirect them to `result['payment_url']`, and
373
+ treat your `backend_notify_url` callback plus a `Transaction::Query` as the
374
+ authoritative answer. TapPay's own guidance is to query the Record API before
375
+ showing a result page to the customer.
274
376
 
275
377
  ### Error Handling
276
378
 
@@ -281,8 +383,12 @@ begin
281
383
  result = Tappay::CreditCard::Pay.by_prime(
282
384
  prime: 'prime_from_tappay_sdk',
283
385
  amount: 100,
386
+ details: 'Order Details',
284
387
  order_number: 'ORDER-123'
285
- )
388
+ ).execute
389
+
390
+ # TapPay reports declined cards as HTTP 200 with a non-zero status.
391
+ raise "Payment failed: #{result['msg']}" unless result.success?
286
392
  rescue Tappay::ValidationError => e
287
393
  # Handle validation errors (e.g., missing required fields)
288
394
  puts "Validation error: #{e.message}"
@@ -292,9 +398,78 @@ rescue Tappay::Error => e
292
398
  end
293
399
  ```
294
400
 
401
+ The gem raises `Tappay::ValidationError` (bad or missing options),
402
+ `Tappay::ConfigurationError` (authentication rejected) and
403
+ `Tappay::ConnectionError` (timeout, unreachable endpoint, unparseable
404
+ response), all of which inherit from `Tappay::Error`. Business failures are
405
+ not exceptions - check `success?`.
406
+
295
407
  ## Development
296
408
 
297
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
409
+ After checking out the repo, run `bin/setup` to install dependencies. Then run
410
+ `bundle exec rspec` for the tests, or `bin/console` for an interactive prompt.
411
+
412
+ ### Mutation testing
413
+
414
+ Line coverage says a line ran, not that anything checked what it did. Both bugs
415
+ fixed in 2.0.0 - a field name that never matched the API and a time filter in
416
+ the wrong unit - sat under 100% line coverage for the life of the gem.
417
+
418
+ [Mutant](https://github.com/mbj/mutant) changes the source in small ways (flips
419
+ a boolean, drops a call, swaps an operator) and reruns the suite. A mutation
420
+ that survives marks behaviour nothing asserts.
421
+
422
+ ```bash
423
+ bundle config set --local with mutant
424
+ bundle install
425
+ bundle exec mutant run
426
+ ```
427
+
428
+ It needs Ruby >= 3.3, so it lives in an optional bundle group rather than the
429
+ gemspec - the gem itself supports >= 2.7. Scope is `config/mutant.yml`:
430
+ `Transaction::Query`, `Response`, and the two payment base classes that five
431
+ payment methods now share.
432
+
433
+ The current score is 95.34% (676 of 709). Mutant exits non-zero whenever
434
+ anything survives, and the 33 survivors here are equivalent mutations that no
435
+ test can distinguish - inside `module Tappay`, `Client.new` and
436
+ `Tappay::Client.new` are the same call, and `is_a?(Hash)` and
437
+ `instance_of?(Hash)` differ only for a Hash subclass nothing passes. Each one
438
+ is listed in `config/mutant.yml` with its reason, and CI gates on the score
439
+ rather than the exit code.
440
+
441
+ If the score drops, an assertion went missing. Read the new survivor rather
442
+ than lowering the floor: the first run here scored 77.90%, and every one of
443
+ those gaps was genuine - including two tests that passed whether or not the
444
+ code they covered was there at all.
445
+
446
+ ### Contract tests against the sandbox
447
+
448
+ Stubbed tests assert the gem against its own assumptions, so they cannot catch
449
+ the gem being wrong about TapPay. Every bug fixed in 2.0.0 was exactly that -
450
+ a field name that did not exist, a time filter in the wrong unit - and all of
451
+ them survived a suite with 100% line coverage. One real call catches the lot.
452
+
453
+ ```bash
454
+ export TAPPAY_SANDBOX_PARTNER_KEY=...
455
+ bundle exec rspec spec/contract --tag contract
456
+ ```
457
+
458
+ The specs are excluded from the default run and skip with an explanation if
459
+ the key is unset, so a clone without credentials behaves normally. Nothing is
460
+ written: the Record API is read-only and needs only a partner key.
461
+
462
+ They check that a millisecond window is accepted, that every field
463
+ `spec/fixtures/transaction_query_response.json` claims still exists in the live
464
+ response, that the transaction timestamp is `time` in milliseconds, and that
465
+ the amounts and capture state refunds reconcile against are all present. New
466
+ fields TapPay has added are reported rather than failed - the gem passes
467
+ everything through, so they break nothing; the fixture just wants re-capturing.
468
+
469
+ Only `Transaction::Query` is covered. Charging needs a `prime`, which the
470
+ frontend SDK mints from a test card - it is single-use, expires in seconds, and
471
+ there is no server-side way to obtain one, so payment and refund flows cannot
472
+ be driven from a test suite without automating a browser.
298
473
 
299
474
  ## Contributing
300
475
 
@@ -1,32 +1,9 @@
1
- require 'json'
1
+ # frozen_string_literal: true
2
2
 
3
3
  module Tappay
4
4
  module ApplePay
5
- class Pay < PaymentBase
6
-
7
- def endpoint_url
8
- Tappay::Endpoints::Payment.pay_by_prime_url
9
- end
10
-
11
- private
12
-
13
- def get_merchant_id
14
- return nil if Tappay.configuration.merchant_group_id
15
-
16
- Tappay.configuration.apple_pay_merchant_id || super
17
- end
18
-
19
- def additional_required_options
20
- [:prime, :cardholder]
21
- end
22
-
23
- protected
24
-
25
- def payment_data
26
- super.merge(
27
- prime: options[:prime]
28
- )
29
- end
5
+ class Pay < PrimePayment
6
+ uses_merchant_id :apple_pay_merchant_id
30
7
  end
31
8
  end
32
9
  end
data/lib/tappay/client.rb CHANGED
@@ -62,24 +62,48 @@ module Tappay
62
62
  attr_reader :code, :body, :headers
63
63
 
64
64
  def initialize(net_http_response)
65
- @response = net_http_response
66
65
  @code = net_http_response.code.to_i
67
66
  @body = net_http_response.body
68
67
  @headers = net_http_response.to_hash
69
68
  end
70
69
 
70
+ # Every TapPay endpoint answers with a JSON object, so a body that is not
71
+ # one means something other than TapPay replied - a maintenance page, a
72
+ # proxy, a WAF. Returning the raw body instead turned
73
+ # `parsed_response['status']` into a String#[] substring search, which
74
+ # answers nil without complaining. Raise rather than hand back something
75
+ # that reads like a result.
71
76
  def parsed_response
72
- @parsed_response ||= JSON.parse(@body)
77
+ @parsed_response ||= JSON.parse(@body.to_s).then do |parsed|
78
+ raise ConnectionError, unexpected_body unless parsed.is_a?(Hash)
79
+
80
+ parsed
81
+ end
73
82
  rescue JSON::ParserError
74
- @body
83
+ raise ConnectionError, unexpected_body
75
84
  end
76
85
 
86
+ # TapPay signals business failures (declined card, insufficient funds,
87
+ # expired card) with HTTP 200 and a non-zero `status`, so the HTTP code
88
+ # alone says nothing. Client only builds a Response once validate_response
89
+ # has accepted the HTTP code, so `status` is the only question left.
90
+ #
91
+ # This means "TapPay processed the request", not "the money moved": for a
92
+ # credit card the transaction is authorised but capture is asynchronous,
93
+ # and for LINE Pay / JKO Pay / iPass Money it means only that a payment_url
94
+ # was created and the customer has yet to pay. Confirm with Transaction::Query.
77
95
  def success?
78
- @code >= 200 && @code < 300
96
+ parsed_response['status'] == 0
79
97
  end
80
98
 
81
99
  def [](key)
82
100
  parsed_response[key]
83
101
  end
102
+
103
+ private
104
+
105
+ def unexpected_body
106
+ "Expected a JSON object from TapPay, got: #{@body.to_s[0, 200]}"
107
+ end
84
108
  end
85
109
  end
@@ -6,15 +6,9 @@ module Tappay
6
6
  :line_pay_merchant_id, :jko_pay_merchant_id, :currency,
7
7
  :google_pay_merchant_id, :apple_pay_merchant_id,
8
8
  :ipass_money_merchant_id
9
- attr_writer :api_version
10
9
 
11
10
  def initialize
12
11
  @mode = :sandbox
13
- @api_version = '3'
14
- end
15
-
16
- def api_version
17
- @api_version.to_s
18
12
  end
19
13
 
20
14
  def sandbox?
@@ -49,7 +49,7 @@ module Tappay
49
49
  private
50
50
 
51
51
  def additional_required_options
52
- [:card_key, :card_token, :currency]
52
+ [:card_key, :card_token]
53
53
  end
54
54
  end
55
55
  end
@@ -33,26 +33,6 @@ module Tappay
33
33
  def query_url
34
34
  "#{Endpoints.base_url}/tpc/transaction/query"
35
35
  end
36
-
37
- def trade_history_url
38
- "#{Endpoints.base_url}/tpc/transaction/trade-history"
39
- end
40
-
41
- def cap_url
42
- "#{Endpoints.base_url}/tpc/transaction/cap"
43
- end
44
- end
45
- end
46
-
47
- module Bind
48
- class << self
49
- def bind_card_url
50
- "#{Endpoints.base_url}/tpc/card/bind"
51
- end
52
-
53
- def remove_card_url
54
- "#{Endpoints.base_url}/tpc/card/remove"
55
- end
56
36
  end
57
37
  end
58
38
  end
data/lib/tappay/errors.rb CHANGED
@@ -4,7 +4,4 @@ module Tappay
4
4
  class ConfigurationError < Error; end
5
5
  class ConnectionError < Error; end
6
6
  class ValidationError < Error; end
7
- class PaymentError < Error; end
8
- class RefundError < Error; end
9
- class QueryError < Error; end
10
7
  end
@@ -1,32 +1,9 @@
1
- require 'json'
1
+ # frozen_string_literal: true
2
2
 
3
3
  module Tappay
4
4
  module GooglePay
5
- class Pay < PaymentBase
6
-
7
- def endpoint_url
8
- Tappay::Endpoints::Payment.pay_by_prime_url
9
- end
10
-
11
- private
12
-
13
- def get_merchant_id
14
- return nil if Tappay.configuration.merchant_group_id
15
-
16
- Tappay.configuration.google_pay_merchant_id || super
17
- end
18
-
19
- def additional_required_options
20
- [:prime, :cardholder]
21
- end
22
-
23
- protected
24
-
25
- def payment_data
26
- super.merge(
27
- prime: options[:prime]
28
- )
29
- end
5
+ class Pay < PrimePayment
6
+ uses_merchant_id :google_pay_merchant_id
30
7
  end
31
8
  end
32
9
  end
@@ -2,63 +2,8 @@
2
2
 
3
3
  module Tappay
4
4
  module IPassMoney
5
- class Pay < PaymentBase
6
- def endpoint_url
7
- Tappay::Endpoints::Payment.pay_by_prime_url
8
- end
9
-
10
- private
11
-
12
- def get_merchant_id
13
- # If merchant_group_id is set, it takes precedence
14
- return nil if Tappay.configuration.merchant_group_id
15
-
16
- # Otherwise, use ipass_money_merchant_id or fall back to default merchant_id
17
- Tappay.configuration.ipass_money_merchant_id || super
18
- end
19
-
20
- def additional_required_options
21
- [:prime, :frontend_redirect_url, :backend_notify_url, :cardholder]
22
- end
23
-
24
- def validate_options!
25
- super
26
- validate_result_url_format!
27
- end
28
-
29
- def validate_result_url_format!
30
- # First validate that if result_url is provided, it's a hash with required fields
31
- if options.key?(:result_url)
32
- raise ValidationError, "result_url must be a hash" unless options[:result_url].is_a?(Hash)
33
-
34
- result_url = options[:result_url]
35
- required_fields = %w[frontend_redirect_url backend_notify_url]
36
- missing = required_fields.select { |field| result_url[field.to_sym].nil? && result_url[field].nil? }
37
-
38
- if missing.any?
39
- raise ValidationError, "result_url must contain both frontend_redirect_url and backend_notify_url"
40
- end
41
- end
42
-
43
- # Then validate frontend_redirect_url and backend_notify_url are present and not empty
44
- if !options[:frontend_redirect_url].to_s.strip.empty? && !options[:backend_notify_url].to_s.strip.empty?
45
- return
46
- end
47
-
48
- raise ValidationError, "result_url must contain both frontend_redirect_url and backend_notify_url"
49
- end
50
-
51
- protected
52
-
53
- def payment_data
54
- super.merge(
55
- prime: options[:prime],
56
- result_url: {
57
- frontend_redirect_url: options[:frontend_redirect_url],
58
- backend_notify_url: options[:backend_notify_url]
59
- }
60
- )
61
- end
5
+ class Pay < RedirectPayment
6
+ uses_merchant_id :ipass_money_merchant_id
62
7
  end
63
8
  end
64
9
  end
@@ -2,63 +2,8 @@
2
2
 
3
3
  module Tappay
4
4
  module JkoPay
5
- class Pay < PaymentBase
6
- def endpoint_url
7
- Tappay::Endpoints::Payment.pay_by_prime_url
8
- end
9
-
10
- private
11
-
12
- def get_merchant_id
13
- # If merchant_group_id is set, it takes precedence
14
- return nil if Tappay.configuration.merchant_group_id
15
-
16
- # Otherwise, use jko_pay_merchant_id or fall back to default merchant_id
17
- Tappay.configuration.jko_pay_merchant_id || super
18
- end
19
-
20
- def additional_required_options
21
- [:prime, :frontend_redirect_url, :backend_notify_url, :cardholder]
22
- end
23
-
24
- def validate_options!
25
- super
26
- validate_result_url_format!
27
- end
28
-
29
- def validate_result_url_format!
30
- # First validate that if result_url is provided, it's a hash with required fields
31
- if options.key?(:result_url)
32
- raise ValidationError, "result_url must be a hash" unless options[:result_url].is_a?(Hash)
33
-
34
- result_url = options[:result_url]
35
- required_fields = %w[frontend_redirect_url backend_notify_url]
36
- missing = required_fields.select { |field| result_url[field.to_sym].nil? && result_url[field].nil? }
37
-
38
- if missing.any?
39
- raise ValidationError, "result_url must contain both frontend_redirect_url and backend_notify_url"
40
- end
41
- end
42
-
43
- # Then validate frontend_redirect_url and backend_notify_url are present and not empty
44
- if !options[:frontend_redirect_url].to_s.strip.empty? && !options[:backend_notify_url].to_s.strip.empty?
45
- return
46
- end
47
-
48
- raise ValidationError, "result_url must contain both frontend_redirect_url and backend_notify_url"
49
- end
50
-
51
- protected
52
-
53
- def payment_data
54
- super.merge(
55
- prime: options[:prime],
56
- result_url: {
57
- frontend_redirect_url: options[:frontend_redirect_url],
58
- backend_notify_url: options[:backend_notify_url]
59
- }
60
- )
61
- end
5
+ class Pay < RedirectPayment
6
+ uses_merchant_id :jko_pay_merchant_id
62
7
  end
63
8
  end
64
9
  end
@@ -2,63 +2,8 @@
2
2
 
3
3
  module Tappay
4
4
  module LinePay
5
- class Pay < PaymentBase
6
- def endpoint_url
7
- Tappay::Endpoints::Payment.pay_by_prime_url
8
- end
9
-
10
- private
11
-
12
- def get_merchant_id
13
- # If merchant_group_id is set, it takes precedence
14
- return nil if Tappay.configuration.merchant_group_id
15
-
16
- # Otherwise, use line_pay_merchant_id or fall back to default merchant_id
17
- Tappay.configuration.line_pay_merchant_id || super
18
- end
19
-
20
- def additional_required_options
21
- [:prime, :frontend_redirect_url, :backend_notify_url, :cardholder]
22
- end
23
-
24
- def validate_options!
25
- super
26
- validate_result_url_format!
27
- end
28
-
29
- def validate_result_url_format!
30
- # First validate that if result_url is provided, it's a hash with required fields
31
- if options.key?(:result_url)
32
- raise ValidationError, "result_url must be a hash" unless options[:result_url].is_a?(Hash)
33
-
34
- result_url = options[:result_url]
35
- required_fields = %w[frontend_redirect_url backend_notify_url]
36
- missing = required_fields.select { |field| result_url[field.to_sym].nil? && result_url[field].nil? }
37
-
38
- if missing.any?
39
- raise ValidationError, "result_url must contain both frontend_redirect_url and backend_notify_url"
40
- end
41
- end
42
-
43
- # Then validate frontend_redirect_url and backend_notify_url are present and not empty
44
- if !options[:frontend_redirect_url].to_s.strip.empty? && !options[:backend_notify_url].to_s.strip.empty?
45
- return
46
- end
47
-
48
- raise ValidationError, "result_url must contain both frontend_redirect_url and backend_notify_url"
49
- end
50
-
51
- protected
52
-
53
- def payment_data
54
- super.merge(
55
- prime: options[:prime],
56
- result_url: {
57
- frontend_redirect_url: options[:frontend_redirect_url],
58
- backend_notify_url: options[:backend_notify_url]
59
- }
60
- )
61
- end
5
+ class Pay < RedirectPayment
6
+ uses_merchant_id :line_pay_merchant_id
62
7
  end
63
8
  end
64
9
  end
@@ -4,6 +4,22 @@ module Tappay
4
4
  class PaymentBase < Client
5
5
  VALID_INSTALMENT_VALUES = [0, 3, 6, 12, 18, 24, 30].freeze
6
6
 
7
+ class << self
8
+ # Some payment methods get their own merchant ID from TapPay. Declaring
9
+ # the config key here replaces a get_merchant_id override per class.
10
+ def uses_merchant_id(key)
11
+ @merchant_id_key = key
12
+ end
13
+
14
+ # Walks the ancestry, because a class-level ivar is not inherited and
15
+ # the get_merchant_id overrides this replaced were. Without this, a
16
+ # subclass of a payment class silently charges the default merchant.
17
+ def merchant_id_key
18
+ @merchant_id_key ||
19
+ (superclass.merchant_id_key if superclass.respond_to?(:merchant_id_key))
20
+ end
21
+ end
22
+
7
23
  def initialize(options = {})
8
24
  super
9
25
  validate_options!
@@ -36,7 +52,7 @@ module Tappay
36
52
  partner_key: Tappay.configuration.partner_key,
37
53
  amount: options[:amount],
38
54
  details: options[:details],
39
- currency: options[:currency] || 'TWD',
55
+ currency: options[:currency] || Tappay.configuration.currency || 'TWD',
40
56
  order_number: options[:order_number],
41
57
  three_domain_secure: options[:three_domain_secure] || false
42
58
  }).tap do |data|
@@ -82,11 +98,11 @@ module Tappay
82
98
  end
83
99
 
84
100
  def get_merchant_id
85
- # If merchant_group_id is set, it takes precedence over all other merchant IDs
101
+ # merchant_group_id takes precedence over every merchant ID.
86
102
  return nil if Tappay.configuration.merchant_group_id
87
103
 
88
- # Otherwise, return the default merchant_id
89
- Tappay.configuration.merchant_id
104
+ key = self.class.merchant_id_key
105
+ (key && Tappay.configuration.public_send(key)) || Tappay.configuration.merchant_id
90
106
  end
91
107
 
92
108
  def base_required_options
@@ -105,7 +121,12 @@ module Tappay
105
121
  end
106
122
 
107
123
  def validate_result_url!
108
- result_url = options[:result_url]
124
+ validate_result_url_hash!(options[:result_url])
125
+ end
126
+
127
+ # The one definition of what a result_url hash has to look like. Both the
128
+ # 3DS path above and the redirect payment methods check against this.
129
+ def validate_result_url_hash!(result_url)
109
130
  raise ValidationError, "result_url must be a hash" unless result_url.is_a?(Hash)
110
131
 
111
132
  required_fields = %w[frontend_redirect_url backend_notify_url]
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tappay
4
+ # Payment methods charged with a prime from TapPay's SDK, where TapPay also
5
+ # requires cardholder details. Apple Pay and Google Pay are this and nothing
6
+ # more; the redirect methods add a result_url on top.
7
+ class PrimePayment < PaymentBase
8
+ def endpoint_url
9
+ Tappay::Endpoints::Payment.pay_by_prime_url
10
+ end
11
+
12
+ protected
13
+
14
+ def payment_data
15
+ super.merge(prime: options[:prime])
16
+ end
17
+
18
+ private
19
+
20
+ def additional_required_options
21
+ [:prime, :cardholder]
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tappay
4
+ # Payment methods where TapPay answers with a payment_url and the customer
5
+ # finishes paying on the provider's own page: LINE Pay, JKO Pay, iPass Money.
6
+ # A status of 0 here means the URL was created, not that anyone has paid.
7
+ class RedirectPayment < PrimePayment
8
+ protected
9
+
10
+ def payment_data
11
+ super.merge(
12
+ result_url: {
13
+ frontend_redirect_url: options[:frontend_redirect_url],
14
+ backend_notify_url: options[:backend_notify_url]
15
+ }
16
+ )
17
+ end
18
+
19
+ private
20
+
21
+ def additional_required_options
22
+ super + [:frontend_redirect_url, :backend_notify_url]
23
+ end
24
+
25
+ def validate_options!
26
+ super
27
+ validate_redirect_urls!
28
+ end
29
+
30
+ def validate_redirect_urls!
31
+ validate_result_url_hash!(options[:result_url]) if options.key?(:result_url)
32
+
33
+ return unless options[:frontend_redirect_url].to_s.strip.empty? ||
34
+ options[:backend_notify_url].to_s.strip.empty?
35
+
36
+ raise ValidationError, "result_url must contain both frontend_redirect_url and backend_notify_url"
37
+ end
38
+ end
39
+ end
@@ -3,6 +3,14 @@
3
3
  module Tappay
4
4
  module Transaction
5
5
  class Query
6
+ # TapPay's time filter is in milliseconds. Values at the wrong scale are
7
+ # accepted by the API but match nothing (seconds land in 1970,
8
+ # microseconds in the year 50000), so reject them loudly rather than
9
+ # returning an empty list.
10
+ # ponytail: magnitude heuristic, only valid for 1973-03-03 .. 5138-11-16
11
+ MILLIS_FLOOR = 100_000_000_000
12
+ MILLIS_CEILING = 100_000_000_000_000
13
+
6
14
  def initialize(time:, order_number: nil, bank_transaction_id: nil, records_per_page: 50, page: 0, order_by: nil)
7
15
  @time = validate_time!(time)
8
16
  @order_number = order_number
@@ -16,15 +24,9 @@ module Tappay
16
24
  client = Tappay::Client.new
17
25
  response = client.post(Endpoints::Transaction.query_url, request_params)
18
26
 
19
- {
20
- status: response['status'],
21
- msg: response['msg'],
22
- records_per_page: response['records_per_page'],
23
- page: response['page'],
24
- total_page_count: response['total_page_count'],
25
- number_of_transactions: response['number_of_transactions'],
26
- trade_records: parse_trade_records(response['trade_records'])
27
- }
27
+ result = symbolize_keys(response.parsed_response)
28
+ result[:trade_records] ||= []
29
+ result
28
30
  end
29
31
 
30
32
  private
@@ -53,7 +55,17 @@ module Tappay
53
55
  end
54
56
 
55
57
  unless time[:start_time].is_a?(Integer) && time[:end_time].is_a?(Integer)
56
- raise Tappay::ValidationError, "start_time and end_time must be Unix timestamps (integers)"
58
+ raise Tappay::ValidationError, "start_time and end_time must be Unix timestamps in milliseconds (integers)"
59
+ end
60
+
61
+ if time[:start_time] < MILLIS_FLOOR || time[:end_time] < MILLIS_FLOOR
62
+ raise Tappay::ValidationError,
63
+ "start_time and end_time must be in milliseconds, not seconds (multiply by 1000)"
64
+ end
65
+
66
+ if time[:start_time] >= MILLIS_CEILING || time[:end_time] >= MILLIS_CEILING
67
+ raise Tappay::ValidationError,
68
+ "start_time and end_time are too large to be milliseconds (microseconds?)"
57
69
  end
58
70
 
59
71
  if time[:start_time] > time[:end_time]
@@ -63,36 +75,16 @@ module Tappay
63
75
  time
64
76
  end
65
77
 
66
- def parse_trade_records(records)
67
- return [] unless records&.any?
68
-
69
- records.map do |record|
70
- {
71
- record_status: record['record_status'],
72
- rec_trade_id: record['rec_trade_id'],
73
- amount: record['amount'],
74
- currency: record['currency'],
75
- order_number: record['order_number'],
76
- bank_transaction_id: record['bank_transaction_id'],
77
- auth_code: record['auth_code'],
78
- cardholder: parse_cardholder(record['cardholder']),
79
- merchant_id: record['merchant_id'],
80
- transaction_time: record['transaction_time'],
81
- tsp: record['tsp'],
82
- card_identifier: record['card_identifier']
83
- }
78
+ # Pass every field TapPay returns straight through, at the envelope level
79
+ # as well as per trade record. A whitelist silently drops new fields and
80
+ # silently returns nil for a mistyped key.
81
+ def symbolize_keys(value)
82
+ case value
83
+ when Hash then value.to_h { |k, v| [k.to_sym, symbolize_keys(v)] }
84
+ when Array then value.map { |v| symbolize_keys(v) }
85
+ else value
84
86
  end
85
87
  end
86
-
87
- def parse_cardholder(cardholder)
88
- return unless cardholder
89
-
90
- {
91
- phone_number: cardholder['phone_number'],
92
- name: cardholder['name'],
93
- email: cardholder['email']
94
- }
95
- end
96
88
  end
97
89
  end
98
90
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tappay
4
- VERSION = '1.1.0'
4
+ VERSION = '2.0.0'
5
5
  end
data/lib/tappay.rb CHANGED
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'csv'
4
3
  require 'json'
5
4
  require 'net/http'
6
5
  require 'uri'
@@ -11,6 +10,8 @@ require_relative "tappay/client"
11
10
  require_relative "tappay/errors"
12
11
  require_relative "tappay/refund"
13
12
  require_relative "tappay/payment_base"
13
+ require_relative "tappay/prime_payment"
14
+ require_relative "tappay/redirect_payment"
14
15
  require_relative "tappay/card_holder"
15
16
  require_relative "tappay/endpoints"
16
17
  require_relative "tappay/transaction/query"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tappay_ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Zac
@@ -115,6 +115,7 @@ executables: []
115
115
  extensions: []
116
116
  extra_rdoc_files: []
117
117
  files:
118
+ - LICENSE.txt
118
119
  - README.md
119
120
  - lib/tappay.rb
120
121
  - lib/tappay/apple_pay/pay.rb
@@ -130,6 +131,8 @@ files:
130
131
  - lib/tappay/jko_pay/pay.rb
131
132
  - lib/tappay/line_pay/pay.rb
132
133
  - lib/tappay/payment_base.rb
134
+ - lib/tappay/prime_payment.rb
135
+ - lib/tappay/redirect_payment.rb
133
136
  - lib/tappay/refund.rb
134
137
  - lib/tappay/transaction/query.rb
135
138
  - lib/tappay/version.rb
@@ -155,7 +158,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
155
158
  - !ruby/object:Gem::Version
156
159
  version: '0'
157
160
  requirements: []
158
- rubygems_version: 4.0.6
161
+ rubygems_version: 4.0.16
159
162
  specification_version: 4
160
163
  summary: Ruby wrapper for TapPay payment gateway
161
164
  test_files: []