tosspayments2-rails 0.2.0 → 0.3.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: 007d24a406cc641580afd61afc507157d5014d51c14103d8b6fdf8cdf6abd2a2
4
- data.tar.gz: 798fe8448f2d7eb4b99e55b497689fa9782aff09e167d514e0de8d03cc6540da
3
+ metadata.gz: fd88d1af83f21b3f11746c4825425fd7c9bfee9aa793bd0c6adfde623b25a8c2
4
+ data.tar.gz: 780b41fdccead7e7b6e26beb5f0ad701f756cc19036c7a713aa1b6c978cbbe19
5
5
  SHA512:
6
- metadata.gz: 83a9e5874ec186b065e019f503a98d25534ae47abc4d7d41397b32b83a29d3e2f62cb0510f8db3a1fb8e566d86a35db13b17325923d59ce840eca22f19b8ebd7
7
- data.tar.gz: ee531d730c66f0834fb0d37759759f3183dc617f6a2952d975d9cf781157a4194b1f9ce141a9aa0fd6b73ca5424480e0e054b4ead452054b1a66e61ad5e35728
6
+ metadata.gz: 8804a4ec3d7f980fca6cdba867592f73a763ec933bf963c90bb52e30837be9b59fc4e0cabf320e4915da4eaa9e8fc475e52419620db1565a52d0a714e85eafac
7
+ data.tar.gz: fe9a02353d00025acb304f67ff7b6e90d2b5bedd6e69760921be3352eb9e06608ce2f073cf24934191550d123561f0102a754f0de0e169dd73a576f06c7d97c2
@@ -0,0 +1,26 @@
1
+ name: CI
2
+ on:
3
+ push:
4
+ branches: [ main ]
5
+ pull_request:
6
+ branches: [ main ]
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ matrix:
13
+ ruby: ['3.2', '3.3']
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - uses: ruby/setup-ruby@v1
17
+ with:
18
+ ruby-version: ${{ matrix.ruby }}
19
+ bundler-cache: true
20
+ - name: Run RSpec
21
+ run: bundle exec rspec --format progress
22
+ - name: Build YARD Docs
23
+ run: bundle exec yard doc --no-progress
24
+ - name: Lint (Rubocop if present)
25
+ if: hashFiles('**/.rubocop.yml') != ''
26
+ run: bundle exec rubocop
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --require spec_helper
2
+ --format documentation
data/.rubocop.yml ADDED
@@ -0,0 +1,29 @@
1
+ AllCops:
2
+ TargetRubyVersion: 3.2
3
+ NewCops: enable
4
+ Exclude:
5
+ - 'spec/spec_helper.rb'
6
+ - 'bin/**/*'
7
+ - 'vendor/**/*'
8
+
9
+ Layout/LineLength:
10
+ Max: 120
11
+
12
+ Metrics/BlockLength:
13
+ Exclude:
14
+ - 'spec/**/*'
15
+
16
+ Metrics/MethodLength:
17
+ Max: 20
18
+
19
+ Style/Documentation:
20
+ Enabled: false
21
+
22
+ Style/StringLiterals:
23
+ EnforcedStyle: single_quotes
24
+
25
+ Style/FrozenStringLiteralComment:
26
+ Enabled: true
27
+
28
+ Lint/MissingSuper:
29
+ Enabled: true
data/.yardopts ADDED
@@ -0,0 +1,3 @@
1
+ --markup markdown
2
+ --hide-void-return
3
+ lib/**/*.rb
data/CHANGELOG.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  _No changes yet._
4
4
 
5
+ ## [0.3.0] - 2025-08-21
6
+ ### Changed
7
+ - General maintenance and improvements for release process
8
+
5
9
  ## [0.2.0] - 2025-08-19
6
10
  ### Added
