├── .rspec ├── lib ├── cloud_payments │ ├── version.rb │ ├── client │ │ ├── serializer.rb │ │ ├── serializer │ │ │ ├── multi_json.rb │ │ │ └── base.rb │ │ ├── response.rb │ │ ├── gateway_errors.rb │ │ └── errors.rb │ ├── models │ │ ├── secure3d.rb │ │ ├── model.rb │ │ ├── order.rb │ │ ├── like_subscription.rb │ │ ├── on_kassa_receipt.rb │ │ ├── on_fail.rb │ │ ├── on_recurrent.rb │ │ ├── on_pay.rb │ │ ├── subscription.rb │ │ └── transaction.rb │ ├── namespaces │ │ ├── orders.rb │ │ ├── tokens.rb │ │ ├── apple_pay.rb │ │ ├── cards.rb │ │ ├── kassa.rb │ │ ├── subscriptions.rb │ │ ├── payments.rb │ │ └── base.rb │ ├── models.rb │ ├── namespaces.rb │ ├── webhooks.rb │ ├── config.rb │ └── client.rb └── cloud_payments.rb ├── spec ├── fixtures │ └── apis │ │ ├── ping │ │ ├── failed.yml │ │ └── successful.yml │ │ ├── orders │ │ ├── cancel │ │ │ ├── successful.yml │ │ │ └── failed.yml │ │ └── create │ │ │ └── successful.yml │ │ ├── payments │ │ ├── void │ │ │ ├── failed.yml │ │ │ ├── successful.yml │ │ │ └── failed_with_message.yml │ │ ├── refund │ │ │ ├── failed.yml │ │ │ ├── successful.yml │ │ │ └── failed_with_message.yml │ │ ├── confirm │ │ │ ├── failed.yml │ │ │ ├── successful.yml │ │ │ └── failed_with_message.yml │ │ ├── find │ │ │ ├── failed_with_message.yml │ │ │ ├── failed.yml │ │ │ └── successful.yml │ │ ├── get │ │ │ ├── failed_with_message.yml │ │ │ ├── failed.yml │ │ │ ├── refunded.yml │ │ │ └── successful.yml │ │ └── post3ds │ │ │ ├── failed.yml │ │ │ └── successful.yml │ │ ├── subscriptions │ │ ├── cancel │ │ │ └── successful.yml │ │ ├── get │ │ │ └── successful.yml │ │ ├── find │ │ │ └── successful.yml │ │ ├── update │ │ │ └── successful.yml │ │ └── create │ │ │ └── successful.yml │ │ ├── cards │ │ ├── auth │ │ │ ├── secure3d.yml │ │ │ ├── failed.yml │ │ │ └── successful.yml │ │ ├── charge │ │ │ ├── secure3d.yml │ │ │ ├── failed.yml │ │ │ └── successful.yml │ │ └── post3ds │ │ │ ├── failed.yml │ │ │ └── successful.yml │ │ └── tokens │ │ ├── auth │ │ ├── failed.yml │ │ └── successful.yml │ │ └── charge │ │ ├── failed.yml │ │ └── successful.yml ├── cloud_payments │ ├── namespaces │ │ ├── apple_pay_spec.rb │ │ ├── kassa_spec.rb │ │ ├── orders_spec.rb │ │ ├── base_spec.rb │ │ ├── subscriptions_spec.rb │ │ ├── tokens_spec.rb │ │ ├── cards_spec.rb │ │ └── payments_spec.rb │ ├── client │ │ ├── serializer │ │ │ └── multi_json_spec.rb │ │ └── response_spec.rb │ ├── models │ │ ├── secure3d_spec.rb │ │ ├── order_spec.rb │ │ ├── subscription_spec.rb │ │ └── transaction_spec.rb │ ├── namespaces_spec.rb │ └── webhooks_spec.rb ├── support │ ├── examples.rb │ └── helpers.rb ├── spec_helper.rb └── cloud_payments_spec.rb ├── Rakefile ├── .gitignore ├── .travis.yml ├── Gemfile ├── bin └── release ├── LICENSE.txt ├── cloud_payments.gemspec ├── config.ru └── README.md /.rspec: -------------------------------------------------------------------------------- 1 | --format progress 2 | --color 3 | --backtrace 4 | --order random 5 | -------------------------------------------------------------------------------- /lib/cloud_payments/version.rb: -------------------------------------------------------------------------------- 1 | module CloudPayments 2 | VERSION = "1.2.0" 3 | end 4 | -------------------------------------------------------------------------------- /spec/fixtures/apis/ping/failed.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :request: 3 | :url: '/test' 4 | :response: 5 | :body: '{"Success":false}' 6 | -------------------------------------------------------------------------------- /spec/fixtures/apis/ping/successful.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :request: 3 | :url: '/test' 4 | :response: 5 | :body: '{"Success":true}' 6 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'bundler/gem_tasks' 3 | require 'rspec/core/rake_task' 4 | 5 | RSpec::Core::RakeTask.new(:spec) 6 | 7 | task default: :spec 8 | 9 | -------------------------------------------------------------------------------- /spec/fixtures/apis/orders/cancel/successful.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :request: 3 | :url: '/orders/cancel' 4 | :body: '{"Id":"12345"}' 5 | :response: 6 | :body: '{"Success":true,"Message":null}' 7 | -------------------------------------------------------------------------------- /spec/fixtures/apis/orders/cancel/failed.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :request: 3 | :url: '/orders/cancel' 4 | :body: '{"Id":"12345"}' 5 | :response: 6 | :body: '{"Success":false,"Message":"Error message"}' 7 | -------------------------------------------------------------------------------- /spec/fixtures/apis/payments/void/failed.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :request: 3 | :url: '/payments/void' 4 | :body: '{"TransactionId":12345}' 5 | :response: 6 | :body: '{"Success":false,"Message":null}' 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | *.bundle 11 | *.so 12 | *.o 13 | *.a 14 | mkmf.log 15 | -------------------------------------------------------------------------------- /spec/fixtures/apis/payments/void/successful.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :request: 3 | :url: '/payments/void' 4 | :body: '{"TransactionId":12345}' 5 | :response: 6 | :body: '{"Success":true,"Message":null}' 7 | -------------------------------------------------------------------------------- /spec/fixtures/apis/payments/refund/failed.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :request: 3 | :url: '/payments/refund' 4 | :body: '{"TransactionId":12345,"Amount":120}' 5 | :response: 6 | :body: '{"Success":false,"Message":null}' 7 | -------------------------------------------------------------------------------- /spec/fixtures/apis/payments/confirm/failed.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :request: 3 | :url: '/payments/confirm' 4 | :body: '{"TransactionId":12345,"Amount":120}' 5 | :response: 6 | :body: '{"Success":false,"Message":null}' 7 | -------------------------------------------------------------------------------- /spec/fixtures/apis/payments/confirm/successful.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :request: 3 | :url: '/payments/confirm' 4 | :body: '{"TransactionId":12345,"Amount":120}' 5 | :response: 6 | :body: '{"Success":true,"Message":null}' 7 | -------------------------------------------------------------------------------- /spec/fixtures/apis/payments/find/failed_with_message.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :request: 3 | :url: '/payments/find' 4 | :body: '{"InvoiceId":"1234567"}' 5 | :response: 6 | :body: '{"Success":false,"Message":"Not found"}' 7 | -------------------------------------------------------------------------------- /spec/fixtures/apis/payments/get/failed_with_message.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :request: 3 | :url: '/payments/get' 4 | :body: '{"TransactionId":12345}' 5 | :response: 6 | :body: '{"Success":false,"Message":"Not found"}' 7 | -------------------------------------------------------------------------------- /spec/fixtures/apis/payments/refund/successful.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :request: 3 | :url: '/payments/refund' 4 | :body: '{"TransactionId":12345,"Amount":120}' 5 | :response: 6 | :body: '{"Success":true,"Message":null}' 7 | -------------------------------------------------------------------------------- /spec/fixtures/apis/subscriptions/cancel/successful.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :request: 3 | :url: '/subscriptions/cancel' 4 | :body: '{"Id":"sc_8cf8a9338fb"}' 5 | :response: 6 | :body: '{"Success":true,"Message": null}' 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | 3 | rvm: 4 | - 2.3 5 | - 2.4 6 | - 2.5 7 | - 2.6 8 | - 2.7 9 | 10 | cache: bundler 11 | 12 | before_install: gem install bundler 13 | 14 | script: bundle exec rspec 15 | -------------------------------------------------------------------------------- /spec/fixtures/apis/payments/void/failed_with_message.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :request: 3 | :url: '/payments/void' 4 | :body: '{"TransactionId":12345}' 5 | :response: 6 | :body: '{"Success":false,"Message":"Error message"}' 7 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | source 'https://rubygems.org' 3 | 4 | # Specify your gem's dependencies in cloud_payments.gemspec 5 | gemspec 6 | 7 | gem 'oj' 8 | gem 'pry' 9 | gem 'rack' 10 | gem 'webmock' 11 | -------------------------------------------------------------------------------- /spec/fixtures/apis/payments/refund/failed_with_message.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :request: 3 | :url: '/payments/refund' 4 | :body: '{"TransactionId":12345,"Amount":120}' 5 | :response: 6 | :body: '{"Success":false,"Message":"Error message"}' 7 | -------------------------------------------------------------------------------- /spec/fixtures/apis/payments/confirm/failed_with_message.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :request: 3 | :url: '/payments/confirm' 4 | :body: '{"TransactionId":12345,"Amount":120}' 5 | :response: 6 | :body: '{"Success":false,"Message":"Error message"}' 7 | -------------------------------------------------------------------------------- /lib/cloud_payments/client/serializer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'cloud_payments/client/serializer/base' 3 | require 'cloud_payments/client/serializer/multi_json' 4 | 5 | module CloudPayments 6 | class Client 7 | module Serializer 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/cloud_payments/models/secure3d.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module CloudPayments 3 | class Secure3D < Model 4 | property :transaction_id, required: true 5 | property :pa_req, required: true 6 | property :acs_url, required: true 7 | 8 | def id 9 | transaction_id 10 | end 11 | 12 | def required_secure3d? 13 | true 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/cloud_payments/namespaces/orders.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module CloudPayments 3 | module Namespaces 4 | class Orders < Base 5 | def create(attributes) 6 | response = request(:create, attributes) 7 | Order.new(response[:model]) 8 | end 9 | 10 | def cancel(order_id) 11 | request(:cancel, id: order_id)[:success] 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/cloud_payments/namespaces/tokens.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module CloudPayments 3 | module Namespaces 4 | class Tokens < Base 5 | def charge(attributes) 6 | response = request(:charge, attributes) 7 | Transaction.new(response[:model]) 8 | end 9 | 10 | def auth(attributes) 11 | response = request(:auth, attributes) 12 | Transaction.new(response[:model]) 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/cloud_payments/client/serializer/multi_json.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module CloudPayments 3 | class Client 4 | module Serializer 5 | class MultiJson < Base 6 | def load(json) 7 | return nil if json.empty? 8 | super(::MultiJson.load(json)) 9 | end 10 | 11 | def dump(data) 12 | return '' if data.nil? 13 | ::MultiJson.dump(super(data)) 14 | end 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/cloud_payments/models/model.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'bigdecimal' 3 | require 'bigdecimal/util' 4 | 5 | module CloudPayments 6 | class Model < Hashie::Trash 7 | include Hashie::Extensions::IgnoreUndeclared 8 | 9 | DateTimeTransform = ->(v) { DateTime.parse(v) if v && v.respond_to?(:to_s) } 10 | DecimalTransform = ->(v) { v.to_d if v } 11 | IntegralTransform = ->(v) { v.to_i if v } 12 | BooleanTransform = ->(v) { (v == '0') ? false : !!v } 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /bin/release: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | VERSION=$1 4 | 5 | if [ -z $1 ] ; then 6 | echo "Please provide version number: bin/release 1.0.0" && exit 1; 7 | fi 8 | 9 | printf "module CloudPayments\n VERSION = \"$VERSION\"\nend\n" > ./lib/cloud_payments/version.rb 10 | bundle 11 | git add Gemfile.lock lib/cloud_payments/version.rb 12 | git commit -m "Bump version for $VERSION" 13 | git push 14 | git tag v$VERSION 15 | git push --tags 16 | gem build cloud_payments.gemspec 17 | gem push "cloud_payments-$VERSION.gem" 18 | -------------------------------------------------------------------------------- /lib/cloud_payments/models.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'cloud_payments/models/model' 3 | require 'cloud_payments/models/like_subscription' 4 | require 'cloud_payments/models/secure3d' 5 | require 'cloud_payments/models/transaction' 6 | require 'cloud_payments/models/subscription' 7 | require 'cloud_payments/models/order' 8 | 9 | require 'cloud_payments/models/on_recurrent' 10 | require 'cloud_payments/models/on_pay' 11 | require 'cloud_payments/models/on_fail' 12 | require 'cloud_payments/models/on_kassa_receipt' 13 | -------------------------------------------------------------------------------- /lib/cloud_payments/models/order.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module CloudPayments 3 | class Order < Model 4 | property :id, required: true 5 | property :number, required: true 6 | property :amount, transform_with: DecimalTransform, required: true 7 | property :currency, required: true 8 | property :currency_code, required: true 9 | property :email 10 | property :description, required: true 11 | property :require_confirmation, transform_with: BooleanTransform, required: true 12 | property :url, required: true 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/cloud_payments/namespaces/apple_pay.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module CloudPayments 3 | module Namespaces 4 | class ApplePay < Base 5 | ValidationUrlMissing = Class.new(StandardError) 6 | 7 | def self.resource_name 8 | 'applepay' 9 | end 10 | 11 | def start_session(attributes) 12 | validation_url = attributes.fetch(:validation_url) { raise ValidationUrlMissing.new('validation_url is required') } 13 | 14 | request(:startsession, { "ValidationUrl" => validation_url }) 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/fixtures/apis/cards/auth/secure3d.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :request: 3 | :url: '/payments/cards/auth' 4 | :body: '{"Amount":10,"Currency":"RUB","InvoiceId":"1234567","Description":"Payment for goods on example.com","AccountId":"user_x","Name":"CARDHOLDER NAME","CardCryptogramPacket":"01492500008719030128SM"}' 5 | :response: 6 | :body: > 7 | { 8 | "Model": { 9 | "TransactionId": 12345, 10 | "PaReq": "eJxVUdtugkAQ", 11 | "AcsUrl": "https://test.paymentgate.ru/acs/auth/start.do" 12 | }, 13 | "Success": false, 14 | "Message": null 15 | } 16 | -------------------------------------------------------------------------------- /spec/fixtures/apis/cards/charge/secure3d.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :request: 3 | :url: '/payments/cards/charge' 4 | :body: '{"Amount":10,"Currency":"RUB","InvoiceId":"1234567","Description":"Payment for goods on example.com","AccountId":"user_x","Name":"CARDHOLDER NAME","CardCryptogramPacket":"01492500008719030128SM"}' 5 | :response: 6 | :body: > 7 | { 8 | "Model": { 9 | "TransactionId": 12345, 10 | "PaReq": "eJxVUdtugkAQ", 11 | "AcsUrl": "https://test.paymentgate.ru/acs/auth/start.do" 12 | }, 13 | "Success": false, 14 | "Message": null 15 | } 16 | -------------------------------------------------------------------------------- /spec/cloud_payments/namespaces/apple_pay_spec.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # encoding: utf-8 3 | # frozen_string_literal: true 4 | require 'spec_helper' 5 | 6 | describe CloudPayments::Namespaces::ApplePay do 7 | subject{ described_class.new(CloudPayments.client) } 8 | 9 | describe '#receipt' do 10 | let(:attributes) do 11 | { 12 | validation_url: '' 13 | } 14 | end 15 | 16 | context do 17 | before{ attributes.delete(:validation_url) } 18 | specify{ expect{subject.start_session(attributes)}.to raise_error(described_class::ValidationUrlMissing) } 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/cloud_payments/models/like_subscription.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module CloudPayments 3 | module LikeSubscription 4 | ACTIVE = 'Active' 5 | PAST_DUE = 'PastDue' 6 | CANCELLED = 'Cancelled' 7 | REJECTED = 'Rejected' 8 | EXPIRED = 'Expired' 9 | 10 | def active? 11 | status == ACTIVE 12 | end 13 | 14 | def past_due? 15 | status == PAST_DUE 16 | end 17 | 18 | def cancelled? 19 | status == CANCELLED 20 | end 21 | 22 | def rejected? 23 | status == REJECTED 24 | end 25 | 26 | def expired? 27 | status == EXPIRED 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/cloud_payments/client/serializer/multi_json_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'spec_helper' 3 | 4 | describe CloudPayments::Client::Serializer::MultiJson do 5 | let(:encoded_data){ '{"Model":{"Id":123,"CurrencyCode":"RUB","Amount":120},"Success":true}' } 6 | let(:decoded_data){ { model: { id: 123, currency_code: 'RUB', amount: 120 }, success: true } } 7 | 8 | subject{ CloudPayments::Client::Serializer::MultiJson.new(CloudPayments.config) } 9 | 10 | describe '#load' do 11 | specify{ expect(subject.load(encoded_data)).to eq(decoded_data) } 12 | end 13 | 14 | describe '#dump' do 15 | specify{ expect(subject.dump(decoded_data)).to eq(encoded_data) } 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/cloud_payments/client/response.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module CloudPayments 3 | class Client 4 | class Response 5 | attr_reader :status, :origin_body, :headers 6 | 7 | def initialize(status, body, headers = {}) 8 | @status, @origin_body, @headers = status, body, headers 9 | @origin_body = body.dup.force_encoding('UTF-8') if body.respond_to?(:force_encoding) 10 | end 11 | 12 | def body 13 | @body ||= headers && headers['content-type'] =~ /json/ ? serializer.load(origin_body) : origin_body 14 | end 15 | 16 | private 17 | 18 | def serializer 19 | CloudPayments.config.serializer 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/cloud_payments.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'date' 3 | require 'hashie' 4 | require 'faraday' 5 | require 'multi_json' 6 | require 'cloud_payments/version' 7 | require 'cloud_payments/config' 8 | require 'cloud_payments/namespaces' 9 | require 'cloud_payments/models' 10 | require 'cloud_payments/client' 11 | require 'cloud_payments/webhooks' 12 | 13 | module CloudPayments 14 | extend self 15 | 16 | def config=(value) 17 | @config = value 18 | end 19 | 20 | def config 21 | @config ||= Config.new 22 | end 23 | 24 | def configure 25 | yield config 26 | end 27 | 28 | def client=(value) 29 | @client = value 30 | end 31 | 32 | def client 33 | @client ||= Client.new 34 | end 35 | 36 | def webhooks 37 | @webhooks ||= Webhooks.new 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/cloud_payments/namespaces/cards.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module CloudPayments 3 | module Namespaces 4 | class Cards < Base 5 | def charge(attributes) 6 | response = request(:charge, attributes) 7 | instantiate(response[:model]) 8 | end 9 | 10 | def auth(attributes) 11 | response = request(:auth, attributes) 12 | instantiate(response[:model]) 13 | end 14 | 15 | def post3ds(attributes) 16 | response = request(:post3ds, attributes) 17 | instantiate(response[:model]) 18 | end 19 | 20 | private 21 | 22 | def instantiate(model) 23 | if model[:pa_req] 24 | Secure3D.new(model) 25 | else 26 | Transaction.new(model) 27 | end 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/support/examples.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | RSpec.shared_examples :not_raise_without_attribute do |key, method = nil| 3 | method = key unless method 4 | 5 | context "without `#{key}` attribute" do 6 | subject do 7 | attrs = attributes.dup 8 | attrs.delete(key) 9 | described_class.new(attrs) 10 | end 11 | 12 | specify{ expect{ subject }.not_to raise_error } 13 | end 14 | end 15 | 16 | RSpec.shared_examples :raise_without_attribute do |key, method = nil| 17 | method = key unless method 18 | 19 | context "without `#{key}` attribute" do 20 | subject do 21 | attrs = attributes.dup 22 | attrs.delete(key) 23 | described_class.new(attrs) 24 | end 25 | 26 | specify{ expect{ subject }.to raise_error(/\'#{method}\' is required/) } 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/fixtures/apis/orders/create/successful.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :request: 3 | :url: '/orders/create' 4 | :body: '{"Amount":10.0,"Currency":"RUB","Description":"Оплата на сайте example.com","Email":"client@test.local","RequireConfirmation":true,"SendEmail":false,"InvoiceId":"invoice_100","AccountId":"account_200","Phone":"+7(495)765-4321","SendSms":false,"SendWhatsApp":false}' 5 | :response: 6 | :body: > 7 | { 8 | "Model":{ 9 | "Id":"f2K8LV6reGE9WBFn", 10 | "Number":61, 11 | "Amount":10.0, 12 | "Currency":"RUB", 13 | "CurrencyCode":0, 14 | "Email":"client@test.local", 15 | "Description":"Оплата на сайте example.com", 16 | "RequireConfirmation":true, 17 | "Url":"https://orders.cloudpayments.ru/d/f2K8LV6reGE9WBFn" 18 | }, 19 | "Success":true 20 | } -------------------------------------------------------------------------------- /lib/cloud_payments/namespaces/kassa.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module CloudPayments 3 | module Namespaces 4 | class Kassa < Base 5 | InnNotProvided = Class.new(StandardError) 6 | TypeNotProvided = Class.new(StandardError) 7 | CustomerReceiptNotProvided = Class.new(StandardError) 8 | 9 | def self.resource_name 10 | 'kkt' 11 | end 12 | 13 | def receipt(attributes) 14 | attributes.fetch(:inn) { raise InnNotProvided.new('inn attribute is required') } 15 | attributes.fetch(:type) { raise TypeNotProvided.new('type attribute is required') } 16 | attributes.fetch(:customer_receipt) { raise CustomerReceiptNotProvided.new('customer_receipt is required') } 17 | 18 | request(:receipt, attributes) 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/cloud_payments/namespaces/subscriptions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module CloudPayments 3 | module Namespaces 4 | class Subscriptions < Base 5 | def find(id) 6 | response = request(:get, id: id) 7 | Subscription.new(response[:model]) 8 | end 9 | 10 | def find_all(account_id) 11 | response = request(:find, account_id: account_id) 12 | Array(response[:model]).map { |item| Subscription.new(item) } 13 | end 14 | 15 | def create(attributes) 16 | response = request(:create, attributes) 17 | Subscription.new(response[:model]) 18 | end 19 | 20 | def update(id, attributes) 21 | response = request(:update, attributes.merge(id: id)) 22 | Subscription.new(response[:model]) 23 | end 24 | 25 | def cancel(id) 26 | request(:cancel, id: id)[:success] 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/cloud_payments/models/on_kassa_receipt.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module CloudPayments 3 | # @see https://cloudpayments.ru/Docs/Notifications#receipt CloudPayments API 4 | class OnKassaReceipt < Model 5 | property :id, required: true 6 | property :document_number, required: true 7 | property :session_number, required: true 8 | property :fiscal_sign, required: true 9 | property :device_number, required: true 10 | property :reg_number, required: true 11 | property :inn, required: true 12 | property :type, required: true 13 | property :ofd, required: true 14 | property :url, required: true 15 | property :qr_code_url, required: true 16 | property :amount, transform_with: DecimalTransform, required: true 17 | property :date_time, transform_with: DateTimeTransform 18 | property :receipt 19 | property :invoice_id 20 | property :transaction_id 21 | property :account_id 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/fixtures/apis/subscriptions/get/successful.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :request: 3 | :url: '/subscriptions/get' 4 | :body: '{"Id":"sc_8cf8a9338fb"}' 5 | :response: 6 | :body: > 7 | { 8 | "Model":{ 9 | "Id":"sc_8cf8a9338fb", 10 | "AccountId":"user@example.com", 11 | "Description":"Monthly subscription", 12 | "Email":"user@example.com", 13 | "Amount":1.02, 14 | "CurrencyCode":0, 15 | "Currency":"RUB", 16 | "RequireConfirmation":false, 17 | "StartDateIso":"2014-08-09T11:49:41", 18 | "IntervalCode":1, 19 | "Interval":"Month", 20 | "Period":1, 21 | "MaxPeriods":12, 22 | "StatusCode":0, 23 | "Status":"Active", 24 | "SuccessfulTransactionsNumber":0, 25 | "FailedTransactionsNumber":0, 26 | "LastTransactionDateIso":"2014-08-09T11:49:41", 27 | "NextTransactionDateIso":"2014-08-09T11:49:41" 28 | }, 29 | "Success":true, 30 | "Message": null 31 | } 32 | -------------------------------------------------------------------------------- /spec/fixtures/apis/subscriptions/find/successful.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :request: 3 | :url: '/subscriptions/find' 4 | :body: '{"AccountId":"user@example.com"}' 5 | :response: 6 | :body: > 7 | { 8 | "Model":[{ 9 | "Id":"sc_8cf8a9338fb", 10 | "AccountId":"user@example.com", 11 | "Description":"Monthly subscription", 12 | "Email":"user@example.com", 13 | "Amount":1.02, 14 | "CurrencyCode":0, 15 | "Currency":"RUB", 16 | "RequireConfirmation":false, 17 | "StartDateIso":"2014-08-09T11:49:41", 18 | "IntervalCode":1, 19 | "Interval":"Month", 20 | "Period":1, 21 | "MaxPeriods":12, 22 | "StatusCode":0, 23 | "Status":"Active", 24 | "SuccessfulTransactionsNumber":0, 25 | "FailedTransactionsNumber":0, 26 | "LastTransactionDateIso":"2014-08-09T11:49:41", 27 | "NextTransactionDateIso":"2014-08-09T11:49:41" 28 | }], 29 | "Success":true, 30 | "Message": null 31 | } 32 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 3 | 4 | require 'bundler' 5 | Bundler.require(:default, :test) 6 | 7 | require 'webmock/rspec' 8 | 9 | WebMock.enable! 10 | WebMock.disable_net_connect! 11 | 12 | Dir["./spec/support/**/*.rb"].each { |f| require f } 13 | 14 | RSpec.configure do |config| 15 | config.mock_with :rspec 16 | config.include CloudPayments::RSpec::Helpers 17 | 18 | # these params are used in stubs, in basic auth 19 | # see webmock_stub in spec/support/helpers.rb 20 | def default_config 21 | CloudPayments::Config.new do |c| 22 | c.public_key = 'user' 23 | c.secret_key = 'pass' 24 | c.host = 'http://localhost:9292' 25 | c.log = false 26 | # c.raise_banking_errors = true 27 | end 28 | end 29 | 30 | CloudPayments.config = default_config 31 | 32 | config.after :each do 33 | CloudPayments.config = default_config 34 | CloudPayments.client = CloudPayments::Client.new 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/cloud_payments/namespaces.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'cloud_payments/namespaces/base' 3 | require 'cloud_payments/namespaces/cards' 4 | require 'cloud_payments/namespaces/tokens' 5 | require 'cloud_payments/namespaces/payments' 6 | require 'cloud_payments/namespaces/subscriptions' 7 | require 'cloud_payments/namespaces/orders' 8 | require 'cloud_payments/namespaces/kassa' 9 | require 'cloud_payments/namespaces/apple_pay' 10 | 11 | module CloudPayments 12 | module Namespaces 13 | def payments 14 | Payments.new(self) 15 | end 16 | 17 | def kassa 18 | Kassa.new(self) 19 | end 20 | 21 | def subscriptions 22 | Subscriptions.new(self) 23 | end 24 | 25 | def orders 26 | Orders.new(self) 27 | end 28 | 29 | def apple_pay 30 | ApplePay.new(self) 31 | end 32 | 33 | def ping 34 | !!(perform_request('/test').body || {})[:success] 35 | rescue ::Faraday::ConnectionFailed, ::Faraday::TimeoutError, CloudPayments::Client::ServerError => e 36 | false 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/fixtures/apis/subscriptions/update/successful.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :request: 3 | :url: '/subscriptions/update' 4 | :body: '{"AccountId":"user2@example.com","Email":"user2@example.com","MaxPeriods":6,"Id":"sc_8cf8a9338fb"}' 5 | :response: 6 | :body: > 7 | { 8 | "Model":{ 9 | "Id":"sc_8cf8a9338fb", 10 | "AccountId":"user2@example.com", 11 | "Description":"Monthly subscription", 12 | "Email":"user2@example.com", 13 | "Amount":1.02, 14 | "CurrencyCode":0, 15 | "Currency":"RUB", 16 | "RequireConfirmation":false, 17 | "StartDateIso":"2014-08-09T11:49:41", 18 | "IntervalCode":1, 19 | "Interval":"Month", 20 | "Period":1, 21 | "MaxPeriods":6, 22 | "StatusCode":0, 23 | "Status":"Active", 24 | "SuccessfulTransactionsNumber":0, 25 | "FailedTransactionsNumber":0, 26 | "LastTransactionDateIso":"2014-08-09T11:49:41", 27 | "NextTransactionDateIso":"2014-08-09T11:49:41" 28 | }, 29 | "Success":true, 30 | "Message": null 31 | } 32 | -------------------------------------------------------------------------------- /lib/cloud_payments/webhooks.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'openssl' 3 | require 'base64' 4 | 5 | module CloudPayments 6 | class Webhooks 7 | class HMACError < StandardError; end 8 | 9 | attr_reader :config 10 | 11 | def initialize(config = nil) 12 | @config = config || CloudPayments.config 13 | @digest = OpenSSL::Digest.new('sha256') 14 | @serializer = Client::Serializer::Base.new(config) 15 | end 16 | 17 | def data_valid?(data, hmac) 18 | Base64.decode64(hmac) == OpenSSL::HMAC.digest(@digest, config.secret_key, data) 19 | end 20 | 21 | def validate_data!(data, hmac) 22 | raise HMACError unless data_valid?(data, hmac) 23 | true 24 | end 25 | 26 | def kassa_receipt(data) 27 | OnKassaReceipt.new(@serializer.load(data)) 28 | end 29 | 30 | def on_recurrent(data) 31 | OnRecurrent.new(@serializer.load(data)) 32 | end 33 | 34 | def on_pay(data) 35 | OnPay.new(@serializer.load(data)) 36 | end 37 | 38 | def on_fail(data) 39 | OnFail.new(@serializer.load(data)) 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 undr 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /spec/cloud_payments_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'spec_helper' 3 | 4 | describe CloudPayments do 5 | describe '#config=' do 6 | specify{ expect{ CloudPayments.config = 'config' }.to change{ CloudPayments.config }.to('config') } 7 | end 8 | 9 | it 'supports global configuration' do 10 | CloudPayments.config.secret_key = "OLD_KEY" 11 | 12 | CloudPayments.configure do |c| 13 | c.secret_key = "NEW_KEY" 14 | end 15 | 16 | expect(CloudPayments.config.secret_key).to eq "NEW_KEY" 17 | expect(CloudPayments.client.config.secret_key).to eq "NEW_KEY" 18 | end 19 | 20 | it 'supports local configuration' do 21 | CloudPayments.config.secret_key = "OLD_KEY" 22 | 23 | config = CloudPayments::Config.new do |c| 24 | c.secret_key = "NEW_KEY" 25 | end 26 | client = CloudPayments::Client.new(config) 27 | webhooks = CloudPayments::Webhooks.new(config) 28 | 29 | expect(CloudPayments.config.secret_key).to eq "OLD_KEY" 30 | expect(config.secret_key).to eq "NEW_KEY" 31 | expect(client.config.secret_key).to eq "NEW_KEY" 32 | expect(webhooks.config.secret_key).to eq "NEW_KEY" 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/cloud_payments/models/on_fail.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module CloudPayments 3 | # @see https://cloudpayments.ru/Docs/Notifications#fail CloudPayments API 4 | class OnFail < Model 5 | property :id, from: :transaction_id, required: true 6 | property :amount, transform_with: DecimalTransform, required: true 7 | property :currency, required: true 8 | property :date_time, transform_with: DateTimeTransform 9 | property :card_first_six, required: true 10 | property :card_last_four, required: true 11 | property :card_type, required: true 12 | property :card_exp_date, required: true 13 | property :test_mode, required: true 14 | property :reason, required: true 15 | property :reason_code, required: true 16 | property :invoice_id 17 | property :account_id 18 | property :subscription_id 19 | property :name 20 | property :email 21 | property :ip_address 22 | property :ip_country 23 | property :ip_city 24 | property :ip_region 25 | property :ip_district 26 | property :issuer 27 | property :issuer_bank_country 28 | property :description 29 | property :metadata, from: :data, default: {} 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /cloud_payments.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # frozen_string_literal: true 3 | lib = File.expand_path('../lib', __FILE__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | require 'cloud_payments/version' 6 | 7 | Gem::Specification.new do |spec| 8 | spec.name = 'cloud_payments' 9 | spec.version = CloudPayments::VERSION 10 | spec.authors = ['undr', 'kirillplatonov'] 11 | spec.email = ['undr@yandex.ru', 'mail@kirillplatonov.com'] 12 | spec.summary = %q{CloudPayments ruby client} 13 | spec.description = %q{CloudPayments ruby client} 14 | spec.homepage = 'https://github.com/platmart/cloud_payments' 15 | spec.license = 'MIT' 16 | 17 | spec.files = `git ls-files -z`.split("\x0") 18 | spec.executables = spec.files.grep(%r{^bin/}){|f| File.basename(f) } 19 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 20 | spec.require_paths = ['lib'] 21 | 22 | spec.add_dependency 'faraday', '< 3.0' 23 | spec.add_dependency 'multi_json', '~> 1.11' 24 | spec.add_dependency 'hashie', '~> 3.4' 25 | 26 | spec.add_development_dependency 'rake', '~> 13.0' 27 | spec.add_development_dependency 'rspec', '~> 3.9' 28 | end 29 | -------------------------------------------------------------------------------- /lib/cloud_payments/models/on_recurrent.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module CloudPayments 3 | class OnRecurrent < Model 4 | property :id, required: true 5 | property :account_id, required: true 6 | property :description, required: true 7 | property :email, required: true 8 | property :amount, transform_with: DecimalTransform, required: true 9 | property :currency, required: true 10 | property :require_confirmation, transform_with: BooleanTransform, required: true 11 | property :started_at, from: :start_date, with: DateTimeTransform, required: true 12 | property :interval, required: true 13 | property :period, transform_with: IntegralTransform, required: true 14 | property :status, required: true 15 | property :successful_transactions, from: :successful_transactions_number, with: IntegralTransform, required: true 16 | property :failed_transactions, from: :failed_transactions_number, with: IntegralTransform, required: true 17 | property :last_transaction_at, from: :last_transaction_date, with: DateTimeTransform 18 | property :next_transaction_at, from: :next_transaction_date, with: DateTimeTransform 19 | 20 | include LikeSubscription 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/cloud_payments/namespaces/payments.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module CloudPayments 3 | module Namespaces 4 | class Payments < Base 5 | def cards 6 | Cards.new(client, resource_path) 7 | end 8 | 9 | def tokens 10 | Tokens.new(client, resource_path) 11 | end 12 | 13 | def confirm(id, amount) 14 | request(:confirm, transaction_id: id, amount: amount)[:success] 15 | end 16 | 17 | def void(id) 18 | request(:void, transaction_id: id)[:success] 19 | end 20 | 21 | alias :cancel :void 22 | 23 | def refund(id, amount) 24 | request(:refund, transaction_id: id, amount: amount)[:success] 25 | end 26 | 27 | def post3ds(id, pa_res) 28 | response = request(:post3ds, transaction_id: id, pa_res: pa_res) 29 | Transaction.new(response[:model]) 30 | end 31 | 32 | def get(id) 33 | response = request(:get, transaction_id: id) 34 | Transaction.new(response[:model]) 35 | end 36 | 37 | def find(invoice_id) 38 | response = request(:find, invoice_id: invoice_id) 39 | Transaction.new(response[:model]) 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/fixtures/apis/subscriptions/create/successful.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :request: 3 | :url: '/subscriptions/create' 4 | :body: '{"Token":"477BBA133C182267F","AccountId":"user@example.com","Description":"Monthly subscription","Email":"user@example.com","Amount":1.02,"Currency":"RUB","RequireConfirmation":false,"StartDate":"2014-08-09T11:49:41","Interval":"Month","Period":1,"MaxPeriods":12}' 5 | :response: 6 | :body: > 7 | { 8 | "Model":{ 9 | "Id":"sc_8cf8a9338fb", 10 | "AccountId":"user@example.com", 11 | "Description":"Monthly subscription", 12 | "Email":"user@example.com", 13 | "Amount":1.02, 14 | "CurrencyCode":0, 15 | "Currency":"RUB", 16 | "RequireConfirmation":false, 17 | "StartDateIso":"2014-08-09T11:49:41", 18 | "IntervalCode":1, 19 | "Interval":"Month", 20 | "Period":1, 21 | "MaxPeriods":12, 22 | "StatusCode":0, 23 | "Status":"Active", 24 | "SuccessfulTransactionsNumber":0, 25 | "FailedTransactionsNumber":0, 26 | "LastTransactionDateIso":"2014-08-09T11:49:41", 27 | "NextTransactionDateIso":"2014-08-09T11:49:41" 28 | }, 29 | "Success":true, 30 | "Message": null 31 | } 32 | -------------------------------------------------------------------------------- /lib/cloud_payments/client/gateway_errors.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module CloudPayments 3 | class Client 4 | class ReasonedGatewayError < StandardError; end 5 | module GatewayErrors; end 6 | 7 | REASON_CODES = { 8 | 5001 => 'ReferToCardIssuer', 9 | 5005 => 'DoNotHonor', 10 | 5006 => 'Error', 11 | 5012 => 'Invalid', 12 | 5013 => 'AmountError', 13 | 5030 => 'FormatError', 14 | 5031 => 'BankNotSupportedBySwitch', 15 | 5034 => 'SuspectedFraud', 16 | 5041 => 'LostCard', 17 | 5043 => 'StolenCard', 18 | 5051 => 'InsufficientFunds', 19 | 5054 => 'ExpiredCard', 20 | 5057 => 'TransactionNotPermitted', 21 | 5065 => 'ExceedWithdrawalFrequency', 22 | 5082 => 'IncorrectCVV', 23 | 5091 => 'Timeout', 24 | 5092 => 'CannotReachNetwork', 25 | 5096 => 'SystemError', 26 | 5204 => 'UnableToProcess', 27 | 5206 => 'AuthenticationFailed', 28 | 5207 => 'AuthenticationUnavailable', 29 | 5300 => 'AntiFraud' 30 | } 31 | 32 | GATEWAY_ERRORS = REASON_CODES.inject({}) do |result, error| 33 | status, name = error 34 | result[status] = GatewayErrors.const_set(name, Class.new(ReasonedGatewayError)) 35 | result 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/cloud_payments/models/on_pay.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module CloudPayments 3 | class OnPay < Model 4 | property :id, from: :transaction_id, required: true 5 | property :amount, transform_with: DecimalTransform, required: true 6 | property :currency, required: true 7 | property :invoice_id 8 | property :account_id 9 | property :subscription_id 10 | property :email 11 | property :description 12 | property :metadata, from: :data, default: {} 13 | property :date_time, transform_with: DateTimeTransform 14 | property :auth_code 15 | property :test_mode, required: true 16 | property :ip_address 17 | property :ip_country 18 | property :ip_city 19 | property :ip_region 20 | property :ip_district 21 | property :ip_lat, from: :ip_latitude 22 | property :ip_lng, from: :ip_longitude 23 | property :card_first_six, required: true 24 | property :card_last_four, required: true 25 | property :card_type, required: true 26 | property :card_type_code 27 | property :card_exp_date 28 | property :name 29 | property :issuer 30 | property :issuer_bank_country 31 | property :status, required: true 32 | property :status_code 33 | property :reason 34 | property :reason_code 35 | property :token 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/cloud_payments/namespaces/kassa_spec.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # encoding: utf-8 3 | # frozen_string_literal: true 4 | require 'spec_helper' 5 | 6 | describe CloudPayments::Namespaces::Kassa do 7 | subject{ described_class.new(CloudPayments.client) } 8 | 9 | describe '#receipt' do 10 | let(:attributes) do 11 | { 12 | inn: '7708806666', 13 | type: 'Income', 14 | customer_receipt: { 15 | items: [ 16 | { 17 | amount: '13350.00', 18 | label: 'Good Description', 19 | price: '13350.00', 20 | quantity: 1.0, 21 | vat: 0 22 | } 23 | ] 24 | } 25 | } 26 | end 27 | 28 | context do 29 | before{ attributes.delete(:inn) } 30 | specify{ expect{subject.receipt(attributes)}.to raise_error(described_class::InnNotProvided) } 31 | end 32 | 33 | context do 34 | before{ attributes.delete(:type) } 35 | specify{ expect{subject.receipt(attributes)}.to raise_error(described_class::TypeNotProvided) } 36 | end 37 | 38 | context do 39 | before{ attributes.delete(:customer_receipt) } 40 | specify{ expect{subject.receipt(attributes)}.to raise_error(described_class::CustomerReceiptNotProvided) } 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/cloud_payments/config.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module CloudPayments 3 | class Config 4 | attr_accessor :connection_options, :serializer, :log, :public_key, :secret_key, :host, :raise_banking_errors 5 | attr_writer :logger 6 | 7 | DEFAULT_LOGGER = ->{ 8 | require 'logger' 9 | logger = Logger.new(STDERR) 10 | logger.progname = 'cloud_payments' 11 | logger.formatter = ->(severity, datetime, progname, msg){ "#{datetime} (#{progname}): #{msg}\n" } 12 | logger 13 | } 14 | 15 | def initialize 16 | @log = false 17 | @serializer = Client::Serializer::MultiJson.new(self) 18 | @connection_options = {} 19 | @connection_block = nil 20 | @host = 'https://api.cloudpayments.ru' 21 | if block_given? 22 | yield self 23 | end 24 | end 25 | 26 | def logger 27 | @logger ||= log ? DEFAULT_LOGGER.call : nil 28 | end 29 | 30 | def available_currencies 31 | %w{RUB USD EUR} 32 | end 33 | 34 | def connection_block(&block) 35 | if block_given? 36 | @connection_block = block 37 | else 38 | @connection_block 39 | end 40 | end 41 | 42 | def dup 43 | clone = super 44 | clone.connection_options = connection_options.dup 45 | clone 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/cloud_payments/models/subscription.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module CloudPayments 3 | class Subscription < Model 4 | include LikeSubscription 5 | 6 | property :id, required: true 7 | property :account_id, required: true 8 | property :description 9 | property :email, required: true 10 | property :amount, transform_with: DecimalTransform, required: true 11 | property :currency, required: true 12 | property :currency_code, required: true 13 | property :require_confirmation, transform_with: BooleanTransform, required: true 14 | property :started_at, from: :start_date_iso, with: DateTimeTransform, required: true 15 | property :interval, required: true 16 | property :interval_code, required: true 17 | property :period, transform_with: IntegralTransform, required: true 18 | property :max_periods 19 | property :status, required: true 20 | property :status_code, required: true 21 | property :successful_transactions, from: :successful_transactions_number, required: true 22 | property :failed_transactions, from: :failed_transactions_number, required: true 23 | property :last_transaction_at, from: :last_transaction_date_iso, with: DateTimeTransform 24 | property :next_transaction_at, from: :next_transaction_date_iso, with: DateTimeTransform 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/cloud_payments/client/response_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'spec_helper' 3 | 4 | describe CloudPayments::Client::Response do 5 | let(:status){ 200 } 6 | let(:body){ '{"Model":{"Id":123,"CurrencyCode":"RUB","Amount":120},"Success":true}'.dup.force_encoding('CP1251').freeze } 7 | let(:headers){ { 'content-type' => 'application/json' } } 8 | 9 | subject{ CloudPayments::Client::Response.new(status, body, headers) } 10 | 11 | describe '#body' do 12 | specify{ expect(subject.body).to eq(model: { id: 123, currency_code: 'RUB', amount: 120 }, success: true) } 13 | 14 | context 'wnen content type does not match /json/' do 15 | let(:headers){ { 'content-type' => 'text/plain' } } 16 | specify{ expect(subject.body).to eq(body) } 17 | specify{ expect(subject.body.encoding.name).to eq('UTF-8') } 18 | end 19 | end 20 | 21 | describe '#origin_body' do 22 | specify{ expect(subject.origin_body).to eq(body) } 23 | 24 | context 'wnen content type does not match /json/' do 25 | let(:headers){ { 'content-type' => 'text/plain' } } 26 | specify{ expect(subject.origin_body).to eq(body) } 27 | end 28 | end 29 | 30 | describe '#headers' do 31 | specify{ expect(subject.headers).to eq(headers) } 32 | end 33 | 34 | describe '#status' do 35 | specify{ expect(subject.status).to eq(status) } 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/cloud_payments/models/secure3d_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'spec_helper' 3 | 4 | describe CloudPayments::Secure3D do 5 | describe 'properties' do 6 | let(:attributes){ { 7 | transaction_id: 504, 8 | pa_req: 'pa_req', 9 | acs_url: 'acs_url' 10 | } } 11 | 12 | subject{ CloudPayments::Secure3D.new(attributes) } 13 | 14 | specify{ expect(subject.transaction_id).to eq(504) } 15 | specify{ expect(subject.pa_req).to eq('pa_req') } 16 | specify{ expect(subject.acs_url).to eq('acs_url') } 17 | 18 | context 'without any attributes' do 19 | let(:attributes){ {} } 20 | specify{ expect{ subject }.to raise_error(/\'transaction_id\' is required/) } 21 | end 22 | 23 | context 'without `transaction_id` attribute' do 24 | let(:attributes){ { pa_req: 'pa_req', acs_url: 'acs_url' } } 25 | specify{ expect{ subject }.to raise_error(/\'transaction_id\' is required/) } 26 | end 27 | 28 | context 'without `pa_req` attribute' do 29 | let(:attributes){ { transaction_id: 504, acs_url: 'acs_url' } } 30 | specify{ expect{ subject }.to raise_error(/\'pa_req\' is required/) } 31 | end 32 | 33 | context 'without `acs_url` attribute' do 34 | let(:attributes){ { transaction_id: 504, pa_req: 'pa_req' } } 35 | specify{ expect{ subject }.to raise_error(/\'acs_url\' is required/) } 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/fixtures/apis/payments/find/failed.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :request: 3 | :url: '/payments/find' 4 | :body: '{"InvoiceId":"1234567"}' 5 | :response: 6 | :body: > 7 | { 8 | "Model": { 9 | "TransactionId": 12345, 10 | "Amount": 10.0, 11 | "Currency": "RUB", 12 | "CurrencyCode": 0, 13 | "PaymentAmount": 10.0, 14 | "PaymentCurrency": "RUB", 15 | "PaymentCurrencyCode": 0, 16 | "InvoiceId": "1234567", 17 | "AccountId": "user_x", 18 | "Email": null, 19 | "Description": "Payment for goods on example.com", 20 | "JsonData": null, 21 | "CreatedDate": "\/Date(1401718880000)\/", 22 | "CreatedDateIso":"2014-08-09T11:49:41", 23 | "TestMode": true, 24 | "IpAddress": "195.91.194.13", 25 | "IpCountry": "RU", 26 | "IpCity": "Ufa", 27 | "IpRegion": "Bashkortostan Republic", 28 | "IpDistrict": "Volga Federal District", 29 | "IpLatitude": 54.7355, 30 | "IpLongitude": 55.991982, 31 | "CardFirstSix": "411111", 32 | "CardLastFour": "1111", 33 | "CardType": "Visa", 34 | "CardTypeCode": 0, 35 | "IssuerBankCountry": "RU", 36 | "Status": "Declined", 37 | "StatusCode": 5, 38 | "Reason": "InsufficientFunds", 39 | "ReasonCode": 5051, 40 | "CardHolderMessage": "Insufficient funds on account", 41 | "Name": "CARDHOLDER NAME" 42 | }, 43 | "Success": false, 44 | "Message": null 45 | } 46 | -------------------------------------------------------------------------------- /spec/fixtures/apis/payments/post3ds/failed.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :request: 3 | :url: '/payments/post3ds' 4 | :body: '{"TransactionId":12345,"PaRes":"eJxVUdtugkAQ"}' 5 | :response: 6 | :body: > 7 | { 8 | "Model":{ 9 | "TransactionId":12345, 10 | "Amount":10.0, 11 | "Currency":"RUB", 12 | "CurrencyCode":0, 13 | "PaymentAmount":10.0, 14 | "PaymentCurrency":"RUB", 15 | "PaymentCurrencyCode":0, 16 | "InvoiceId":"1234567", 17 | "AccountId":"user_x", 18 | "Email":null, 19 | "Description":"Payment for goods on example.com", 20 | "JsonData":null, 21 | "CreatedDate":"\/Date(1401718880000)\/", 22 | "CreatedDateIso":"2014-08-09T11:49:41", 23 | "TestMode":true, 24 | "IpAddress":"195.91.194.13", 25 | "IpCountry":"RU", 26 | "IpCity":"Ufa", 27 | "IpRegion":"Bashkortostan Republic", 28 | "IpDistrict":"Volga Federal District", 29 | "IpLatitude":54.7355, 30 | "IpLongitude":55.991982, 31 | "CardFirstSix":"411111", 32 | "CardLastFour":"1111", 33 | "CardType":"Visa", 34 | "CardTypeCode":0, 35 | "IssuerBankCountry":"RU", 36 | "Status":"Declined", 37 | "StatusCode":5, 38 | "Reason":"InsufficientFunds", 39 | "ReasonCode":5051, //decline code 40 | "CardHolderMessage":"Insufficient funds on account", 41 | "Name":"CARDHOLDER NAME" 42 | }, 43 | "Success":false, 44 | "Message": null 45 | } 46 | -------------------------------------------------------------------------------- /spec/fixtures/apis/cards/post3ds/failed.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :request: 3 | :url: '/payments/cards/post3ds' 4 | :body: '{"TransactionId":12345,"PaRes":"AQ=="}' 5 | :response: 6 | :body: > 7 | { 8 | "Model": { 9 | "TransactionId": 12345, 10 | "Amount": 10.0, 11 | "Currency": "RUB", 12 | "CurrencyCode": 0, 13 | "PaymentAmount": 10.0, 14 | "PaymentCurrency": "RUB", 15 | "PaymentCurrencyCode": 0, 16 | "InvoiceId": "1234567", 17 | "AccountId": "user_x", 18 | "Email": null, 19 | "Description": "Payment for goods on example.com", 20 | "JsonData": null, 21 | "CreatedDate": "\/Date(1401718880000)\/", 22 | "CreatedDateIso":"2014-08-09T11:49:41", 23 | "TestMode": true, 24 | "IpAddress": "195.91.194.13", 25 | "IpCountry": "RU", 26 | "IpCity": "Ufa", 27 | "IpRegion": "Bashkortostan Republic", 28 | "IpDistrict": "Volga Federal District", 29 | "IpLatitude": 54.7355, 30 | "IpLongitude": 55.991982, 31 | "CardFirstSix": "411111", 32 | "CardLastFour": "1111", 33 | "CardType": "Visa", 34 | "CardTypeCode": 0, 35 | "IssuerBankCountry": "RU", 36 | "Status": "Declined", 37 | "StatusCode": 5, 38 | "Reason": "InsufficientFunds", 39 | "ReasonCode": 5051, 40 | "CardHolderMessage": "Insufficient funds on account", 41 | "Name": "CARDHOLDER NAME" 42 | }, 43 | "Success": false, 44 | "Message": null 45 | } 46 | -------------------------------------------------------------------------------- /spec/fixtures/apis/payments/get/failed.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :request: 3 | :url: '/payments/get' 4 | :body: '{"TransactionId":12345}' 5 | :response: 6 | :body: > 7 | { 8 | "Model": { 9 | "TransactionId": 12345, 10 | "Amount": 10.0, 11 | "Currency": "RUB", 12 | "CurrencyCode": 0, 13 | "PaymentAmount": 10.0, 14 | "PaymentCurrency": "RUB", 15 | "PaymentCurrencyCode": 0, 16 | "InvoiceId": "1234567", 17 | "AccountId": "user_x", 18 | "Email": null, 19 | "Description": "Payment for goods on example.com", 20 | "JsonData": null, 21 | "CreatedDate": "\/Date(1401718880000)\/", 22 | "CreatedDateIso":"2014-08-09T11:49:41", 23 | "TestMode": true, 24 | "IpAddress": "195.91.194.13", 25 | "IpCountry": "RU", 26 | "IpCity": "Ufa", 27 | "IpRegion": "Bashkortostan Republic", 28 | "IpDistrict": "Volga Federal District", 29 | "IpLatitude": 54.7355, 30 | "IpLongitude": 55.991982, 31 | "CardFirstSix": "411111", 32 | "CardLastFour": "1111", 33 | "CardType": "Visa", 34 | "CardTypeCode": 0, 35 | "IssuerBankCountry": "RU", 36 | "Status": "Declined", 37 | "StatusCode": 5, 38 | "Reason": "InsufficientFunds", 39 | "ReasonCode": 5051, 40 | "Refunded": false, 41 | "CardHolderMessage": "Insufficient funds on account", 42 | "Name": "CARDHOLDER NAME" 43 | }, 44 | "Success": false, 45 | "Message": null 46 | } 47 | -------------------------------------------------------------------------------- /lib/cloud_payments/client/serializer/base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module CloudPayments 3 | class Client 4 | module Serializer 5 | class Base 6 | attr_reader :config 7 | 8 | def initialize(config) 9 | @config = config 10 | end 11 | 12 | def load(json) 13 | convert_keys_from_api(json) 14 | end 15 | 16 | def dump(hash) 17 | convert_keys_to_api(hash) 18 | end 19 | 20 | protected 21 | 22 | def convert_keys_from_api(attributes) 23 | attributes.each_with_object({}) do |(key, value), result| 24 | value = case value 25 | when Hash 26 | convert_keys_from_api(value) 27 | when Array 28 | value.map { |item| convert_keys_from_api(item) } 29 | else 30 | value 31 | end 32 | 33 | key = key.to_s.gsub(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2') 34 | key.gsub!(/([a-z\d])([A-Z])/, '\1_\2') 35 | key.tr!('-', '_') 36 | key.downcase! 37 | result[key.to_sym] = value 38 | end 39 | end 40 | 41 | def convert_keys_to_api(attributes) 42 | attributes.each_with_object({}) do |(key, value), result| 43 | value = convert_keys_to_api(value) if value.is_a?(Hash) 44 | 45 | key = key.to_s.gsub(/^[a-z\d]*/){ $&.capitalize } 46 | key.gsub!(/(?:_|(\/))([a-z\d]*)/i){ "#{$1}#{$2.capitalize}" } 47 | key.gsub!('/', '::') 48 | result[key] = value 49 | end 50 | end 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /spec/cloud_payments/namespaces_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'spec_helper' 3 | 4 | describe CloudPayments::Namespaces do 5 | subject{ CloudPayments::Client.new } 6 | 7 | describe '#payments' do 8 | specify{ expect(subject.payments).to be_instance_of(CloudPayments::Namespaces::Payments) } 9 | end 10 | 11 | describe '#subscriptions' do 12 | specify{ expect(subject.subscriptions).to be_instance_of(CloudPayments::Namespaces::Subscriptions) } 13 | end 14 | 15 | describe '#ping' do 16 | context 'when successful response' do 17 | before{ stub_api_request('ping/successful').perform } 18 | specify{ expect(subject.ping).to be_truthy } 19 | end 20 | 21 | context 'when failed response' do 22 | before{ stub_api_request('ping/failed').perform } 23 | specify{ expect(subject.ping).to be_falsy } 24 | end 25 | 26 | context 'when empty response' do 27 | before{ stub_api_request('ping/failed').to_return(body: '') } 28 | specify{ expect(subject.ping).to be_falsy } 29 | end 30 | 31 | context 'when error response' do 32 | before{ stub_api_request('ping/failed').to_return(status: 404) } 33 | specify{ expect(subject.ping).to be_falsy } 34 | end 35 | 36 | context 'when exception occurs while request' do 37 | before{ stub_api_request('ping/failed').to_raise(::Faraday::ConnectionFailed) } 38 | specify{ expect(subject.ping).to be_falsy } 39 | end 40 | 41 | context 'when timeout occurs while request' do 42 | before{ stub_api_request('ping/failed').to_timeout } 43 | specify{ expect(subject.ping).to be_falsy } 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /spec/fixtures/apis/payments/find/successful.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :request: 3 | :url: '/payments/find' 4 | :body: '{"InvoiceId":"1234567"}' 5 | :response: 6 | :body: > 7 | { 8 | "Model":{ 9 | "TransactionId":12345, 10 | "Amount":10.0, 11 | "Currency":"RUB", 12 | "CurrencyCode":0, 13 | "InvoiceId":"1234567", 14 | "AccountId":"user_x", 15 | "Email":null, 16 | "Description":"Payment for goods on example.com", 17 | "JsonData":null, 18 | "CreatedDate":"\/Date(1401718880000)\/", 19 | "CreatedDateIso":"2014-08-09T11:49:41", 20 | "AuthDate":"\/Date(1401733880523)\/", 21 | "AuthDateIso":"2014-08-09T11:49:42", 22 | "ConfirmDate":"\/Date(1401733880523)\/", 23 | "ConfirmDateIso":"2014-08-09T11:49:42", 24 | "AuthCode":"123456", 25 | "TestMode":true, 26 | "IpAddress":"195.91.194.13", 27 | "IpCountry":"RU", 28 | "IpCity":"Ufa", 29 | "IpRegion":"Bashkortostan Republic", 30 | "IpDistrict":"Volga Federal District", 31 | "IpLatitude":54.7355, 32 | "IpLongitude":55.991982, 33 | "CardFirstSix":"411111", 34 | "CardLastFour":"1111", 35 | "CardType":"Visa", 36 | "CardTypeCode":0, 37 | "IssuerBankCountry":"RU", 38 | "Status":"Completed", 39 | "StatusCode":3, 40 | "Reason":"Approved", 41 | "ReasonCode":0, 42 | "CardHolderMessage":"Payment successful", 43 | "Name":"CARDHOLDER NAME", 44 | "Token":"a4e67841-abb0-42de-a364-d1d8f9f4b3c0" 45 | }, 46 | "Success":true, 47 | "Message": null 48 | } 49 | -------------------------------------------------------------------------------- /spec/fixtures/apis/cards/post3ds/successful.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :request: 3 | :url: '/payments/cards/post3ds' 4 | :body: '{"TransactionId":12345,"PaRes":"AQ=="}' 5 | :response: 6 | :body: > 7 | { 8 | "Model":{ 9 | "TransactionId":12345, 10 | "Amount":10.0, 11 | "Currency":"RUB", 12 | "CurrencyCode":0, 13 | "InvoiceId":"1234567", 14 | "AccountId":"user_x", 15 | "Email":null, 16 | "Description":"Payment for goods on example.com", 17 | "JsonData":null, 18 | "CreatedDate":"\/Date(1401718880000)\/", 19 | "CreatedDateIso":"2014-08-09T11:49:41", 20 | "AuthDate":"\/Date(1401733880523)\/", 21 | "AuthDateIso":"2014-08-09T11:49:42", 22 | "ConfirmDate":"\/Date(1401733880523)\/", 23 | "ConfirmDateIso":"2014-08-09T11:49:42", 24 | "AuthCode":"123456", 25 | "TestMode":true, 26 | "IpAddress":"195.91.194.13", 27 | "IpCountry":"RU", 28 | "IpCity":"Ufa", 29 | "IpRegion":"Bashkortostan Republic", 30 | "IpDistrict":"Volga Federal District", 31 | "IpLatitude":54.7355, 32 | "IpLongitude":55.991982, 33 | "CardFirstSix":"411111", 34 | "CardLastFour":"1111", 35 | "CardType":"Visa", 36 | "CardTypeCode":0, 37 | "IssuerBankCountry":"RU", 38 | "Status":"Completed", 39 | "StatusCode":3, 40 | "Reason":"Approved", 41 | "ReasonCode":0, 42 | "CardHolderMessage":"Payment successful", 43 | "Name":"CARDHOLDER NAME", 44 | "Token":"a4e67841-abb0-42de-a364-d1d8f9f4b3c0" 45 | }, 46 | "Success":true, 47 | "Message": null 48 | } 49 | -------------------------------------------------------------------------------- /spec/fixtures/apis/payments/get/refunded.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :request: 3 | :url: '/payments/get' 4 | :body: '{"TransactionId":12345}' 5 | :response: 6 | :body: > 7 | { 8 | "Model":{ 9 | "TransactionId":12345, 10 | "Amount":10.0, 11 | "Currency":"RUB", 12 | "CurrencyCode":0, 13 | "InvoiceId":"1234567", 14 | "AccountId":"user_x", 15 | "Email":null, 16 | "Description":"Payment for goods on example.com", 17 | "JsonData":null, 18 | "CreatedDate":"\/Date(1401718880000)\/", 19 | "CreatedDateIso":"2014-08-09T11:49:41", 20 | "AuthDate":"\/Date(1401733880523)\/", 21 | "AuthDateIso":"2014-08-09T11:49:42", 22 | "ConfirmDate":"\/Date(1401733880523)\/", 23 | "ConfirmDateIso":"2014-08-09T11:49:42", 24 | "AuthCode":"123456", 25 | "TestMode":true, 26 | "IpAddress":"195.91.194.13", 27 | "IpCountry":"RU", 28 | "IpCity":"Ufa", 29 | "IpRegion":"Bashkortostan Republic", 30 | "IpDistrict":"Volga Federal District", 31 | "IpLatitude":54.7355, 32 | "IpLongitude":55.991982, 33 | "CardFirstSix":"411111", 34 | "CardLastFour":"1111", 35 | "CardType":"Visa", 36 | "CardTypeCode":0, 37 | "IssuerBankCountry":"RU", 38 | "Status":"Completed", 39 | "StatusCode":3, 40 | "Reason":"Approved", 41 | "ReasonCode":0, 42 | "Refunded": true, 43 | "CardHolderMessage":"Payment successful", 44 | "Name":"CARDHOLDER NAME", 45 | "Token":"a4e67841-abb0-42de-a364-d1d8f9f4b3c0" 46 | }, 47 | "Success":true, 48 | "Message": null 49 | } 50 | -------------------------------------------------------------------------------- /spec/fixtures/apis/payments/get/successful.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :request: 3 | :url: '/payments/get' 4 | :body: '{"TransactionId":12345}' 5 | :response: 6 | :body: > 7 | { 8 | "Model":{ 9 | "TransactionId":12345, 10 | "Amount":10.0, 11 | "Currency":"RUB", 12 | "CurrencyCode":0, 13 | "InvoiceId":"1234567", 14 | "AccountId":"user_x", 15 | "Email":null, 16 | "Description":"Payment for goods on example.com", 17 | "JsonData":null, 18 | "CreatedDate":"\/Date(1401718880000)\/", 19 | "CreatedDateIso":"2014-08-09T11:49:41", 20 | "AuthDate":"\/Date(1401733880523)\/", 21 | "AuthDateIso":"2014-08-09T11:49:42", 22 | "ConfirmDate":"\/Date(1401733880523)\/", 23 | "ConfirmDateIso":"2014-08-09T11:49:42", 24 | "AuthCode":"123456", 25 | "TestMode":true, 26 | "IpAddress":"195.91.194.13", 27 | "IpCountry":"RU", 28 | "IpCity":"Ufa", 29 | "IpRegion":"Bashkortostan Republic", 30 | "IpDistrict":"Volga Federal District", 31 | "IpLatitude":54.7355, 32 | "IpLongitude":55.991982, 33 | "CardFirstSix":"411111", 34 | "CardLastFour":"1111", 35 | "CardType":"Visa", 36 | "CardTypeCode":0, 37 | "IssuerBankCountry":"RU", 38 | "Status":"Completed", 39 | "StatusCode":3, 40 | "Reason":"Approved", 41 | "ReasonCode":0, 42 | "Refunded": false, 43 | "CardHolderMessage":"Payment successful", 44 | "Name":"CARDHOLDER NAME", 45 | "Token":"a4e67841-abb0-42de-a364-d1d8f9f4b3c0" 46 | }, 47 | "Success":true, 48 | "Message": null 49 | } 50 | -------------------------------------------------------------------------------- /spec/fixtures/apis/payments/post3ds/successful.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :request: 3 | :url: '/payments/post3ds' 4 | :body: '{"TransactionId":12345,"PaRes":"eJxVUdtugkAQ"}' 5 | :response: 6 | :body: > 7 | { 8 | "Model":{ 9 | "TransactionId":12345, 10 | "Amount":10.0, 11 | "Currency":"RUB", 12 | "CurrencyCode":0, 13 | "InvoiceId":"1234567", 14 | "AccountId":"user_x", 15 | "Email":null, 16 | "Description":"Payment for goods on example.com", 17 | "JsonData":null, 18 | "CreatedDate":"\/Date(1401718880000)\/", 19 | "CreatedDateIso":"2014-08-09T11:49:41", 20 | "AuthDate":"\/Date(1401733880523)\/", 21 | "AuthDateIso":"2014-08-09T11:49:42", 22 | "ConfirmDate":"\/Date(1401733880523)\/", 23 | "ConfirmDateIso":"2014-08-09T11:49:42", 24 | "AuthCode":"123456", 25 | "TestMode":true, 26 | "IpAddress":"195.91.194.13", 27 | "IpCountry":"RU", 28 | "IpCity":"Ufa", 29 | "IpRegion":"Bashkortostan Republic", 30 | "IpDistrict":"Volga Federal District", 31 | "IpLatitude":54.7355, 32 | "IpLongitude":55.991982, 33 | "CardFirstSix":"411111", 34 | "CardLastFour":"1111", 35 | "CardType":"Visa", 36 | "CardTypeCode":0, 37 | "IssuerBankCountry":"RU", 38 | "Status":"Completed", 39 | "StatusCode":3, 40 | "Reason":"Approved", 41 | "ReasonCode":0, 42 | "CardHolderMessage":"Payment successful", 43 | "Name":"CARDHOLDER NAME", 44 | "Token":"a4e67841-abb0-42de-a364-d1d8f9f4b3c0" 45 | }, 46 | "Success":true, 47 | "Message": null 48 | } 49 | -------------------------------------------------------------------------------- /spec/fixtures/apis/cards/auth/failed.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :request: 3 | :url: '/payments/cards/auth' 4 | :body: '{"Amount":10,"Currency":"RUB","InvoiceId":"1234567","Description":"Payment for goods on example.com","AccountId":"user_x","Name":"CARDHOLDER NAME","CardCryptogramPacket":"01492500008719030128SM"}' 5 | :response: 6 | :body: > 7 | { 8 | "Model": { 9 | "TransactionId": 12345, 10 | "Amount": 10.0, 11 | "Currency": "RUB", 12 | "CurrencyCode": 0, 13 | "PaymentAmount": 10.0, 14 | "PaymentCurrency": "RUB", 15 | "PaymentCurrencyCode": 0, 16 | "InvoiceId": "1234567", 17 | "AccountId": "user_x", 18 | "Email": null, 19 | "Description": "Payment for goods on example.com", 20 | "JsonData": null, 21 | "CreatedDate": "\/Date(1401718880000)\/", 22 | "CreatedDateIso":"2014-08-09T11:49:41", 23 | "TestMode": true, 24 | "IpAddress": "195.91.194.13", 25 | "IpCountry": "RU", 26 | "IpCity": "Ufa", 27 | "IpRegion": "Bashkortostan Republic", 28 | "IpDistrict": "Volga Federal District", 29 | "IpLatitude": 54.7355, 30 | "IpLongitude": 55.991982, 31 | "CardFirstSix": "411111", 32 | "CardLastFour": "1111", 33 | "CardType": "Visa", 34 | "CardTypeCode": 0, 35 | "IssuerBankCountry": "RU", 36 | "Status": "Declined", 37 | "StatusCode": 5, 38 | "Reason": "InsufficientFunds", 39 | "ReasonCode": 5051, 40 | "CardHolderMessage": "Insufficient funds on account", 41 | "Name": "CARDHOLDER NAME" 42 | }, 43 | "Success": false, 44 | "Message": null 45 | } 46 | -------------------------------------------------------------------------------- /spec/fixtures/apis/tokens/auth/failed.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :request: 3 | :url: '/payments/tokens/auth' 4 | :body: '{"Amount":10,"Currency":"RUB","InvoiceId":"1234567","Description":"Payment for goods on example.com","AccountId":"user_x","Name":"CARDHOLDER NAME","Token":"a4e67841-abb0-42de-a364-d1d8f9f4b3c0"}' 5 | :response: 6 | :body: > 7 | { 8 | "Model": { 9 | "TransactionId": 12345, 10 | "Amount": 10.0, 11 | "Currency": "RUB", 12 | "CurrencyCode": 0, 13 | "PaymentAmount": 10.0, 14 | "PaymentCurrency": "RUB", 15 | "PaymentCurrencyCode": 0, 16 | "InvoiceId": "1234567", 17 | "AccountId": "user_x", 18 | "Email": null, 19 | "Description": "Payment for goods on example.com", 20 | "JsonData": null, 21 | "CreatedDate": "\/Date(1401718880000)\/", 22 | "CreatedDateIso":"2014-08-09T11:49:41", 23 | "TestMode": true, 24 | "IpAddress": "195.91.194.13", 25 | "IpCountry": "RU", 26 | "IpCity": "Ufa", 27 | "IpRegion": "Bashkortostan Republic", 28 | "IpDistrict": "Volga Federal District", 29 | "IpLatitude": 54.7355, 30 | "IpLongitude": 55.991982, 31 | "CardFirstSix": "411111", 32 | "CardLastFour": "1111", 33 | "CardType": "Visa", 34 | "CardTypeCode": 0, 35 | "IssuerBankCountry": "RU", 36 | "Status": "Declined", 37 | "StatusCode": 5, 38 | "Reason": "InsufficientFunds", 39 | "ReasonCode": 5051, 40 | "CardHolderMessage": "Insufficient funds on account", 41 | "Name": "CARDHOLDER NAME" 42 | }, 43 | "Success": false, 44 | "Message": null 45 | } 46 | -------------------------------------------------------------------------------- /spec/fixtures/apis/cards/charge/failed.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :request: 3 | :url: '/payments/cards/charge' 4 | :body: '{"Amount":10,"Currency":"RUB","InvoiceId":"1234567","Description":"Payment for goods on example.com","AccountId":"user_x","Name":"CARDHOLDER NAME","CardCryptogramPacket":"01492500008719030128SM"}' 5 | :response: 6 | :body: > 7 | { 8 | "Model": { 9 | "TransactionId": 12345, 10 | "Amount": 10.0, 11 | "Currency": "RUB", 12 | "CurrencyCode": 0, 13 | "PaymentAmount": 10.0, 14 | "PaymentCurrency": "RUB", 15 | "PaymentCurrencyCode": 0, 16 | "InvoiceId": "1234567", 17 | "AccountId": "user_x", 18 | "Email": null, 19 | "Description": "Payment for goods on example.com", 20 | "JsonData": null, 21 | "CreatedDate": "\/Date(1401718880000)\/", 22 | "CreatedDateIso":"2014-08-09T11:49:41", 23 | "TestMode": true, 24 | "IpAddress": "195.91.194.13", 25 | "IpCountry": "RU", 26 | "IpCity": "Ufa", 27 | "IpRegion": "Bashkortostan Republic", 28 | "IpDistrict": "Volga Federal District", 29 | "IpLatitude": 54.7355, 30 | "IpLongitude": 55.991982, 31 | "CardFirstSix": "411111", 32 | "CardLastFour": "1111", 33 | "CardType": "Visa", 34 | "CardTypeCode": 0, 35 | "IssuerBankCountry": "RU", 36 | "Status": "Declined", 37 | "StatusCode": 5, 38 | "Reason": "InsufficientFunds", 39 | "ReasonCode": 5051, 40 | "CardHolderMessage": "Insufficient funds on account", 41 | "Name": "CARDHOLDER NAME" 42 | }, 43 | "Success": false, 44 | "Message": null 45 | } 46 | -------------------------------------------------------------------------------- /spec/fixtures/apis/tokens/charge/failed.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :request: 3 | :url: '/payments/tokens/charge' 4 | :body: '{"Amount":10,"Currency":"RUB","InvoiceId":"1234567","Description":"Payment for goods on example.com","AccountId":"user_x","Name":"CARDHOLDER NAME","Token":"a4e67841-abb0-42de-a364-d1d8f9f4b3c0"}' 5 | :response: 6 | :body: > 7 | { 8 | "Model": { 9 | "TransactionId": 12345, 10 | "Amount": 10.0, 11 | "Currency": "RUB", 12 | "CurrencyCode": 0, 13 | "PaymentAmount": 10.0, 14 | "PaymentCurrency": "RUB", 15 | "PaymentCurrencyCode": 0, 16 | "InvoiceId": "1234567", 17 | "AccountId": "user_x", 18 | "Email": null, 19 | "Description": "Payment for goods on example.com", 20 | "JsonData": null, 21 | "CreatedDate": "\/Date(1401718880000)\/", 22 | "CreatedDateIso":"2014-08-09T11:49:41", 23 | "TestMode": true, 24 | "IpAddress": "195.91.194.13", 25 | "IpCountry": "RU", 26 | "IpCity": "Ufa", 27 | "IpRegion": "Bashkortostan Republic", 28 | "IpDistrict": "Volga Federal District", 29 | "IpLatitude": 54.7355, 30 | "IpLongitude": 55.991982, 31 | "CardFirstSix": "411111", 32 | "CardLastFour": "1111", 33 | "CardType": "Visa", 34 | "CardTypeCode": 0, 35 | "IssuerBankCountry": "RU", 36 | "Status": "Declined", 37 | "StatusCode": 5, 38 | "Reason": "InsufficientFunds", 39 | "ReasonCode": 5051, 40 | "CardHolderMessage": "Insufficient funds on account", 41 | "Name": "CARDHOLDER NAME" 42 | }, 43 | "Success": false, 44 | "Message": null 45 | } 46 | -------------------------------------------------------------------------------- /lib/cloud_payments/namespaces/base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module CloudPayments 3 | module Namespaces 4 | class Base 5 | attr_reader :client, :parent_path 6 | 7 | class << self 8 | def resource_name 9 | self.name.split('::').last.downcase 10 | end 11 | end 12 | 13 | def initialize(client, parent_path = nil) 14 | @client = client 15 | @parent_path = parent_path 16 | end 17 | 18 | def request(path, params = {}) 19 | response = client.perform_request(resource_path(path), params) 20 | raise_gateway_error(response.body) unless response.body[:success] 21 | response.body 22 | end 23 | 24 | protected 25 | 26 | def api_exceptions 27 | [::Faraday::ConnectionFailed, ::Faraday::TimeoutError, Client::ServerError, Client::GatewayError] 28 | end 29 | 30 | def resource_path(path = nil) 31 | [parent_path, self.class.resource_name, path].flatten.compact.join(?/).squeeze(?/) 32 | end 33 | 34 | def raise_gateway_error(body) 35 | raise_reasoned_gateway_error(body) || raise_raw_gateway_error(body) 36 | end 37 | 38 | def raise_reasoned_gateway_error(body) 39 | fail Client::GATEWAY_ERRORS[body[:model][:reason_code]].new(body) if reason_present?(body) 40 | end 41 | 42 | def raise_raw_gateway_error(body) 43 | fail Client::GatewayError.new(body[:message], body) if !body[:message].nil? && !body[:message].empty? 44 | end 45 | 46 | def reason_present?(body) 47 | !body[:model].nil? && !body[:model].empty? && !body[:model][:reason_code].nil? && CloudPayments.config.raise_banking_errors 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /spec/fixtures/apis/cards/auth/successful.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :request: 3 | :url: '/payments/cards/auth' 4 | :body: '{"Amount":10,"Currency":"RUB","InvoiceId":"1234567","Description":"Payment for goods on example.com","AccountId":"user_x","Name":"CARDHOLDER NAME","CardCryptogramPacket":"01492500008719030128SM"}' 5 | :response: 6 | :body: > 7 | { 8 | "Model":{ 9 | "TransactionId":12345, 10 | "Amount":10.0, 11 | "Currency":"RUB", 12 | "CurrencyCode":0, 13 | "InvoiceId":"1234567", 14 | "AccountId":"user_x", 15 | "Email":null, 16 | "Description":"Payment for goods on example.com", 17 | "JsonData":null, 18 | "CreatedDate":"\/Date(1401718880000)\/", 19 | "CreatedDateIso":"2014-08-09T11:49:41", 20 | "AuthDate":"\/Date(1401733880523)\/", 21 | "AuthDateIso":"2014-08-09T11:49:42", 22 | "ConfirmDate":null, 23 | "ConfirmDateIso":null, 24 | "AuthCode":"123456", 25 | "TestMode":true, 26 | "IpAddress":"195.91.194.13", 27 | "IpCountry":"RU", 28 | "IpCity":"Ufa", 29 | "IpRegion":"Bashkortostan Republic", 30 | "IpDistrict":"Volga Federal District", 31 | "IpLatitude":54.7355, 32 | "IpLongitude":55.991982, 33 | "CardFirstSix":"411111", 34 | "CardLastFour":"1111", 35 | "CardType":"Visa", 36 | "CardTypeCode":0, 37 | "IssuerBankCountry":"RU", 38 | "Status":"Authorized", 39 | "StatusCode":3, 40 | "Reason":"Approved", 41 | "ReasonCode":0, 42 | "CardHolderMessage":"Payment successful", 43 | "Name":"CARDHOLDER NAME", 44 | "Token":"a4e67841-abb0-42de-a364-d1d8f9f4b3c0" 45 | }, 46 | "Success":true, 47 | "Message": null 48 | } 49 | -------------------------------------------------------------------------------- /spec/fixtures/apis/cards/charge/successful.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :request: 3 | :url: '/payments/cards/charge' 4 | :body: '{"Amount":10,"Currency":"RUB","InvoiceId":"1234567","Description":"Payment for goods on example.com","AccountId":"user_x","Name":"CARDHOLDER NAME","CardCryptogramPacket":"01492500008719030128SM"}' 5 | :response: 6 | :body: > 7 | { 8 | "Model":{ 9 | "TransactionId":12345, 10 | "Amount":10.0, 11 | "Currency":"RUB", 12 | "CurrencyCode":0, 13 | "InvoiceId":"1234567", 14 | "AccountId":"user_x", 15 | "Email":null, 16 | "Description":"Payment for goods on example.com", 17 | "JsonData":null, 18 | "CreatedDate":"\/Date(1401718880000)\/", 19 | "CreatedDateIso":"2014-08-09T11:49:41", 20 | "AuthDate":"\/Date(1401733880523)\/", 21 | "AuthDateIso":"2014-08-09T11:49:42", 22 | "ConfirmDate":"\/Date(1401733880523)\/", 23 | "ConfirmDateIso":"2014-08-09T11:49:42", 24 | "AuthCode":"123456", 25 | "TestMode":true, 26 | "IpAddress":"195.91.194.13", 27 | "IpCountry":"RU", 28 | "IpCity":"Ufa", 29 | "IpRegion":"Bashkortostan Republic", 30 | "IpDistrict":"Volga Federal District", 31 | "IpLatitude":54.7355, 32 | "IpLongitude":55.991982, 33 | "CardFirstSix":"411111", 34 | "CardLastFour":"1111", 35 | "CardType":"Visa", 36 | "CardTypeCode":0, 37 | "IssuerBankCountry":"RU", 38 | "Status":"Completed", 39 | "StatusCode":3, 40 | "Reason":"Approved", 41 | "ReasonCode":0, 42 | "CardHolderMessage":"Payment successful", 43 | "Name":"CARDHOLDER NAME", 44 | "Token":"a4e67841-abb0-42de-a364-d1d8f9f4b3c0" 45 | }, 46 | "Success":true, 47 | "Message": null 48 | } 49 | -------------------------------------------------------------------------------- /spec/fixtures/apis/tokens/auth/successful.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :request: 3 | :url: '/payments/tokens/auth' 4 | :body: '{"Amount":10,"Currency":"RUB","InvoiceId":"1234567","Description":"Payment for goods on example.com","AccountId":"user_x","Name":"CARDHOLDER NAME","Token":"a4e67841-abb0-42de-a364-d1d8f9f4b3c0"}' 5 | :response: 6 | :body: > 7 | { 8 | "Model":{ 9 | "TransactionId":12345, 10 | "Amount":10.0, 11 | "Currency":"RUB", 12 | "CurrencyCode":0, 13 | "InvoiceId":"1234567", 14 | "AccountId":"user_x", 15 | "Email":null, 16 | "Description":"Payment for goods on example.com", 17 | "JsonData":null, 18 | "CreatedDate":"\/Date(1401718880000)\/", 19 | "CreatedDateIso":"2014-08-09T11:49:41", 20 | "AuthDate":"\/Date(1401733880523)\/", 21 | "AuthDateIso":"2014-08-09T11:49:42", 22 | "ConfirmDate":null, 23 | "ConfirmDateIso":null, 24 | "AuthCode":"123456", 25 | "TestMode":true, 26 | "IpAddress":"195.91.194.13", 27 | "IpCountry":"RU", 28 | "IpCity":"Ufa", 29 | "IpRegion":"Bashkortostan Republic", 30 | "IpDistrict":"Volga Federal District", 31 | "IpLatitude":54.7355, 32 | "IpLongitude":55.991982, 33 | "CardFirstSix":"411111", 34 | "CardLastFour":"1111", 35 | "CardType":"Visa", 36 | "CardTypeCode":0, 37 | "IssuerBankCountry":"RU", 38 | "Status":"Authorized", 39 | "StatusCode":3, 40 | "Reason":"Approved", 41 | "ReasonCode":0, 42 | "CardHolderMessage":"Payment successful", 43 | "Name":"CARDHOLDER NAME", 44 | "Token":"a4e67841-abb0-42de-a364-d1d8f9f4b3c0", 45 | "EscrowAccumulationId": "119d1f05-4fa8-4f35-85b6-09216a5a4fb6" 46 | }, 47 | "Success":true, 48 | "Message": null 49 | } 50 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 3 | require 'cloud_payments' 4 | require 'rack' 5 | require 'pp' 6 | 7 | app = ->(env){ 8 | pp env 9 | pp Rack::Request.new(env).params 10 | 11 | json = <<-JSON 12 | { 13 | "Model":{ 14 | "TransactionId": 12345, 15 | "Amount": 120, 16 | "Currency": "RUB", 17 | "CurrencyCode": 0, 18 | "InvoiceId": "1234567", 19 | "AccountId": "user_x", 20 | "Email": null, 21 | "Description": "Payment for goods on example.com", 22 | "JsonData": null, 23 | "CreatedDate": "\/Date(1401718880000)\/", 24 | "CreatedDateIso":"2014-08-09T11:49:41", 25 | "AuthDate": "\/Date(1401733880523)\/", 26 | "AuthDateIso":"2014-08-09T11:49:42", 27 | "ConfirmDate": "\/Date(1401733880523)\/", 28 | "ConfirmDateIso":"2014-08-09T11:49:42", 29 | "AuthCode": "123456", 30 | "TestMode": true, 31 | "IpAddress": "195.91.194.13", 32 | "IpCountry": "RU", 33 | "IpCity": "Ufa", 34 | "IpRegion": "Bashkortostan Republic", 35 | "IpDistrict": "Volga Federal District", 36 | "IpLatitude": 54.7355, 37 | "IpLongitude": 55.991982, 38 | "CardFirstSix": "411111", 39 | "CardLastFour": "1111", 40 | "CardType": "Visa", 41 | "CardTypeCode": 0, 42 | "IssuerBankCountry": "RU", 43 | "Status": "Completed", 44 | "StatusCode": 3, 45 | "Reason": "Approved", 46 | "ReasonCode": 0, 47 | "CardHolderMessage": "Payment successful", 48 | "Name": "CARDHOLDER NAME", 49 | "Token": "a4e67841-abb0-42de-a364-d1d8f9f4b3c0" 50 | }, 51 | "Success":true, 52 | "Message":null 53 | } 54 | JSON 55 | 56 | [200, { 'Content-Type' => 'application/json' }, [json]] 57 | } 58 | 59 | run app 60 | -------------------------------------------------------------------------------- /spec/fixtures/apis/tokens/charge/successful.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :request: 3 | :url: '/payments/tokens/charge' 4 | :body: '{"Amount":10,"Currency":"RUB","InvoiceId":"1234567","Description":"Payment for goods on example.com","AccountId":"user_x","Name":"CARDHOLDER NAME","Token":"a4e67841-abb0-42de-a364-d1d8f9f4b3c0"}' 5 | :response: 6 | :body: > 7 | { 8 | "Model":{ 9 | "TransactionId":12345, 10 | "Amount":10.0, 11 | "Currency":"RUB", 12 | "CurrencyCode":0, 13 | "InvoiceId":"1234567", 14 | "AccountId":"user_x", 15 | "Email":null, 16 | "Description":"Payment for goods on example.com", 17 | "JsonData":null, 18 | "CreatedDate":"\/Date(1401718880000)\/", 19 | "CreatedDateIso":"2014-08-09T11:49:41", 20 | "AuthDate":"\/Date(1401733880523)\/", 21 | "AuthDateIso":"2014-08-09T11:49:42", 22 | "ConfirmDate":"\/Date(1401733880523)\/", 23 | "ConfirmDateIso":"2014-08-09T11:49:42", 24 | "AuthCode":"123456", 25 | "TestMode":true, 26 | "IpAddress":"195.91.194.13", 27 | "IpCountry":"RU", 28 | "IpCity":"Ufa", 29 | "IpRegion":"Bashkortostan Republic", 30 | "IpDistrict":"Volga Federal District", 31 | "IpLatitude":54.7355, 32 | "IpLongitude":55.991982, 33 | "CardFirstSix":"411111", 34 | "CardLastFour":"1111", 35 | "CardType":"Visa", 36 | "CardTypeCode":0, 37 | "IssuerBankCountry":"RU", 38 | "Status":"Completed", 39 | "StatusCode":3, 40 | "Reason":"Approved", 41 | "ReasonCode":0, 42 | "CardHolderMessage":"Payment successful", 43 | "Name":"CARDHOLDER NAME", 44 | "Token":"a4e67841-abb0-42de-a364-d1d8f9f4b3c0", 45 | "EscrowAccumulationId": "119d1f05-4fa8-4f35-85b6-09216a5a4fb6" 46 | }, 47 | "Success":true, 48 | "Message": null 49 | } 50 | -------------------------------------------------------------------------------- /lib/cloud_payments/client.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'cloud_payments/client/errors' 3 | require 'cloud_payments/client/gateway_errors' 4 | require 'cloud_payments/client/response' 5 | require 'cloud_payments/client/serializer' 6 | 7 | module CloudPayments 8 | class Client 9 | include Namespaces 10 | 11 | attr_reader :config, :connection 12 | 13 | def initialize(config = nil) 14 | @config = config || CloudPayments.config 15 | @connection = build_connection 16 | end 17 | 18 | def perform_request(path, params = nil) 19 | response = connection.post(path, (params ? convert_to_json(params) : nil), headers) 20 | 21 | Response.new(response.status, response.body, response.headers).tap do |response| 22 | raise_transport_error(response) if response.status.to_i >= 300 23 | end 24 | end 25 | 26 | private 27 | 28 | def convert_to_json(data) 29 | config.serializer.dump(data) 30 | end 31 | 32 | def headers 33 | { 'Content-Type' => 'application/json' } 34 | end 35 | 36 | def logger 37 | config.logger 38 | end 39 | 40 | def raise_transport_error(response) 41 | logger.fatal "[#{response.status}] #{response.origin_body}" if logger 42 | error = ERRORS[response.status] || ServerError 43 | raise error.new "[#{response.status}] #{response.origin_body}" 44 | end 45 | 46 | def build_connection 47 | Faraday::Connection.new(config.host, config.connection_options) do |conn| 48 | 49 | # https://github.com/lostisland/faraday/blob/main/UPGRADING.md#authentication-helper-methods-in-connection-have-been-removed 50 | # https://lostisland.github.io/faraday/#/middleware/included/authentication?id=faraday-1x-usage 51 | if Faraday::VERSION.start_with?("1.") 52 | conn.request :basic_auth, config.public_key, config.secret_key 53 | else 54 | conn.request :authorization, :basic, config.public_key, config.secret_key 55 | end 56 | 57 | config.connection_block.call(conn) if config.connection_block 58 | end 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /spec/cloud_payments/namespaces/orders_spec.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # encoding: utf-8 3 | # frozen_string_literal: true 4 | require 'spec_helper' 5 | 6 | describe CloudPayments::Namespaces::Orders do 7 | subject{ described_class.new(CloudPayments.client) } 8 | 9 | describe '#create' do 10 | let(:attributes) do 11 | { 12 | amount: 10.0, 13 | currency: 'RUB', 14 | description: 'Оплата на сайте example.com', 15 | email: 'client@test.local', 16 | require_confirmation: true, 17 | send_email: false, 18 | invoice_id: 'invoice_100', 19 | account_id: 'account_200', 20 | phone: '+7(495)765-4321', 21 | send_sms: false, 22 | send_whats_app: false 23 | } 24 | end 25 | 26 | context do 27 | before{ stub_api_request('orders/create/successful').perform } 28 | 29 | specify{ expect(subject.create(attributes)).to be_instance_of(CloudPayments::Order) } 30 | 31 | context do 32 | let(:sub){ subject.create(attributes) } 33 | 34 | specify{ expect(sub.id).to eq('f2K8LV6reGE9WBFn') } 35 | specify{ expect(sub.amount).to eq(10.0) } 36 | specify{ expect(sub.currency).to eq('RUB') } 37 | specify{ expect(sub.currency_code).to eq(0) } 38 | specify{ expect(sub.email).to eq('client@test.local') } 39 | specify{ expect(sub.description).to eq('Оплата на сайте example.com') } 40 | specify{ expect(sub.require_confirmation).to eq(true) } 41 | specify{ expect(sub.url).to eq('https://orders.cloudpayments.ru/d/f2K8LV6reGE9WBFn') } 42 | end 43 | end 44 | end 45 | 46 | describe '#cancel' do 47 | context do 48 | before{ stub_api_request('orders/cancel/successful').perform } 49 | specify{ expect(subject.cancel('12345')).to be_truthy } 50 | end 51 | 52 | context do 53 | before{ stub_api_request('orders/cancel/failed').perform } 54 | specify{ expect{ subject.cancel('12345') }.to raise_error(CloudPayments::Client::GatewayError, 'Error message') } 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/cloud_payments/client/errors.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module CloudPayments 3 | class Client 4 | class Error < StandardError; end 5 | class ServerError < StandardError; end 6 | class GatewayError < StandardError 7 | attr_reader :body 8 | 9 | def initialize(message, body) 10 | super(message) 11 | @body = body 12 | end 13 | end 14 | 15 | module Errors; end 16 | 17 | HTTP_STATUSES = { 18 | 300 => 'MultipleChoices', 19 | 301 => 'MovedPermanently', 20 | 302 => 'Found', 21 | 303 => 'SeeOther', 22 | 304 => 'NotModified', 23 | 305 => 'UseProxy', 24 | 307 => 'TemporaryRedirect', 25 | 308 => 'PermanentRedirect', 26 | 27 | 400 => 'BadRequest', 28 | 401 => 'Unauthorized', 29 | 402 => 'PaymentRequired', 30 | 403 => 'Forbidden', 31 | 404 => 'NotFound', 32 | 405 => 'MethodNotAllowed', 33 | 406 => 'NotAcceptable', 34 | 407 => 'ProxyAuthenticationRequired', 35 | 408 => 'RequestTimeout', 36 | 409 => 'Conflict', 37 | 410 => 'Gone', 38 | 411 => 'LengthRequired', 39 | 412 => 'PreconditionFailed', 40 | 413 => 'RequestEntityTooLarge', 41 | 414 => 'RequestURITooLong', 42 | 415 => 'UnsupportedMediaType', 43 | 416 => 'RequestedRangeNotSatisfiable', 44 | 417 => 'ExpectationFailed', 45 | 418 => 'ImATeapot', 46 | 421 => 'TooManyConnectionsFromThisIP', 47 | 426 => 'UpgradeRequired', 48 | 450 => 'BlockedByWindowsParentalControls', 49 | 494 => 'RequestHeaderTooLarge', 50 | 497 => 'HTTPToHTTPS', 51 | 499 => 'ClientClosedRequest', 52 | 53 | 500 => 'InternalServerError', 54 | 501 => 'NotImplemented', 55 | 502 => 'BadGateway', 56 | 503 => 'ServiceUnavailable', 57 | 504 => 'GatewayTimeout', 58 | 505 => 'HTTPVersionNotSupported', 59 | 506 => 'VariantAlsoNegotiates', 60 | 510 => 'NotExtended' 61 | } 62 | 63 | ERRORS = HTTP_STATUSES.inject({}) do |result, error| 64 | status, name = error 65 | result[status] = Errors.const_set(name, Class.new(ServerError)) 66 | result 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /spec/cloud_payments/models/order_spec.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # encoding: utf-8 3 | # frozen_string_literal: true 4 | require 'spec_helper' 5 | 6 | describe CloudPayments::Order do 7 | subject{ described_class.new(attributes) } 8 | 9 | let(:attributes) do 10 | { 11 | id: 'f2K8LV6reGE9WBFn', 12 | number: 61, 13 | amount: 10.0, 14 | currency: 'RUB', 15 | currency_code: 0, 16 | email: 'client@test.local', 17 | description: 'Оплата на сайте example.com', 18 | require_confirmation: true, 19 | url:'https://orders.cloudpayments.ru/d/f2K8LV6reGE9WBFn' 20 | } 21 | end 22 | 23 | describe 'properties' do 24 | specify{ expect(subject.id).to eq('f2K8LV6reGE9WBFn') } 25 | specify{ expect(subject.number).to eq(61) } 26 | specify{ expect(subject.amount).to eq(10.0) } 27 | specify{ expect(subject.currency).to eq('RUB') } 28 | specify{ expect(subject.currency_code).to eq(0) } 29 | specify{ expect(subject.email).to eq('client@test.local') } 30 | specify{ expect(subject.description).to eq('Оплата на сайте example.com') } 31 | specify{ expect(subject.require_confirmation).to eq(true) } 32 | specify{ expect(subject.url).to eq('https://orders.cloudpayments.ru/d/f2K8LV6reGE9WBFn') } 33 | 34 | it_behaves_like :raise_without_attribute, :id 35 | it_behaves_like :raise_without_attribute, :number 36 | it_behaves_like :raise_without_attribute, :amount 37 | it_behaves_like :raise_without_attribute, :currency 38 | it_behaves_like :raise_without_attribute, :currency_code 39 | it_behaves_like :raise_without_attribute, :description 40 | it_behaves_like :raise_without_attribute, :require_confirmation 41 | it_behaves_like :raise_without_attribute, :url 42 | 43 | it_behaves_like :not_raise_without_attribute, :email 44 | end 45 | 46 | describe 'transformations' do 47 | context 'amount from string' do 48 | before { attributes[:amount] = '293.42' } 49 | specify{ expect(subject.amount).to eql(293.42) } 50 | end 51 | 52 | context 'require_confirmation from "1"' do 53 | before { attributes[:require_confirmation] = '1' } 54 | specify{ expect(subject.require_confirmation).to eql(true) } 55 | end 56 | 57 | context 'require_confirmation from "0"' do 58 | before { attributes[:require_confirmation] = '0' } 59 | specify{ expect(subject.require_confirmation).to eql(false) } 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /spec/support/helpers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'yaml' 3 | 4 | module CloudPayments 5 | module RSpec 6 | module Helpers 7 | class StubApiRequest 8 | attr_reader :name 9 | 10 | def initialize(name) 11 | @name = name 12 | end 13 | 14 | def perform 15 | webmock_stub.to_return( 16 | status: response[:status] || 200, 17 | body: response[:body] || '', 18 | headers: response_headers 19 | ) if webmock_stub 20 | end 21 | 22 | def to_return(options) 23 | webmock_stub.to_return(return_options.merge(options)) if webmock_stub 24 | end 25 | 26 | def to_raise(*args) 27 | webmock_stub.to_raise(*args) if webmock_stub 28 | end 29 | 30 | def to_timeout 31 | webmock_stub.to_timeout if webmock_stub 32 | end 33 | 34 | private 35 | 36 | def return_options 37 | { 38 | status: response[:status] || 200, 39 | body: response[:body] || '', 40 | headers: response_headers 41 | } 42 | end 43 | 44 | def webmock_stub 45 | @webmock_stub ||= begin 46 | if fixture 47 | WebMock::StubRegistry.instance.register_request_stub(WebMock::RequestStub.new(:post, url)). 48 | with(body: request[:body] || '', headers: request_headers, basic_auth: ['user', 'pass']) 49 | end 50 | end 51 | end 52 | 53 | def url 54 | "http://localhost:9292#{request[:url]}" 55 | end 56 | 57 | def request_headers 58 | { 'Content-Type' => 'application/json' }.merge(request[:headers] || {}) 59 | end 60 | 61 | def response_headers 62 | { 'Content-Type' => 'application/json' }.merge(response[:headers] || {}) 63 | end 64 | 65 | def request 66 | fixture[:request] || {} 67 | end 68 | 69 | def response 70 | fixture[:response] || {} 71 | end 72 | 73 | def fixture 74 | @fixture ||= begin 75 | file = fixture_path.join("#{name}.yml").to_s 76 | YAML.load(File.read(file)) if File.exist?(file) 77 | end 78 | end 79 | 80 | def fixture_path 81 | Pathname.new(File.expand_path('../../fixtures/apis', __FILE__)) 82 | end 83 | end 84 | 85 | def stub_api_request(name) 86 | StubApiRequest.new(name) 87 | end 88 | end 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /lib/cloud_payments/models/transaction.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module CloudPayments 3 | class Transaction < Model 4 | AWAITING_AUTHENTICATION = 'AwaitingAuthentication' 5 | COMPLECTED = 'Completed' 6 | AUTHORIZED = 'Authorized' 7 | CANCELLED = 'Cancelled' 8 | DECLINED = 'Declined' 9 | 10 | property :id, from: :transaction_id, required: true 11 | property :amount, required: true 12 | property :currency, required: true 13 | property :currency_code 14 | property :invoice_id 15 | property :account_id 16 | property :subscription_id 17 | property :email 18 | property :description 19 | property :metadata, from: :json_data, default: {} 20 | property :date_time, transform_with: DateTimeTransform 21 | property :created_at, from: :created_date_iso, with: DateTimeTransform 22 | property :authorized_at, from: :auth_date_iso, with: DateTimeTransform 23 | property :confirmed_at, from: :confirm_date_iso, with: DateTimeTransform 24 | property :auth_code 25 | property :test_mode, required: true 26 | property :ip_address 27 | property :ip_country 28 | property :ip_city 29 | property :ip_region 30 | property :ip_district 31 | property :ip_lat, from: :ip_latitude 32 | property :ip_lng, from: :ip_longitude 33 | property :card_first_six, required: true 34 | property :card_last_four, required: true 35 | property :card_type, required: true 36 | property :card_type_code 37 | property :card_exp_date 38 | property :name 39 | property :issuer 40 | property :issuer_bank_country 41 | property :status, required: true 42 | property :status_code 43 | property :reason 44 | property :reason_code 45 | property :refunded 46 | property :card_holder_message 47 | property :token 48 | property :apple_pay 49 | property :android_pay 50 | property :escrow_accumulation_id 51 | 52 | def required_secure3d? 53 | false 54 | end 55 | 56 | def subscription 57 | @subscription ||= CloudPayments.client.subscriptions.find(subscription_id) if subscription_id 58 | end 59 | 60 | def card_number 61 | @card_number ||= "#{card_first_six}XXXXXX#{card_last_four}".gsub(/(.{4})/, '\1 ').rstrip 62 | end 63 | 64 | def ip_location 65 | [ip_lat, ip_lng] if ip_lng && ip_lat 66 | end 67 | 68 | def awaiting_authentication? 69 | status == AWAITING_AUTHENTICATION 70 | end 71 | 72 | def completed? 73 | status == COMPLECTED 74 | end 75 | 76 | def authorized? 77 | status == AUTHORIZED 78 | end 79 | 80 | def cancelled? 81 | status == CANCELLED 82 | end 83 | 84 | def declined? 85 | status == DECLINED 86 | end 87 | 88 | def refunded? 89 | refunded 90 | end 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /spec/cloud_payments/namespaces/base_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'spec_helper' 3 | 4 | class TestNamespace < CloudPayments::Namespaces::Base 5 | end 6 | 7 | describe CloudPayments::Namespaces::Base do 8 | let(:headers){ { 'Content-Type' => 'application/json' } } 9 | let(:successful_body){ '{"Model":{},"Success":true}' } 10 | let(:failed_body){ '{"Success":false,"Message":"Error message"}' } 11 | let(:failed_transaction_body){ '{"Model":{"ReasonCode":5041,"CardHolderMessage":"Contact your bank"},"Success":false}' } 12 | let(:request_body){ '{"Amount":120,"CurrencyCode":"RUB"}' } 13 | let(:request_params){ { amount: 120, currency_code: 'RUB' } } 14 | 15 | subject{ TestNamespace.new(CloudPayments.client) } 16 | 17 | def stub_api(path, body = '') 18 | url = "http://localhost:9292#{path}" 19 | stub_request(:post, url).with(body: body, headers: headers, basic_auth: ['user', 'pass']) 20 | end 21 | 22 | describe '#request' do 23 | context do 24 | before{ stub_api('/testnamespace', request_body).to_return(body: successful_body, headers: headers) } 25 | specify{ expect(subject.request(nil, request_params)) } 26 | end 27 | 28 | context 'with path' do 29 | before{ stub_api('/testnamespace/path', request_body).to_return(body: successful_body, headers: headers) } 30 | 31 | specify{ expect(subject.request(:path, request_params)) } 32 | end 33 | 34 | context 'with path and parent path' do 35 | subject{ TestNamespace.new(CloudPayments.client, 'parent') } 36 | 37 | before{ stub_api('/parent/testnamespace/path', request_body).to_return(body: successful_body, headers: headers) } 38 | 39 | specify{ expect(subject.request(:path, request_params)) } 40 | end 41 | 42 | context 'when status is greater than 300' do 43 | before{ stub_api('/testnamespace/path', request_body).to_return(status: 404, headers: headers) } 44 | 45 | specify{ expect{ subject.request(:path, request_params) }.to raise_error(CloudPayments::Client::Errors::NotFound) } 46 | end 47 | 48 | context 'when failed request' do 49 | before{ stub_api('/testnamespace/path', request_body).to_return(body: failed_body, headers: headers) } 50 | 51 | context 'config.raise_banking_errors = true' do 52 | before { CloudPayments.config.raise_banking_errors = true } 53 | specify{ expect{ subject.request(:path, request_params) }.to raise_error(CloudPayments::Client::GatewayError, 'Error message') } 54 | end 55 | 56 | context 'config.raise_banking_errors = false' do 57 | before { CloudPayments.config.raise_banking_errors = false } 58 | specify{ expect{ subject.request(:path, request_params) }.to raise_error(CloudPayments::Client::GatewayError, 'Error message') } 59 | end 60 | end 61 | 62 | context 'when failed transaction' do 63 | before{ stub_api('/testnamespace/path', request_body).to_return(body: failed_transaction_body, headers: headers) } 64 | 65 | context 'config.raise_banking_errors = true' do 66 | before { CloudPayments.config.raise_banking_errors = true } 67 | specify do 68 | begin 69 | subject.request(:path, request_params) 70 | rescue CloudPayments::Client::GatewayErrors::LostCard => err 71 | expect(err).to be_a CloudPayments::Client::ReasonedGatewayError 72 | end 73 | end 74 | end 75 | 76 | context 'config.raise_banking_errors = false' do 77 | before { CloudPayments.config.raise_banking_errors = false } 78 | specify{ expect{ subject.request(:path, request_params) }.not_to raise_error } 79 | end 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /spec/cloud_payments/namespaces/subscriptions_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'spec_helper' 3 | 4 | describe CloudPayments::Namespaces::Subscriptions do 5 | subject{ CloudPayments::Namespaces::Subscriptions.new(CloudPayments.client) } 6 | 7 | describe '#find' do 8 | context do 9 | before{ stub_api_request('subscriptions/get/successful').perform } 10 | 11 | specify{ expect(subject.find('sc_8cf8a9338fb')).to be_instance_of(CloudPayments::Subscription) } 12 | 13 | context do 14 | let(:sub){ subject.find('sc_8cf8a9338fb') } 15 | 16 | specify{ expect(sub.id).to eq('sc_8cf8a9338fb') } 17 | specify{ expect(sub.account_id).to eq('user@example.com') } 18 | specify{ expect(sub.description).to eq('Monthly subscription') } 19 | specify{ expect(sub.started_at).to eq(DateTime.parse('2014-08-09T11:49:41')) } 20 | specify{ expect(sub).to be_active } 21 | end 22 | end 23 | end 24 | 25 | describe '#find_all' do 26 | context do 27 | before{ stub_api_request('subscriptions/find/successful').perform } 28 | 29 | specify{ expect(subject.find_all("user@example.com")).to be_instance_of(Array) } 30 | specify{ expect(subject.find_all("user@example.com").first).to be_instance_of(CloudPayments::Subscription) } 31 | 32 | context do 33 | let(:sub){ subject.find_all("user@example.com").first } 34 | 35 | specify{ expect(sub.id).to eq('sc_8cf8a9338fb') } 36 | specify{ expect(sub.account_id).to eq('user@example.com') } 37 | specify{ expect(sub.description).to eq('Monthly subscription') } 38 | specify{ expect(sub.started_at).to eq(DateTime.parse('2014-08-09T11:49:41')) } 39 | specify{ expect(sub).to be_active } 40 | end 41 | end 42 | end 43 | 44 | describe '#create' do 45 | let(:attributes){ { 46 | token: '477BBA133C182267F', 47 | account_id: 'user@example.com', 48 | description: 'Monthly subscription', 49 | email: 'user@example.com', 50 | amount: 1.02, 51 | currency: 'RUB', 52 | require_confirmation: false, 53 | start_date: '2014-08-09T11:49:41', 54 | interval: 'Month', 55 | period: 1, 56 | max_periods: 12 57 | } } 58 | 59 | context do 60 | before{ stub_api_request('subscriptions/create/successful').perform } 61 | 62 | specify{ expect(subject.create(attributes)).to be_instance_of(CloudPayments::Subscription) } 63 | 64 | context do 65 | let(:sub){ subject.create(attributes) } 66 | 67 | specify{ expect(sub.id).to eq('sc_8cf8a9338fb') } 68 | specify{ expect(sub.account_id).to eq('user@example.com') } 69 | specify{ expect(sub.description).to eq('Monthly subscription') } 70 | specify{ expect(sub.started_at).to eq(DateTime.parse('2014-08-09T11:49:41')) } 71 | specify{ expect(sub).to be_active } 72 | end 73 | end 74 | end 75 | 76 | describe '#update' do 77 | let(:attributes){ { account_id: 'user2@example.com', email: 'user2@example.com', max_periods: 6 } } 78 | 79 | context do 80 | before{ stub_api_request('subscriptions/update/successful').perform } 81 | 82 | specify{ expect(subject.update('sc_8cf8a9338fb', attributes)).to be_instance_of(CloudPayments::Subscription) } 83 | 84 | context do 85 | let(:sub){ subject.update('sc_8cf8a9338fb', attributes) } 86 | 87 | specify{ expect(sub.id).to eq('sc_8cf8a9338fb') } 88 | specify{ expect(sub.account_id).to eq('user2@example.com') } 89 | specify{ expect(sub.max_periods).to eq(6) } 90 | specify{ expect(sub).to be_active } 91 | end 92 | end 93 | end 94 | 95 | describe '#cancel' do 96 | context do 97 | before{ stub_api_request('subscriptions/cancel/successful').perform } 98 | 99 | specify{ expect(subject.cancel('sc_8cf8a9338fb')).to be_truthy } 100 | end 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /spec/cloud_payments/namespaces/tokens_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'spec_helper' 3 | 4 | describe CloudPayments::Namespaces::Tokens do 5 | subject{ CloudPayments::Namespaces::Tokens.new(CloudPayments.client, '/payments') } 6 | 7 | let(:attributes){ { 8 | amount: 10, 9 | currency: 'RUB', 10 | invoice_id: '1234567', 11 | description: 'Payment for goods on example.com', 12 | account_id: 'user_x', 13 | name: 'CARDHOLDER NAME', 14 | token: 'a4e67841-abb0-42de-a364-d1d8f9f4b3c0' 15 | } } 16 | 17 | describe '#charge' do 18 | context 'config.raise_banking_errors = true' do 19 | before { CloudPayments.config.raise_banking_errors = true } 20 | after { CloudPayments.config.raise_banking_errors = false } 21 | 22 | context do 23 | before { stub_api_request('tokens/charge/successful').perform } 24 | specify{ expect{ subject.charge(attributes) }.not_to raise_error } 25 | end 26 | 27 | context do 28 | before { stub_api_request('tokens/charge/failed').perform } 29 | specify{ expect{ subject.charge(attributes) }.to raise_error(CloudPayments::Client::GatewayErrors::InsufficientFunds) } 30 | end 31 | end 32 | 33 | context 'config.raise_banking_errors = false' do 34 | before { CloudPayments.config.raise_banking_errors = false } 35 | 36 | context do 37 | before{ stub_api_request('tokens/charge/successful').perform } 38 | specify{ expect(subject.charge(attributes)).to be_instance_of(CloudPayments::Transaction) } 39 | specify{ expect(subject.charge(attributes)).not_to be_required_secure3d } 40 | specify{ expect(subject.charge(attributes)).to be_completed } 41 | specify{ expect(subject.charge(attributes).id).to eq(12345) } 42 | end 43 | 44 | context do 45 | before{ stub_api_request('tokens/charge/failed').perform } 46 | specify{ expect(subject.charge(attributes)).to be_instance_of(CloudPayments::Transaction) } 47 | specify{ expect(subject.charge(attributes)).not_to be_required_secure3d } 48 | specify{ expect(subject.charge(attributes)).to be_declined } 49 | specify{ expect(subject.charge(attributes).id).to eq(12345) } 50 | specify{ expect(subject.charge(attributes).reason).to eq('InsufficientFunds') } 51 | end 52 | end 53 | end 54 | 55 | describe '#auth' do 56 | context 'config.raise_banking_errors = true' do 57 | before { CloudPayments.config.raise_banking_errors = true } 58 | 59 | context do 60 | before { stub_api_request('tokens/auth/successful').perform } 61 | specify{ expect{ subject.auth(attributes) }.not_to raise_error } 62 | end 63 | 64 | context do 65 | before { stub_api_request('tokens/auth/failed').perform } 66 | specify{ expect{ subject.auth(attributes) }.to raise_error(CloudPayments::Client::GatewayErrors::InsufficientFunds) } 67 | end 68 | end 69 | 70 | context 'config.raise_banking_errors = false' do 71 | before { CloudPayments.config.raise_banking_errors = false } 72 | 73 | context do 74 | before{ stub_api_request('tokens/auth/successful').perform } 75 | specify{ expect(subject.auth(attributes)).to be_instance_of(CloudPayments::Transaction) } 76 | specify{ expect(subject.auth(attributes)).not_to be_required_secure3d } 77 | specify{ expect(subject.auth(attributes)).to be_authorized } 78 | specify{ expect(subject.auth(attributes).id).to eq(12345) } 79 | end 80 | 81 | context do 82 | before{ stub_api_request('tokens/auth/failed').perform } 83 | specify{ expect(subject.auth(attributes)).to be_instance_of(CloudPayments::Transaction) } 84 | specify{ expect(subject.auth(attributes)).not_to be_required_secure3d } 85 | specify{ expect(subject.auth(attributes)).to be_declined } 86 | specify{ expect(subject.auth(attributes).id).to eq(12345) } 87 | specify{ expect(subject.auth(attributes).reason).to eq('InsufficientFunds') } 88 | end 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CloudPayments 2 | 3 | CloudPayments ruby client (https://developers.cloudpayments.ru/en/) 4 | 5 | [![Build Status](https://travis-ci.org/platmart/cloud_payments.svg)](https://travis-ci.org/platmart/cloud_payments) 6 | 7 | ## Installation 8 | 9 | Add this line to your application's Gemfile: 10 | 11 | ```ruby 12 | gem 'cloud_payments' 13 | ``` 14 | 15 | And then execute: 16 | 17 | ``` 18 | $ bundle 19 | ``` 20 | 21 | Or install it yourself as: 22 | 23 | ``` 24 | $ gem install cloud_payments 25 | ``` 26 | 27 | ## Usage 28 | 29 | ### Configuration 30 | 31 | #### Global configuration 32 | 33 | ```ruby 34 | CloudPayments.configure do |c| 35 | c.host = 'http://localhost:3000' # By default, it is https://api.cloudpayments.ru 36 | c.public_key = '' 37 | c.secret_key = '' 38 | c.log = false # By default. it is true 39 | c.logger = Logger.new('/dev/null') # By default, it writes logs to stdout 40 | c.raise_banking_errors = true # By default, it is not raising banking errors 41 | end 42 | 43 | # API client 44 | CloudPayments.client.payments.cards.charge(...) 45 | 46 | # Webhooks 47 | CloudPayments.webhooks.on_pay(...) 48 | ``` 49 | 50 | #### Local configuration 51 | 52 | ```ruby 53 | config = CloudPayments::Config.new do |c| 54 | # ... 55 | end 56 | 57 | # API client 58 | client = CloudPayments::Client.new(config) 59 | client.payments.cards.charge(...) 60 | 61 | # Webhooks 62 | webhooks = CloudPayments::Webhooks.new(config) 63 | webhooks.on_pay(...) 64 | ``` 65 | 66 | ### Test method 67 | 68 | ```ruby 69 | CloudPayments.client.ping 70 | # => true 71 | ``` 72 | 73 | ### Cryptogram-based payments 74 | 75 | ```ruby 76 | transaction = CloudPayments.client.payments.cards.charge( 77 | amount: 120, 78 | currency: 'RUB', 79 | ip_address: request.remote_ip, 80 | name: params[:name], 81 | card_cryptogram_packet: params[:card_cryptogram_packet] 82 | ) 83 | # => {:metadata=>nil, 84 | # :id=>12345, 85 | # :amount=>120, 86 | # :currency=>"RUB", 87 | # :currency_code=>0, 88 | # :invoice_id=>"1234567", 89 | # :account_id=>"user_x", 90 | # :email=>nil, 91 | # :description=>"Payment for goods on example.com", 92 | # :created_at=>#, 93 | # :authorized_at=>#, 94 | # :confirmed_at=>#, 95 | # :auth_code=>"123456", 96 | # :test_mode=>true, 97 | # :ip_address=>"195.91.194.13", 98 | # :ip_country=>"RU", 99 | # :ip_city=>"Ufa", 100 | # :ip_region=>"Bashkortostan Republic", 101 | # :ip_district=>"Volga Federal District", 102 | # :ip_lat=>54.7355, 103 | # :ip_lng=>55.991982, 104 | # :card_first_six=>"411111", 105 | # :card_last_four=>"1111", 106 | # :card_type=>"Visa", 107 | # :card_type_code=>0, 108 | # :issuer=>"Sberbank of Russia", 109 | # :issuer_bank_country=>"RU", 110 | # :status=>"Completed", 111 | # :status_code=>3, 112 | # :reason=>"Approved", 113 | # :reason_code=>0, 114 | # :card_holder_message=>"Payment successful", 115 | # :name=>"CARDHOLDER NAME", 116 | # :token=>"a4e67841-abb0-42de-a364-d1d8f9f4b3c0"} 117 | transaction.class 118 | # => CloudPayments::Transaction 119 | transaction.token 120 | # => "a4e67841-abb0-42de-a364-d1d8f9f4b3c0" 121 | ``` 122 | 123 | ## Kassa Receipt 124 | 125 | CloudPayments Kassa API (https://cloudpayments.ru/docs/api/kassa) 126 | 127 | ```ruby 128 | CloudPayments.client.kassa.receipt({ 129 | account_id: "user@example.com", 130 | customer_receipt: { 131 | items: [ 132 | { 133 | amount: "13350.00", 134 | ean13: nil, 135 | label: "Good Description", 136 | price: "13350.00", 137 | quantity: 1.0, 138 | vat: nil 139 | } 140 | ] 141 | }, 142 | inn: "7708806666", 143 | invoice_id: "231312312", 144 | type: "Income" 145 | }) 146 | ``` 147 | 148 | ## Apple Pay Start Session 149 | [Start Apple Pay session](https://developers.cloudpayments.ru/#zapusk-sessii-dlya-oplaty-cherez-apple-pay) 150 | ```ruby 151 | CloudPayments.client.apple_pay.start_session({validation_url: "https://apple-pay-gateway-pr-pod2.apple.com/paymentservices/startSession"}) 152 | # => { 153 | # :message => nil, 154 | # :model => { 155 | # :display_name => "example.com, 156 | # :domain_name => "example.com", 157 | # :epoch_timestamp => 1594072416294, 158 | # :expires_at => 1594076016294, 159 | # :merchant_identifier => "5DCCE3A52CFC3FAF9F4EA8421472E47BC503E03051B04D2ED67A3834386B52F2", 160 | # :merchant_session_identifier => "SSHDA3C703BD69B45EDB8934E6BFCC159B2B83AAFC02DB625F1F1E3997CCC2FE2CFD11F636558", 161 | # :nonce => "51c77142", 162 | # :signature => "30800.....0" 163 | # }, 164 | # :success => true 165 | # } 166 | 167 | ``` 168 | 169 | ## Webhooks 170 | 171 | ```ruby 172 | if CloudPayments.webhooks.data_valid?(payload, hmac_token) 173 | event = CloudPayments.webhooks.on_recurrent(payload) 174 | # or 175 | event = CloudPayments.webhooks.on_pay(payload) 176 | # or 177 | event = CloudPayments.webhooks.on_fail(payload) 178 | end 179 | ``` 180 | 181 | with capturing of an exception 182 | 183 | ```ruby 184 | rescue_from CloudPayments::Webhooks::HMACError, :handle_hmac_error 185 | 186 | before_action -> { CloudPayments.webhooks.validate_data!(payload, hmac_token) } 187 | 188 | def pay 189 | event = CloudPayments.webhooks.on_pay(payload) 190 | # ... 191 | end 192 | 193 | def fail 194 | event = CloudPayments.webhooks.on_fail(payload) 195 | # ... 196 | end 197 | 198 | def recurrent 199 | event = CloudPayments.webhooks.on_recurrent(payload) 200 | # ... 201 | end 202 | ``` 203 | 204 | ## Contributing 205 | 206 | 1. Fork it ( https://github.com/platmart/cloud_payments/fork ) 207 | 2. Create your feature branch (`git checkout -b my-new-feature`) 208 | 3. Commit your changes (`git commit -am 'Add some feature'`) 209 | 4. Push to the branch (`git push origin my-new-feature`) 210 | 5. Create a new Pull Request 211 | -------------------------------------------------------------------------------- /spec/cloud_payments/models/subscription_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'spec_helper' 3 | 4 | describe CloudPayments::Subscription do 5 | subject{ CloudPayments::Subscription.new(attributes) } 6 | 7 | describe 'properties' do 8 | let(:attributes){ { 9 | id: 'sc_8cf8a9338fb8ebf7202b08d09c938', 10 | account_id: 'user@example.com', 11 | description: 'Monthly subscription', 12 | email: 'user@example.com', 13 | amount: 1.02, 14 | currency_code: 0, 15 | currency: 'RUB', 16 | require_confirmation: false, 17 | start_date_iso: '2014-08-09T11:49:41', 18 | interval_code: 1, 19 | interval: 'Month', 20 | period: 1, 21 | max_periods: 12, 22 | status_code: 0, 23 | status: 'Active', 24 | successful_transactions_number: 0, 25 | failed_transactions_number: 0, 26 | last_transaction_date_iso: '2014-08-09T11:49:41', 27 | next_transaction_date_iso: '2014-08-09T11:49:41' 28 | } } 29 | 30 | specify{ expect(subject.id).to eq('sc_8cf8a9338fb8ebf7202b08d09c938') } 31 | specify{ expect(subject.account_id).to eq('user@example.com') } 32 | specify{ expect(subject.description).to eq('Monthly subscription') } 33 | specify{ expect(subject.email).to eq('user@example.com') } 34 | specify{ expect(subject.amount).to eq(1.02) } 35 | specify{ expect(subject.currency_code).to eq(0) } 36 | specify{ expect(subject.currency).to eq('RUB') } 37 | specify{ expect(subject.require_confirmation).to be_falsy } 38 | specify{ expect(subject.started_at).to eq(DateTime.parse('2014-08-09T11:49:41')) } 39 | specify{ expect(subject.interval_code).to eq(1) } 40 | specify{ expect(subject.interval).to eq('Month') } 41 | specify{ expect(subject.period).to eq(1) } 42 | specify{ expect(subject.max_periods).to eq(12) } 43 | specify{ expect(subject.status_code).to eq(0) } 44 | specify{ expect(subject.status).to eq('Active') } 45 | specify{ expect(subject.successful_transactions).to eq(0) } 46 | specify{ expect(subject.failed_transactions).to eq(0) } 47 | specify{ expect(subject.last_transaction_at).to eq(DateTime.parse('2014-08-09T11:49:41')) } 48 | specify{ expect(subject.next_transaction_at).to eq(DateTime.parse('2014-08-09T11:49:41')) } 49 | 50 | context 'without any attributes' do 51 | let(:attributes){ {} } 52 | specify{ expect{ subject }.to raise_error(/\'id\' is required/) } 53 | end 54 | 55 | it_behaves_like :raise_without_attribute, :id 56 | it_behaves_like :raise_without_attribute, :account_id 57 | it_behaves_like :raise_without_attribute, :email 58 | it_behaves_like :raise_without_attribute, :amount 59 | it_behaves_like :raise_without_attribute, :currency_code 60 | it_behaves_like :raise_without_attribute, :currency 61 | it_behaves_like :raise_without_attribute, :require_confirmation 62 | it_behaves_like :raise_without_attribute, :start_date_iso, :started_at 63 | it_behaves_like :raise_without_attribute, :interval_code 64 | it_behaves_like :raise_without_attribute, :interval 65 | it_behaves_like :raise_without_attribute, :period 66 | it_behaves_like :raise_without_attribute, :status_code 67 | it_behaves_like :raise_without_attribute, :status 68 | it_behaves_like :raise_without_attribute, :successful_transactions_number, :successful_transactions 69 | it_behaves_like :raise_without_attribute, :failed_transactions_number, :failed_transactions 70 | 71 | it_behaves_like :not_raise_without_attribute, :max_periods 72 | it_behaves_like :not_raise_without_attribute, :last_transaction_date_iso, :last_transaction_at 73 | it_behaves_like :not_raise_without_attribute, :next_transaction_date_iso, :next_transaction_at 74 | end 75 | 76 | describe 'status' do 77 | let(:attributes){ { 78 | id: 'sc_8cf8a9338fb8ebf7202b08d09c938', 79 | account_id: 'user@example.com', 80 | description: 'Monthly subscription', 81 | email: 'user@example.com', 82 | amount: 1.02, 83 | currency_code: 0, 84 | currency: 'RUB', 85 | require_confirmation: false, 86 | start_date_iso: '2014-08-09T11:49:41', 87 | interval_code: 1, 88 | interval: 'Month', 89 | period: 1, 90 | status_code: 0, 91 | status: status, 92 | successful_transactions_number: 0, 93 | failed_transactions_number: 0 94 | } } 95 | 96 | context 'when status == Active' do 97 | let(:status){ 'Active' } 98 | specify{ expect(subject).to be_active } 99 | specify{ expect(subject).not_to be_past_due } 100 | specify{ expect(subject).not_to be_cancelled } 101 | specify{ expect(subject).not_to be_rejected } 102 | specify{ expect(subject).not_to be_expired } 103 | end 104 | 105 | context 'when status == PastDue' do 106 | let(:status){ 'PastDue' } 107 | specify{ expect(subject).not_to be_active } 108 | specify{ expect(subject).to be_past_due } 109 | specify{ expect(subject).not_to be_cancelled } 110 | specify{ expect(subject).not_to be_rejected } 111 | specify{ expect(subject).not_to be_expired } 112 | end 113 | 114 | context 'when status == Cancelled' do 115 | let(:status){ 'Cancelled' } 116 | specify{ expect(subject).not_to be_active } 117 | specify{ expect(subject).not_to be_past_due } 118 | specify{ expect(subject).to be_cancelled } 119 | specify{ expect(subject).not_to be_rejected } 120 | specify{ expect(subject).not_to be_expired } 121 | end 122 | 123 | context 'when status == Rejected' do 124 | let(:status){ 'Rejected' } 125 | specify{ expect(subject).not_to be_active } 126 | specify{ expect(subject).not_to be_past_due } 127 | specify{ expect(subject).not_to be_cancelled } 128 | specify{ expect(subject).to be_rejected } 129 | specify{ expect(subject).not_to be_expired } 130 | end 131 | 132 | context 'when status == Expired' do 133 | let(:status){ 'Expired' } 134 | specify{ expect(subject).not_to be_active } 135 | specify{ expect(subject).not_to be_past_due } 136 | specify{ expect(subject).not_to be_cancelled } 137 | specify{ expect(subject).not_to be_rejected } 138 | specify{ expect(subject).to be_expired } 139 | end 140 | end 141 | end 142 | -------------------------------------------------------------------------------- /spec/cloud_payments/namespaces/cards_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'spec_helper' 3 | 4 | describe CloudPayments::Namespaces::Cards do 5 | subject{ CloudPayments::Namespaces::Cards.new(CloudPayments.client, '/payments') } 6 | 7 | let(:attributes){ { 8 | amount: 10, 9 | currency: 'RUB', 10 | invoice_id: '1234567', 11 | description: 'Payment for goods on example.com', 12 | account_id: 'user_x', 13 | name: 'CARDHOLDER NAME', 14 | card_cryptogram_packet: '01492500008719030128SM' 15 | } } 16 | 17 | describe '#charge' do 18 | context 'config.raise_banking_errors = false' do 19 | before { CloudPayments.config.raise_banking_errors = false } 20 | 21 | context do 22 | before{ stub_api_request('cards/charge/successful').perform } 23 | specify{ expect(subject.charge(attributes)).to be_instance_of(CloudPayments::Transaction) } 24 | specify{ expect(subject.charge(attributes)).not_to be_required_secure3d } 25 | specify{ expect(subject.charge(attributes)).to be_completed } 26 | specify{ expect(subject.charge(attributes).id).to eq(12345) } 27 | end 28 | 29 | context do 30 | before{ stub_api_request('cards/charge/secure3d').perform } 31 | specify{ expect(subject.charge(attributes)).to be_instance_of(CloudPayments::Secure3D) } 32 | specify{ expect(subject.charge(attributes)).to be_required_secure3d } 33 | specify{ expect(subject.charge(attributes).id).to eq(12345) } 34 | specify{ expect(subject.charge(attributes).transaction_id).to eq(12345) } 35 | specify{ expect(subject.charge(attributes).pa_req).to eq('eJxVUdtugkAQ') } 36 | specify{ expect(subject.charge(attributes).acs_url).to eq('https://test.paymentgate.ru/acs/auth/start.do') } 37 | end 38 | 39 | context do 40 | before{ stub_api_request('cards/charge/failed').perform } 41 | specify{ expect(subject.charge(attributes)).to be_instance_of(CloudPayments::Transaction) } 42 | specify{ expect(subject.charge(attributes)).not_to be_required_secure3d } 43 | specify{ expect(subject.charge(attributes)).to be_declined } 44 | specify{ expect(subject.charge(attributes).id).to eq(12345) } 45 | specify{ expect(subject.charge(attributes).reason).to eq('InsufficientFunds') } 46 | end 47 | end 48 | 49 | context 'config.raise_banking_errors = true' do 50 | before { CloudPayments.config.raise_banking_errors = true } 51 | 52 | context do 53 | before{ stub_api_request('cards/charge/successful').perform } 54 | specify{ expect{ subject.charge(attributes) }.not_to raise_error } 55 | end 56 | 57 | context do 58 | before{ stub_api_request('cards/charge/secure3d').perform } 59 | specify{ expect{ subject.charge(attributes) }.not_to raise_error } 60 | end 61 | 62 | context do 63 | before{ stub_api_request('cards/charge/failed').perform } 64 | specify{ expect{ subject.charge(attributes) }.to raise_error(CloudPayments::Client::GatewayErrors::InsufficientFunds) } 65 | end 66 | end 67 | end 68 | 69 | describe '#auth' do 70 | context 'config.raise_banking_errors = false' do 71 | before { CloudPayments.config.raise_banking_errors = false } 72 | 73 | context do 74 | before{ stub_api_request('cards/auth/successful').perform } 75 | specify{ expect(subject.auth(attributes)).to be_instance_of(CloudPayments::Transaction) } 76 | specify{ expect(subject.auth(attributes)).not_to be_required_secure3d } 77 | specify{ expect(subject.auth(attributes)).to be_authorized } 78 | specify{ expect(subject.auth(attributes).id).to eq(12345) } 79 | end 80 | 81 | context do 82 | before{ stub_api_request('cards/auth/secure3d').perform } 83 | specify{ expect(subject.auth(attributes)).to be_instance_of(CloudPayments::Secure3D) } 84 | specify{ expect(subject.auth(attributes)).to be_required_secure3d } 85 | specify{ expect(subject.auth(attributes).id).to eq(12345) } 86 | specify{ expect(subject.auth(attributes).transaction_id).to eq(12345) } 87 | specify{ expect(subject.auth(attributes).pa_req).to eq('eJxVUdtugkAQ') } 88 | specify{ expect(subject.auth(attributes).acs_url).to eq('https://test.paymentgate.ru/acs/auth/start.do') } 89 | end 90 | 91 | context do 92 | before{ stub_api_request('cards/auth/failed').perform } 93 | specify{ expect(subject.auth(attributes)).to be_instance_of(CloudPayments::Transaction) } 94 | specify{ expect(subject.auth(attributes)).not_to be_required_secure3d } 95 | specify{ expect(subject.auth(attributes)).to be_declined } 96 | specify{ expect(subject.auth(attributes).id).to eq(12345) } 97 | specify{ expect(subject.auth(attributes).reason).to eq('InsufficientFunds') } 98 | end 99 | end 100 | 101 | context 'config.raise_banking_errors = true' do 102 | before { CloudPayments.config.raise_banking_errors = true } 103 | 104 | context do 105 | before{ stub_api_request('cards/auth/successful').perform } 106 | specify{ expect{ subject.auth(attributes) }.not_to raise_error } 107 | end 108 | 109 | context do 110 | before{ stub_api_request('cards/auth/secure3d').perform } 111 | specify{ expect{ subject.auth(attributes) }.not_to raise_error } 112 | end 113 | 114 | context do 115 | before{ stub_api_request('cards/auth/failed').perform } 116 | specify{ expect{ subject.auth(attributes) }.to raise_error(CloudPayments::Client::GatewayErrors::InsufficientFunds) } 117 | end 118 | end 119 | end 120 | 121 | describe '#post3ds' do 122 | let(:attributes){ { transaction_id: 12345, pa_res: 'AQ==' } } 123 | 124 | context 'config.raise_banking_errors = false' do 125 | before { CloudPayments.config.raise_banking_errors = false } 126 | 127 | context do 128 | before{ stub_api_request('cards/post3ds/successful').perform } 129 | specify{ expect(subject.post3ds(attributes)).to be_instance_of(CloudPayments::Transaction) } 130 | specify{ expect(subject.post3ds(attributes)).not_to be_required_secure3d } 131 | specify{ expect(subject.post3ds(attributes)).to be_completed } 132 | specify{ expect(subject.post3ds(attributes).id).to eq(12345) } 133 | end 134 | 135 | context do 136 | before{ stub_api_request('cards/post3ds/failed').perform } 137 | specify{ expect(subject.post3ds(attributes)).to be_instance_of(CloudPayments::Transaction) } 138 | specify{ expect(subject.post3ds(attributes)).not_to be_required_secure3d } 139 | specify{ expect(subject.post3ds(attributes)).to be_declined } 140 | specify{ expect(subject.post3ds(attributes).id).to eq(12345) } 141 | specify{ expect(subject.post3ds(attributes).reason).to eq('InsufficientFunds') } 142 | end 143 | end 144 | 145 | context 'config.raise_banking_errors = true' do 146 | before { CloudPayments.config.raise_banking_errors = true } 147 | 148 | context do 149 | before{ stub_api_request('cards/post3ds/successful').perform } 150 | specify{ expect{ subject.post3ds(attributes) }.not_to raise_error } 151 | end 152 | 153 | context do 154 | before{ stub_api_request('cards/post3ds/failed').perform } 155 | specify{ expect{ subject.post3ds(attributes) }.to raise_error(CloudPayments::Client::GatewayErrors::InsufficientFunds) } 156 | end 157 | end 158 | end 159 | end 160 | -------------------------------------------------------------------------------- /spec/cloud_payments/namespaces/payments_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'spec_helper' 3 | 4 | describe CloudPayments::Namespaces::Payments do 5 | subject{ CloudPayments::Namespaces::Payments.new(CloudPayments.client) } 6 | 7 | describe '#cards' do 8 | specify{ expect(subject.cards).to be_instance_of(CloudPayments::Namespaces::Cards) } 9 | specify{ expect(subject.cards.parent_path).to eq('payments') } 10 | end 11 | 12 | describe '#tokens' do 13 | specify{ expect(subject.tokens).to be_instance_of(CloudPayments::Namespaces::Tokens) } 14 | specify{ expect(subject.tokens.parent_path).to eq('payments') } 15 | end 16 | 17 | describe '#confirm' do 18 | context do 19 | before{ stub_api_request('payments/confirm/successful').perform } 20 | specify{ expect(subject.confirm(12345, 120)).to be_truthy } 21 | end 22 | 23 | context do 24 | before{ stub_api_request('payments/confirm/failed').perform } 25 | specify{ expect(subject.confirm(12345, 120)).to be_falsy } 26 | end 27 | 28 | context do 29 | before{ stub_api_request('payments/confirm/failed_with_message').perform } 30 | specify{ expect{ subject.confirm(12345, 120) }.to raise_error(CloudPayments::Client::GatewayError, 'Error message') } 31 | end 32 | end 33 | 34 | describe '#void' do 35 | context do 36 | before{ stub_api_request('payments/void/successful').perform } 37 | specify{ expect(subject.void(12345)).to be_truthy } 38 | end 39 | 40 | context do 41 | before{ stub_api_request('payments/void/failed').perform } 42 | specify{ expect(subject.void(12345)).to be_falsy } 43 | end 44 | 45 | context do 46 | before{ stub_api_request('payments/void/failed_with_message').perform } 47 | specify{ expect{ subject.void(12345) }.to raise_error(CloudPayments::Client::GatewayError, 'Error message') } 48 | end 49 | end 50 | 51 | describe '#refund' do 52 | context do 53 | before{ stub_api_request('payments/refund/successful').perform } 54 | specify{ expect(subject.refund(12345, 120)).to be_truthy } 55 | end 56 | 57 | context do 58 | before{ stub_api_request('payments/refund/failed').perform } 59 | specify{ expect(subject.refund(12345, 120)).to be_falsy } 60 | end 61 | 62 | context do 63 | before{ stub_api_request('payments/refund/failed_with_message').perform } 64 | specify{ expect{ subject.refund(12345, 120) }.to raise_error(CloudPayments::Client::GatewayError, 'Error message') } 65 | end 66 | end 67 | 68 | describe '#post3ds' do 69 | context 'config.raise_banking_errors = false' do 70 | before { CloudPayments.config.raise_banking_errors = false } 71 | 72 | context do 73 | before{ stub_api_request('payments/post3ds/successful').perform } 74 | specify{ expect(subject.post3ds(12345, 'eJxVUdtugkAQ')).to be_instance_of(CloudPayments::Transaction) } 75 | end 76 | 77 | context do 78 | before{ stub_api_request('payments/post3ds/failed').perform } 79 | specify{ expect{ subject.post3ds(12345, 'eJxVUdtugkAQ') }.not_to raise_error } 80 | end 81 | end 82 | 83 | context 'config.raise_banking_errors = true' do 84 | before { CloudPayments.config.raise_banking_errors = true } 85 | 86 | context do 87 | before{ stub_api_request('payments/post3ds/successful').perform } 88 | specify{ expect{ subject.post3ds(12345, 'eJxVUdtugkAQ') }.not_to raise_error } 89 | end 90 | 91 | context do 92 | before{ stub_api_request('payments/post3ds/failed').perform } 93 | specify{ expect{ subject.post3ds(12345, 'eJxVUdtugkAQ') }.to raise_error(CloudPayments::Client::GatewayErrors::InsufficientFunds) } 94 | end 95 | end 96 | end 97 | 98 | describe '#get' do 99 | let(:transaction_id) { 12345 } 100 | 101 | context 'config.raise_banking_errors = false' do 102 | before { CloudPayments.config.raise_banking_errors = false } 103 | 104 | context 'transaction not found' do 105 | before{ stub_api_request('payments/get/failed_with_message').perform } 106 | specify{ expect{subject.get(transaction_id)}.to raise_error(CloudPayments::Client::GatewayError, 'Not found') } 107 | end 108 | 109 | context 'transaction is failed' do 110 | before{ stub_api_request('payments/get/failed').perform } 111 | specify{ expect(subject.get(transaction_id)).to be_instance_of(CloudPayments::Transaction) } 112 | specify{ expect(subject.get(transaction_id)).not_to be_required_secure3d } 113 | specify{ expect(subject.get(transaction_id)).to be_declined } 114 | specify{ expect(subject.get(transaction_id).id).to eq(transaction_id) } 115 | specify{ expect(subject.get(transaction_id).reason).to eq('InsufficientFunds') } 116 | end 117 | 118 | context 'transaction is successful' do 119 | before{ stub_api_request('payments/get/successful').perform } 120 | specify{ expect(subject.get(transaction_id)).to be_instance_of(CloudPayments::Transaction) } 121 | specify{ expect(subject.get(transaction_id)).not_to be_required_secure3d } 122 | specify{ expect(subject.get(transaction_id)).to be_completed } 123 | specify{ expect(subject.get(transaction_id).id).to eq(transaction_id) } 124 | specify{ expect(subject.get(transaction_id).reason).to eq('Approved') } 125 | end 126 | 127 | context 'transaction is refunded' do 128 | before{ stub_api_request('payments/get/refunded').perform } 129 | specify{ expect(subject.get(transaction_id)).to be_completed } 130 | specify{ expect(subject.get(transaction_id)).to be_refunded } 131 | end 132 | end 133 | 134 | context 'config.raise_banking_errors = true' do 135 | before { CloudPayments.config.raise_banking_errors = true} 136 | 137 | context 'transaction not found' do 138 | before{ stub_api_request('payments/get/failed_with_message').perform } 139 | specify{ expect{subject.get(transaction_id)}.to raise_error(CloudPayments::Client::GatewayError, 'Not found') } 140 | end 141 | 142 | context 'transaction is failed' do 143 | before{ stub_api_request('payments/get/failed').perform } 144 | specify{ expect{subject.get(transaction_id)}.to raise_error(CloudPayments::Client::GatewayErrors::InsufficientFunds) } 145 | end 146 | 147 | context 'transaction is successful' do 148 | before{ stub_api_request('payments/get/successful').perform } 149 | specify{ expect{subject.get(transaction_id)}.to_not raise_error } 150 | specify{ expect(subject.get(transaction_id)).to be_instance_of(CloudPayments::Transaction) } 151 | specify{ expect(subject.get(transaction_id)).not_to be_required_secure3d } 152 | specify{ expect(subject.get(transaction_id)).to be_completed } 153 | specify{ expect(subject.get(transaction_id).id).to eq(transaction_id) } 154 | specify{ expect(subject.get(transaction_id).reason).to eq('Approved') } 155 | end 156 | end 157 | end 158 | 159 | describe '#find' do 160 | let(:invoice_id) { '1234567' } 161 | 162 | context 'config.raise_banking_errors = false' do 163 | before { CloudPayments.config.raise_banking_errors = false } 164 | 165 | context 'payment is not found' do 166 | before{ stub_api_request('payments/find/failed_with_message').perform } 167 | specify{ expect{subject.find(invoice_id)}.to raise_error(CloudPayments::Client::GatewayError, 'Not found') } 168 | end 169 | 170 | context 'payment is failed' do 171 | before{ stub_api_request('payments/find/failed').perform } 172 | specify{ expect(subject.find(invoice_id)).to be_instance_of(CloudPayments::Transaction) } 173 | specify{ expect(subject.find(invoice_id)).not_to be_required_secure3d } 174 | specify{ expect(subject.find(invoice_id)).to be_declined } 175 | specify{ expect(subject.find(invoice_id).id).to eq(12345) } 176 | specify{ expect(subject.find(invoice_id).invoice_id).to eq(invoice_id) } 177 | specify{ expect(subject.find(invoice_id).reason).to eq('InsufficientFunds') } 178 | end 179 | 180 | context 'transaction is successful' do 181 | before{ stub_api_request('payments/find/successful').perform } 182 | specify{ expect(subject.find(invoice_id)).to be_instance_of(CloudPayments::Transaction) } 183 | specify{ expect(subject.find(invoice_id)).not_to be_required_secure3d } 184 | specify{ expect(subject.find(invoice_id)).to be_completed } 185 | specify{ expect(subject.find(invoice_id).id).to eq(12345) } 186 | specify{ expect(subject.find(invoice_id).invoice_id).to eq(invoice_id) } 187 | specify{ expect(subject.find(invoice_id).reason).to eq('Approved') } 188 | end 189 | end 190 | 191 | context 'config.raise_banking_errors = true' do 192 | before { CloudPayments.config.raise_banking_errors = true} 193 | 194 | context 'payment is not found' do 195 | before{ stub_api_request('payments/find/failed_with_message').perform } 196 | specify{ expect{subject.find(invoice_id)}.to raise_error(CloudPayments::Client::GatewayError, 'Not found') } 197 | end 198 | 199 | context 'payment is failed' do 200 | before{ stub_api_request('payments/find/failed').perform } 201 | specify{ expect{subject.find(invoice_id)}.to raise_error(CloudPayments::Client::GatewayErrors::InsufficientFunds) } 202 | end 203 | 204 | context 'transaction is successful' do 205 | before{ stub_api_request('payments/find/successful').perform } 206 | specify{ expect(subject.find(invoice_id)).to be_instance_of(CloudPayments::Transaction) } 207 | specify{ expect(subject.find(invoice_id)).not_to be_required_secure3d } 208 | specify{ expect(subject.find(invoice_id)).to be_completed } 209 | specify{ expect(subject.find(invoice_id).id).to eq(12345) } 210 | specify{ expect(subject.find(invoice_id).invoice_id).to eq(invoice_id) } 211 | specify{ expect(subject.find(invoice_id).reason).to eq('Approved') } 212 | end 213 | end 214 | end 215 | end 216 | -------------------------------------------------------------------------------- /spec/cloud_payments/models/transaction_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'spec_helper' 3 | 4 | describe CloudPayments::Transaction do 5 | subject{ CloudPayments::Transaction.new(attributes) } 6 | 7 | describe 'properties' do 8 | let(:attributes){ { 9 | transaction_id: 504, 10 | amount: 10.0, 11 | currency: 'RUB', 12 | currency_code: 0, 13 | invoice_id: '1234567', 14 | account_id: 'user_x', 15 | subscription_id: 'sc_8cf8a9338fb8ebf7202b08d09c938', 16 | email: 'user@example.com', 17 | description: 'Buying goods in example.com', 18 | json_data: { key: 'value' }, 19 | date_time: '2014-08-09T11:49:41', 20 | created_date_iso: '2014-08-09T11:49:41', 21 | auth_date_iso: '2014-08-09T11:49:41', 22 | confirm_date_iso: '2014-08-09T11:49:41', 23 | auth_code: '123456', 24 | test_mode: true, 25 | ip_address: '195.91.194.13', 26 | ip_country: 'RU', 27 | ip_city: 'Ufa', 28 | ip_region: 'Republic of Bashkortostan', 29 | ip_district: 'Volga Federal District', 30 | ip_latitude: 54.7355, 31 | ip_longitude: 55.991982, 32 | card_first_six: '411111', 33 | card_last_four: '1111', 34 | card_type: 'Visa', 35 | card_type_code: 0, 36 | card_exp_date: '10/17', 37 | issuer: 'Sberbank of Russia', 38 | issuer_bank_country: 'RU', 39 | status: 'Completed', 40 | status_code: 3, 41 | reason: 'Approved', 42 | reason_code: 0, 43 | refunded: false, 44 | card_holder_message: 'Payment successful', 45 | name: 'CARDHOLDER NAME', 46 | token: 'a4e67841-abb0-42de-a364-d1d8f9f4b3c0', 47 | escrow_accumulation_id: '119d1f05-4fa8-4f35-85b6-09216a5a4fb6' 48 | } } 49 | 50 | specify{ expect(subject.id).to eq(504) } 51 | specify{ expect(subject.amount).to eq(10.0) } 52 | specify{ expect(subject.currency).to eq('RUB') } 53 | specify{ expect(subject.currency_code).to eq(0) } 54 | specify{ expect(subject.invoice_id).to eq('1234567') } 55 | specify{ expect(subject.account_id).to eq('user_x') } 56 | specify{ expect(subject.subscription_id).to eq('sc_8cf8a9338fb8ebf7202b08d09c938') } 57 | specify{ expect(subject.email).to eq('user@example.com') } 58 | specify{ expect(subject.description).to eq('Buying goods in example.com') } 59 | specify{ expect(subject.metadata).to eq(key: 'value') } 60 | specify{ expect(subject.date_time).to eq(DateTime.parse('2014-08-09T11:49:41')) } 61 | specify{ expect(subject.created_at).to eq(DateTime.parse('2014-08-09T11:49:41')) } 62 | specify{ expect(subject.authorized_at).to eq(DateTime.parse('2014-08-09T11:49:41')) } 63 | specify{ expect(subject.confirmed_at).to eq(DateTime.parse('2014-08-09T11:49:41')) } 64 | specify{ expect(subject.auth_code).to eq('123456') } 65 | specify{ expect(subject.test_mode).to be_truthy } 66 | specify{ expect(subject.ip_address).to eq('195.91.194.13') } 67 | specify{ expect(subject.ip_country).to eq('RU') } 68 | specify{ expect(subject.ip_city).to eq('Ufa') } 69 | specify{ expect(subject.ip_region).to eq('Republic of Bashkortostan') } 70 | specify{ expect(subject.ip_district).to eq('Volga Federal District') } 71 | specify{ expect(subject.ip_lat).to eq(54.7355) } 72 | specify{ expect(subject.ip_lng).to eq(55.991982) } 73 | specify{ expect(subject.card_first_six).to eq('411111') } 74 | specify{ expect(subject.card_last_four).to eq('1111') } 75 | specify{ expect(subject.card_type).to eq('Visa') } 76 | specify{ expect(subject.card_type_code).to eq(0) } 77 | specify{ expect(subject.card_exp_date).to eq('10/17') } 78 | specify{ expect(subject.issuer).to eq('Sberbank of Russia') } 79 | specify{ expect(subject.issuer_bank_country).to eq('RU') } 80 | specify{ expect(subject.reason).to eq('Approved') } 81 | specify{ expect(subject.reason_code).to eq(0) } 82 | specify{ expect(subject.card_holder_message).to eq('Payment successful') } 83 | specify{ expect(subject.name).to eq('CARDHOLDER NAME') } 84 | specify{ expect(subject.token).to eq('a4e67841-abb0-42de-a364-d1d8f9f4b3c0') } 85 | specify{ expect(subject.refunded).to eq(false) } 86 | specify{ expect(subject.escrow_accumulation_id).to eq('119d1f05-4fa8-4f35-85b6-09216a5a4fb6') } 87 | 88 | context 'without any attributes' do 89 | let(:attributes){ {} } 90 | specify{ expect{ subject }.to raise_error(/\'id\' is required/) } 91 | end 92 | 93 | it_behaves_like :raise_without_attribute, :transaction_id, :id 94 | it_behaves_like :raise_without_attribute, :amount 95 | it_behaves_like :raise_without_attribute, :currency 96 | it_behaves_like :raise_without_attribute, :test_mode 97 | it_behaves_like :raise_without_attribute, :card_first_six 98 | it_behaves_like :raise_without_attribute, :card_last_four 99 | it_behaves_like :raise_without_attribute, :card_type 100 | 101 | it_behaves_like :not_raise_without_attribute, :currency_code 102 | it_behaves_like :not_raise_without_attribute, :invoice_id 103 | it_behaves_like :not_raise_without_attribute, :account_id 104 | it_behaves_like :not_raise_without_attribute, :email 105 | it_behaves_like :not_raise_without_attribute, :description 106 | it_behaves_like :not_raise_without_attribute, :date_time 107 | it_behaves_like :not_raise_without_attribute, :created_date_iso, :created_at 108 | it_behaves_like :not_raise_without_attribute, :auth_date_iso, :authorized_at 109 | it_behaves_like :not_raise_without_attribute, :confirm_date_iso, :confirmed_at 110 | it_behaves_like :not_raise_without_attribute, :auth_code 111 | it_behaves_like :not_raise_without_attribute, :ip_address 112 | it_behaves_like :not_raise_without_attribute, :ip_country 113 | it_behaves_like :not_raise_without_attribute, :ip_city 114 | it_behaves_like :not_raise_without_attribute, :ip_region 115 | it_behaves_like :not_raise_without_attribute, :ip_district 116 | it_behaves_like :not_raise_without_attribute, :ip_latitude, :ip_lat 117 | it_behaves_like :not_raise_without_attribute, :ip_longitude, :ip_lng 118 | it_behaves_like :not_raise_without_attribute, :card_type_code 119 | it_behaves_like :not_raise_without_attribute, :card_exp_date 120 | it_behaves_like :not_raise_without_attribute, :issuer 121 | it_behaves_like :not_raise_without_attribute, :issuer_bank_country 122 | it_behaves_like :not_raise_without_attribute, :reason 123 | it_behaves_like :not_raise_without_attribute, :reason_code 124 | it_behaves_like :not_raise_without_attribute, :refunded 125 | it_behaves_like :not_raise_without_attribute, :card_holder_message 126 | it_behaves_like :not_raise_without_attribute, :name 127 | it_behaves_like :not_raise_without_attribute, :token 128 | 129 | context 'without `metadata` attribute' do 130 | subject do 131 | attrs = attributes.dup 132 | attrs.delete(:json_data) 133 | described_class.new(attrs) 134 | end 135 | 136 | specify{ expect{ subject }.not_to raise_error } 137 | specify{ expect(subject.metadata).to eq({}) } 138 | end 139 | end 140 | 141 | describe 'status' do 142 | let(:attributes){ { 143 | transaction_id: 504, 144 | amount: 10.0, 145 | currency: 'RUB', 146 | test_mode: true, 147 | card_first_six: '411111', 148 | card_last_four: '1111', 149 | card_type: 'Visa', 150 | card_exp_date: '10/17', 151 | status: status 152 | } } 153 | 154 | context 'when status == AwaitingAuthentication' do 155 | let(:status){ 'AwaitingAuthentication' } 156 | specify{ expect(subject).to be_awaiting_authentication } 157 | specify{ expect(subject).not_to be_completed } 158 | specify{ expect(subject).not_to be_authorized } 159 | specify{ expect(subject).not_to be_cancelled } 160 | specify{ expect(subject).not_to be_declined } 161 | end 162 | 163 | context 'when status == Completed' do 164 | let(:status){ 'Completed' } 165 | specify{ expect(subject).not_to be_awaiting_authentication } 166 | specify{ expect(subject).to be_completed } 167 | specify{ expect(subject).not_to be_authorized } 168 | specify{ expect(subject).not_to be_cancelled } 169 | specify{ expect(subject).not_to be_declined } 170 | end 171 | 172 | context 'when status == Authorized' do 173 | let(:status){ 'Authorized' } 174 | specify{ expect(subject).not_to be_awaiting_authentication } 175 | specify{ expect(subject).not_to be_completed } 176 | specify{ expect(subject).to be_authorized } 177 | specify{ expect(subject).not_to be_cancelled } 178 | specify{ expect(subject).not_to be_declined } 179 | end 180 | 181 | context 'when status == Cancelled' do 182 | let(:status){ 'Cancelled' } 183 | specify{ expect(subject).not_to be_awaiting_authentication } 184 | specify{ expect(subject).not_to be_completed } 185 | specify{ expect(subject).not_to be_authorized } 186 | specify{ expect(subject).to be_cancelled } 187 | specify{ expect(subject).not_to be_declined } 188 | end 189 | 190 | context 'when status == Declined' do 191 | let(:status){ 'Declined' } 192 | specify{ expect(subject).not_to be_awaiting_authentication } 193 | specify{ expect(subject).not_to be_completed } 194 | specify{ expect(subject).not_to be_authorized } 195 | specify{ expect(subject).not_to be_cancelled } 196 | specify{ expect(subject).to be_declined } 197 | end 198 | end 199 | 200 | context do 201 | let(:attributes){ {} } 202 | let(:default_attributes){ { 203 | transaction_id: 504, 204 | amount: 10.0, 205 | currency: 'RUB', 206 | test_mode: true, 207 | card_first_six: '411111', 208 | card_last_four: '1111', 209 | card_type: 'Visa', 210 | card_exp_date: '10/17', 211 | status: 'Completed' 212 | } } 213 | 214 | subject{ CloudPayments::Transaction.new(default_attributes.merge(attributes)) } 215 | 216 | describe '#subscription' do 217 | before{ stub_api_request('subscriptions/get/successful').perform } 218 | 219 | context 'with subscription_id' do 220 | let(:attributes){ { subscription_id: 'sc_8cf8a9338fb' } } 221 | 222 | specify{ expect(subject.subscription).to be_instance_of(CloudPayments::Subscription) } 223 | 224 | context do 225 | let(:sub){ subject.subscription } 226 | 227 | specify{ expect(sub.id).to eq('sc_8cf8a9338fb') } 228 | specify{ expect(sub.account_id).to eq('user@example.com') } 229 | specify{ expect(sub.description).to eq('Monthly subscription') } 230 | specify{ expect(sub.started_at).to eq(DateTime.parse('2014-08-09T11:49:41')) } 231 | specify{ expect(sub).to be_active } 232 | end 233 | end 234 | 235 | context 'without subscription_id' do 236 | specify{ expect(subject.subscription).to be_nil } 237 | end 238 | end 239 | 240 | describe '#card_number' do 241 | specify{ expect(subject.card_number).to eq('4111 11XX XXXX 1111') } 242 | end 243 | 244 | describe '#ip_location' do 245 | specify{ expect(subject.ip_location).to be_nil } 246 | 247 | context do 248 | let(:attributes){ { ip_latitude: 12.34, ip_longitude: 56.78 } } 249 | specify{ expect(subject.ip_location).to eq([12.34, 56.78]) } 250 | end 251 | 252 | context do 253 | let(:attributes){ { ip_latitude: 12.34 } } 254 | specify{ expect(subject.ip_location).to be_nil } 255 | end 256 | 257 | context do 258 | let(:attributes){ { ip_longitude: 12.34 } } 259 | specify{ expect(subject.ip_location).to be_nil } 260 | end 261 | end 262 | 263 | describe '#refunded?' do 264 | context do 265 | let(:attributes) { { refunded: false } } 266 | specify { expect(subject.refunded?).to be_falsey } 267 | end 268 | 269 | context do 270 | let(:attributes) { { refunded: true } } 271 | specify { expect(subject.refunded?).to be_truthy } 272 | end 273 | 274 | context do 275 | let(:attributes) { { refunded: nil } } 276 | specify { expect(subject.refunded?).to be_falsey } 277 | end 278 | end 279 | end 280 | end 281 | -------------------------------------------------------------------------------- /spec/cloud_payments/webhooks_spec.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # encoding: utf-8 3 | # frozen_string_literal: true 4 | require 'spec_helper' 5 | 6 | describe CloudPayments::Webhooks do 7 | describe 'HMAC validation' do 8 | let(:valid_hmac) { 'tJW02TMAce4Em8eJTNqjhOax+BYRM5K2D8mX9xsmUUc=' } 9 | let(:invalid_hmac) { '6sUXv4W0wmhfpkkDtp+3Hw/M8deAPkMRVV3OWANcqro=' } 10 | let(:data) { 11 | "TransactionId=666&Amount=123.00&Currency=RUB&PaymentAmount=123.00&PaymentCurrency=RUB&InvoiceId=1234567&AccountId=user%40example.com&SubscriptionId=&Name=VLADIMIR+KOCHNEV&Email=user%40example.com&DateTime=2015-11-17+14%3a51%3a20&IpAddress=127.0.0.1&IpCountry=RU&IpCity=%d0%a1%d0%b0%d0%bd%d0%ba%d1%82-%d0%9f%d0%b5%d1%82%d0%b5%d1%80%d0%b1%d1%83%d1%80%d0%b3&IpRegion=%d0%a1%d0%b0%d0%bd%d0%ba%d1%82-%d0%9f%d0%b5%d1%82%d0%b5%d1%80%d0%b1%d1%83%d1%80%d0%b3&IpDistrict=%d0%a1%d0%b5%d0%b2%d0%b5%d1%80%d0%be-%d0%97%d0%b0%d0%bf%d0%b0%d0%b4%d0%bd%d1%8b%d0%b9+%d1%84%d0%b5%d0%b4%d0%b5%d1%80%d0%b0%d0%bb%d1%8c%d0%bd%d1%8b%d0%b9+%d0%be%d0%ba%d1%80%d1%83%d0%b3&IpLatitude=59.939000&IpLongitude=30.315800&CardFirstSix=411111&CardLastFour=1111&CardType=Visa&CardExpDate=01%2f19&Issuer=&IssuerBankCountry=&Description=%d0%9e%d0%bf%d0%bb%d0%b0%d1%82%d0%b0+%d0%b2+example.com&AuthCode=DEADBEEF&Token=1234567890&TestMode=1&Status=Completed" 12 | } 13 | 14 | context 'with data_valid?' do 15 | it 'returns true on valid hmac' do 16 | expect(CloudPayments.webhooks.data_valid?(data, valid_hmac)).to be_truthy 17 | end 18 | 19 | it 'returns false on invalid hmac' do 20 | expect(CloudPayments.webhooks.data_valid?(data, invalid_hmac)).to be_falsey 21 | end 22 | end 23 | 24 | context 'with validate_data!' do 25 | it 'returns true on valid hmac' do 26 | expect(CloudPayments.webhooks.validate_data!(data, valid_hmac)).to be_truthy 27 | end 28 | 29 | it 'raises a HMACError on invalid hmac' do 30 | expect { 31 | CloudPayments.webhooks.validate_data!(data, invalid_hmac) 32 | }.to raise_error(CloudPayments::Webhooks::HMACError) 33 | end 34 | end 35 | end 36 | 37 | describe 'on_check' do 38 | let(:raw_data) do 39 | {"TransactionId"=>"1701609", 40 | "Amount"=>"123.00", 41 | "Currency"=>"RUB", 42 | "PaymentAmount"=>"123.00", 43 | "PaymentCurrency"=>"RUB", 44 | "InvoiceId"=>"1234567", 45 | "AccountId"=>"user@example.com", 46 | "SubscriptionId"=>"", 47 | "Name"=>"OLEG FOMIN", 48 | "Email"=>"user@example.com", 49 | "DateTime"=>"2015-11-17 23:06:15", 50 | "IpAddress"=>"127.0.0.1", 51 | "IpCountry"=>"RU", 52 | "IpCity"=>"Санкт-Петербург", 53 | "IpRegion"=>"Санкт-Петербург", 54 | "IpDistrict"=>"Северо-Западный федеральный округ", 55 | "IpLatitude"=>"59.939000", 56 | "IpLongitude"=>"30.315000", 57 | "CardFirstSix"=>"411111", 58 | "CardLastFour"=>"1111", 59 | "CardType"=>"Visa", 60 | "CardExpDate"=>"01/19", 61 | "Issuer"=>"", 62 | "IssuerBankCountry"=>"", 63 | "Description"=>"Оплата в example.com", 64 | "TestMode"=>"1", 65 | "Status"=>"Completed", 66 | "Data"=> 67 | "{\"cloudPayments\":{\"recurrent\":{\"interval\":\"Month\",\"period\":1}}}"} 68 | end 69 | 70 | subject { CloudPayments.webhooks.on_check(raw_data) } 71 | end 72 | 73 | describe 'on_pay' do 74 | let(:raw_data) do 75 | {"TransactionId"=>"1701609", 76 | "Amount"=>"123.00", 77 | "Currency"=>"RUB", 78 | "PaymentAmount"=>"123.00", 79 | "PaymentCurrency"=>"RUB", 80 | "InvoiceId"=>"1234567", 81 | "AccountId"=>"user@example.com", 82 | "SubscriptionId"=>"sc_b865df3d4f27c54dc8067520c071a", 83 | "Name"=>"OLEG FOMIN", 84 | "Email"=>"user@example.com", 85 | "DateTime"=>"2015-11-17 23:06:17", 86 | "IpAddress"=>"127.0.0.1", 87 | "IpCountry"=>"RU", 88 | "IpCity"=>"Санкт-Петербург", 89 | "IpRegion"=>"Санкт-Петербург", 90 | "IpDistrict"=>"Северо-Западный федеральный округ", 91 | "IpLatitude"=>"59.939037", 92 | "IpLongitude"=>"30.315784", 93 | "CardFirstSix"=>"411111", 94 | "CardLastFour"=>"1111", 95 | "CardType"=>"Visa", 96 | "CardExpDate"=>"01/19", 97 | "Issuer"=>"", 98 | "IssuerBankCountry"=>"", 99 | "Description"=>"Оплата в example.com", 100 | "AuthCode"=>"A1B2C3", 101 | "Token"=>"9BBEF19476623CA56C17DA75FD57734DBF82530686043A6E491C6D71BEFE8F6E", 102 | "TestMode"=>"1", 103 | "Status"=>"Completed", 104 | "Data"=> 105 | "{\"cloudPayments\":{\"recurrent\":{\"interval\":\"Month\",\"period\":1}}}"} 106 | end 107 | 108 | subject { CloudPayments.webhooks.on_pay(raw_data) } 109 | 110 | specify { expect(subject.id).to eq '1701609' } 111 | specify { expect(subject.amount).to eq 123.00 } 112 | specify { expect(subject.currency).to eq 'RUB' } 113 | specify { expect(subject.invoice_id).to eq '1234567' } 114 | specify { expect(subject.account_id).to eq 'user@example.com' } 115 | specify { expect(subject.subscription_id).to eq 'sc_b865df3d4f27c54dc8067520c071a' } 116 | specify { expect(subject.name).to eq 'OLEG FOMIN' } 117 | specify { expect(subject.email).to eq 'user@example.com' } 118 | specify { expect(subject.date_time).to eq DateTime.parse('2015-11-17 23:06:17') } 119 | specify { expect(subject.ip_address).to eq '127.0.0.1' } 120 | specify { expect(subject.ip_country).to eq 'RU' } 121 | specify { expect(subject.ip_city).to eq 'Санкт-Петербург' } 122 | specify { expect(subject.ip_region).to eq 'Санкт-Петербург' } 123 | specify { expect(subject.ip_district).to eq 'Северо-Западный федеральный округ' } 124 | specify { expect(subject.ip_lat).to eq '59.939037' } 125 | specify { expect(subject.ip_lng).to eq '30.315784' } 126 | specify { expect(subject.card_first_six).to eq '411111' } 127 | specify { expect(subject.card_last_four).to eq '1111' } 128 | specify { expect(subject.card_type).to eq 'Visa' } 129 | specify { expect(subject.card_exp_date).to eq '01/19' } 130 | specify { expect(subject.description).to eq 'Оплата в example.com' } 131 | specify { expect(subject.auth_code).to eq 'A1B2C3' } 132 | specify { expect(subject.token).to eq '9BBEF19476623CA56C17DA75FD57734DBF82530686043A6E491C6D71BEFE8F6E' } 133 | specify { expect(subject.status).to eq 'Completed' } 134 | end 135 | 136 | describe 'on_fail' do 137 | let(:raw_data) do 138 | {"TransactionId"=>"1701658", 139 | "Amount"=>"123.00", 140 | "Currency"=>"RUB", 141 | "PaymentAmount"=>"123.00", 142 | "PaymentCurrency"=>"RUB", 143 | "InvoiceId"=>"1234567", 144 | "AccountId"=>"user@example.com", 145 | "SubscriptionId"=>"", 146 | "Name"=>"OLEG FOMIN", 147 | "Email"=>"user@example.com", 148 | "DateTime"=>"2015-11-17 23:35:09", 149 | "IpAddress"=>"127.0.0.1", 150 | "IpCountry"=>"RU", 151 | "IpCity"=>"Санкт-Петербург", 152 | "IpRegion"=>"Санкт-Петербург", 153 | "IpDistrict"=>"Северо-Западный федеральный округ", 154 | "IpLatitude"=>"59.939037", 155 | "IpLongitude"=>"30.315784", 156 | "CardFirstSix"=>"400005", 157 | "CardLastFour"=>"5556", 158 | "CardType"=>"Visa", 159 | "CardExpDate"=>"01/19", 160 | "Issuer"=>"", 161 | "IssuerBankCountry"=>"", 162 | "Description"=>"Оплата в example.com", 163 | "TestMode"=>"1", 164 | "Status"=>"Declined", 165 | "StatusCode"=>"5", 166 | "Reason"=>"InsufficientFunds", 167 | "ReasonCode"=>"5051", 168 | "Data"=> 169 | "{\"cloudPayments\":{\"recurrent\":{\"interval\":\"Month\",\"period\":1}}}"} 170 | end 171 | 172 | subject { CloudPayments.webhooks.on_fail(raw_data) } 173 | 174 | specify { expect(subject.id).to eq '1701658' } 175 | specify { expect(subject.amount).to eq 123.00 } 176 | specify { expect(subject.currency).to eq 'RUB' } 177 | specify { expect(subject.invoice_id).to eq '1234567' } 178 | specify { expect(subject.account_id).to eq 'user@example.com' } 179 | specify { expect(subject.subscription_id).to eq '' } 180 | specify { expect(subject.name).to eq 'OLEG FOMIN' } 181 | specify { expect(subject.email).to eq 'user@example.com' } 182 | specify { expect(subject.date_time).to eq DateTime.parse('2015-11-17 23:35:09') } 183 | specify { expect(subject.ip_address).to eq '127.0.0.1' } 184 | specify { expect(subject.ip_country).to eq 'RU' } 185 | specify { expect(subject.ip_city).to eq 'Санкт-Петербург' } 186 | specify { expect(subject.ip_region).to eq 'Санкт-Петербург' } 187 | specify { expect(subject.ip_district).to eq 'Северо-Западный федеральный округ' } 188 | specify { expect(subject.card_first_six).to eq '400005' } 189 | specify { expect(subject.card_last_four).to eq '5556' } 190 | specify { expect(subject.card_type).to eq 'Visa' } 191 | specify { expect(subject.card_exp_date).to eq '01/19' } 192 | specify { expect(subject.description).to eq 'Оплата в example.com' } 193 | end 194 | 195 | describe 'on_recurrent' do 196 | let(:raw_data) do 197 | {"Id"=>"sc_a38ca02005d40db7d32b36a0097b0", 198 | "AccountId"=>"1234", 199 | "Description"=>"just description", 200 | "Email"=>"user@example.com", 201 | "Amount"=>"2.00", 202 | "Currency"=>"RUB", 203 | "RequireConfirmation"=>"0", 204 | "StartDate"=>"2015-12-17 20:22:14", 205 | "Interval"=>"Month", 206 | "Period"=>"1", 207 | "Status"=>"PastDue", 208 | "SuccessfulTransactionsNumber"=>"11", 209 | "FailedTransactionsNumber"=>"22", 210 | "NextTransactionDate"=>"2015-11-18 20:29:05"} 211 | end 212 | 213 | subject { CloudPayments.webhooks.on_recurrent(raw_data) } 214 | 215 | specify { expect(subject.id).to eq 'sc_a38ca02005d40db7d32b36a0097b0' } 216 | specify { expect(subject.account_id).to eq '1234' } 217 | specify { expect(subject.description).to eq 'just description' } 218 | specify { expect(subject.email).to eq 'user@example.com' } 219 | specify { expect(subject.amount).to eq 2.00 } 220 | specify { expect(subject.currency).to eq 'RUB' } 221 | specify { expect(subject.require_confirmation).to eq false } 222 | specify { expect(subject.started_at).to eq DateTime.parse('2015-12-17 20:22:14') } 223 | specify { expect(subject.interval).to eq 'Month' } 224 | specify { expect(subject.period).to eq 1 } 225 | specify { expect(subject.status).to eq 'PastDue' } 226 | specify { expect(subject.successful_transactions).to eq 11 } 227 | specify { expect(subject.failed_transactions).to eq 22 } 228 | specify { expect(subject.next_transaction_at).to eq DateTime.parse('2015-11-18 20:29:05') } 229 | end 230 | 231 | describe 'on_kassa_receipt' do 232 | let(:raw_data) do 233 | {"Id"=>"sc_a38ca02005d40db7d32b36a0097b0", 234 | "DocumentNumber"=>"1234", 235 | "SessionNumber"=>"12345", 236 | "FiscalSign"=>"signsgin", 237 | "DeviceNumber"=>"123465", 238 | "RegNumber"=>"12345", 239 | "Inn"=>"0", 240 | "Type"=>"Type", 241 | "Ofd"=>"Ofd", 242 | "Url"=>"http://example.com/url/", 243 | "QrCodeUrl"=>"http://example.com/url", 244 | "Amount"=>"11.11", 245 | "DateTime"=>"2015-11-18 20:29:05", 246 | "Receipt"=>"{}", 247 | "TransactionId" => "12321123", 248 | "InvoiceId" => "123123", 249 | "AccountId" => "3213213"} 250 | end 251 | 252 | subject { CloudPayments.webhooks.kassa_receipt(raw_data) } 253 | 254 | specify { expect(subject.id).to eq "sc_a38ca02005d40db7d32b36a0097b0" } 255 | specify { expect(subject.document_number).to eq '1234' } 256 | specify { expect(subject.session_number).to eq '12345' } 257 | specify { expect(subject.fiscal_sign).to eq 'signsgin' } 258 | specify { expect(subject.device_number).to eq '123465' } 259 | specify { expect(subject.reg_number).to eq '12345' } 260 | specify { expect(subject.inn).to eq '0' } 261 | specify { expect(subject.type).to eq 'Type' } 262 | specify { expect(subject.ofd).to eq 'Ofd' } 263 | specify { expect(subject.url).to eq 'http://example.com/url/' } 264 | specify { expect(subject.qr_code_url).to eq 'http://example.com/url' } 265 | specify { expect(subject.amount).to eq 11.11 } 266 | specify { expect(subject.date_time).to eq DateTime.parse('2015-11-18 20:29:05') } 267 | specify { expect(subject.receipt).to eq '{}' } 268 | specify { expect(subject.transaction_id).to eq '12321123' } 269 | specify { expect(subject.invoice_id).to eq '123123' } 270 | specify { expect(subject.account_id).to eq '3213213' } 271 | end 272 | end 273 | --------------------------------------------------------------------------------