diff --git a/README.md b/README.md index c39e8ac..1a6219d 100644 --- a/README.md +++ b/README.md @@ -25,12 +25,34 @@ Or install it yourself as: ## 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 -# 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: { value: 100, 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) - -client.get_refund_info(payment_id: '12345') +```ruby +payment = Yookassa.refunds.create(payment: payload) +# or +client = Yookassa::Client.new(shop_id: 'shop_1', api_key: '123') +payment = client.refunds.create(payment: payload) ``` ## Contributing diff --git a/lib/yookassa.rb b/lib/yookassa.rb index aaebd55..9ab3420 100644 --- a/lib/yookassa.rb +++ b/lib/yookassa.rb @@ -1,22 +1,31 @@ # frozen_string_literal: true -require "http" require "dry-struct" +require "forwardable" 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/http_helpers" +require "yookassa/client" module Yookassa - def self.configure - yield(config) - end + ConfigError = Class.new(StandardError) - def self.config - @config ||= Config.new + class << self + extend Forwardable + + def configure + yield(config) + end + + def config + @config ||= Config.new + 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 diff --git a/lib/yookassa/client.rb b/lib/yookassa/client.rb new file mode 100644 index 0000000..03e3369 --- /dev/null +++ b/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 diff --git a/lib/yookassa/entity/error.rb b/lib/yookassa/entity/error.rb index 6d225e4..5e99f65 100644 --- a/lib/yookassa/entity/error.rb +++ b/lib/yookassa/entity/error.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require_relative "./types" + module Yookassa module Entity class Error < Dry::Struct diff --git a/lib/yookassa/entity/payment.rb b/lib/yookassa/entity/payment.rb index 3eee770..f64d642 100644 --- a/lib/yookassa/entity/payment.rb +++ b/lib/yookassa/entity/payment.rb @@ -16,6 +16,8 @@ module Yookassa # Payment ID in YooMoney. attribute :id, Types::String + attribute? :idempotency_key, Types::String + # status [string, required] # 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 diff --git a/lib/yookassa/entity/refund.rb b/lib/yookassa/entity/refund.rb index 03613cb..920547b 100644 --- a/lib/yookassa/entity/refund.rb +++ b/lib/yookassa/entity/refund.rb @@ -7,6 +7,7 @@ module Yookassa module Entity class Refund < Dry::Struct attribute :id, Types::String + attribute? :idempotency_key, Types::String attribute :status, Types::String attribute :payment_id, Types::String attribute :created_at, Types::String diff --git a/lib/yookassa/http_helpers.rb b/lib/yookassa/http_helpers.rb deleted file mode 100644 index 26e80d6..0000000 --- a/lib/yookassa/http_helpers.rb +++ /dev/null @@ -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 diff --git a/lib/yookassa/payment.rb b/lib/yookassa/payment.rb deleted file mode 100644 index 982d2f6..0000000 --- a/lib/yookassa/payment.rb +++ /dev/null @@ -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 diff --git a/lib/yookassa/payments.rb b/lib/yookassa/payments.rb new file mode 100644 index 0000000..47623ff --- /dev/null +++ b/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 diff --git a/lib/yookassa/refund.rb b/lib/yookassa/refund.rb deleted file mode 100644 index 09cc6e5..0000000 --- a/lib/yookassa/refund.rb +++ /dev/null @@ -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 diff --git a/lib/yookassa/refunds.rb b/lib/yookassa/refunds.rb new file mode 100644 index 0000000..43869c0 --- /dev/null +++ b/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 diff --git a/spec/yookassa/payment_spec.rb b/spec/yookassa/payments_spec.rb similarity index 91% rename from spec/yookassa/payment_spec.rb rename to spec/yookassa/payments_spec.rb index f13b39a..2286edb 100644 --- a/spec/yookassa/payment_spec.rb +++ b/spec/yookassa/payments_spec.rb @@ -1,9 +1,11 @@ # frozen_string_literal: true -RSpec.describe Yookassa::Payment do - let(:settings) { { shop_id: "SHOP_ID", api_key: "API_KEY" } } - let(:idempotency_key) { 12_345 } - let(:payment) { described_class.new(**settings) } +RSpec.describe Yookassa::Payments do + let(:config) { { shop_id: "SHOP_ID", api_key: "API_KEY" } } + let(:client) { Yookassa::Client.new(**config) } + let(:idempotency_key) { SecureRandom.hex(1) } + + let(:payment) { client.payments } let(:body) { File.read("spec/fixtures/payment_response.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" end - describe "#get_payment_info" do + describe "#find" do let(:payment_id) { "2490ded1-000f-5000-8000-1f64111bc63e" } 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 subject diff --git a/spec/yookassa/refund_spec.rb b/spec/yookassa/refunds_spec.rb similarity index 79% rename from spec/yookassa/refund_spec.rb rename to spec/yookassa/refunds_spec.rb index 526d3d9..d075d65 100644 --- a/spec/yookassa/refund_spec.rb +++ b/spec/yookassa/refunds_spec.rb @@ -1,9 +1,10 @@ # frozen_string_literal: true -RSpec.describe Yookassa::Refund do - let(:settings) { { shop_id: "SHOP_ID", api_key: "API_KEY" } } - let(:idempotency_key) { 12_345 } - let(:payment) { described_class.new(**settings) } +RSpec.describe Yookassa::Refunds do + let(:config) { { shop_id: "SHOP_ID", api_key: "API_KEY" } } + let(:client) { Yookassa::Client.new(**config) } + let(:idempotency_key) { SecureRandom.hex(1) } + let(:refund) { client.refunds } let(:body) { File.read("spec/fixtures/refund_response.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(: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 subject @@ -37,11 +38,11 @@ RSpec.describe Yookassa::Refund do it_behaves_like "returns_refund_object" end - describe "#get_refund_info" do + describe "#find" do let(:payment_id) { "2490ded1-000f-5000-8000-1f64111bc63e" } 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 subject diff --git a/spec/yookassa_spec.rb b/spec/yookassa_spec.rb index ec16e61..1acdf7c 100644 --- a/spec/yookassa_spec.rb +++ b/spec/yookassa_spec.rb @@ -5,17 +5,40 @@ RSpec.describe Yookassa do expect(Yookassa::VERSION).not_to be nil end - describe ".configure" do - before do - Yookassa.configure do |config| - config.shop_id = 123 - config.api_key = "test_321" - end + before do + Yookassa.configure do |config| + config.shop_id = 123 + config.api_key = "test_321" end + end + describe ".configure" do it "stores settings and provides access to credentials" do expect(Yookassa.config.shop_id).to eq(123) expect(Yookassa.config.api_key).to eq("test_321") 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