7
11
  - WebhookVerifier HMAC (SHA256 + Base64) verification
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024
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.
@@ -0,0 +1,53 @@
1
+ # Release 0.2.0
2
+
3
+ Tag: v0.2.0 (2025-08-19)
4
+
5
+ ## Highlights
6
+ - New WebhookVerifier with HMAC-SHA256 + Base64 signature checking
7
+ - HTTP client retries with backoff & request ID extraction
8
+ - CallbackVerifier predicate rename to `match_amount?`
9
+ - Predicate rename for `WebhookVerifier#verify?`
10
+ - Clean RuboCop baseline & refactored controller template
11
+ - Development dependencies relocated from gemspec to Gemfile
12
+
13
+ ## Added
14
+ - Webhook verification helper
15
+ - Retry/backoff logic in client
16
+ - Request ID extraction (`X-Request-Id`) surfaced in `APIError`
17
+
18
+ ## Changed
19
+ - Method renames for predicate semantics (`match_amount?`, `verify?`)
20
+ - Reduced complexity in generator payments controller
21
+ - Style & lint conformance; added quality and release rake tasks
22
+
23
+ ## Fixed
24
+ - Secure constant-time compare naming clarity
25
+ - Gem build now excludes packaged `.gem` artifacts & `pkg/`
26
+
27
+ ## Upgrade Notes
28
+ - Replace any `CallbackVerifier#verify!` calls with `match_amount?`
29
+ - Replace any `WebhookVerifier#verify` calls with `verify?`
30
+ - Regenerate initializer/controller via `rails generate tosspayments2:install` if you want updated template.
31
+
32
+ ```ruby
33
+ # Old
34
+ verifier.verify(body, sig)
35
+ # New
36
+ verifier.verify?(body, sig)
37
+ ```
38
+
39
+ ```ruby
40
+ # Old
41
+ CallbackVerifier.new.verify!(order_id: oid, amount: amt) { ... }
42
+ # New
43
+ CallbackVerifier.new.match_amount?(order_id: oid, amount: amt) { ... }
44
+ ```
45
+
46
+ ## Publishing Steps (for maintainer)
47
+ 1. Ensure environment variable RUBYGEMS_API_KEY is configured (or `~/.gem/credentials`).
48
+ 2. (Optional) Run: `bundle exec rake release:check`
49
+ 3. Build: `bundle exec rake build`
50
+ 4. Push gem: `gem push pkg/tosspayments2-rails-0.2.0.gem`
51
+ 5. Create GitHub Release using this file as the body.
52
+
53
+ SHA: PLACEHOLDER (update with commit SHA of tag)
data/Rakefile CHANGED
@@ -1,4 +1,43 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'bundler/gem_tasks'
4
- task default: %i[]
4
+
5
+ begin
6
+ require 'rspec/core/rake_task'
7
+ RSpec::Core::RakeTask.new(:spec)
8
+ rescue LoadError
9
+ warn 'RSpec not available; spec task skipped'
10
+ end
11
+
12
+ begin
13
+ require 'rubocop/rake_task'
14
+ RuboCop::RakeTask.new(:rubocop)
15
+ rescue LoadError
16
+ warn 'RuboCop not available; rubocop task skipped'
17
+ end
18
+
19
+ desc 'Generate YARD docs'
20
+ task :yard do
21
+ sh 'bundle exec yard doc'
22
+ end
23
+
24
+ desc 'Run specs and RuboCop'
25
+ task quality: %i[rubocop spec]
26
+
27
+ namespace :release do
28
+ desc 'Pre-release checks (clean git, specs, rubocop, changelog entry)'
29
+ task :check do
30
+ version_file = File.join(__dir__, 'lib', 'tosspayments2', 'rails', 'version.rb')
31
+ version = File.read(version_file)[/VERSION = '([^']+)'/, 1]
32
+ abort 'Uncommitted changes present' unless `git status --porcelain`.strip.empty?
33
+ sh 'bundle exec rubocop'
34
+ sh 'bundle exec rspec'
35
+ changelog = File.read('CHANGELOG.md')
36
+ abort 'CHANGELOG missing current version section' unless changelog.include?("[#{version}]")
37
+ puts "Release checks passed for v#{version}"
38
+ end
39
+ end
40
+
41
+ Rake::Task['release'].enhance(['release:check']) if Rake::Task.task_defined?('release')
42
+
43
+ task default: %i[quality]
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+ module Tosspayments2
5
+ module Generators
6
+ class InstallGenerator < ::Rails::Generators::Base
7
+ source_root File.expand_path('templates', __dir__)
8
+ class_option :controller, type: :boolean, default: false, desc: 'Generate example payments controller'
9
+
10
+ def create_initializer
11
+ template 'initializer.rb', 'config/initializers/tosspayments2.rb'
12
+ end
13
+
14
+ def create_controller
15
+ return unless options[:controller]
16
+
17
+ template 'payments_controller.rb', 'app/controllers/payments_controller.rb'
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ # TossPayments2 configuration
4
+ Tosspayments2::Rails.configure do |c|
5
+ c.client_key = ENV.fetch('TOSSPAYMENTS_CLIENT_KEY', nil)
6
+ c.secret_key = ENV.fetch('TOSSPAYMENTS_SECRET_KEY', nil)
7
+ # c.widget_version = 'v2'
8
+ # c.api_base = 'https://api.tosspayments.com'
9
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ class PaymentsController < ApplicationController
4
+ include Tosspayments2::Rails::ControllerConcern
5
+
6
+ def success
7
+ load_params
8
+ verify_amount!
9
+ @payment = toss_client.confirm(payment_key: @payment_key, order_id: @order_id, amount: @amount)
10
+ rescue Tosspayments2::Rails::VerificationError, Tosspayments2::Rails::APIError => e
11
+ Rails.logger.error("Payment error: #{e.class} #{e.message}")
12
+ redirect_to root_path, alert: '결제 승인 실패'
13
+ end
14
+
15
+ def fail
16
+ flash[:alert] = "결제 실패: #{params[:message] || params[:errorMessage]}"
17
+ redirect_to root_path
18
+ end
19
+
20
+ private
21
+
22
+ def load_params
23
+ @payment_key = params[:paymentKey]
24
+ @order_id = params[:orderId]
25
+ @amount = params[:amount].to_i
26
+ end
27
+
28
+ def verify_amount!
29
+ Tosspayments2::Rails::CallbackVerifier.new.match_amount?(order_id: @order_id, amount: @amount) do |_oid|
30
+ # Lookup the expected amount for the given order id in your domain model.
31
+ # Example:
32
+ # Order.find_by!(uuid: _oid).amount
33
+ # For demo purposes we return a fixed integer:
34
+ 1000
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tosspayments2
4
+ module Rails
5
+ class CallbackVerifier
6
+ def match_amount?(order_id:, amount:, &block)
7
+ raise ArgumentError, 'block required' unless block
8
+
9
+ expected = block.call(order_id)
10
+ unless expected.to_i == amount.to_i
11
+ raise ::Tosspayments2::Rails::VerificationError,
12
+ "Amount mismatch expected=#{expected} got=#{amount}"
13
+ end
14
+
15
+ true
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tosspayments2
4
+ module Rails
5
+ class Error < StandardError; end
6
+ class ConfigurationError < Error; end
7
+ class VerificationError < Error; end
8
+
9
+ class APIError < Error
10
+ attr_reader :status, :body, :code, :request_id
11
+
12
+ def initialize(message, status:, body: nil, request_id: nil)
13
+ super(message)
14
+ @status = status
15
+ @body = body
16
+ @code = (body && body[:code]) || (body && body[:errorCode])
17
+ @request_id = request_id || (body && body[:request_id])
18
+ end
19
+ end
20
+ end
21
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Tosspayments2
4
4
  module Rails
