From ce94b394b28764b9f4f36986683d4a5f031e0d58 Mon Sep 17 00:00:00 2001 From: Zil Norvilis Date: Sun, 22 Feb 2026 22:01:54 +0200 Subject: [PATCH 1/6] feat: Add `brand=` setter to `Spree::CreditCard` for `cc_type` assignment. --- app/models/spree/credit_card.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/models/spree/credit_card.rb b/app/models/spree/credit_card.rb index 74e40596a0..2baae1ed37 100644 --- a/app/models/spree/credit_card.rb +++ b/app/models/spree/credit_card.rb @@ -28,6 +28,10 @@ module Spree # needed for some of the ActiveMerchant gateways (eg. SagePay) alias_attribute :brand, :cc_type + def brand=(type) + self.cc_type = type + end + def expiry=(expiry) self[:month], self[:year] = expiry.split(" / ") self[:year] = "20#{self[:year]}" From 2cfd386ad7eab168f7c474712744bc50036f009a Mon Sep 17 00:00:00 2001 From: Zil Norvilis Date: Sun, 22 Feb 2026 22:28:40 +0200 Subject: [PATCH 2/6] test: add spec for `Spree::CreditCard#brand=` setter to verify card type reformatting --- spec/models/spree/credit_card_spec.rb | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/spec/models/spree/credit_card_spec.rb b/spec/models/spree/credit_card_spec.rb index 29f1fdf753..3da871b385 100644 --- a/spec/models/spree/credit_card_spec.rb +++ b/spec/models/spree/credit_card_spec.rb @@ -198,6 +198,15 @@ RSpec.describe Spree::CreditCard do end end + context "#brand=" do + let(:credit_card) { build(:credit_card) } + + it "delegates to #cc_type= and reformats the card type" do + credit_card.brand = 'mastercard' + expect(credit_card.cc_type).to eq 'master' + end + end + context "on save" do it "converts the card type format" do expect_any_instance_of(described_class).to receive(:reformat_card_type!). From 0bc4b1c8851d4b6057f70f5ac73c8f58b9431344 Mon Sep 17 00:00:00 2001 From: Zil Norvilis Date: Wed, 25 Feb 2026 19:35:53 +0200 Subject: [PATCH 3/6] refactor: Standardize credit card type attribute to `cc_type` across the application, removing the `brand` alias and related methods. --- app/helpers/checkout_helper.rb | 2 +- app/models/spree/credit_card.rb | 7 ------- app/models/spree/gateway.rb | 4 ++-- app/serializers/api/credit_card_serializer.rb | 6 +++--- app/views/spree/users/_saved_cards.html.haml | 2 +- spec/models/spree/credit_card_spec.rb | 9 --------- spec/serializers/api/credit_card_serializer_spec.rb | 6 +++++- 7 files changed, 12 insertions(+), 24 deletions(-) diff --git a/app/helpers/checkout_helper.rb b/app/helpers/checkout_helper.rb index a8ac1702b6..ac56d20d8f 100644 --- a/app/helpers/checkout_helper.rb +++ b/app/helpers/checkout_helper.rb @@ -139,7 +139,7 @@ module CheckoutHelper def stripe_card_options(cards) cards.map do |cc| [ - "#{cc.brand} #{cc.last_digits} #{I18n.t(:card_expiry_abbreviation)}:" \ + "#{cc.cc_type} #{cc.last_digits} #{I18n.t(:card_expiry_abbreviation)}:" \ "#{cc.month.to_s.rjust(2, '0')}/#{cc.year}", cc.id ] end diff --git a/app/models/spree/credit_card.rb b/app/models/spree/credit_card.rb index 2baae1ed37..47fa1b8fca 100644 --- a/app/models/spree/credit_card.rb +++ b/app/models/spree/credit_card.rb @@ -25,13 +25,6 @@ module Spree scope :with_payment_profile, -> { where.not(gateway_customer_profile_id: nil) } - # needed for some of the ActiveMerchant gateways (eg. SagePay) - alias_attribute :brand, :cc_type - - def brand=(type) - self.cc_type = type - end - def expiry=(expiry) self[:month], self[:year] = expiry.split(" / ") self[:year] = "20#{self[:year]}" diff --git a/app/models/spree/gateway.rb b/app/models/spree/gateway.rb index c480d9c251..08f6d20e27 100644 --- a/app/models/spree/gateway.rb +++ b/app/models/spree/gateway.rb @@ -52,9 +52,9 @@ module Spree def supports?(source) return true unless provider_class.respond_to? :supports? - return false unless source.brand + return false unless source.cc_type - provider_class.supports?(source.brand) + provider_class.supports?(source.cc_type) end end end diff --git a/app/serializers/api/credit_card_serializer.rb b/app/serializers/api/credit_card_serializer.rb index d7ac10d001..be078d0e15 100644 --- a/app/serializers/api/credit_card_serializer.rb +++ b/app/serializers/api/credit_card_serializer.rb @@ -2,9 +2,9 @@ module Api class CreditCardSerializer < ActiveModel::Serializer - attributes :id, :brand, :number, :expiry, :formatted, :delete_link, :is_default + attributes :id, :cc_type, :number, :expiry, :formatted, :delete_link, :is_default - def brand + def cc_type object.cc_type.capitalize end @@ -19,7 +19,7 @@ module Api end def formatted - "#{brand} #{number} #{I18n.t(:card_expiry_abbreviation)}:#{expiry}" + "#{cc_type} #{number} #{I18n.t(:card_expiry_abbreviation)}:#{expiry}" end def delete_link diff --git a/app/views/spree/users/_saved_cards.html.haml b/app/views/spree/users/_saved_cards.html.haml index 035d0849b1..8a117c2883 100644 --- a/app/views/spree/users/_saved_cards.html.haml +++ b/app/views/spree/users/_saved_cards.html.haml @@ -6,7 +6,7 @@ %th= t('.default?') %th= t('.delete?') %tr.card{ id: "card{{ card.id }}", "ng-repeat": "card in savedCreditCards" } - %td.brand{ "ng-bind": '::card.brand' } + %td.brand{ "ng-bind": '::card.cc_type' } %td.number{ "ng-bind": '::card.number' } %td.expiry{ "ng-bind": '::card.expiry' } %td.is-default diff --git a/spec/models/spree/credit_card_spec.rb b/spec/models/spree/credit_card_spec.rb index 3da871b385..29f1fdf753 100644 --- a/spec/models/spree/credit_card_spec.rb +++ b/spec/models/spree/credit_card_spec.rb @@ -198,15 +198,6 @@ RSpec.describe Spree::CreditCard do end end - context "#brand=" do - let(:credit_card) { build(:credit_card) } - - it "delegates to #cc_type= and reformats the card type" do - credit_card.brand = 'mastercard' - expect(credit_card.cc_type).to eq 'master' - end - end - context "on save" do it "converts the card type format" do expect_any_instance_of(described_class).to receive(:reformat_card_type!). diff --git a/spec/serializers/api/credit_card_serializer_spec.rb b/spec/serializers/api/credit_card_serializer_spec.rb index 3df2adcaac..c980ff32ad 100644 --- a/spec/serializers/api/credit_card_serializer_spec.rb +++ b/spec/serializers/api/credit_card_serializer_spec.rb @@ -5,7 +5,11 @@ RSpec.describe Api::CreditCardSerializer do let(:serializer) { Api::CreditCardSerializer.new card } it "serializes a credit card" do - expect(serializer.to_json).to match card.last_digits.to_s + expect(serializer.as_json).to include( + id: card.id, + cc_type: "Visa", + number: "x-1111" + ) end it "formats an identifying string with the card number masked" do From 58520a0c4c4bda45cbffeed7010daaa61b044ffe Mon Sep 17 00:00:00 2001 From: Zil Norvilis Date: Wed, 25 Feb 2026 19:49:10 +0200 Subject: [PATCH 4/6] test: Add specs for the `stripe_card_options` helper method, verifying card formatting and month padding. --- spec/helpers/checkout_helper_spec.rb | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/spec/helpers/checkout_helper_spec.rb b/spec/helpers/checkout_helper_spec.rb index d42ef87e87..f949a237fc 100644 --- a/spec/helpers/checkout_helper_spec.rb +++ b/spec/helpers/checkout_helper_spec.rb @@ -193,4 +193,25 @@ RSpec.describe CheckoutHelper do end end end + + describe "#stripe_card_options" do + let(:year) { Time.zone.now.year + 1 } + let(:card) { create(:credit_card, cc_type: 'visa', last_digits: '1111', month: 1, year:) } + let(:cards) { [card] } + + it "formats credit cards for Stripe options" do + options = helper.stripe_card_options(cards) + + expect(options).to eq([ + ["visa 1111 Exp:01/#{year}", card.id] + ]) + end + + it "zero-pads the month" do + card.update(month: 5) + options = helper.stripe_card_options(cards) + + expect(options.first.first).to match(%r{05/#{year}}) + end + end end From 503429960ae34a6978c64d981e867944eaf5408d Mon Sep 17 00:00:00 2001 From: Zil Norvilis Date: Thu, 26 Feb 2026 15:10:21 +0200 Subject: [PATCH 5/6] Reverse credit_card_serializer changes --- app/serializers/api/credit_card_serializer.rb | 6 +++--- spec/serializers/api/credit_card_serializer_spec.rb | 6 +----- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/app/serializers/api/credit_card_serializer.rb b/app/serializers/api/credit_card_serializer.rb index be078d0e15..d7ac10d001 100644 --- a/app/serializers/api/credit_card_serializer.rb +++ b/app/serializers/api/credit_card_serializer.rb @@ -2,9 +2,9 @@ module Api class CreditCardSerializer < ActiveModel::Serializer - attributes :id, :cc_type, :number, :expiry, :formatted, :delete_link, :is_default + attributes :id, :brand, :number, :expiry, :formatted, :delete_link, :is_default - def cc_type + def brand object.cc_type.capitalize end @@ -19,7 +19,7 @@ module Api end def formatted - "#{cc_type} #{number} #{I18n.t(:card_expiry_abbreviation)}:#{expiry}" + "#{brand} #{number} #{I18n.t(:card_expiry_abbreviation)}:#{expiry}" end def delete_link diff --git a/spec/serializers/api/credit_card_serializer_spec.rb b/spec/serializers/api/credit_card_serializer_spec.rb index c980ff32ad..3df2adcaac 100644 --- a/spec/serializers/api/credit_card_serializer_spec.rb +++ b/spec/serializers/api/credit_card_serializer_spec.rb @@ -5,11 +5,7 @@ RSpec.describe Api::CreditCardSerializer do let(:serializer) { Api::CreditCardSerializer.new card } it "serializes a credit card" do - expect(serializer.as_json).to include( - id: card.id, - cc_type: "Visa", - number: "x-1111" - ) + expect(serializer.to_json).to match card.last_digits.to_s end it "formats an identifying string with the card number masked" do From 9488e9b459575bd5c4c5fadc5ae97bc7a82b1ffe Mon Sep 17 00:00:00 2001 From: Zil Norvilis Date: Thu, 26 Feb 2026 15:52:45 +0200 Subject: [PATCH 6/6] feat: display credit card brand instead of card type in the saved cards list. --- app/views/spree/users/_saved_cards.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/spree/users/_saved_cards.html.haml b/app/views/spree/users/_saved_cards.html.haml index 8a117c2883..035d0849b1 100644 --- a/app/views/spree/users/_saved_cards.html.haml +++ b/app/views/spree/users/_saved_cards.html.haml @@ -6,7 +6,7 @@ %th= t('.default?') %th= t('.delete?') %tr.card{ id: "card{{ card.id }}", "ng-repeat": "card in savedCreditCards" } - %td.brand{ "ng-bind": '::card.cc_type' } + %td.brand{ "ng-bind": '::card.brand' } %td.number{ "ng-bind": '::card.number' } %td.expiry{ "ng-bind": '::card.expiry' } %td.is-default