зеркало из
https://github.com/glebtv/yookassa.git
synced 2026-09-07 19:35:51 +03:00
Merge pull request #11 from PaymentInstruments/adding-client-for-payments
Adding client for payments
Этот коммит содержится в:
54
README.md
54
README.md
@@ -25,12 +25,34 @@ Or install it yourself as:
|
|||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
First of all you need to setup credentials to use Yookassa.
|
||||||
|
You can configure your instance of Yookassa once in a application booting time. Ex. if you use rails, just put these lines into initializer file
|
||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
# Payment
|
# config/initializers/yookassa.rb
|
||||||
|
|
||||||
client = Yookassa::Payment.new(shop_id: 'shop_id', api_key: 'api_key')
|
Yookassa.configure do |config|
|
||||||
|
config.shop_id = ENV.fetch('YOOKASSA_SHOP_ID') # or put your shop_id and api_key here directly
|
||||||
|
config.api_key = ENV.fetch('YOOKASSA_API_KEY') # can be taken from Rails.credentials too
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
payment = {
|
There are some cases, when you need to connect to different Yookassa accounts (say, your clients need to connect to Yookassa). That probably means that you run a marketplace or multitenant system. There is a solution for this one from Yookassa, see https://yookassa.ru/en/developers/special-solutions/checkout-for-platforms/basics or https://yookassa.ru/en/developers/partners-api/basics
|
||||||
|
|
||||||
|
If that is not your case, and you still have multiple shop_ids and api_keys, and need to handle all of them under one application, then you need to instantiate clients inline
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
client1 = Yookassa::Client.new(shop_id: 'shop_1', api_key: '123')
|
||||||
|
client2 = Yookassa::Client.new(shop_id: 'shop_2', api_key: '456')
|
||||||
|
```
|
||||||
|
|
||||||
|
### Making Payments
|
||||||
|
|
||||||
|
#### Creating payment
|
||||||
|
```ruby
|
||||||
|
payload = {
|
||||||
amount: {
|
amount: {
|
||||||
value: 100,
|
value: 100,
|
||||||
currency: 'RUB'
|
currency: 'RUB'
|
||||||
@@ -42,23 +64,31 @@ payment = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
client.create(payment: payment)
|
payment = Yookassa.payments.create(payment: payload)
|
||||||
|
|
||||||
client.get_payment_info(payment_id: '12345')
|
# or
|
||||||
|
|
||||||
client.capture(payment_id: '12345')
|
client = Yookassa::Client.new(shop_id: 'shop_1', api_key: '123')
|
||||||
|
payment = client.payments.create(payment: payload)
|
||||||
|
```
|
||||||
|
|
||||||
client.cancel(payment_id: '12345')
|
#### Other payment requests
|
||||||
|
|
||||||
# Refund
|
```ruby
|
||||||
|
Yookassa.payments.find(payment_id: '12345')
|
||||||
|
Yookassa.payments.capture(payment_id: '12345')
|
||||||
|
Yookassa.payments.cancel(payment_id: '12345')
|
||||||
|
```
|
||||||
|
|
||||||
client = Yookassa::Refund.new(shop_id: 'shop_id', api_key: 'api_key')
|
### Refunds
|
||||||
|
|
||||||
client.create(payload: payload)
|
```ruby
|
||||||
|
payment = Yookassa.refunds.create(payment: payload)
|
||||||
client.get_refund_info(payment_id: '12345')
|
|
||||||
|
|
||||||
|
# or
|
||||||
|
|
||||||
|
client = Yookassa::Client.new(shop_id: 'shop_1', api_key: '123')
|
||||||
|
payment = client.refunds.create(payment: payload)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|||||||
@@ -1,22 +1,31 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require "http"
|
|
||||||
require "dry-struct"
|
require "dry-struct"
|
||||||
|
require "forwardable"
|
||||||
require "yookassa/version"
|
require "yookassa/version"
|
||||||
require "yookassa/payment"
|
|
||||||
require "yookassa/refund"
|
|
||||||
require "yookassa/entity/payment"
|
|
||||||
require "yookassa/entity/refund"
|
|
||||||
require "yookassa/entity/error"
|
|
||||||
require "yookassa/config"
|
require "yookassa/config"
|
||||||
require "yookassa/http_helpers"
|
require "yookassa/client"
|
||||||
|
|
||||||
module Yookassa
|
module Yookassa
|
||||||
def self.configure
|
ConfigError = Class.new(StandardError)
|
||||||
|
|
||||||
|
class << self
|
||||||
|
extend Forwardable
|
||||||
|
|
||||||
|
def configure
|
||||||
yield(config)
|
yield(config)
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.config
|
def config
|
||||||
@config ||= Config.new
|
@config ||= Config.new
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def client
|
||||||
|
raise ConfigError, "Specify `shop_id` and `api_key` settins in a `.configure` block" if @config.nil?
|
||||||
|
|
||||||
|
@client ||= Client.new(shop_id: @config.shop_id, api_key: @config.api_key)
|
||||||
|
end
|
||||||
|
|
||||||
|
def_delegators :client, :payments, :refunds
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
44
lib/yookassa/client.rb
Обычный файл
44
lib/yookassa/client.rb
Обычный файл
@@ -0,0 +1,44 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require "http"
|
||||||
|
require_relative "./payments"
|
||||||
|
require_relative "./refunds"
|
||||||
|
require_relative "./entity/error"
|
||||||
|
|
||||||
|
module Yookassa
|
||||||
|
class Client
|
||||||
|
API_URL = "https://api.yookassa.ru/v3/"
|
||||||
|
|
||||||
|
attr_reader :http
|
||||||
|
|
||||||
|
def initialize(shop_id:, api_key:)
|
||||||
|
@http = HTTP.basic_auth(user: shop_id, pass: api_key).headers(accept: "application/json")
|
||||||
|
end
|
||||||
|
|
||||||
|
def payments
|
||||||
|
@payments ||= Payments.new(self)
|
||||||
|
end
|
||||||
|
|
||||||
|
def refunds
|
||||||
|
@refunds ||= Refunds.new(self)
|
||||||
|
end
|
||||||
|
|
||||||
|
def get(endpoint, query: {})
|
||||||
|
api_call { http.get("#{API_URL}#{endpoint}", params: query) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def post(endpoint, idempotency_key:, payload: {})
|
||||||
|
api_call { http.headers("Idempotence-Key" => idempotency_key).post("#{API_URL}#{endpoint}", json: payload) }
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def api_call
|
||||||
|
response = yield if block_given?
|
||||||
|
body = JSON.parse(response.body.to_s, symbolize_names: true)
|
||||||
|
return body if response.status.success?
|
||||||
|
|
||||||
|
Entity::Error.new(**body)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require_relative "./types"
|
||||||
|
|
||||||
module Yookassa
|
module Yookassa
|
||||||
module Entity
|
module Entity
|
||||||
class Error < Dry::Struct
|
class Error < Dry::Struct
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ module Yookassa
|
|||||||
# Payment ID in YooMoney.
|
# Payment ID in YooMoney.
|
||||||
attribute :id, Types::String
|
attribute :id, Types::String
|
||||||
|
|
||||||
|
attribute? :idempotency_key, Types::String
|
||||||
|
|
||||||
# status [string, required]
|
# status [string, required]
|
||||||
# Payment status. Possible values: pending, waiting_for_capture, succeeded, and canceled.
|
# Payment status. Possible values: pending, waiting_for_capture, succeeded, and canceled.
|
||||||
# More about the life cycle of a payment https://yookassa.ru/en/developers/api#:~:text=life%20cycle%20of%20a%20payment%C2%A0
|
# More about the life cycle of a payment https://yookassa.ru/en/developers/api#:~:text=life%20cycle%20of%20a%20payment%C2%A0
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ module Yookassa
|
|||||||
module Entity
|
module Entity
|
||||||
class Refund < Dry::Struct
|
class Refund < Dry::Struct
|
||||||
attribute :id, Types::String
|
attribute :id, Types::String
|
||||||
|
attribute? :idempotency_key, Types::String
|
||||||
attribute :status, Types::String
|
attribute :status, Types::String
|
||||||
attribute :payment_id, Types::String
|
attribute :payment_id, Types::String
|
||||||
attribute :created_at, Types::String
|
attribute :created_at, Types::String
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module Yookassa
|
|
||||||
module HttpHelpers
|
|
||||||
API_URL = "https://api.yookassa.ru/v3/"
|
|
||||||
|
|
||||||
def get(endpoint, query: {})
|
|
||||||
response = client.get("#{API_URL}#{endpoint}", params: query)
|
|
||||||
body = JSON.parse(response.body.to_s, symbolize_names: true)
|
|
||||||
|
|
||||||
return Entity::Error.new(**body) if response.status.client_error?
|
|
||||||
|
|
||||||
yield(body) if block_given?
|
|
||||||
end
|
|
||||||
|
|
||||||
def post(endpoint, idempotency_key:, payload: {})
|
|
||||||
response = client.headers("Idempotence-Key" => idempotency_key).post("#{API_URL}#{endpoint}", json: payload)
|
|
||||||
body = JSON.parse(response.body.to_s, symbolize_names: true)
|
|
||||||
|
|
||||||
return Entity::Error.new(**body) if response.status.client_error?
|
|
||||||
|
|
||||||
yield(body) if block_given?
|
|
||||||
end
|
|
||||||
|
|
||||||
def client
|
|
||||||
HTTP.basic_auth(user: shop_id, pass: api_key).headers(accept: "application/json")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require_relative "http_helpers"
|
|
||||||
|
|
||||||
module Yookassa
|
|
||||||
class Payment
|
|
||||||
include HttpHelpers
|
|
||||||
|
|
||||||
attr_reader :shop_id, :api_key
|
|
||||||
|
|
||||||
def initialize(shop_id: Yookassa.config.shop_id, api_key: Yookassa.config.api_key)
|
|
||||||
@shop_id = shop_id
|
|
||||||
@api_key = api_key
|
|
||||||
end
|
|
||||||
|
|
||||||
def get_payment_info(payment_id:)
|
|
||||||
get("payments/#{payment_id}") do |response|
|
|
||||||
Entity::Payment.new(**response)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def create(payment:, idempotency_key: SecureRandom.hex(10))
|
|
||||||
post("payments", payload: payment, idempotency_key: idempotency_key) do |response|
|
|
||||||
Entity::Payment.new(**response)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def capture(payment_id:, idempotency_key: SecureRandom.hex(10))
|
|
||||||
post("payments/#{payment_id}/capture", idempotency_key: idempotency_key) do |response|
|
|
||||||
Entity::Payment.new(**response)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def cancel(payment_id:, idempotency_key: SecureRandom.hex(10))
|
|
||||||
post("payments/#{payment_id}/cancel", idempotency_key: idempotency_key) do |response|
|
|
||||||
Entity::Payment.new(**response)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# def self.list(shop_id: Yookassa.config.shop_id, api_key: Yookassa.config.api_key)
|
|
||||||
# get("payments", shop_id: shop_id, api_key: api_key) do |resp|
|
|
||||||
# resp['items'].map { Entity::Payment.new(_1) }
|
|
||||||
# end
|
|
||||||
# end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
35
lib/yookassa/payments.rb
Обычный файл
35
lib/yookassa/payments.rb
Обычный файл
@@ -0,0 +1,35 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require_relative "./entity/payment"
|
||||||
|
|
||||||
|
module Yookassa
|
||||||
|
class Payments
|
||||||
|
def initialize(api)
|
||||||
|
@api = api
|
||||||
|
end
|
||||||
|
|
||||||
|
def find(payment_id:)
|
||||||
|
data = api.get("payments/#{payment_id}")
|
||||||
|
Entity::Payment.new(**data)
|
||||||
|
end
|
||||||
|
|
||||||
|
def create(payment:, idempotency_key: SecureRandom.hex(10))
|
||||||
|
data = api.post("payments", payload: payment, idempotency_key: idempotency_key)
|
||||||
|
Entity::Payment.new(**data.merge(idempotency_key: idempotency_key))
|
||||||
|
end
|
||||||
|
|
||||||
|
def capture(payment_id:, idempotency_key: SecureRandom.hex(10))
|
||||||
|
data = api.post("payments/#{payment_id}/capture", idempotency_key: idempotency_key)
|
||||||
|
Entity::Payment.new(**data.merge(idempotency_key: idempotency_key))
|
||||||
|
end
|
||||||
|
|
||||||
|
def cancel(payment_id:, idempotency_key: SecureRandom.hex(10))
|
||||||
|
data = api.post("payments/#{payment_id}/cancel", idempotency_key: idempotency_key)
|
||||||
|
Entity::Payment.new(**data.merge(idempotency_key: idempotency_key))
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
attr_reader :api
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require_relative "http_helpers"
|
|
||||||
|
|
||||||
module Yookassa
|
|
||||||
class Refund
|
|
||||||
include HttpHelpers
|
|
||||||
|
|
||||||
attr_reader :shop_id, :api_key
|
|
||||||
|
|
||||||
def initialize(shop_id: Yookassa.config.shop_id, api_key: Yookassa.config.api_key)
|
|
||||||
@shop_id = shop_id
|
|
||||||
@api_key = api_key
|
|
||||||
end
|
|
||||||
|
|
||||||
def get_refund_info(payment_id:)
|
|
||||||
get("refunds/#{payment_id}") do |response|
|
|
||||||
Entity::Refund.new(**response)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def create(payload:, idempotency_key: SecureRandom.hex(10))
|
|
||||||
post("refunds", payload: payload, idempotency_key: idempotency_key) do |response|
|
|
||||||
Entity::Refund.new(**response)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
25
lib/yookassa/refunds.rb
Обычный файл
25
lib/yookassa/refunds.rb
Обычный файл
@@ -0,0 +1,25 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require_relative "./entity/refund"
|
||||||
|
|
||||||
|
module Yookassa
|
||||||
|
class Refunds
|
||||||
|
def initialize(api)
|
||||||
|
@api = api
|
||||||
|
end
|
||||||
|
|
||||||
|
def find(payment_id:)
|
||||||
|
data = api.get("refunds/#{payment_id}")
|
||||||
|
Entity::Refund.new(**data)
|
||||||
|
end
|
||||||
|
|
||||||
|
def create(payload:, idempotency_key: SecureRandom.hex(10))
|
||||||
|
data = api.post("refunds", payload: payload, idempotency_key: idempotency_key)
|
||||||
|
Entity::Refund.new(**data.merge(idempotency_key: idempotency_key))
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
attr_reader :api
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
RSpec.describe Yookassa::Payment do
|
RSpec.describe Yookassa::Payments do
|
||||||
let(:settings) { { shop_id: "SHOP_ID", api_key: "API_KEY" } }
|
let(:config) { { shop_id: "SHOP_ID", api_key: "API_KEY" } }
|
||||||
let(:idempotency_key) { 12_345 }
|
let(:client) { Yookassa::Client.new(**config) }
|
||||||
let(:payment) { described_class.new(**settings) }
|
let(:idempotency_key) { SecureRandom.hex(1) }
|
||||||
|
|
||||||
|
let(:payment) { client.payments }
|
||||||
let(:body) { File.read("spec/fixtures/payment_response.json") }
|
let(:body) { File.read("spec/fixtures/payment_response.json") }
|
||||||
|
|
||||||
before { stub_request(:any, //).to_return(body: body, headers: { "Content-Type" => "application/json" }) }
|
before { stub_request(:any, //).to_return(body: body, headers: { "Content-Type" => "application/json" }) }
|
||||||
@@ -55,11 +57,11 @@ RSpec.describe Yookassa::Payment do
|
|||||||
it_behaves_like "returns_payment_object"
|
it_behaves_like "returns_payment_object"
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "#get_payment_info" do
|
describe "#find" do
|
||||||
let(:payment_id) { "2490ded1-000f-5000-8000-1f64111bc63e" }
|
let(:payment_id) { "2490ded1-000f-5000-8000-1f64111bc63e" }
|
||||||
let(:url) { "https://api.yookassa.ru/v3/payments/#{payment_id}" }
|
let(:url) { "https://api.yookassa.ru/v3/payments/#{payment_id}" }
|
||||||
|
|
||||||
subject { payment.get_payment_info(payment_id: payment_id) }
|
subject { payment.find(payment_id: payment_id) }
|
||||||
|
|
||||||
it "sends a request" do
|
it "sends a request" do
|
||||||
subject
|
subject
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
RSpec.describe Yookassa::Refund do
|
RSpec.describe Yookassa::Refunds do
|
||||||
let(:settings) { { shop_id: "SHOP_ID", api_key: "API_KEY" } }
|
let(:config) { { shop_id: "SHOP_ID", api_key: "API_KEY" } }
|
||||||
let(:idempotency_key) { 12_345 }
|
let(:client) { Yookassa::Client.new(**config) }
|
||||||
let(:payment) { described_class.new(**settings) }
|
let(:idempotency_key) { SecureRandom.hex(1) }
|
||||||
|
let(:refund) { client.refunds }
|
||||||
let(:body) { File.read("spec/fixtures/refund_response.json") }
|
let(:body) { File.read("spec/fixtures/refund_response.json") }
|
||||||
|
|
||||||
before { stub_request(:any, //).to_return(body: body, headers: { "Content-Type" => "application/json" }) }
|
before { stub_request(:any, //).to_return(body: body, headers: { "Content-Type" => "application/json" }) }
|
||||||
@@ -27,7 +28,7 @@ RSpec.describe Yookassa::Refund do
|
|||||||
let(:payload) { JSON.parse(File.read("spec/fixtures/refund.json")) }
|
let(:payload) { JSON.parse(File.read("spec/fixtures/refund.json")) }
|
||||||
let(:url) { "https://api.yookassa.ru/v3/refunds" }
|
let(:url) { "https://api.yookassa.ru/v3/refunds" }
|
||||||
|
|
||||||
subject { payment.create(payload: payload, idempotency_key: idempotency_key) }
|
subject { refund.create(payload: payload, idempotency_key: idempotency_key) }
|
||||||
|
|
||||||
it "sends a request" do
|
it "sends a request" do
|
||||||
subject
|
subject
|
||||||
@@ -37,11 +38,11 @@ RSpec.describe Yookassa::Refund do
|
|||||||
it_behaves_like "returns_refund_object"
|
it_behaves_like "returns_refund_object"
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "#get_refund_info" do
|
describe "#find" do
|
||||||
let(:payment_id) { "2490ded1-000f-5000-8000-1f64111bc63e" }
|
let(:payment_id) { "2490ded1-000f-5000-8000-1f64111bc63e" }
|
||||||
let(:url) { "https://api.yookassa.ru/v3/refunds/#{payment_id}" }
|
let(:url) { "https://api.yookassa.ru/v3/refunds/#{payment_id}" }
|
||||||
|
|
||||||
subject { payment.get_refund_info(payment_id: payment_id) }
|
subject { refund.find(payment_id: payment_id) }
|
||||||
|
|
||||||
it "sends a request" do
|
it "sends a request" do
|
||||||
subject
|
subject
|
||||||
@@ -5,7 +5,6 @@ RSpec.describe Yookassa do
|
|||||||
expect(Yookassa::VERSION).not_to be nil
|
expect(Yookassa::VERSION).not_to be nil
|
||||||
end
|
end
|
||||||
|
|
||||||
describe ".configure" do
|
|
||||||
before do
|
before do
|
||||||
Yookassa.configure do |config|
|
Yookassa.configure do |config|
|
||||||
config.shop_id = 123
|
config.shop_id = 123
|
||||||
@@ -13,9 +12,33 @@ RSpec.describe Yookassa do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe ".configure" do
|
||||||
it "stores settings and provides access to credentials" do
|
it "stores settings and provides access to credentials" do
|
||||||
expect(Yookassa.config.shop_id).to eq(123)
|
expect(Yookassa.config.shop_id).to eq(123)
|
||||||
expect(Yookassa.config.api_key).to eq("test_321")
|
expect(Yookassa.config.api_key).to eq("test_321")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe ".client" do
|
||||||
|
context "when no settings are provided" do
|
||||||
|
before { Yookassa.instance_variable_set(:@config, nil) }
|
||||||
|
|
||||||
|
it "raises an error" do
|
||||||
|
expect { Yookassa.client }.to raise_error(Yookassa::ConfigError)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when instance configured" do
|
||||||
|
it "creates and stores client" do
|
||||||
|
expect(Yookassa.client).to be_a(Yookassa::Client)
|
||||||
|
expect(Yookassa.client).to eq(Yookassa.client)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe ".payments" do
|
||||||
|
it "delegates request to client and creates an instance" do
|
||||||
|
expect(Yookassa.payments).to be_a(Yookassa::Payments)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user