From 0da245fc49ffd1d030a7f6d64f17ef47b34836f6 Mon Sep 17 00:00:00 2001 From: Kirill Mokevnin Date: Sun, 27 Sep 2026 14:47:08 -0400 Subject: [PATCH] fix: send the type of payment confirmation models Each confirmation variant inherited a required type from ConfirmationData but never set it, so ConfirmationDataRedirect.new(return_url:) reached the API without the type the gateway requires. The variant constructors were also typed without the inherited fields, so typed callers could not pass type or locale at all. Each variant now declares its type as a constant, the union picks variants by type, and the RBI/RBS constructors take locale and type. Fixes #10 Co-Authored-By: Claude Opus 5.5 --- lib/yoomoney/models/payment_create_params.rb | 56 +++++++-- rbi/yoomoney/models/payment_create_params.rbi | 108 +++++++++++++++--- sig/yoomoney/models/payment_create_params.rbs | 79 +++++++++++-- .../models/payment_create_params_test.rb | 60 ++++++++++ 4 files changed, 268 insertions(+), 35 deletions(-) create mode 100644 test/yoomoney/models/payment_create_params_test.rb diff --git a/lib/yoomoney/models/payment_create_params.rb b/lib/yoomoney/models/payment_create_params.rb index f709572..81c1407 100644 --- a/lib/yoomoney/models/payment_create_params.rb +++ b/lib/yoomoney/models/payment_create_params.rb @@ -181,17 +181,25 @@ class PaymentCreateParams < Yoomoney::Internal::Type::BaseModel module Confirmation extend Yoomoney::Internal::Type::Union - variant -> { Yoomoney::PaymentCreateParams::Confirmation::ConfirmationDataRedirect } + discriminator :type - variant -> { Yoomoney::PaymentCreateParams::Confirmation::ConfirmationDataExternal } + variant :redirect, -> { Yoomoney::PaymentCreateParams::Confirmation::ConfirmationDataRedirect } - variant -> { Yoomoney::ConfirmationData } + variant :external, -> { Yoomoney::PaymentCreateParams::Confirmation::ConfirmationDataExternal } - variant -> { Yoomoney::PaymentCreateParams::Confirmation::ConfirmationDataEmbedded } + variant :qr, -> { Yoomoney::ConfirmationData } - variant -> { Yoomoney::PaymentCreateParams::Confirmation::ConfirmationDataMobileApplication } + variant :embedded, -> { Yoomoney::PaymentCreateParams::Confirmation::ConfirmationDataEmbedded } + + variant :mobile_application, -> { Yoomoney::PaymentCreateParams::Confirmation::ConfirmationDataMobileApplication } class ConfirmationDataRedirect < Yoomoney::Models::ConfirmationData + # @!attribute type + # Код сценария подтверждения. + # + # @return [Symbol, :redirect] + required :type, const: :redirect + # @!attribute return_url # URL, на который необходимо перенаправить пользователя после оплаты. # @@ -204,43 +212,73 @@ class ConfirmationDataRedirect < Yoomoney::Models::ConfirmationData # @return [Boolean, nil] optional :enforce, Yoomoney::Internal::Type::Boolean - # @!method initialize(return_url:, enforce: nil) + # @!method initialize(return_url:, enforce: nil, locale: nil, type: :redirect) # @param return_url [String] URL, на который необходимо перенаправить пользователя после оплаты. # # @param enforce [Boolean] Требование пройти аутентификацию при оплате банковской картой по 3-D Secure. + # + # @param locale [Symbol, Yoomoney::Models::Locale] Язык интерфейса, писем и смс. + # + # @param type [Symbol, :redirect] Код сценария подтверждения. end class ConfirmationDataExternal < Yoomoney::Models::ConfirmationData + # @!attribute type + # Код сценария подтверждения. + # + # @return [Symbol, :external] + required :type, const: :external + # @!attribute locale # Язык интерфейса. # # @return [Symbol, Yoomoney::Models::Locale, nil] optional :locale, enum: -> { Yoomoney::Locale } - # @!method initialize(locale: nil) + # @!method initialize(locale: nil, type: :external) # @param locale [Symbol, Yoomoney::Models::Locale] Язык интерфейса. + # + # @param type [Symbol, :external] Код сценария подтверждения. end class ConfirmationDataEmbedded < Yoomoney::Models::ConfirmationData + # @!attribute type + # Код сценария подтверждения. + # + # @return [Symbol, :embedded] + required :type, const: :embedded + # @!attribute locale # Язык интерфейса. # # @return [Symbol, Yoomoney::Models::Locale, nil] optional :locale, enum: -> { Yoomoney::Locale } - # @!method initialize(locale: nil) + # @!method initialize(locale: nil, type: :embedded) # @param locale [Symbol, Yoomoney::Models::Locale] Язык интерфейса. + # + # @param type [Symbol, :embedded] Код сценария подтверждения. end class ConfirmationDataMobileApplication < Yoomoney::Models::ConfirmationData + # @!attribute type + # Код сценария подтверждения. + # + # @return [Symbol, :mobile_application] + required :type, const: :mobile_application + # @!attribute return_url # URL, на который необходимо перенаправить пользователя после оплаты. # # @return [String] required :return_url, String - # @!method initialize(return_url:) + # @!method initialize(return_url:, locale: nil, type: :mobile_application) # @param return_url [String] URL, на который необходимо перенаправить пользователя после оплаты. + # + # @param locale [Symbol, Yoomoney::Models::Locale] Язык интерфейса, писем и смс. + # + # @param type [Symbol, :mobile_application] Код сценария подтверждения. end # @!method self.variants diff --git a/rbi/yoomoney/models/payment_create_params.rbi b/rbi/yoomoney/models/payment_create_params.rbi index 5e17b85..14391c1 100644 --- a/rbi/yoomoney/models/payment_create_params.rbi +++ b/rbi/yoomoney/models/payment_create_params.rbi @@ -407,6 +407,17 @@ module Yoomoney ) end + # Код сценария подтверждения. + sig { returns(Symbol) } + attr_accessor :type + + # Язык интерфейса, писем и смс. + sig { returns(T.nilable(Yoomoney::Locale::OrSymbol)) } + attr_reader :locale + + sig { params(locale: Yoomoney::Locale::OrSymbol).void } + attr_writer :locale + # URL, на который необходимо перенаправить пользователя после оплаты. sig { returns(String) } attr_accessor :return_url @@ -419,19 +430,35 @@ module Yoomoney attr_writer :enforce sig do - params(return_url: String, enforce: T::Boolean).returns( - T.attached_class - ) + params( + return_url: String, + enforce: T::Boolean, + locale: Yoomoney::Locale::OrSymbol, + type: Symbol + ).returns(T.attached_class) end def self.new( # URL, на который необходимо перенаправить пользователя после оплаты. return_url:, # Требование пройти аутентификацию при оплате банковской картой по 3-D Secure. - enforce: nil + enforce: nil, + # Язык интерфейса, писем и смс. + locale: nil, + # Код сценария подтверждения. + type: :redirect ) end - sig { override.returns({ return_url: String, enforce: T::Boolean }) } + sig do + override.returns( + { + return_url: String, + enforce: T::Boolean, + locale: Yoomoney::Locale::OrSymbol, + type: Symbol + } + ) + end def to_hash end end @@ -445,6 +472,10 @@ module Yoomoney ) end + # Код сценария подтверждения. + sig { returns(Symbol) } + attr_accessor :type + # Язык интерфейса. sig { returns(T.nilable(Yoomoney::Locale::OrSymbol)) } attr_reader :locale @@ -453,15 +484,23 @@ module Yoomoney attr_writer :locale sig do - params(locale: Yoomoney::Locale::OrSymbol).returns(T.attached_class) + params(locale: Yoomoney::Locale::OrSymbol, type: Symbol).returns( + T.attached_class + ) end def self.new( # Язык интерфейса. - locale: nil + locale: nil, + # Код сценария подтверждения. + type: :external ) end - sig { override.returns({ locale: Yoomoney::Locale::OrSymbol }) } + sig do + override.returns( + { locale: Yoomoney::Locale::OrSymbol, type: Symbol } + ) + end def to_hash end end @@ -475,6 +514,10 @@ module Yoomoney ) end + # Код сценария подтверждения. + sig { returns(Symbol) } + attr_accessor :type + # Язык интерфейса. sig { returns(T.nilable(Yoomoney::Locale::OrSymbol)) } attr_reader :locale @@ -483,15 +526,23 @@ module Yoomoney attr_writer :locale sig do - params(locale: Yoomoney::Locale::OrSymbol).returns(T.attached_class) + params(locale: Yoomoney::Locale::OrSymbol, type: Symbol).returns( + T.attached_class + ) end def self.new( # Язык интерфейса. - locale: nil + locale: nil, + # Код сценария подтверждения. + type: :embedded ) end - sig { override.returns({ locale: Yoomoney::Locale::OrSymbol }) } + sig do + override.returns( + { locale: Yoomoney::Locale::OrSymbol, type: Symbol } + ) + end def to_hash end end @@ -505,18 +556,47 @@ module Yoomoney ) end + # Код сценария подтверждения. + sig { returns(Symbol) } + attr_accessor :type + + # Язык интерфейса, писем и смс. + sig { returns(T.nilable(Yoomoney::Locale::OrSymbol)) } + attr_reader :locale + + sig { params(locale: Yoomoney::Locale::OrSymbol).void } + attr_writer :locale + # URL, на который необходимо перенаправить пользователя после оплаты. sig { returns(String) } attr_accessor :return_url - sig { params(return_url: String).returns(T.attached_class) } + sig do + params( + return_url: String, + locale: Yoomoney::Locale::OrSymbol, + type: Symbol + ).returns(T.attached_class) + end def self.new( # URL, на который необходимо перенаправить пользователя после оплаты. - return_url: + return_url:, + # Язык интерфейса, писем и смс. + locale: nil, + # Код сценария подтверждения. + type: :mobile_application ) end - sig { override.returns({ return_url: String }) } + sig do + override.returns( + { + return_url: String, + locale: Yoomoney::Locale::OrSymbol, + type: Symbol + } + ) + end def to_hash end end diff --git a/sig/yoomoney/models/payment_create_params.rbs b/sig/yoomoney/models/payment_create_params.rbs index 019b3fa..8ea4e79 100644 --- a/sig/yoomoney/models/payment_create_params.rbs +++ b/sig/yoomoney/models/payment_create_params.rbs @@ -175,9 +175,23 @@ module Yoomoney module Confirmation extend Yoomoney::Internal::Type::Union - type confirmation_data_redirect = { return_url: String, enforce: bool } + type confirmation_data_redirect = + { + return_url: String, + enforce: bool, + locale: Yoomoney::Models::locale, + type: :redirect + } class ConfirmationDataRedirect < Yoomoney::Models::ConfirmationData + def type: -> :redirect + + def type=: (:redirect _) -> :redirect + + def locale: -> Yoomoney::Models::locale? + + def locale=: (Yoomoney::Models::locale _) -> Yoomoney::Models::locale + def return_url: -> String def return_url=: (String _) -> String @@ -186,45 +200,86 @@ module Yoomoney def enforce=: (bool _) -> bool - def initialize: (return_url: String, ?enforce: bool) -> void + def initialize: ( + return_url: String, + ?enforce: bool, + ?locale: Yoomoney::Models::locale, + ?type: :redirect + ) -> void - def to_hash: -> { return_url: String, enforce: bool } + def to_hash: -> { + return_url: String, + enforce: bool, + locale: Yoomoney::Models::locale, + type: :redirect + } end - type confirmation_data_external = { locale: Yoomoney::Models::locale } + type confirmation_data_external = + { locale: Yoomoney::Models::locale, type: :external } class ConfirmationDataExternal < Yoomoney::Models::ConfirmationData + def type: -> :external + + def type=: (:external _) -> :external + def locale: -> Yoomoney::Models::locale? def locale=: (Yoomoney::Models::locale _) -> Yoomoney::Models::locale - def initialize: (?locale: Yoomoney::Models::locale) -> void + def initialize: (?locale: Yoomoney::Models::locale, ?type: :external) -> void - def to_hash: -> { locale: Yoomoney::Models::locale } + def to_hash: -> { locale: Yoomoney::Models::locale, type: :external } end - type confirmation_data_embedded = { locale: Yoomoney::Models::locale } + type confirmation_data_embedded = + { locale: Yoomoney::Models::locale, type: :embedded } class ConfirmationDataEmbedded < Yoomoney::Models::ConfirmationData + def type: -> :embedded + + def type=: (:embedded _) -> :embedded + def locale: -> Yoomoney::Models::locale? def locale=: (Yoomoney::Models::locale _) -> Yoomoney::Models::locale - def initialize: (?locale: Yoomoney::Models::locale) -> void + def initialize: (?locale: Yoomoney::Models::locale, ?type: :embedded) -> void - def to_hash: -> { locale: Yoomoney::Models::locale } + def to_hash: -> { locale: Yoomoney::Models::locale, type: :embedded } end - type confirmation_data_mobile_application = { return_url: String } + type confirmation_data_mobile_application = + { + return_url: String, + locale: Yoomoney::Models::locale, + type: :mobile_application + } class ConfirmationDataMobileApplication < Yoomoney::Models::ConfirmationData + def type: -> :mobile_application + + def type=: (:mobile_application _) -> :mobile_application + + def locale: -> Yoomoney::Models::locale? + + def locale=: (Yoomoney::Models::locale _) -> Yoomoney::Models::locale + def return_url: -> String def return_url=: (String _) -> String - def initialize: (return_url: String) -> void + def initialize: ( + return_url: String, + ?locale: Yoomoney::Models::locale, + ?type: :mobile_application + ) -> void - def to_hash: -> { return_url: String } + def to_hash: -> { + return_url: String, + locale: Yoomoney::Models::locale, + type: :mobile_application + } end def self?.variants: -> ::Array[Yoomoney::Models::PaymentCreateParams::confirmation] diff --git a/test/yoomoney/models/payment_create_params_test.rb b/test/yoomoney/models/payment_create_params_test.rb new file mode 100644 index 0000000..4d5da93 --- /dev/null +++ b/test/yoomoney/models/payment_create_params_test.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require_relative "../test_helper" + +class Yoomoney::Test::PaymentCreateParamsModelTest < Minitest::Test + extend Minitest::Serial + include WebMock::API + + Confirmation = Yoomoney::PaymentCreateParams::Confirmation + + def before_all + super + WebMock.enable! + end + + def teardown + WebMock.reset! + super + end + + def after_all + WebMock.disable! + super + end + + def test_confirmation_models_send_their_type + { + Confirmation::ConfirmationDataRedirect.new(return_url: "https://example.com", locale: :ru_RU) => + {"type" => "redirect", "return_url" => "https://example.com", "locale" => "ru_RU"}, + Confirmation::ConfirmationDataExternal.new => {"type" => "external"}, + Confirmation::ConfirmationDataEmbedded.new(locale: :en_US) => {"type" => "embedded", "locale" => "en_US"}, + Confirmation::ConfirmationDataMobileApplication.new(return_url: "app://return") => + {"type" => "mobile_application", "return_url" => "app://return"} + }.each do |confirmation, expected| + assert_equal(expected, sent_confirmation(confirmation)) + end + end + + def test_confirmation_hash_picks_variant_by_type + confirmation = Confirmation.coerce({type: "redirect", return_url: "https://example.com"}, state: coerce_state) + + assert_kind_of(Confirmation::ConfirmationDataRedirect, confirmation) + end + + private + + def sent_confirmation(confirmation) + body = nil + stub_request(:post, "http://localhost/payments").to_return_json(status: 500, body: {}) + .with { body = JSON.parse(_1.body) } + client = Yoomoney::Client.new(base_url: "http://localhost", username: "u", password: "p", max_retries: 0) + + assert_raises(Yoomoney::Errors::InternalServerError) do + client.payments.create(amount: {currency: :RUB, value: "1.00"}, idempotence_key: "key", confirmation:) + end + body.fetch("confirmation") + end + + def coerce_state = {translate_names: false, strictness: true, exactness: {yes: 0, no: 0, maybe: 0}, branched: 0} +end