5
- VERSION = '0.2.0'
5
+ VERSION = '0.3.0'
6
6
  end
7
7
  end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'webmock/rspec'
5
+
6
+ RSpec.describe Tosspayments2::Rails::Client do
7
+ before do
8
+ Tosspayments2::Rails.configure do |c|
9
+ c.secret_key = 'sk_test_123'
10
+ end
11
+ end
12
+
13
+ let(:client) { described_class.new }
14
+
15
+ it 'raises ConfigurationError without secret key' do
16
+ Tosspayments2::Rails.configure { |c| c.secret_key = nil }
17
+ expect { described_class.new }.to raise_error(Tosspayments2::Rails::ConfigurationError)
18
+ end
19
+
20
+ it 'performs confirm success' do
21
+ body_hash = { paymentKey: 'pk', orderId: 'o1', status: 'DONE' }
22
+ stub = stub_request(:post, 'https://api.tosspayments.com/v1/payments/confirm')
23
+ stub.to_return(status: 200, body: body_hash.to_json, headers: { 'Content-Type' => 'application/json' })
24
+ response = client.confirm(payment_key: 'pk', order_id: 'o1', amount: 1000)
25
+ expect(response[:status]).to eq('DONE')
26
+ expect(stub).to have_been_requested
27
+ end
28
+
29
+ it 'raises APIError on failure' do
30
+ stub_request(:post, 'https://api.tosspayments.com/v1/payments/confirm').to_return(
31
+ status: 400,
32
+ body: { code: 'INVALID', message: 'Bad' }.to_json,
33
+ headers: { 'Content-Type' => 'application/json' }
34
+ )
35
+ expect { client.confirm(payment_key: 'pk', order_id: 'o1', amount: 1000) }
36
+ .to raise_error(Tosspayments2::Rails::APIError) { |e| expect(e.code).to eq('INVALID') }
37
+ end
38
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'rails'
5
+ require 'action_controller'
6
+ require_relative '../lib/tosspayments2/rails'
7
+ require 'tosspayments2/rails/engine'
8
+
9
+ RSpec.describe Tosspayments2::Rails::Engine do
10
+ it 'isolates namespace' do
11
+ expect(described_class.isolated?).to be true
12
+ end
13
+
14
+ it 'loads configuration via initializer' do
15
+ Tosspayments2::Rails.configure do |c|
16
+ c.client_key = 'ck'
17
+ c.secret_key = 'sk'
18
+ end
19
+ expect(Tosspayments2::Rails.configuration.client_key).to eq('ck')
20
+ end
21
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+ require 'simplecov'
3
+ SimpleCov.start do
4
+ enable_coverage :branch
5
+ add_filter '/spec/'
6
+ end
7
+
8
+ require 'rspec'
9
+ require_relative '../lib/tosspayments2/rails'
10
+
11
+ RSpec.configure do |config|
12
+ config.expect_with :rspec do |c|
13
+ c.syntax = :expect
14
+ end
15
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Tosspayments2::Rails::CallbackVerifier do
6
+ let(:verifier) { described_class.new }
7
+
8
+ it 'returns true when amount matches' do
9
+ expect(
10
+ verifier.match_amount?(order_id: 'o1', amount: 100) { |id| id == 'o1' ? 100 : 0 }
11
+ ).to be true
12
+ end
13
+
14
+ it 'raises VerificationError when mismatch' do
15
+ expect do
16
+ verifier.match_amount?(order_id: 'o1', amount: 150) { |_id| 100 }
17
+ end.to raise_error(Tosspayments2::Rails::VerificationError)
18
+ end
19
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'webmock/rspec'
5
+
6
+ RSpec.describe Tosspayments2::Rails::Client do
7
+ let(:secret) { 'sk_test_x' }
8
+ let(:client) { described_class.new(secret_key: secret, api_base: 'https://api.tosspayments.com') }
9
+
10
+ before do
11
+ stub_request(:post, 'https://api.tosspayments.com/v1/payments/confirm')
12
+ end
13
+
14
+ describe '#cancel' do
15
+ it 'cancels successfully (full cancel)' do
16
+ cancel_body = { paymentKey: 'pay_123', status: 'CANCELED' }
17
+ stub = stub_request(:post, 'https://api.tosspayments.com/v1/payments/pay_123/cancel')
18
+ stub.to_return(status: 200, body: cancel_body.to_json, headers: { 'Content-Type' => 'application/json' })
19
+ response = client.cancel(payment_key: 'pay_123', cancel_reason: 'test')
20
+ expect(response[:status]).to eq('CANCELED')
21
+ expect(stub).to have_been_requested
22
+ end
23
+
24
+ it 'raises APIError on failure' do
25
+ error_body = { code: 'INVALID', message: 'bad' }
26
+ stub = stub_request(:post, 'https://api.tosspayments.com/v1/payments/pay_456/cancel')
27
+ stub.to_return(status: 400, body: error_body.to_json, headers: { 'Content-Type' => 'application/json' })
28
+ expect { client.cancel(payment_key: 'pay_456', cancel_reason: 'test') }
29
+ .to raise_error(Tosspayments2::Rails::APIError) { |e| expect(e.body[:code]).to eq('INVALID') }
30
+ expect(stub).to have_been_requested
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'action_view'
5
+ require 'action_controller'
6
+
7
+ RSpec.describe Tosspayments2::Rails::ScriptTagHelper do
8
+ let(:view) do
9
+ paths = ActionView::PathSet.new
10
+ lookup_context = ActionView::LookupContext.new(paths)
11
+ assigns = {}.freeze
12
+ controller = ActionController::Base.new
13
+ klass = Class.new(ActionView::Base) do
14
+ include Tosspayments2::Rails::ScriptTagHelper
15
+ end
16
+ klass.new(lookup_context, assigns, controller)
17
+ end
18
+
19
+ before do
20
+ Tosspayments2::Rails.configure do |c|
21
+ c.client_key = 'ck_test_x'
22
+ c.secret_key = 'sk_test_x'
23
+ end
24
+ end
25
+
26
+ it 'renders script tag with versioned src' do
27
+ html = view.tosspayments_script_tag
28
+ expect(html).to include('<script')
29
+ expect(html).to include('https://js.tosspayments.com/v2/standard')
30
+ end
31
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Tosspayments2::Rails::WebhookVerifier do
6
+ let(:secret) { 'sk_test_webhook' }
7
+ let(:body) { '{"event":"payment.approved","data":{"id":"123"}}' }
8
+ let(:verifier) { described_class.new(secret_key: secret) }
9
+
10
+ it 'verifies correct signature' do
11
+ sig = verifier.compute_signature(body)
12
+ expect(verifier.verify?(body, sig)).to be true
13
+ end
14
+
15
+ it 'rejects invalid signature' do
16
+ expect(verifier.verify?(body, 'invalid')).to be false
17
+ end
18
+
19
+ it 'rejects when missing data' do
20
+ expect(verifier.verify?(nil, nil)).to be false
21
+ end
22
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tosspayments2-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lucius Choi
@@ -46,21 +46,40 @@ files:
46
46
  - ".claude/agents/kfc/spec-test.md"
