diff --git a/lib/checkout_sdk/api_client.rb b/lib/checkout_sdk/api_client.rb index bc513fc..bbeea66 100644 --- a/lib/checkout_sdk/api_client.rb +++ b/lib/checkout_sdk/api_client.rb @@ -68,8 +68,9 @@ def default_headers(authorization) # Attribute name to HTTP header name, for every typed headers container the SDK supports. # - # The header names are case sensitive in the spec: return-encrypted-cvv is lower case where a - # naive conversion would produce Return-Encrypted-Cvv. + # The header names are emitted with an exact spelling: return-encrypted-cvv (sent by the + # deprecated CardUpdateHeaders) is lower case where a naive conversion would produce + # Return-Encrypted-Cvv. # # The values are Strings, not Symbols, and that is load bearing. Faraday::Utils::Headers # preserves a String key verbatim but rewrites a Symbol key as diff --git a/lib/checkout_sdk/issuing/card_update_headers.rb b/lib/checkout_sdk/issuing/card_update_headers.rb index 4e32316..8a86ef0 100644 --- a/lib/checkout_sdk/issuing/card_update_headers.rb +++ b/lib/checkout_sdk/issuing/card_update_headers.rb @@ -4,26 +4,25 @@ module CheckoutSdk module Issuing # The optional HTTP headers accepted when updating a card's details. # + # @deprecated The API no longer documents these headers; use the method without headers. Will + # be removed in the next major version. Headers that are set are still sent unchanged. + # # Modelled as its own class rather than added to {CheckoutSdk::Common::Headers} because these # two headers belong to one endpoint, and Common::Headers is shared by every client. # # @!attribute return_encrypted_cvv - # Set to "true" to retrieve the card's encrypted credentials in the response. Requires an - # RSA public key in the Encryption-Key header, otherwise the API answers 422 with error code - # encryption_key_required. - # - # A String rather than a boolean: the value is emitted verbatim as an HTTP header, and "true" - # is what the spec shows. + # @deprecated The API no longer documents these headers; use the method without headers. + # Will be removed in the next major version. + # No longer documented by the API. Still sent unchanged as the return-encrypted-cvv HTTP + # header when set. # [Optional] - # Maps to HTTP header return-encrypted-cvv. - # Example: "true" # @return [String] # @!attribute encryption_key - # The RSA public key used to encrypt returned credentials. Required when - # return_encrypted_cvv is set. Provide the public key with the BEGIN PUBLIC KEY and END - # PUBLIC KEY headers and any newline characters removed, encoded as Base64. + # @deprecated The API no longer documents these headers; use the method without headers. + # Will be removed in the next major version. + # No longer documented by the API. Still sent unchanged as the Encryption-Key HTTP header + # when set. # [Optional] - # Maps to HTTP header Encryption-Key. # @return [String] class CardUpdateHeaders attr_accessor :return_encrypted_cvv, :encryption_key diff --git a/lib/checkout_sdk/issuing/issuing_client.rb b/lib/checkout_sdk/issuing/issuing_client.rb index 23c031e..212854f 100644 --- a/lib/checkout_sdk/issuing/issuing_client.rb +++ b/lib/checkout_sdk/issuing/issuing_client.rb @@ -90,13 +90,13 @@ def get_card_details(card_id) # # @param [String] card_id # @param [Hash, UpdateCardRequest] update_card_request - # @param [CardUpdateHeaders, nil] headers the optional return-encrypted-cvv and - # Encryption-Key HTTP headers. Setting return_encrypted_cvv without encryption_key returns - # a 422 with error code encryption_key_required. - # @return [OpenStruct] the update response, carrying encrypted_cvv when requested. For a - # virtual card the response may also carry is_single_use (boolean, default false), - # specifying whether the card is set to expire after a single use. Physical cards never - # send it. + # @param [CardUpdateHeaders, nil] headers deprecated. The API no longer documents these + # headers; use the method without headers. Will be removed in the next major version. + # When set, the return-encrypted-cvv and Encryption-Key headers are still sent unchanged. + # @return [OpenStruct] the update-card-response: the updated card details and _links. The + # type field discriminates the variant; a virtual card response also carries + # is_single_use (boolean, default false), whether the card expires after a single use. + # Physical card responses never carry it. def update_card(card_id, update_card_request, headers = nil) api_client.invoke_patch( build_path(ISSUING, CARDS, card_id), @@ -134,15 +134,24 @@ def get_card_credentials(card_id, query) api_client.invoke_get(build_path(ISSUING, CARDS, card_id, CREDENTIALS), sdk_authorization, query) end + # Revoke a card. POST /issuing/cards/{cardId}/revoke. The request body is optional; when + # omitted an empty JSON object is sent. # @param [String] card_id - # @param [Hash] revoke_request - def revoke_card(card_id, revoke_request) + # @param [Hash, nil] revoke_request optional, may carry reason (expired, reported_lost, + # reported_stolen) + # @return [OpenStruct] the revoke-card-response, carrying _links with self and controls. + def revoke_card(card_id, revoke_request = nil) api_client.invoke_post(build_path(ISSUING, CARDS, card_id, REVOKE), sdk_authorization, revoke_request) end + # Suspend a card. POST /issuing/cards/{cardId}/suspend. The request body is optional; when + # omitted an empty JSON object is sent. # @param [String] card_id - # @param [Hash] suspend_request - def suspend_card(card_id, suspend_request) + # @param [Hash, nil] suspend_request optional, may carry reason (suspected_lost, + # suspected_stolen) + # @return [OpenStruct] the suspend-card-response, carrying _links with self, activate, + # revoke and controls. + def suspend_card(card_id, suspend_request = nil) api_client.invoke_post(build_path(ISSUING, CARDS, card_id, SUSPEND), sdk_authorization, suspend_request) end diff --git a/spec/checkout_sdk/api_client_spec.rb b/spec/checkout_sdk/api_client_spec.rb index c508dd8..f5160fd 100644 --- a/spec/checkout_sdk/api_client_spec.rb +++ b/spec/checkout_sdk/api_client_spec.rb @@ -199,7 +199,8 @@ api_client.invoke_patch('issuing/cards/crd_1', auth, {}, extra) - # return-encrypted-cvv is lower case in the spec and Encryption-Key is title case. String + # The deprecated headers are still sent when set: return-encrypted-cvv is lower case and + # Encryption-Key is title case. String # keys are what preserves both; Symbol keys would have become Return-encrypted-cvv and # Encryption-key by the time Faraday wrote the request. expect(captured['return-encrypted-cvv']).to eq('true') @@ -254,6 +255,18 @@ expect(captured['return-encrypted-cvv']).to eq('true') end + it 'sends an empty JSON object when a POST has no body, as revoke and suspend card do' do + captured = nil + allow(http_client).to receive(:run_request) do |_method, _path, body, _headers| + captured = body + response + end + + api_client.invoke_post('issuing/cards/crd_1/revoke', auth) + + expect(captured).to eq('{}') + end + it 'omits the If-Match header when no headers container is passed' do captured = nil allow(http_client).to receive(:run_request) do |_method, _path, _body, headers| diff --git a/spec/checkout_sdk/identities/identities_response_shape_spec.rb b/spec/checkout_sdk/identities/identities_response_shape_spec.rb index 42b1175..346ff66 100644 --- a/spec/checkout_sdk/identities/identities_response_shape_spec.rb +++ b/spec/checkout_sdk/identities/identities_response_shape_spec.rb @@ -158,14 +158,14 @@ def parse(body, status: 200) end end - # Part D: the API answers 422 with error code encryption_key_required when return-encrypted-cvv - # is set to "true" without an Encryption-Key header. parse_response raises for status >= 400, so - # the error body has to survive into the exception for a caller to act on it. - describe 'the card update 422' do - it 'raises CheckoutApiException carrying the encryption_key_required code' do + # Part D: an API error body has to survive into the exception for a caller to act on it. + # transition_invalid is what the API answers when activating a revoked card. The error_type + # invalid_request matches what the live API returns. + describe 'a card management 422' do + it 'raises CheckoutApiException carrying the error codes' do body = '{"request_id":"0HLHPN8802NUF:00000003",' \ - '"error_type":"request_invalid",' \ - '"error_codes":["encryption_key_required"]}' + '"error_type":"invalid_request",' \ + '"error_codes":["transition_invalid"]}' response = double('Response', status: 422, body: body, headers: { 'Content-Type' => 'application/json' }) allow(CheckoutSdk::CheckoutUtils).to receive(:map_to_http_metadata).with(response).and_return( @@ -175,27 +175,22 @@ def parse(body, status: 200) expect { api_client.send(:parse_response, response) } .to raise_error(CheckoutSdk::CheckoutApiException) { |error| expect(error.http_metadata.status_code).to eq(422) - expect(error.error_details.error_type).to eq('request_invalid') - expect(error.error_details.error_codes).to include('encryption_key_required') + expect(error.error_details.error_type).to eq('invalid_request') + expect(error.error_details.error_codes).to include('transition_invalid') } end end - # The success counterpart: encrypted_cvv is returned only when the headers ask for it, so it is - # absent on an ordinary update. + # The update card response carries no encrypted credentials, with or without the deprecated + # headers. describe 'the card update response' do - it 'exposes encrypted_cvv when the headers requested it' do - response = parse('{"last_modified_date":"2026-06-01T10:00:00Z","encrypted_cvv":"oJMoNMEEUiQKYOsQ4Zd"}') + it 'parses the update response fields' do + response = parse('{"id":"crd_1","status":"active","last_modified_date":"2026-06-01T10:00:00Z"}') - expect(response.encrypted_cvv).to eq('oJMoNMEEUiQKYOsQ4Zd') + expect(response.id).to eq('crd_1') + expect(response.status).to eq('active') expect(response.last_modified_date).to eq('2026-06-01T10:00:00Z') end - - it 'leaves encrypted_cvv nil on an ordinary update' do - response = parse('{"last_modified_date":"2026-06-01T10:00:00Z"}') - - expect(response.encrypted_cvv).to be_nil - end end # Part C is a no-op in ruby because responses are OpenStruct, but the key still changed, so the diff --git a/spec/checkout_sdk/issuing/issuing_cards_serialization_spec.rb b/spec/checkout_sdk/issuing/issuing_cards_serialization_spec.rb index e161951..90523e6 100644 --- a/spec/checkout_sdk/issuing/issuing_cards_serialization_spec.rb +++ b/spec/checkout_sdk/issuing/issuing_cards_serialization_spec.rb @@ -72,7 +72,7 @@ def serialize(request) end describe CheckoutSdk::Issuing::CardUpdateHeaders do - it 'exposes the two headers the endpoint accepts' do + it 'still exposes the two deprecated headers' do headers = described_class.new expect(headers).to respond_to(:return_encrypted_cvv=) expect(headers).to respond_to(:encryption_key=) diff --git a/spec/checkout_sdk/issuing/issuing_extras_spec.rb b/spec/checkout_sdk/issuing/issuing_extras_spec.rb index 8212e7a..d81b642 100644 --- a/spec/checkout_sdk/issuing/issuing_extras_spec.rb +++ b/spec/checkout_sdk/issuing/issuing_extras_spec.rb @@ -27,6 +27,36 @@ end end + describe '#revoke_card' do + it 'POSTs issuing/cards/{id}/revoke with no body' do + expect(api_client_mock).to receive(:invoke_post) + .with('issuing/cards/crd_1/revoke', 'secret_key', nil).and_return('r') + expect(client.revoke_card('crd_1')).to eq('r') + end + + it 'POSTs issuing/cards/{id}/revoke with a reason' do + req = { reason: 'reported_lost' } + expect(api_client_mock).to receive(:invoke_post) + .with('issuing/cards/crd_1/revoke', 'secret_key', req).and_return('r') + expect(client.revoke_card('crd_1', req)).to eq('r') + end + end + + describe '#suspend_card' do + it 'POSTs issuing/cards/{id}/suspend with no body' do + expect(api_client_mock).to receive(:invoke_post) + .with('issuing/cards/crd_1/suspend', 'secret_key', nil).and_return('r') + expect(client.suspend_card('crd_1')).to eq('r') + end + + it 'POSTs issuing/cards/{id}/suspend with a reason' do + req = { reason: 'suspected_stolen' } + expect(api_client_mock).to receive(:invoke_post) + .with('issuing/cards/crd_1/suspend', 'secret_key', req).and_return('r') + expect(client.suspend_card('crd_1', req)).to eq('r') + end + end + describe '#renew_card' do it 'POSTs issuing/cards/{id}/renew' do expect(api_client_mock).to receive(:invoke_post)