sepay_pg_ruby 0.0.1
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 +7 -0
- data/README.md +66 -0
- data/lib/sepay_pg_ruby/checkout.rb +59 -0
- data/lib/sepay_pg_ruby/client.rb +98 -0
- data/lib/sepay_pg_ruby/errors.rb +5 -0
- data/lib/sepay_pg_ruby/ipn.rb +31 -0
- data/lib/sepay_pg_ruby/order.rb +31 -0
- data/lib/sepay_pg_ruby/version.rb +3 -0
- data/lib/sepay_pg_ruby.rb +9 -0
- data/sepay_pg_ruby.gemspec +21 -0
- metadata +53 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 980e4390afecf243f51eb965074c00e391d868dbdd349fec1c046beef294e5e6
|
|
4
|
+
data.tar.gz: e4599d686032db231c65e6b693dca349df62097555f702c8601bb4281a56e0a2
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 3c70415760129e44bc87be0dfdfd2cebc0b7ac4435d57715e095c4b6c7f977c187c18109c2a4947f3e76a711b47cf3279ebde3177c94eced357874415e14e4d0
|
|
7
|
+
data.tar.gz: 5313fd157ac12fb915009e47089a2af820cb5cf6b5ac1ab9a92574583e36ef197b01539803299a9c69a640bc92f50ca71bff0c10027f820f543dd0c3b9fe9be1
|
data/README.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# sepay_pg_ruby
|
|
2
|
+
|
|
3
|
+
Ruby SDK cho SePay Payment Gateway, dùng cho Rails project.
|
|
4
|
+
|
|
5
|
+
## Cài đặt trong Rails
|
|
6
|
+
|
|
7
|
+
Trong `Gemfile`:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem 'sepay_pg_ruby', path: 'gem/sepay_pg_ruby'
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Sau đó chạy:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bundle install
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Khởi tạo client
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
client = SepayPgRuby::Client.new(
|
|
23
|
+
env: Rails.env.production? ? 'production' : 'sandbox',
|
|
24
|
+
merchant_id: ENV.fetch('SEPAY_MERCHANT_ID'),
|
|
25
|
+
secret_key: ENV.fetch('SEPAY_SECRET_KEY')
|
|
26
|
+
)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Orders API
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
# Danh sách orders
|
|
33
|
+
client.order.all(per_page: 20, q: 'ORDER-001')
|
|
34
|
+
|
|
35
|
+
# Chi tiết order
|
|
36
|
+
client.order.retrieve('ORDER-001')
|
|
37
|
+
|
|
38
|
+
# Void transaction
|
|
39
|
+
client.order.void_transaction('ORDER-001')
|
|
40
|
+
|
|
41
|
+
# Cancel order
|
|
42
|
+
client.order.cancel('ORDER-001')
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Checkout API
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
checkout_url = client.checkout.init_checkout_url
|
|
49
|
+
|
|
50
|
+
fields = client.checkout.init_one_time_payment_fields(
|
|
51
|
+
order_invoice_number: 'ORDER-001',
|
|
52
|
+
order_amount: 100_000,
|
|
53
|
+
currency: 'VND',
|
|
54
|
+
order_description: 'Thanh toan don hang ORDER-001',
|
|
55
|
+
payment_method: 'BANK_TRANSFER',
|
|
56
|
+
success_url: 'https://your-app.com/payment/success',
|
|
57
|
+
error_url: 'https://your-app.com/payment/error',
|
|
58
|
+
cancel_url: 'https://your-app.com/payment/cancel'
|
|
59
|
+
)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Verify IPN signature
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
is_valid = SepayPgRuby::IPN.verify_signature(params.to_unsafe_h, ENV['SEPAY_SECRET_KEY'])
|
|
66
|
+
```
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
require 'openssl'
|
|
2
|
+
require 'base64'
|
|
3
|
+
|
|
4
|
+
module SepayPgRuby
|
|
5
|
+
class Checkout
|
|
6
|
+
SIGNABLE_FIELDS = [
|
|
7
|
+
'merchant',
|
|
8
|
+
'operation',
|
|
9
|
+
'payment_method',
|
|
10
|
+
'order_amount',
|
|
11
|
+
'currency',
|
|
12
|
+
'order_invoice_number',
|
|
13
|
+
'order_description',
|
|
14
|
+
'customer_id',
|
|
15
|
+
'success_url',
|
|
16
|
+
'error_url',
|
|
17
|
+
'cancel_url',
|
|
18
|
+
'order_id',
|
|
19
|
+
'agreement_id',
|
|
20
|
+
'agreement_name',
|
|
21
|
+
'agreement_type',
|
|
22
|
+
'agreement_payment_frequency',
|
|
23
|
+
'agreement_amount_per_payment'
|
|
24
|
+
].freeze
|
|
25
|
+
|
|
26
|
+
def initialize(client)
|
|
27
|
+
@client = client
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def init_checkout_url
|
|
31
|
+
"#{@client.base_checkout_url}/init"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def init_one_time_payment_fields(fields)
|
|
35
|
+
payload = fields.transform_keys(&:to_s).dup
|
|
36
|
+
payload['merchant'] = @client.merchant_id
|
|
37
|
+
payload['operation'] ||= 'PURCHASE'
|
|
38
|
+
payload['signature'] = sign_fields(payload)
|
|
39
|
+
payload
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def sign_fields(fields)
|
|
43
|
+
normalized = fields.transform_keys(&:to_s)
|
|
44
|
+
signed = []
|
|
45
|
+
|
|
46
|
+
# We must iterate over the keys in the order they were added to the hash
|
|
47
|
+
# to match the insertion-order behavior of the Node.js SDK
|
|
48
|
+
normalized.keys.each do |field|
|
|
49
|
+
next unless SIGNABLE_FIELDS.include?(field)
|
|
50
|
+
next if normalized[field].nil?
|
|
51
|
+
|
|
52
|
+
signed << "#{field}=#{normalized[field]}"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
digest = OpenSSL::HMAC.digest('sha256', @client.secret_key, signed.join(','))
|
|
56
|
+
Base64.strict_encode64(digest)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
require 'base64'
|
|
2
|
+
require 'json'
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'uri'
|
|
5
|
+
require_relative 'errors'
|
|
6
|
+
|
|
7
|
+
module SepayPgRuby
|
|
8
|
+
class Client
|
|
9
|
+
attr_reader :env, :merchant_id, :secret_key, :api_version, :checkout_version, :order, :checkout
|
|
10
|
+
|
|
11
|
+
def initialize(env:, merchant_id:, secret_key:, api_version: 'v1', checkout_version: 'v1')
|
|
12
|
+
@env = env
|
|
13
|
+
@merchant_id = merchant_id
|
|
14
|
+
@secret_key = secret_key
|
|
15
|
+
@api_version = api_version
|
|
16
|
+
@checkout_version = checkout_version
|
|
17
|
+
|
|
18
|
+
validate_config!
|
|
19
|
+
|
|
20
|
+
@order = Order.new(self)
|
|
21
|
+
@checkout = Checkout.new(self)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def base_api_url
|
|
25
|
+
if env == 'sandbox'
|
|
26
|
+
"https://pgapi-sandbox.sepay.vn/#{api_version}"
|
|
27
|
+
else
|
|
28
|
+
"https://pgapi.sepay.vn/#{api_version}"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def base_checkout_url
|
|
33
|
+
if env == 'sandbox'
|
|
34
|
+
"https://pay-sandbox.sepay.vn/#{checkout_version}/checkout"
|
|
35
|
+
else
|
|
36
|
+
"https://pay.sepay.vn/#{checkout_version}/checkout"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def request(method:, endpoint:, params: nil, body: nil)
|
|
41
|
+
uri = URI.parse("#{base_api_url}/#{endpoint}")
|
|
42
|
+
uri.query = URI.encode_www_form(params) if params.present?
|
|
43
|
+
|
|
44
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
45
|
+
http.use_ssl = true
|
|
46
|
+
|
|
47
|
+
req = build_request(method, uri)
|
|
48
|
+
req['Content-Type'] = 'application/json'
|
|
49
|
+
req['Authorization'] = "Basic #{Base64.strict_encode64("#{merchant_id}:#{secret_key}")}"
|
|
50
|
+
req.body = JSON.generate(body) if body
|
|
51
|
+
|
|
52
|
+
res = http.request(req)
|
|
53
|
+
parsed = parse_response_body(res.body)
|
|
54
|
+
|
|
55
|
+
raise RequestError, "SePay request failed (#{res.code}): #{parsed.inspect}" if res.code.to_i >= 400
|
|
56
|
+
|
|
57
|
+
{ status: res.code.to_i, headers: res.to_hash, body: parsed }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def build_request(method, uri)
|
|
63
|
+
case method.to_s.upcase
|
|
64
|
+
when 'GET'
|
|
65
|
+
Net::HTTP::Get.new(uri)
|
|
66
|
+
when 'POST'
|
|
67
|
+
Net::HTTP::Post.new(uri)
|
|
68
|
+
when 'PUT'
|
|
69
|
+
Net::HTTP::Put.new(uri)
|
|
70
|
+
when 'DELETE'
|
|
71
|
+
Net::HTTP::Delete.new(uri)
|
|
72
|
+
else
|
|
73
|
+
raise ArgumentError, "Unsupported HTTP method: #{method}"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def parse_response_body(body)
|
|
78
|
+
return nil if body.nil? || body.strip.empty?
|
|
79
|
+
|
|
80
|
+
JSON.parse(body)
|
|
81
|
+
rescue JSON::ParserError
|
|
82
|
+
body
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def validate_config!
|
|
86
|
+
unless ['sandbox', 'production'].include?(env)
|
|
87
|
+
raise ConfigurationError, "Invalid env '#{env}'. Use 'sandbox' or 'production'."
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
if merchant_id.to_s.strip.empty? || secret_key.to_s.strip.empty?
|
|
91
|
+
raise ConfigurationError, 'merchant_id and secret_key are required'
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
raise ConfigurationError, 'Only v1 is supported for api_version' unless api_version == 'v1'
|
|
95
|
+
raise ConfigurationError, 'Only v1 is supported for checkout_version' unless checkout_version == 'v1'
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
require 'openssl'
|
|
2
|
+
|
|
3
|
+
module SepayPgRuby
|
|
4
|
+
module IPN
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def verify_signature(payload, secret_key)
|
|
8
|
+
payload_hash = payload.transform_keys(&:to_s)
|
|
9
|
+
received_signature = payload_hash['signature']
|
|
10
|
+
return false if received_signature.nil? || received_signature.empty?
|
|
11
|
+
|
|
12
|
+
data_to_verify = payload_hash.reject { |k, _| k == 'signature' }
|
|
13
|
+
sorted_keys = data_to_verify.keys.select { |k| !data_to_verify[k].nil? }.sort
|
|
14
|
+
query_string = sorted_keys.map { |key| "#{key}=#{data_to_verify[key]}" }.join('&')
|
|
15
|
+
|
|
16
|
+
calculated = OpenSSL::HMAC.hexdigest('sha256', secret_key.to_s, query_string)
|
|
17
|
+
secure_compare(received_signature, calculated)
|
|
18
|
+
rescue StandardError
|
|
19
|
+
false
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def secure_compare(a, b)
|
|
23
|
+
return false unless a.is_a?(String) && b.is_a?(String)
|
|
24
|
+
return false unless a.bytesize == b.bytesize
|
|
25
|
+
|
|
26
|
+
result = 0
|
|
27
|
+
a.bytes.zip(b.bytes) { |x, y| result |= x ^ y }
|
|
28
|
+
result.zero?
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module SepayPgRuby
|
|
2
|
+
class Order
|
|
3
|
+
def initialize(client)
|
|
4
|
+
@client = client
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def all(query_params = {})
|
|
8
|
+
@client.request(method: 'GET', endpoint: 'order', params: query_params)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def retrieve(order_invoice_number)
|
|
12
|
+
@client.request(method: 'GET', endpoint: "order/detail/#{order_invoice_number}")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def void_transaction(order_invoice_number)
|
|
16
|
+
@client.request(
|
|
17
|
+
method: 'POST',
|
|
18
|
+
endpoint: 'order/voidTransaction',
|
|
19
|
+
body: { order_invoice_number: order_invoice_number }
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def cancel(order_invoice_number)
|
|
24
|
+
@client.request(
|
|
25
|
+
method: 'POST',
|
|
26
|
+
endpoint: 'order/cancel',
|
|
27
|
+
body: { order_invoice_number: order_invoice_number }
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
require_relative 'sepay_pg_ruby/version'
|
|
2
|
+
require_relative 'sepay_pg_ruby/errors'
|
|
3
|
+
require_relative 'sepay_pg_ruby/client'
|
|
4
|
+
require_relative 'sepay_pg_ruby/order'
|
|
5
|
+
require_relative 'sepay_pg_ruby/checkout'
|
|
6
|
+
require_relative 'sepay_pg_ruby/ipn'
|
|
7
|
+
|
|
8
|
+
module SepayPgRuby
|
|
9
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
require_relative 'lib/sepay_pg_ruby/version'
|
|
2
|
+
|
|
3
|
+
Gem::Specification.new do |spec|
|
|
4
|
+
spec.name = 'sepay_pg_ruby'
|
|
5
|
+
spec.version = SepayPgRuby::VERSION
|
|
6
|
+
spec.authors = ['Hiun']
|
|
7
|
+
spec.email = ['hieufix1710@gmail.com']
|
|
8
|
+
|
|
9
|
+
spec.summary = 'Ruby SDK for SePay Payment Gateway'
|
|
10
|
+
spec.description = 'Ruby SDK for SePay payment gateway with order and checkout support.'
|
|
11
|
+
spec.homepage = 'https://sepay.vn'
|
|
12
|
+
spec.license = 'MIT'
|
|
13
|
+
spec.required_ruby_version = '>= 3.0.0'
|
|
14
|
+
|
|
15
|
+
spec.files = Dir.chdir(__dir__) do
|
|
16
|
+
Dir['lib/**/*.rb'] + ['README.md', 'sepay_pg_ruby.gemspec']
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
spec.require_paths = ['lib']
|
|
20
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
|
21
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: sepay_pg_ruby
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Hiun
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-07-28 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: Ruby SDK for SePay payment gateway with order and checkout support.
|
|
14
|
+
email:
|
|
15
|
+
- hieufix1710@gmail.com
|
|
16
|
+
executables: []
|
|
17
|
+
extensions: []
|
|
18
|
+
extra_rdoc_files: []
|
|
19
|
+
files:
|
|
20
|
+
- README.md
|
|
21
|
+
- lib/sepay_pg_ruby.rb
|
|
22
|
+
- lib/sepay_pg_ruby/checkout.rb
|
|
23
|
+
- lib/sepay_pg_ruby/client.rb
|
|
24
|
+
- lib/sepay_pg_ruby/errors.rb
|
|
25
|
+
- lib/sepay_pg_ruby/ipn.rb
|
|
26
|
+
- lib/sepay_pg_ruby/order.rb
|
|
27
|
+
- lib/sepay_pg_ruby/version.rb
|
|
28
|
+
- sepay_pg_ruby.gemspec
|
|
29
|
+
homepage: https://sepay.vn
|
|
30
|
+
licenses:
|
|
31
|
+
- MIT
|
|
32
|
+
metadata:
|
|
33
|
+
rubygems_mfa_required: 'true'
|
|
34
|
+
post_install_message:
|
|
35
|
+
rdoc_options: []
|
|
36
|
+
require_paths:
|
|
37
|
+
- lib
|
|
38
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
39
|
+
requirements:
|
|
40
|
+
- - ">="
|
|
41
|
+
- !ruby/object:Gem::Version
|
|
42
|
+
version: 3.0.0
|
|
43
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '0'
|
|
48
|
+
requirements: []
|
|
49
|
+
rubygems_version: 3.4.10
|
|
50
|
+
signing_key:
|
|
51
|
+
specification_version: 4
|
|
52
|
+
summary: Ruby SDK for SePay Payment Gateway
|
|
53
|
+
test_files: []
|