diff --git a/README.md b/README.md index 328f3a6..43ab345 100644 --- a/README.md +++ b/README.md @@ -108,10 +108,12 @@ Yookassa.payments.cancel(payment_id: '12345') - [ ] Запись реальных валидных и невалидных запросов и ответов **Работа с чеками** - - [ ] Запрос на создание чека + - [x] Запрос на создание чека - [ ] Запрос на создание чека через билдер - - [ ] Получить информацию о чеке - - [ ] Получить список чеков с фильтрацией + - [x] Получить информацию о чеке + - [x] Получить список чеков с фильтрацией + - [ ] Контракт на добавление возврата + - [ ] Запись реальных валидных и невалидных запросов и ответов ## Contributing diff --git a/lib/yookassa.rb b/lib/yookassa.rb index 9ab3420..b8ef7b4 100644 --- a/lib/yookassa.rb +++ b/lib/yookassa.rb @@ -26,6 +26,6 @@ module Yookassa @client ||= Client.new(shop_id: @config.shop_id, api_key: @config.api_key) end - def_delegators :client, :payments, :refunds + def_delegators :client, :payments, :refunds, :receipts end end diff --git a/lib/yookassa/client.rb b/lib/yookassa/client.rb index 03e3369..db0de33 100644 --- a/lib/yookassa/client.rb +++ b/lib/yookassa/client.rb @@ -3,6 +3,7 @@ require "http" require_relative "./payments" require_relative "./refunds" +require_relative "./receipts" require_relative "./entity/error" module Yookassa @@ -23,6 +24,10 @@ module Yookassa @refunds ||= Refunds.new(self) end + def receipts + @receipts ||= Receipts.new(self) + end + def get(endpoint, query: {}) api_call { http.get("#{API_URL}#{endpoint}", params: query) } end diff --git a/lib/yookassa/entity/payment_receipt.rb b/lib/yookassa/entity/payment_receipt.rb new file mode 100644 index 0000000..59fa099 --- /dev/null +++ b/lib/yookassa/entity/payment_receipt.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require_relative "./types" +require_relative "./customer" +require_relative "./product" + +module Yookassa + module Entity + class PaymentReceipt < Dry::Struct + # User details. You should specify at least the basic contact information: + # email address (customer.email) or phone number (customer.phone) + attribute? :customer, Yookassa::Entity::Customer + + # List of products in an order (maximum 100 items). + attribute :items, Types::Array.of(Yookassa::Entity::Product) + + # Store's tax system. The parameter is only required if you use several tax systems. + # Otherwise, the parameter is not specified. List of possible values: + # https://yookassa.ru/en/developers/54fz/parameters-values#tax-systems + attribute? :tax_system_code, Types::Coercible::Integer + + # User's phone number for sending the receipt. Set in the ITU-T E.164 format, for example, 79000000000. + # !DEPRECATED PARAMETER: we recommend specifying the details in the receipt.customer.phone parameter. + attribute? :phone, Types::Coercible::String + + # User's email address for sending the receipt. + # !DEPRECATED PARAMETER: we recommend specifying the details in the receipt.customer.email parameter. + attribute? :email, Types::String + end + end +end diff --git a/lib/yookassa/entity/product.rb b/lib/yookassa/entity/product.rb index c0d09dc..8191dfd 100644 --- a/lib/yookassa/entity/product.rb +++ b/lib/yookassa/entity/product.rb @@ -1,22 +1,43 @@ # frozen_string_literal: true +require_relative "./types" require_relative "./amount" +require_relative "./supplier" module Yookassa module Entity class Product < Dry::Struct + VatCodes = Types::Coercible::Integer.enum( + 1 => "VAT not included", + 2 => "0% VAT rate", + 3 => "10% VAT rate", + 4 => "20% receipt’s VAT rate", + 5 => "10/110 receipt’s estimate VAT rate", + 6 => "20/120 receipt’s estimate VAT rate" + ) + + AgentTypes = Types::String.enum( + "banking_payment_agent", + "banking_payment_subagent", + "payment_agent", + "payment_subagent", + "attorney", + "commissioner", + "agent" + ) + # Product name (maximum 128 characters). attribute :description, Types::String # Product quantity. Maximum possible value depends on the model of your online sales register. - attribute :quantity, Types::String + attribute :quantity, Types::Coercible::Float # Product price - attribute :amount, Yookassa::Entity::Amount + attribute :amount, Amount # VAT rate. Possible value is a number from 1 to 6. More about VAT rates codes # https://yookassa.ru/en/developers/54fz/parameters-values#vat-codes - attribute :vat_code, Types::Integer + attribute :vat_code, VatCodes # Payment subject attribute. List of possible values: https://yookassa.ru/en/developers/54fz/parameters-values#payment-subject attribute? :payment_subject, Types::String @@ -42,6 +63,16 @@ module Yookassa # Amount of excise tax on products including kopeks. Decimal number with 2 digits after the period. # Online sales register that support this parameter: Orange Data, Kit Invest. attribute? :excise + + # Information about the supplier of product or service. You can specify this parameter if you send the data + # for creating the receipt using the Receipt after payment scenario. + attribute? :supplier, Supplier + + # Type of agent selling goods or services. The parameter is provided for by the format of fiscal documents (FFD) + # and is considered mandatory for versions 1.1 and later. https://yookassa.ru/en/developers/54fz/parameters-values#agent-type + # You can send it if your online sales register is updated to FFD 1.1 + # and if you send the data for creating the receipt using the Receipt after payment scenario + attribute? :agent_type, AgentTypes end end end diff --git a/lib/yookassa/entity/receipt.rb b/lib/yookassa/entity/receipt.rb index 94db3cd..883faf2 100644 --- a/lib/yookassa/entity/receipt.rb +++ b/lib/yookassa/entity/receipt.rb @@ -3,29 +3,62 @@ require_relative "./types" require_relative "./customer" require_relative "./product" +require_relative "./settlement" module Yookassa module Entity class Receipt < Dry::Struct - # User details. You should specify at least the basic contact information: - # email address (customer.email) or phone number (customer.phone) - attribute? :customer, Yookassa::Entity::Customer + TaxSystemCodes = Types::Coercible::Integer.enum( + 1 => "General tax system", + 2 => "Simplified (STS, income)", + 3 => "Simplified (STS, income with costs deducted)", + 4 => "Unified tax on imputed income (ENVD)", + 5 => "Unified agricultural tax (ESN)", + 6 => "Patent Based Tax System" + ) - # List of products in an order (maximum 100 items). - attribute :items, Types::Array.of(Yookassa::Entity::Product) + # Receipt's ID in YooMoney. + attribute :id, Types::String - # Store's tax system. The parameter is only required if you use several tax systems. - # Otherwise, the parameter is not specified. List of possible values: - # https://yookassa.ru/en/developers/54fz/parameters-values#tax-systems - attribute? :tax_system_code, Types::Coercible::Integer + # Type of receipt in the online sales register: payment (payment) or payment refund (refund). + attribute :type, Types::String.enum("payment", "refund") - # User's phone number for sending the receipt. Set in the ITU-T E.164 format, for example, 79000000000. - # !DEPRECATED PARAMETER: we recommend specifying the details in the receipt.customer.phone parameter. - attribute? :phone, Types::Coercible::String + # ID of the payment that the receipt was created for. + attribute? :payment_id, Types::String - # User's email address for sending the receipt. - # !DEPRECATED PARAMETER: we recommend specifying the details in the receipt.customer.email parameter. - attribute? :email, Types::String + # ID of the refund that the receipt was created for. Not included in the payment receipt. + attribute? :refund_id, Types::String + + # Delivery status of receipt data to online sales register (pending, succeeded, or canceled). + attribute :status, Types::String.enum("pending", "succeeded", "canceled") + + # Fiscal document number. + attribute? :fiscal_document_number, Types::String + + # Number of fiscal storage drive in online sales register. + attribute? :fiscal_storage_number, Types::String + + # Fiscal attribute of the receipt. Created by the fiscal storage drive from the data sent for receipt registration. + attribute? :fiscal_attribute, Types::String + + # Date and time of receipt creation in the fiscal storage drive, specified in the ISO 8601 format. + attribute? :registered_at, Types::JSON::DateTime + + # Receipt's ID in online sales register. For successful receipt registration. + attribute? :fiscal_provider_id, Types::String + + # Store's taxation system. + attribute? :tax_system_code, TaxSystemCodes + + # List of products in the receipt (maximum 100 items). + attribute :items, Types::Array.of(Product) + + # List of completed settlements. + attribute? :settlements, Types::Array.of(Settlement) + + # ID of the store on behalf of which you're sending the receipt. Provided by YooMoney. + # The parameter is required if you use the YooMoney solution for marketplaces. + attribute? :on_behalf_of, Types::String end end end diff --git a/lib/yookassa/entity/settlement.rb b/lib/yookassa/entity/settlement.rb new file mode 100644 index 0000000..a2f6f43 --- /dev/null +++ b/lib/yookassa/entity/settlement.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require_relative "./types" +require_relative "./amount" + +module Yookassa + module Entity + class Settlement < Dry::Struct + # Type of settlement. List of possible values + attribute :type, Types::String.enum("cashless", "prepayment", "postpayment", "consideration") + + # Settlement amount. + attribute :amount, Amount + end + end +end diff --git a/lib/yookassa/entity/supplier.rb b/lib/yookassa/entity/supplier.rb new file mode 100644 index 0000000..32d982a --- /dev/null +++ b/lib/yookassa/entity/supplier.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require_relative "./types" + +module Yookassa + module Entity + class Supplier < Dry::Struct + # Supplier name. The parameter is provided for by the format of fiscal documents (FFD) + # and is considered mandatory for versions 1.1 and later. + attribute? :name, Types::String + + # Supplier's phone number. Specified in the ITU-T E.164 format, for example, 79000000000 + # The parameter is provided for by the format of fiscal documents (FFD) and is considered mandatory + # for versions 1.1 and later. + attribute? :phone, Types::String + + # Provider's masked INN/TIN. + # The parameter is provided for by the format of fiscal documents (FFD) and is considered mandatory + # for versions 1.05 and later. + attribute? :inn, Types::String + end + end +end diff --git a/lib/yookassa/receipts.rb b/lib/yookassa/receipts.rb new file mode 100644 index 0000000..9dcb83d --- /dev/null +++ b/lib/yookassa/receipts.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require_relative "./entity/receipt" +require_relative "./entity/collection" + +module Yookassa + class Receipts + def initialize(api) + @api = api + end + + def find(receipt_id:) + data = api.get("receipts/#{receipt_id}") + Entity::Receipt.new(**data) + end + + def create(payload:, idempotency_key: SecureRandom.hex(10)) + data = api.post("receipts", payload: payload, idempotency_key: idempotency_key) + Entity::Receipt.new(**data.merge(idempotency_key: idempotency_key)) + end + + def list(filters: {}) + data = api.get("receipts", query: filters) + Entity::ReceiptCollection.new(**data) + end + + private + + attr_reader :api + end +end diff --git a/spec/fixtures/payments_list_response.json b/spec/fixtures/list_payment_response.json similarity index 100% rename from spec/fixtures/payments_list_response.json rename to spec/fixtures/list_payment_response.json diff --git a/spec/fixtures/list_receipt_response.json b/spec/fixtures/list_receipt_response.json new file mode 100644 index 0000000..2623f6a --- /dev/null +++ b/spec/fixtures/list_receipt_response.json @@ -0,0 +1,71 @@ +{ + "type": "list", + "items": [ + { + "id": "rt_1da5c87d-0984-50e8-a7f3-8de646dd9ec9", + "type": "payment", + "payment_id": "215d8da0-000f-50be-b000-0003308c89be", + "status": "succeeded", + "fiscal_document_number": "3986", + "fiscal_storage_number": "9288000100115785", + "fiscal_attribute": "2617603921", + "registered_at": "2019-05-13T17:56:00.000+03:00", + "fiscal_provider_id": "fd9e9404-eaca-4000-8ec9-dc228ead2345", + "items": [ + { + "description": "Сapybara", + "quantity": 5.000, + "amount": { + "value": "2500.50", + "currency": "RUB" + }, + "vat_code": 2, + "payment_mode": "full_payment", + "payment_subject": "commodity" + } + ], + "tax_system_code": 1 + }, + { + "id": "rt_2da5c87d-0384-50e8-a7f3-8d5646dd9e10", + "type": "payment", + "payment_id": "215d8da0-000f-50be-b000-0003308c89be", + "status": "pending", + "items": [ + { + "description": "Сapybara", + "quantity": 5.000, + "amount": { + "value": "1500.30", + "currency": "RUB" + }, + "vat_code": 2, + "payment_mode": "full_payment", + "payment_subject": "commodity" + } + ], + "tax_system_code": 1 + }, + { + "id": "rt_37a5c87d-3984-51e8-a7f3-8de646d39ec15", + "type": "refund", + "refund_id": "234d8da0-000f-50be-b000-0003308c89be", + "status": "pending", + "items": [ + { + "description": "Сapybara", + "quantity": 5.000, + "amount": { + "value": "2500.50", + "currency": "RUB" + }, + "vat_code": 2, + "payment_mode": "full_payment", + "payment_subject": "commodity" + } + ], + "tax_system_code": 1 + } + ], + "next_cursor": "rt_67a4g56e-8487-56d4-a7f3-7yu646s78ec48" +} diff --git a/spec/fixtures/refunds_list_response.json b/spec/fixtures/list_refund_response.json similarity index 100% rename from spec/fixtures/refunds_list_response.json rename to spec/fixtures/list_refund_response.json diff --git a/spec/fixtures/payment.json b/spec/fixtures/payment_request.json similarity index 100% rename from spec/fixtures/payment.json rename to spec/fixtures/payment_request.json diff --git a/spec/fixtures/receipt_request.json b/spec/fixtures/receipt_request.json new file mode 100644 index 0000000..8c812fd --- /dev/null +++ b/spec/fixtures/receipt_request.json @@ -0,0 +1,53 @@ +{ + "customer" : { + "full_name" : "Ivanov Ivan Ivanovich", + "email" : "email@email.ru", + "phone" : "79211234567", + "inn" : "6321341814" + }, + "payment_id": "24b94598-000f-5000-9000-1b68e7b15f3f", + "type": "payment", + "send": "true", + "items": [ + { + "description": "Наименование товара 1", + "quantity": 1.000, + "amount": { + "value": "14000.00", + "currency": "RUB" + }, + "vat_code": "2", + "payment_mode": "full_payment", + "payment_subject": "commodity", + "country_of_origin_code": "CN" + }, + { + "description": "Наименование товара 2", + "quantity": 1.000, + "amount": { + "value": "1000.00", + "currency": "RUB" + }, + "vat_code": "2", + "payment_mode": "full_payment", + "payment_subject": "commodity", + "country_of_origin_code": "CN" + } + ], + "settlements": [ + { + "type": "prepayment", + "amount": { + "value": "8000.00", + "currency": "RUB" + } + }, + { + "type": "prepayment", + "amount": { + "value": "7000.00", + "currency": "RUB" + } + } + ] +} diff --git a/spec/fixtures/receipt_response.json b/spec/fixtures/receipt_response.json new file mode 100644 index 0000000..b520f4c --- /dev/null +++ b/spec/fixtures/receipt_response.json @@ -0,0 +1,49 @@ +{ + "id": "rt_1da5c87d-0984-50e8-a7f3-8de646dd9ec9", + "type": "payment", + "payment_id": "215d8da0-000f-50be-b000-0003308c89be", + "status": "pending", + "items": [ + { + "description": "Наименование товара 1", + "quantity": 1.000, + "amount": { + "value": "14000.00", + "currency": "RUB" + }, + "vat_code": "2", + "payment_mode": "full_payment", + "payment_subject": "commodity", + "country_of_origin_code": "CN" + }, + { + "description": "Наименование товара 2", + "quantity": 1.000, + "amount": { + "value": "1000.00", + "currency": "RUB" + }, + "vat_code": "2", + "payment_mode": "full_payment", + "payment_subject": "commodity", + "country_of_origin_code": "CN" + } + ], + "settlements": [ + { + "type": "prepayment", + "amount": { + "value": "8000.00", + "currency": "RUB" + } + }, + { + "type": "prepayment", + "amount": { + "value": "7000.00", + "currency": "RUB" + } + } + ], + "tax_system_code": 1 +} diff --git a/spec/fixtures/refund.json b/spec/fixtures/refund_request.json similarity index 100% rename from spec/fixtures/refund.json rename to spec/fixtures/refund_request.json diff --git a/spec/yookassa/payments_spec.rb b/spec/yookassa/payments_spec.rb index c032c76..79f69a7 100644 --- a/spec/yookassa/payments_spec.rb +++ b/spec/yookassa/payments_spec.rb @@ -44,7 +44,7 @@ RSpec.describe Yookassa::Payments do end describe "#create" do - let(:params) { JSON.parse(File.read("spec/fixtures/payment.json")) } + let(:params) { JSON.parse(File.read("spec/fixtures/payment_request.json")) } let(:url) { "https://api.yookassa.ru/v3/payments" } subject { payment.create(payment: params, idempotency_key: idempotency_key) } @@ -100,7 +100,7 @@ RSpec.describe Yookassa::Payments do end describe "#list" do - let(:body) { File.read("spec/fixtures/payments_list_response.json") } + let(:body) { File.read("spec/fixtures/list_payment_response.json") } let(:filters) do { diff --git a/spec/yookassa/receipts_spec.rb b/spec/yookassa/receipts_spec.rb new file mode 100644 index 0000000..af17c4b --- /dev/null +++ b/spec/yookassa/receipts_spec.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +RSpec.describe Yookassa::Receipts do + let(:config) { { shop_id: "SHOP_ID", api_key: "API_KEY" } } + let(:client) { Yookassa::Client.new(**config) } + let(:idempotency_key) { SecureRandom.hex(1) } + let(:receipt) { client.receipts } + let(:body) { File.read("spec/fixtures/receipt_response.json") } + + before { stub_request(:any, //).to_return(body: body, headers: { "Content-Type" => "application/json" }) } + + shared_examples "returns_receipt_object" do + it "returns success" do + expect(subject).to be_a Yookassa::Entity::Receipt + expect(subject.id).to eq "rt_1da5c87d-0984-50e8-a7f3-8de646dd9ec9" + expect(subject.payment_id).to eq "215d8da0-000f-50be-b000-0003308c89be" + expect(subject.status).to eq "pending" + expect(subject.items.count).to eq 2 + expect(subject.items.first).to be_kind_of Yookassa::Entity::Product + expect(subject.settlements.count).to eq 2 + expect(subject.settlements.first).to be_kind_of Yookassa::Entity::Settlement + end + end + + describe "#create" do + let(:payload) { JSON.parse(File.read("spec/fixtures/receipt_request.json")) } + let(:url) { "https://api.yookassa.ru/v3/receipts" } + + subject { receipt.create(payload: payload, idempotency_key: idempotency_key) } + + it "sends a request" do + subject + expect(a_request(:post, url)).to have_been_made + end + + it_behaves_like "returns_receipt_object" + end + + describe "#find" do + let(:receipt_id) { "2490ded1-000f-5000-8000-1f64111bc63e" } + let(:url) { "https://api.yookassa.ru/v3/receipts/#{receipt_id}" } + + subject { receipt.find(receipt_id: receipt_id) } + + it "sends a request" do + subject + expect(a_request(:get, url)).to have_been_made + end + + it_behaves_like "returns_receipt_object" + end + + describe "#list" do + let(:body) { File.read("spec/fixtures/list_receipt_response.json") } + + let(:filters) do + { + limit: 20, + "created_at.gt": "2018-07-18T10:51:18.139Z" + } + end + + let(:url) { "https://api.yookassa.ru/v3/receipts?limit=20&created_at.gt=2018-07-18T10:51:18.139Z" } + + subject { receipt.list(filters: filters) } + + it "sends a request" do + subject + expect(a_request(:get, url)).to have_been_made + end + + it "returns a collection" do + expect(subject).to be_a Yookassa::Entity::ReceiptCollection + end + end +end diff --git a/spec/yookassa/refunds_spec.rb b/spec/yookassa/refunds_spec.rb index 68fdd9c..0cc82e6 100644 --- a/spec/yookassa/refunds_spec.rb +++ b/spec/yookassa/refunds_spec.rb @@ -25,7 +25,7 @@ RSpec.describe Yookassa::Refunds do end describe "#create" do - let(:payload) { JSON.parse(File.read("spec/fixtures/refund.json")) } + let(:payload) { JSON.parse(File.read("spec/fixtures/refund_request.json")) } let(:url) { "https://api.yookassa.ru/v3/refunds" } subject { refund.create(payload: payload, idempotency_key: idempotency_key) } @@ -53,7 +53,7 @@ RSpec.describe Yookassa::Refunds do end describe "#list" do - let(:body) { File.read("spec/fixtures/refunds_list_response.json") } + let(:body) { File.read("spec/fixtures/list_refund_response.json") } let(:filters) do { diff --git a/spec/yookassa_spec.rb b/spec/yookassa_spec.rb index 1acdf7c..6242b90 100644 --- a/spec/yookassa_spec.rb +++ b/spec/yookassa_spec.rb @@ -41,4 +41,16 @@ RSpec.describe Yookassa do expect(Yookassa.payments).to be_a(Yookassa::Payments) end end + + describe ".refunds" do + it "delegates request to client and creates an instance" do + expect(Yookassa.refunds).to be_a(Yookassa::Refunds) + end + end + + describe ".receipts" do + it "delegates request to client and creates an instance" do + expect(Yookassa.receipts).to be_a(Yookassa::Receipts) + end + end end