47
47
  - ".claude/settings/kfc-settings.json"
48
48
  - ".claude/system-prompts/spec-workflow-starter.md"
49
+ - ".github/workflows/ci.yml"
50
+ - ".rspec"
51
+ - ".rubocop.yml"
49
52
  - ".vscode/mcp.json"
53
+ - ".yardopts"
50
54
  - CHANGELOG.md
55
+ - LICENSE.txt
51
56
  - README.md
57
+ - RELEASE_NOTES_v0.2.0.md
52
58
  - Rakefile
59
+ - lib/generators/tosspayments2/install/install_generator.rb
60
+ - lib/generators/tosspayments2/install/templates/initializer.rb
61
+ - lib/generators/tosspayments2/install/templates/payments_controller.rb
53
62
  - lib/tosspayments2/rails.rb
63
+ - lib/tosspayments2/rails/callback_verifier.rb
54
64
  - lib/tosspayments2/rails/client.rb
55
65
  - lib/tosspayments2/rails/configuration.rb
56
66
  - lib/tosspayments2/rails/controller_concern.rb
57
67
  - lib/tosspayments2/rails/engine.rb
68
+ - lib/tosspayments2/rails/errors.rb
58
69
  - lib/tosspayments2/rails/script_tag_helper.rb
59
70
  - lib/tosspayments2/rails/version.rb
60
71
  - lib/tosspayments2/rails/webhook_verifier.rb
61
72
  - sig/tosspayments2/rails.rbs
73
+ - spec/client_spec.rb
74
+ - spec/engine_spec.rb
75
+ - spec/spec_helper.rb
76
+ - spec/tosspayments2/callback_verifier_spec.rb
77
+ - spec/tosspayments2/client_cancel_spec.rb
78
+ - spec/tosspayments2/script_tag_helper_spec.rb
79
+ - spec/tosspayments2/webhook_verifier_spec.rb
62
80
  homepage: https://github.com/luciuschoi/tosspayments2-rails
63
- licenses: []
81
+ licenses:
82
+ - MIT
64
83
  metadata:
65
84
  homepage_uri: https://github.com/luciuschoi/tosspayments2-rails
66
85
  source_code_uri: https://github.com/luciuschoi/tosspayments2-rails