From 17c933c69aabb7102a5cc023ab31461ea182cd46 Mon Sep 17 00:00:00 2001 From: Tomasz Strzeszewski Date: Mon, 1 Dec 2025 10:31:41 +0100 Subject: [PATCH 01/63] Add coupon service --- app/services/spl/get_coupons_service.rb | 39 +++++++++++++++++++++++++ app/services/spl/url_creator_service.rb | 22 +++++++++++--- 2 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 app/services/spl/get_coupons_service.rb diff --git a/app/services/spl/get_coupons_service.rb b/app/services/spl/get_coupons_service.rb new file mode 100644 index 0000000..143a0b8 --- /dev/null +++ b/app/services/spl/get_coupons_service.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require 'json' + +module Spl + class GetCouponsService + class SplGetCouponError < StandardError; end + + def initialize(user) + @get_coupons_url = URI.parse(Spl::UrlCreatorService.new.coupon_find) + @user = user + end + + def call + body = prepare_me_body + response = send_request(@get_coupons_url, body) + response_body = JSON.parse(response.body) + Rails.logger.debug response_body + raise SplMeError, response_body['msg'] if response_body['errorCode'] != '0' + + response_body + end + + private + + def send_request(url, body) + Spl::SendRequestService.new(url, body).call + end + + def prepare_me_body + { + context: { + prgCode: ENV.fetch('SPL_PRG_CODE'), + oauthToken: @user.private_metadata['spl_access_token'] + } + } + end + end +end diff --git a/app/services/spl/url_creator_service.rb b/app/services/spl/url_creator_service.rb index fac737d..4bcbf27 100644 --- a/app/services/spl/url_creator_service.rb +++ b/app/services/spl/url_creator_service.rb @@ -2,10 +2,12 @@ module Spl class UrlCreatorService - def check_card - "#{eshop_base}/checkCard" + ## Client data + def me + "#{client_base}/me" end + ## Promotions data def find "#{eshop_base}/find" end @@ -18,8 +20,16 @@ def sale_refund "#{eshop_base}/saleRefund" end - def me - "#{client_base}/me" + ## Coupons + + def coupon_find + "#{coupon_base}/find" + end + + ## Authentication and Authorization + + def check_card + "#{eshop_base}/checkCard" end def register @@ -55,5 +65,9 @@ def eshop_base def oauth_base "#{ENV.fetch('SPL_URL')}/api/oauth" end + + def coupon_base + "#{ENV.fetch('SPL_URL')}/api/cwp/coupon" + end end end From 144a00ce184927b98323d942953a157da90fff26 Mon Sep 17 00:00:00 2001 From: Tomasz Strzeszewski Date: Mon, 8 Dec 2025 17:03:16 +0100 Subject: [PATCH 02/63] fix syntax --- app/services/spl/url_creator_service.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/services/spl/url_creator_service.rb b/app/services/spl/url_creator_service.rb index b9bdcaf..af94517 100644 --- a/app/services/spl/url_creator_service.rb +++ b/app/services/spl/url_creator_service.rb @@ -6,8 +6,6 @@ def initialize(url) @url = url end - def check_card - "#{eshop_base}/checkCard" ## Client data def me "#{client_base}/me" From 8fcce88036608134f8f30715daa616200a130c38 Mon Sep 17 00:00:00 2001 From: nika-piotrowska Date: Wed, 10 Dec 2025 21:12:26 +0100 Subject: [PATCH 03/63] Add partial for connect loyalty account button --- .../spl/_loyalty_connect_button.html.erb | 26 +++++++++++++++++++ config/locales/cs.yml | 2 ++ config/locales/en.yml | 2 ++ config/locales/pl.yml | 11 ++++++++ config/locales/sk.yml | 2 ++ 5 files changed, 43 insertions(+) create mode 100644 app/views/spl/_loyalty_connect_button.html.erb create mode 100644 config/locales/pl.yml diff --git a/app/views/spl/_loyalty_connect_button.html.erb b/app/views/spl/_loyalty_connect_button.html.erb new file mode 100644 index 0000000..35662eb --- /dev/null +++ b/app/views/spl/_loyalty_connect_button.html.erb @@ -0,0 +1,26 @@ + + + + + diff --git a/config/locales/cs.yml b/config/locales/cs.yml index 739c2ab..d4898be 100644 --- a/config/locales/cs.yml +++ b/config/locales/cs.yml @@ -7,3 +7,5 @@ cs: card_not_active: Karta není aktivní card_not_match: Karta neodpovídá číslu karty uživatele wrong_owner: Karta je přiřazena jinému uživateli + profile: + connect_button: Connect to your loyalty account diff --git a/config/locales/en.yml b/config/locales/en.yml index cc93ec2..c3ff7dc 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -7,3 +7,5 @@ en: card_not_active: Card is not active card_not_match: Card doesnt match user's card number wrong_owner: Card assigned to a different user + profile: + connect_button: Connect to your loyalty account diff --git a/config/locales/pl.yml b/config/locales/pl.yml new file mode 100644 index 0000000..a63707f --- /dev/null +++ b/config/locales/pl.yml @@ -0,0 +1,11 @@ +pl: + order: + loyalty_card_missing: Do zamówienia nie przypisano karty lojalnościowej + spl: + card_validation: + errors: + card_not_active: Karta nie jest aktywna + card_not_match: Karta nie jest zgodna z numerem karty użytkownika + wrong_owner: Karta przypisana innemu użytkownikowi + profile: + connect_button: Połącz z kontem lojalnościowym diff --git a/config/locales/sk.yml b/config/locales/sk.yml index dd62daf..c2e2869 100644 --- a/config/locales/sk.yml +++ b/config/locales/sk.yml @@ -7,3 +7,5 @@ sk: card_not_active: Karta nie je aktívna card_not_match: Karta nezodpovedá číslu karty používateľa wrong_owner: Karta je priradená inému používateľovi + profile: + connect_button: Connect to your loyalty account From 41712f1e31950d9be8d71be68dca179b0a98cd50 Mon Sep 17 00:00:00 2001 From: nika-piotrowska Date: Fri, 12 Dec 2025 20:42:54 +0100 Subject: [PATCH 04/63] Add loyalty connect form --- .../profile_controller_decorator.rb | 9 ++++++ app/views/spl/_loyalty_connect_form.html.erb | 28 +++++++++++++++++++ config/initializers/spree_spl.rb | 4 +++ config/routes.rb | 5 ++++ 4 files changed, 46 insertions(+) create mode 100644 app/controllers/profile_controller_decorator.rb create mode 100644 app/views/spl/_loyalty_connect_form.html.erb diff --git a/app/controllers/profile_controller_decorator.rb b/app/controllers/profile_controller_decorator.rb new file mode 100644 index 0000000..96a347a --- /dev/null +++ b/app/controllers/profile_controller_decorator.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module ProfileControllerDecorator + def login_code + # spree_authorize! :update, try_spree_current_user + # debugger + end + +end diff --git a/app/views/spl/_loyalty_connect_form.html.erb b/app/views/spl/_loyalty_connect_form.html.erb new file mode 100644 index 0000000..ee1c6ac --- /dev/null +++ b/app/views/spl/_loyalty_connect_form.html.erb @@ -0,0 +1,28 @@ +<%= form_with url: login_code_account_profile_path, method: :patch, local: true do |f| %> +
+ <%= f.label :phone, + Spree.t(:phone), + class: 'text-xs text-neutral-600 block mb-2' %> + + <%= f.telephone_field :phone, + class: 'text-input w-full', + placeholder: Spree.t('loyalty.phone_label'), + value: try_spree_current_user&.phone %> +
+ +
+ <%= check_box_tag :accept_yc_terms, + '1', + ActiveModel::Type::Boolean.new.cast(try_spree_current_user&.public_metadata['accept_yc_terms']), + class: 'mt-1' %> + + +
+ +
+ <%= f.submit Spree.t('loyalty.send_code_button'), + class: 'btn-primary w-full' %> +
+<% end %> diff --git a/config/initializers/spree_spl.rb b/config/initializers/spree_spl.rb index 0bbd3ab..881c84e 100644 --- a/config/initializers/spree_spl.rb +++ b/config/initializers/spree_spl.rb @@ -36,4 +36,8 @@ ::Spree::Payment.prepend( PaymentDecorator ) + + ::Spree::Account::ProfileController.prepend( + ProfileControllerDecorator + ) end diff --git a/config/routes.rb b/config/routes.rb index 1f06ae8..341aea9 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -16,4 +16,9 @@ end end end + namespace :account do + resource :profile, controller: :profile, only: [:edit, :update] do + patch :login_code, to: 'profile#login_code' + end + end end From 00f4651376ee752ee108fb8a2d958dd414b1cbed Mon Sep 17 00:00:00 2001 From: nika-piotrowska Date: Tue, 16 Dec 2025 16:58:11 +0100 Subject: [PATCH 05/63] Improve login code method --- .../profile_controller_decorator.rb | 60 ++++++++++++++++++- app/services/phone_parser_service.rb | 39 ++++++++++++ app/views/spl/_loyalty_connect_form.html.erb | 54 +++++++++-------- config/locales/cs.yml | 4 ++ config/locales/en.yml | 4 ++ config/locales/pl.yml | 4 ++ config/locales/sk.yml | 4 ++ 7 files changed, 142 insertions(+), 27 deletions(-) create mode 100644 app/services/phone_parser_service.rb diff --git a/app/controllers/profile_controller_decorator.rb b/app/controllers/profile_controller_decorator.rb index 96a347a..8a95486 100644 --- a/app/controllers/profile_controller_decorator.rb +++ b/app/controllers/profile_controller_decorator.rb @@ -1,9 +1,65 @@ # frozen_string_literal: true module ProfileControllerDecorator + def self.prepended(base) + base.before_action :validate_login_code_request, only: :login_code + end + def login_code - # spree_authorize! :update, try_spree_current_user - # debugger + phone = phone_parser + Spl::SendOtpService.new(DateTime.current, phone.country_code, phone.national_number, current_store).call + try_spree_current_user.update!(public_metadata: (try_spree_current_user.public_metadata || {}).merge('accept_yc_terms' => true)) + rescue Spl::SendOtpService::SplSendOtpError => e + clear_login_code_errors + try_spree_current_user.errors.add(:base, e.message.presence || I18n.t('spl.user.errors.otp_send_failed')) + + render_login_code_error + end + + private + + def validate_login_code_request + clear_login_code_errors + validate_yc_terms + validate_phone + + render_login_code_error if try_spree_current_user.errors.any? + end + + def validate_yc_terms + return if yc_terms_accepted? + + try_spree_current_user.errors.add(:base, I18n.t('spl.user.errors.must_accept_yc_terms')) + end + + def validate_phone + return if phone_parser.valid? + + try_spree_current_user.errors.add(:phone, I18n.t('spl.user.errors.invalid_phone')) + end + + def yc_terms_accepted? + ActiveModel::Type::Boolean.new.cast(login_code_params[:accept_yc_terms]) + end + + def phone_parser + PhoneParserService.new(login_code_params[:phone]) end + def clear_login_code_errors + try_spree_current_user.errors.clear + end + + def render_login_code_error + render turbo_stream: turbo_stream.replace( + 'loyalty_connect_form', + partial: 'spl/loyalty_connect_form', + locals: { user: try_spree_current_user } + ), + status: :unprocessable_entity + end + + def login_code_params + params.require(:user).permit(:phone, :accept_yc_terms) + end end diff --git a/app/services/phone_parser_service.rb b/app/services/phone_parser_service.rb new file mode 100644 index 0000000..c02a539 --- /dev/null +++ b/app/services/phone_parser_service.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +class PhoneParserService + attr_reader :phone + + def initialize(raw_phone) + @phone = Phonelib.parse(raw_phone) + end + + delegate :valid?, to: :phone + + def country_code + return unless valid? + + "+#{phone.country_code}" + end + + def national_number + return unless valid? + + phone.national_number + end + + def e164 + return unless valid? + + phone.e164 + end + + def to_h + return {} unless valid? + + { + country_code: country_code, + national_number: national_number, + e164: e164 + } + end +end diff --git a/app/views/spl/_loyalty_connect_form.html.erb b/app/views/spl/_loyalty_connect_form.html.erb index ee1c6ac..3089a5d 100644 --- a/app/views/spl/_loyalty_connect_form.html.erb +++ b/app/views/spl/_loyalty_connect_form.html.erb @@ -1,28 +1,32 @@ -<%= form_with url: login_code_account_profile_path, method: :patch, local: true do |f| %> -
- <%= f.label :phone, - Spree.t(:phone), - class: 'text-xs text-neutral-600 block mb-2' %> +<%= turbo_frame_tag "loyalty_connect_form" do %> + <%= render partial: 'spree/shared/error_messages', locals: { target: @user } %> + <%= form_with scope: :user, url: login_code_account_profile_path, method: :patch, data: { turbo_frame: "loyalty_connect_form" } do |f| %> +
+ <%= f.label :phone, + Spree.t(:phone), + class: 'text-xs text-neutral-600 block mb-2' %> + <%= f.telephone_field :phone, + class: 'text-input w-full', + placeholder: Spree.t('loyalty.phone_label'), + value: try_spree_current_user&.phone %> +
- <%= f.telephone_field :phone, - class: 'text-input w-full', - placeholder: Spree.t('loyalty.phone_label'), - value: try_spree_current_user&.phone %> -
+
+ <%= f.check_box :accept_yc_terms, + { + class: 'mt-1', + checked: ActiveModel::Type::Boolean.new.cast( + try_spree_current_user&.public_metadata&.fetch('accept_yc_terms', false) + ) + }, + 'true', + 'false' %> + <%= f.label :accept_yc_terms, Spree.t('loyalty.accept_yc_terms_label'), class: 'text-sm text-neutral-700' %> +
-
- <%= check_box_tag :accept_yc_terms, - '1', - ActiveModel::Type::Boolean.new.cast(try_spree_current_user&.public_metadata['accept_yc_terms']), - class: 'mt-1' %> - - -
- -
- <%= f.submit Spree.t('loyalty.send_code_button'), - class: 'btn-primary w-full' %> -
+
+ <%= f.submit Spree.t('loyalty.send_code_button'), class: 'btn-primary w-full' %> +
+ <% end %> <% end %> + diff --git a/config/locales/cs.yml b/config/locales/cs.yml index d4898be..7988456 100644 --- a/config/locales/cs.yml +++ b/config/locales/cs.yml @@ -2,6 +2,10 @@ cs: order: loyalty_card_missing: K objednávce není přiřazena žádná věrnostní karta spl: + user: + errors: + must_accept_yc_terms: You must accept the loyalty club terms and conditions to continue + invalid_phone: Podaj poprawny numer telefonu card_validation: errors: card_not_active: Karta není aktivní diff --git a/config/locales/en.yml b/config/locales/en.yml index c3ff7dc..4b923ee 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -2,6 +2,10 @@ en: order: loyalty_card_missing: There is no loyalty card assigned to order spl: + user: + errors: + must_accept_yc_terms: You must accept the loyalty club terms and conditions to continue + invalid_phone: Podaj poprawny numer telefonu card_validation: errors: card_not_active: Card is not active diff --git a/config/locales/pl.yml b/config/locales/pl.yml index a63707f..69362f0 100644 --- a/config/locales/pl.yml +++ b/config/locales/pl.yml @@ -2,6 +2,10 @@ pl: order: loyalty_card_missing: Do zamówienia nie przypisano karty lojalnościowej spl: + user: + errors: + must_accept_yc_terms: Musisz zaakceptować regulamin klubu lojalnościowego + invalid_phone: Podaj poprawny numer telefonu card_validation: errors: card_not_active: Karta nie jest aktywna diff --git a/config/locales/sk.yml b/config/locales/sk.yml index c2e2869..f91a2bb 100644 --- a/config/locales/sk.yml +++ b/config/locales/sk.yml @@ -2,6 +2,10 @@ sk: order: loyalty_card_missing: K objednávke nie je priradená žiadna vernostná karta spl: + user: + errors: + must_accept_yc_terms: You must accept the loyalty club terms and conditions to continue + invalid_phone: Podaj poprawny numer telefonu card_validation: errors: card_not_active: Karta nie je aktívna From ab5cf7ead0b8805bc39dfcacb60c73d1d2f9804d Mon Sep 17 00:00:00 2001 From: nika-piotrowska Date: Tue, 16 Dec 2025 17:18:18 +0100 Subject: [PATCH 06/63] Add test for phone parser service --- spec/services/phone_parser_service_spec.rb | 113 +++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 spec/services/phone_parser_service_spec.rb diff --git a/spec/services/phone_parser_service_spec.rb b/spec/services/phone_parser_service_spec.rb new file mode 100644 index 0000000..25f7471 --- /dev/null +++ b/spec/services/phone_parser_service_spec.rb @@ -0,0 +1,113 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe PhoneParserService do + describe "#initialize" do + it "parses the raw phone with Phonelib and stores it as #phone" do + phone_double = instance_double("Phonelib::Phone") + + expect(Phonelib).to receive(:parse).with("+48500600700").and_return(phone_double) + + service = described_class.new("+48500600700") + expect(service.phone).to eq(phone_double) + end + end + + describe "#valid?" do + it "delegates to the parsed phone object" do + phone_double = instance_double("Phonelib::Phone", valid?: true) + allow(Phonelib).to receive(:parse).and_return(phone_double) + + expect(described_class.new("anything")).to be_valid + end + end + + context "when phone is valid" do + subject(:service) { described_class.new("+48500600700") } + + let(:phone_double) do + instance_double( + "Phonelib::Phone", + valid?: true, + country_code: "48", + national_number: "500600700", + e164: "+48500600700" + ) + end + + before do + allow(Phonelib).to receive(:parse).with("+48500600700").and_return(phone_double) + end + + describe "#country_code" do + it "returns country code with leading '+'" do + expect(service.country_code).to eq("+48") + end + end + + describe "#national_number" do + it "returns national number" do + expect(service.national_number).to eq("500600700") + end + end + + describe "#e164" do + it "returns the E.164 formatted number" do + expect(service.e164).to eq("+48500600700") + end + end + + describe "#to_h" do + it "returns a hash with country_code, national_number and e164" do + expect(service.to_h).to eq( + country_code: "+48", + national_number: "500600700", + e164: "+48500600700" + ) + end + end + end + + context "when phone is invalid" do + subject(:service) { described_class.new("not-a-phone") } + + let(:phone_double) do + instance_double( + "Phonelib::Phone", + valid?: false, + country_code: nil, + national_number: nil, + e164: nil + ) + end + + before do + allow(Phonelib).to receive(:parse).with("not-a-phone").and_return(phone_double) + end + + describe "#country_code" do + it "returns nil" do + expect(service.country_code).to be_nil + end + end + + describe "#national_number" do + it "returns nil" do + expect(service.national_number).to be_nil + end + end + + describe "#e164" do + it "returns nil" do + expect(service.e164).to be_nil + end + end + + describe "#to_h" do + it "returns an empty hash" do + expect(service.to_h).to eq({}) + end + end + end +end From 36a21f04ce56c56d11d6502e3dc7be54cb0cd6dd Mon Sep 17 00:00:00 2001 From: nika-piotrowska Date: Tue, 16 Dec 2025 17:18:30 +0100 Subject: [PATCH 07/63] Update gemfile --- Gemfile | 1 + Gemfile.lock | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/Gemfile b/Gemfile index e451ca8..a3b6027 100644 --- a/Gemfile +++ b/Gemfile @@ -21,6 +21,7 @@ group :test do gem 'ffaker' gem 'observer' gem 'spree', '> 5.0' + gem 'spree_storefront', '> 5.0' end gem 'sprockets-rails', '~> 3.5' diff --git a/Gemfile.lock b/Gemfile.lock index 4139642..e6326cf 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -65,6 +65,9 @@ GEM erubi (~> 1.11) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) + active_link_to (1.0.5) + actionpack + addressable active_storage_validations (1.3.0) activejob (>= 6.1.4) activemodel (>= 6.1.4) @@ -149,6 +152,8 @@ GEM builder (3.3.0) byebug (12.0.0) cancancan (3.6.1) + canonical-rails (0.2.17) + actionview (>= 4.1, < 8.1) capybara (3.40.0) addressable matrix @@ -252,6 +257,8 @@ GEM rake (>= 10.0) globalid (1.3.0) activesupport (>= 6.1) + heroicon (1.0.0) + rails (>= 5.2) highline (3.1.2) reline i18n (1.14.7) @@ -271,6 +278,13 @@ GEM image_processing (1.14.0) mini_magick (>= 4.9.5, < 6) ruby-vips (>= 2.0.17, < 3) + importmap-rails (2.2.2) + actionpack (>= 6.0.0) + activesupport (>= 6.0.0) + railties (>= 6.0.0) + inline_svg (1.10.0) + activesupport (>= 3.0) + nokogiri (>= 1.6) io-console (0.8.1) io-endpoint (0.15.2) io-event (1.14.2) @@ -303,6 +317,7 @@ GEM childprocess (~> 5.0) logger (~> 1.6) lint_roller (1.1.0) + local_time (3.0.3) logger (1.7.0) loofah (2.24.1) crass (~> 1.0.2) @@ -313,6 +328,9 @@ GEM net-imap net-pop net-smtp + mail_form (1.11.0) + actionpack (>= 7.0) + activemodel (>= 7.0) marcel (1.1.0) matrix (0.4.3) method_source (1.1.0) @@ -625,6 +643,19 @@ GEM spree_extension (0.1.0) activerecord (>= 4.2) spree_core + spree_storefront (5.2.0) + active_link_to + canonical-rails (~> 0.2.14) + heroicon + importmap-rails + inline_svg (~> 1.10) + local_time (~> 3.0) + mail_form + spree_core (>= 5.2.0) + stimulus-rails + tailwindcss-rails + tailwindcss-ruby + turbo-rails sprockets (4.2.2) concurrent-ruby (~> 1.0) logger @@ -648,8 +679,20 @@ GEM state_machines-activerecord (0.100.0) activerecord (>= 7.2) state_machines-activemodel (>= 0.100.0) + stimulus-rails (1.3.4) + railties (>= 6.0.0) stringex (2.8.6) stringio (3.1.8) + tailwindcss-rails (4.4.0) + railties (>= 7.0.0) + tailwindcss-ruby (~> 4.0) + tailwindcss-ruby (4.1.16) + tailwindcss-ruby (4.1.16-aarch64-linux-gnu) + tailwindcss-ruby (4.1.16-aarch64-linux-musl) + tailwindcss-ruby (4.1.16-arm64-darwin) + tailwindcss-ruby (4.1.16-x86_64-darwin) + tailwindcss-ruby (4.1.16-x86_64-linux-gnu) + tailwindcss-ruby (4.1.16-x86_64-linux-musl) terminal-table (4.0.0) unicode-display_width (>= 1.1.1, < 4) thor (1.4.0) @@ -660,6 +703,9 @@ GEM activesupport (>= 4.2.5) json (>= 1.8.3) tsort (0.2.0) + turbo-rails (2.0.20) + actionpack (>= 7.1.0) + railties (>= 7.1.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) unaccent (0.4.0) @@ -709,6 +755,7 @@ DEPENDENCIES rubocop-rails-omakase spree (> 5.0) spree-spl! + spree_storefront (> 5.0) sprockets-rails (~> 3.5) sqlite3 (>= 2.0) From c10707b136295b28e2b558d7614385a49346b9fd Mon Sep 17 00:00:00 2001 From: nika-piotrowska Date: Tue, 16 Dec 2025 17:49:52 +0100 Subject: [PATCH 08/63] Add store private metadata service to app --- app/services/spl/send_otp_service.rb | 15 +-- .../spl/store_private_metadata_service.rb | 15 +++ .../spl/store_private_metadata_service_pec.rb | 96 +++++++++++++++++++ 3 files changed, 119 insertions(+), 7 deletions(-) create mode 100644 app/services/spl/store_private_metadata_service.rb create mode 100644 spec/services/spl/store_private_metadata_service_pec.rb diff --git a/app/services/spl/send_otp_service.rb b/app/services/spl/send_otp_service.rb index a3be995..180f753 100644 --- a/app/services/spl/send_otp_service.rb +++ b/app/services/spl/send_otp_service.rb @@ -7,11 +7,12 @@ class SendOtpService class SplSendOtpError < StandardError; end def initialize(date, mobile_country, phone_number, store) + debugger @date = date.to_i * 1000 @send_otp_url = URI.parse(Spl::UrlCreatorService.new(store.private_metadata['spl_url']).send_otp) @mobile_country = mobile_country @phone_number = phone_number - @store = store + @env = Spl::StorePrivateMetadata.all(store) end def call @@ -33,11 +34,11 @@ def send_request(url, body) def prepare_sms_otp_body # rubocop:disable Metrics/MethodLength { context: { - prgCode: @store.private_metadata['spl_prg_code'] + prgCode: @env['spl_prg_code'] }, - apiUser: @store.private_metadata['spl_api_user'], - apiToken: @store.private_metadata['spl_api_token'], - signatureSeed: @store.private_metadata['spl_signature_seed'], + apiUser: @env['spl_api_user'], + apiToken: @env['spl_api_token'], + signatureSeed: @env['spl_signature_seed'], date: @date, mobileCountry: @mobile_country, mobile: @phone_number, @@ -47,8 +48,8 @@ def prepare_sms_otp_body # rubocop:disable Metrics/MethodLength def generate_signature Spl::ClientSignatureService.new(@date, - @store.private_metadata['spl_api_token'], - @store.private_metadata['spl_signature_seed']).call + @env['spl_api_token'], + @env['spl_signature_seed']).call end end end diff --git a/app/services/spl/store_private_metadata_service.rb b/app/services/spl/store_private_metadata_service.rb new file mode 100644 index 0000000..13a0efb --- /dev/null +++ b/app/services/spl/store_private_metadata_service.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Spl + module StorePrivateMetadataService + module_function + + def all(store) + (store&.private_metadata || {}).deep_dup + end + + def fetch(store, key, default: nil) + all(store).fetch(key.to_s, default) + end + end +end diff --git a/spec/services/spl/store_private_metadata_service_pec.rb b/spec/services/spl/store_private_metadata_service_pec.rb new file mode 100644 index 0000000..0e1bc7e --- /dev/null +++ b/spec/services/spl/store_private_metadata_service_pec.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe Spl::StorePrivateMetadataService do + let(:private_metadata) do + { + "spl_url" => "https://spl.test", + "spl_api_user" => "user", + "spl_api_token" => "token", + "spl_partner_code" => "partner", + "spl_place_code" => "place", + "spl_update_status_mode" => "mode", + "spl_prg_code" => "PRG", + "spl_mode" => "mode", + "spl_pos_key" => "poskey" + } + end + + let(:store) { instance_double(Spree::Store, private_metadata: private_metadata) } + + describe ".all" do + context "when store is nil" do + it "returns an empty hash" do + expect(described_class.all(nil)).to eq({}) + end + end + + context "when store.private_metadata is nil" do + let(:store) { instance_double(Spree::Store, private_metadata: nil) } + + it "returns an empty hash" do + expect(described_class.all(store)).to eq({}) + end + end + + it "returns the private_metadata contents" do + expect(described_class.all(store)).to eq(private_metadata) + end + + it "returns a copy that does not allow mutation of the original store hash" do + result = described_class.all(store) + result["spl_url"] = "https://changed.test" + + expect(store.private_metadata["spl_url"]).to eq("https://spl.test") + end + + it "deep-dups nested hashes (if present) so nested mutation does not affect the original" do + nested = { "outer" => { "inner" => "value" } } + store_with_nested = instance_double(Spree::Store, private_metadata: nested) + + result = described_class.all(store_with_nested) + result["outer"]["inner"] = "changed" + + expect(store_with_nested.private_metadata["outer"]["inner"]).to eq("value") + end + end + + describe ".fetch" do + context "when store is nil" do + it "returns the default when provided" do + expect(described_class.fetch(nil, :spl_url, default: "x")).to eq("x") + end + + it "raises KeyError when key is missing and no default is provided" do + expect(described_class.fetch(nil, :spl_url)).to be_nil + end + end + + it "fetches by symbol key" do + expect(described_class.fetch(store, :spl_url)).to eq("https://spl.test") + end + + it "fetches by string key" do + expect(described_class.fetch(store, "spl_url")).to eq("https://spl.test") + end + + it "returns default when key is missing" do + expect(described_class.fetch(store, :missing_key, default: "fallback")).to eq("fallback") + end + + it "raises KeyError when key is missing and no default is provided" do + expect(described_class.fetch(store, :missing_key)).to be_nil + end + + it "does not allow callers to mutate nested values through the returned hash" do + nested = { "cfg" => { "token" => "abc" } } + store_with_nested = instance_double(Spree::Store, private_metadata: nested) + + cfg = described_class.fetch(store_with_nested, :cfg) + cfg["token"] = "changed" + + expect(store_with_nested.private_metadata["cfg"]["token"]).to eq("abc") + end + end +end From a014cdb332cff0f252cb33ac090d762f222d212b Mon Sep 17 00:00:00 2001 From: nika-piotrowska Date: Tue, 16 Dec 2025 18:59:12 +0100 Subject: [PATCH 09/63] Add profile controller decorator spec --- Gemfile | 2 + Gemfile.lock | 10 ++ .../profile_controller_decorator.rb | 15 +++ app/services/spl/send_otp_service.rb | 3 +- .../profile_controller_decorator_spec.rb | 106 ++++++++++++++++++ spec/rails_helper.rb | 2 + spec/services/phone_parser_service_spec.rb | 84 +++++++------- .../spl/store_private_metadata_service_pec.rb | 78 ++++++------- 8 files changed, 217 insertions(+), 83 deletions(-) create mode 100644 spec/controllers/profile_controller_decorator_spec.rb diff --git a/Gemfile b/Gemfile index a3b6027..c0c3c59 100644 --- a/Gemfile +++ b/Gemfile @@ -20,8 +20,10 @@ group :test do gem 'factory_bot_rails' gem 'ffaker' gem 'observer' + gem 'rails-controller-testing' gem 'spree', '> 5.0' gem 'spree_storefront', '> 5.0' + gem 'webmock' end gem 'sprockets-rails', '~> 3.5' diff --git a/Gemfile.lock b/Gemfile.lock index e6326cf..e0b7b96 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -180,6 +180,9 @@ GEM json countries (8.0.4) unaccent (~> 0.3) + crack (1.0.1) + bigdecimal + rexml crass (1.0.6) csv (3.3.5) database_cleaner (2.1.0) @@ -257,6 +260,7 @@ GEM rake (>= 10.0) globalid (1.3.0) activesupport (>= 6.1) + hashdiff (1.2.1) heroicon (1.0.0) rails (>= 5.2) highline (3.1.2) @@ -723,6 +727,10 @@ GEM nokogiri (~> 1.6) rubyzip (>= 1.3.0) selenium-webdriver (~> 4.0, < 4.11) + webmock (3.26.1) + addressable (>= 2.8.0) + crack (>= 0.3.2) + hashdiff (>= 0.4.0, < 2.0.0) websocket (1.2.11) websocket-driver (0.8.0) base64 @@ -749,6 +757,7 @@ DEPENDENCIES factory_bot_rails ffaker observer + rails-controller-testing rake (~> 13.0) rubocop (~> 1.79, >= 1.79.2) rubocop-rails (~> 2.33, >= 2.33.3) @@ -758,6 +767,7 @@ DEPENDENCIES spree_storefront (> 5.0) sprockets-rails (~> 3.5) sqlite3 (>= 2.0) + webmock BUNDLED WITH 2.6.2 diff --git a/app/controllers/profile_controller_decorator.rb b/app/controllers/profile_controller_decorator.rb index 8a95486..851d007 100644 --- a/app/controllers/profile_controller_decorator.rb +++ b/app/controllers/profile_controller_decorator.rb @@ -9,6 +9,7 @@ def login_code phone = phone_parser Spl::SendOtpService.new(DateTime.current, phone.country_code, phone.national_number, current_store).call try_spree_current_user.update!(public_metadata: (try_spree_current_user.public_metadata || {}).merge('accept_yc_terms' => true)) + render_login_code_success(phone) rescue Spl::SendOtpService::SplSendOtpError => e clear_login_code_errors try_spree_current_user.errors.add(:base, e.message.presence || I18n.t('spl.user.errors.otp_send_failed')) @@ -16,6 +17,8 @@ def login_code render_login_code_error end + def connect_loyalty_account; end + private def validate_login_code_request @@ -59,6 +62,18 @@ def render_login_code_error status: :unprocessable_entity end + def render_login_code_success(phone) + render turbo_stream: turbo_stream.replace( + 'loyalty_connect_form', + partial: 'spl/otp_code_form', + locals: { + user: try_spree_current_user, + phone_e164: phone.respond_to?(:e164) ? phone.e164 : nil + } + ), + status: :ok + end + def login_code_params params.require(:user).permit(:phone, :accept_yc_terms) end diff --git a/app/services/spl/send_otp_service.rb b/app/services/spl/send_otp_service.rb index 180f753..f546498 100644 --- a/app/services/spl/send_otp_service.rb +++ b/app/services/spl/send_otp_service.rb @@ -7,12 +7,11 @@ class SendOtpService class SplSendOtpError < StandardError; end def initialize(date, mobile_country, phone_number, store) - debugger @date = date.to_i * 1000 @send_otp_url = URI.parse(Spl::UrlCreatorService.new(store.private_metadata['spl_url']).send_otp) @mobile_country = mobile_country @phone_number = phone_number - @env = Spl::StorePrivateMetadata.all(store) + @env = Spl::StorePrivateMetadataService.all(store) end def call diff --git a/spec/controllers/profile_controller_decorator_spec.rb b/spec/controllers/profile_controller_decorator_spec.rb new file mode 100644 index 0000000..ef515f6 --- /dev/null +++ b/spec/controllers/profile_controller_decorator_spec.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Spree::Account::ProfileController, type: :controller do + before do + unless defined?(Spl::SendOtpService::SplSendOtpError) + stub_const('Spl::SendOtpService::SplSendOtpError', Class.new(StandardError)) + end + end + + let(:store) { instance_double(Spree::Store) } + let!(:user) { create(:user, public_metadata: {}) } + + let(:terms_accepted) { 'true' } + let(:phone_valid) { true } + let(:phone_value) { '+48500600700' } + + let(:phone_double) do + instance_double( + 'PhoneParserService', + valid?: phone_valid, + country_code: '+48', + national_number: '500600700', + e164: '+48500600700' + ) + end + + let(:turbo_stream_helper) { instance_double('TurboStreamHelper') } + + before do + allow(controller).to receive(:current_store).and_return(store) + allow(controller).to receive(:try_spree_current_user).and_return(user) + allow(controller).to receive(:login_code_params).and_return( + ActionController::Parameters.new(phone: phone_value, accept_yc_terms: terms_accepted).permit! + ) + allow(controller).to receive(:phone_parser).and_return(phone_double) + allow(controller).to receive(:turbo_stream).and_return(turbo_stream_helper) + allow(turbo_stream_helper).to receive(:replace).and_return('') + + user.errors.clear + end + + describe '#validate_login_code_request (before_action logic)' do + context 'when YC terms are not accepted' do + let(:terms_accepted) { 'false' } + + it 'adds error and renders 422 (does not touch OTP service)' do + expect(Spl::SendOtpService).not_to receive(:new) + expect(controller).to receive(:render).with(hash_including(status: :unprocessable_entity)) + controller.send(:validate_login_code_request) + expect(user.errors.full_messages.join(' ')).to include(I18n.t('spl.user.errors.must_accept_yc_terms')) + end + end + + context 'when phone is invalid' do + let(:phone_valid) { false } + + it 'adds phone error and renders 422 (does not touch OTP service)' do + expect(Spl::SendOtpService).not_to receive(:new) + expect(controller).to receive(:render).with(hash_including(status: :unprocessable_entity)) + controller.send(:validate_login_code_request) + expect(user.errors.full_messages.join(' ')).to include(I18n.t('spl.user.errors.invalid_phone')) + end + end + + context 'when everything is valid' do + it 'does not render and does not add errors' do + expect(controller).not_to receive(:render) + controller.send(:validate_login_code_request) + expect(user.errors).to be_empty + end + end + end + + describe '#login_code (service + success/rescue)' do + context 'when OTP service raises SplSendOtpError' do + it 'renders 422 and shows service message as base error' do + service_instance = instance_double('Spl::SendOtpService') + + expect(Spl::SendOtpService).to receive(:new) + .with(kind_of(DateTime), '+48', '500600700', store) + .and_return(service_instance) + expect(service_instance).to receive(:call) + .and_raise(Spl::SendOtpService::SplSendOtpError.new('Person not found')) + expect(controller).to receive(:render).with(hash_including(status: :unprocessable_entity)) + controller.login_code + expect(user.errors.full_messages.join(' ')).to include('Person not found') + end + end + + context 'when OTP is sent successfully' do + it 'calls OTP service, updates accept_yc_terms in public_metadata, and renders 200' do + service_instance = instance_double('Spl::SendOtpService') + + expect(Spl::SendOtpService).to receive(:new) + .with(kind_of(DateTime), '+48', '500600700', store) + .and_return(service_instance) + expect(service_instance).to receive(:call) + expect(controller).to receive(:render).with(hash_including(status: :ok)) + controller.login_code + expect(user.reload.public_metadata['accept_yc_terms']).to eq(true) + end + end + end +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 6e848dd..cfbfa20 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -34,6 +34,8 @@ require 'spree/core/controller_helpers/strong_parameters' require 'database_cleaner' +require 'webmock/rspec' +WebMock.disable_net_connect!(allow_localhost: true) # Requires supporting ruby files with custom matchers and macros, etc, in # spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are # run as spec files by default. This means that files in spec/support that end diff --git a/spec/services/phone_parser_service_spec.rb b/spec/services/phone_parser_service_spec.rb index 25f7471..bcd0088 100644 --- a/spec/services/phone_parser_service_spec.rb +++ b/spec/services/phone_parser_service_spec.rb @@ -1,80 +1,80 @@ # frozen_string_literal: true -require "rails_helper" +require 'rails_helper' RSpec.describe PhoneParserService do - describe "#initialize" do - it "parses the raw phone with Phonelib and stores it as #phone" do - phone_double = instance_double("Phonelib::Phone") + describe '#initialize' do + it 'parses the raw phone with Phonelib and stores it as #phone' do + phone_double = instance_double('Phonelib::Phone') - expect(Phonelib).to receive(:parse).with("+48500600700").and_return(phone_double) + expect(Phonelib).to receive(:parse).with('+48500600700').and_return(phone_double) - service = described_class.new("+48500600700") + service = described_class.new('+48500600700') expect(service.phone).to eq(phone_double) end end - describe "#valid?" do - it "delegates to the parsed phone object" do - phone_double = instance_double("Phonelib::Phone", valid?: true) + describe '#valid?' do + it 'delegates to the parsed phone object' do + phone_double = instance_double('Phonelib::Phone', valid?: true) allow(Phonelib).to receive(:parse).and_return(phone_double) - expect(described_class.new("anything")).to be_valid + expect(described_class.new('anything')).to be_valid end end - context "when phone is valid" do - subject(:service) { described_class.new("+48500600700") } + context 'when phone is valid' do + subject(:service) { described_class.new('+48500600700') } let(:phone_double) do instance_double( - "Phonelib::Phone", + 'Phonelib::Phone', valid?: true, - country_code: "48", - national_number: "500600700", - e164: "+48500600700" + country_code: '48', + national_number: '500600700', + e164: '+48500600700' ) end before do - allow(Phonelib).to receive(:parse).with("+48500600700").and_return(phone_double) + allow(Phonelib).to receive(:parse).with('+48500600700').and_return(phone_double) end - describe "#country_code" do + describe '#country_code' do it "returns country code with leading '+'" do - expect(service.country_code).to eq("+48") + expect(service.country_code).to eq('+48') end end - describe "#national_number" do - it "returns national number" do - expect(service.national_number).to eq("500600700") + describe '#national_number' do + it 'returns national number' do + expect(service.national_number).to eq('500600700') end end - describe "#e164" do - it "returns the E.164 formatted number" do - expect(service.e164).to eq("+48500600700") + describe '#e164' do + it 'returns the E.164 formatted number' do + expect(service.e164).to eq('+48500600700') end end - describe "#to_h" do - it "returns a hash with country_code, national_number and e164" do + describe '#to_h' do + it 'returns a hash with country_code, national_number and e164' do expect(service.to_h).to eq( - country_code: "+48", - national_number: "500600700", - e164: "+48500600700" + country_code: '+48', + national_number: '500600700', + e164: '+48500600700' ) end end end - context "when phone is invalid" do - subject(:service) { described_class.new("not-a-phone") } + context 'when phone is invalid' do + subject(:service) { described_class.new('not-a-phone') } let(:phone_double) do instance_double( - "Phonelib::Phone", + 'Phonelib::Phone', valid?: false, country_code: nil, national_number: nil, @@ -83,29 +83,29 @@ end before do - allow(Phonelib).to receive(:parse).with("not-a-phone").and_return(phone_double) + allow(Phonelib).to receive(:parse).with('not-a-phone').and_return(phone_double) end - describe "#country_code" do - it "returns nil" do + describe '#country_code' do + it 'returns nil' do expect(service.country_code).to be_nil end end - describe "#national_number" do - it "returns nil" do + describe '#national_number' do + it 'returns nil' do expect(service.national_number).to be_nil end end - describe "#e164" do - it "returns nil" do + describe '#e164' do + it 'returns nil' do expect(service.e164).to be_nil end end - describe "#to_h" do - it "returns an empty hash" do + describe '#to_h' do + it 'returns an empty hash' do expect(service.to_h).to eq({}) end end diff --git a/spec/services/spl/store_private_metadata_service_pec.rb b/spec/services/spl/store_private_metadata_service_pec.rb index 0e1bc7e..a995389 100644 --- a/spec/services/spl/store_private_metadata_service_pec.rb +++ b/spec/services/spl/store_private_metadata_service_pec.rb @@ -1,96 +1,96 @@ # frozen_string_literal: true -require "rails_helper" +require 'rails_helper' RSpec.describe Spl::StorePrivateMetadataService do let(:private_metadata) do { - "spl_url" => "https://spl.test", - "spl_api_user" => "user", - "spl_api_token" => "token", - "spl_partner_code" => "partner", - "spl_place_code" => "place", - "spl_update_status_mode" => "mode", - "spl_prg_code" => "PRG", - "spl_mode" => "mode", - "spl_pos_key" => "poskey" + 'spl_url' => 'https://spl.test', + 'spl_api_user' => 'user', + 'spl_api_token' => 'token', + 'spl_partner_code' => 'partner', + 'spl_place_code' => 'place', + 'spl_update_status_mode' => 'mode', + 'spl_prg_code' => 'PRG', + 'spl_mode' => 'mode', + 'spl_pos_key' => 'poskey' } end let(:store) { instance_double(Spree::Store, private_metadata: private_metadata) } - describe ".all" do - context "when store is nil" do - it "returns an empty hash" do + describe '.all' do + context 'when store is nil' do + it 'returns an empty hash' do expect(described_class.all(nil)).to eq({}) end end - context "when store.private_metadata is nil" do + context 'when store.private_metadata is nil' do let(:store) { instance_double(Spree::Store, private_metadata: nil) } - it "returns an empty hash" do + it 'returns an empty hash' do expect(described_class.all(store)).to eq({}) end end - it "returns the private_metadata contents" do + it 'returns the private_metadata contents' do expect(described_class.all(store)).to eq(private_metadata) end - it "returns a copy that does not allow mutation of the original store hash" do + it 'returns a copy that does not allow mutation of the original store hash' do result = described_class.all(store) - result["spl_url"] = "https://changed.test" + result['spl_url'] = 'https://changed.test' - expect(store.private_metadata["spl_url"]).to eq("https://spl.test") + expect(store.private_metadata['spl_url']).to eq('https://spl.test') end - it "deep-dups nested hashes (if present) so nested mutation does not affect the original" do - nested = { "outer" => { "inner" => "value" } } + it 'deep-dups nested hashes (if present) so nested mutation does not affect the original' do + nested = { 'outer' => { 'inner' => 'value' } } store_with_nested = instance_double(Spree::Store, private_metadata: nested) result = described_class.all(store_with_nested) - result["outer"]["inner"] = "changed" + result['outer']['inner'] = 'changed' - expect(store_with_nested.private_metadata["outer"]["inner"]).to eq("value") + expect(store_with_nested.private_metadata['outer']['inner']).to eq('value') end end - describe ".fetch" do - context "when store is nil" do - it "returns the default when provided" do - expect(described_class.fetch(nil, :spl_url, default: "x")).to eq("x") + describe '.fetch' do + context 'when store is nil' do + it 'returns the default when provided' do + expect(described_class.fetch(nil, :spl_url, default: 'x')).to eq('x') end - it "raises KeyError when key is missing and no default is provided" do + it 'raises KeyError when key is missing and no default is provided' do expect(described_class.fetch(nil, :spl_url)).to be_nil end end - it "fetches by symbol key" do - expect(described_class.fetch(store, :spl_url)).to eq("https://spl.test") + it 'fetches by symbol key' do + expect(described_class.fetch(store, :spl_url)).to eq('https://spl.test') end - it "fetches by string key" do - expect(described_class.fetch(store, "spl_url")).to eq("https://spl.test") + it 'fetches by string key' do + expect(described_class.fetch(store, 'spl_url')).to eq('https://spl.test') end - it "returns default when key is missing" do - expect(described_class.fetch(store, :missing_key, default: "fallback")).to eq("fallback") + it 'returns default when key is missing' do + expect(described_class.fetch(store, :missing_key, default: 'fallback')).to eq('fallback') end - it "raises KeyError when key is missing and no default is provided" do + it 'raises KeyError when key is missing and no default is provided' do expect(described_class.fetch(store, :missing_key)).to be_nil end - it "does not allow callers to mutate nested values through the returned hash" do - nested = { "cfg" => { "token" => "abc" } } + it 'does not allow callers to mutate nested values through the returned hash' do + nested = { 'cfg' => { 'token' => 'abc' } } store_with_nested = instance_double(Spree::Store, private_metadata: nested) cfg = described_class.fetch(store_with_nested, :cfg) - cfg["token"] = "changed" + cfg['token'] = 'changed' - expect(store_with_nested.private_metadata["cfg"]["token"]).to eq("abc") + expect(store_with_nested.private_metadata['cfg']['token']).to eq('abc') end end end From c1b0f5c7fc61f951fbf4d6f70bafec2e5b15acfb Mon Sep 17 00:00:00 2001 From: Tomasz Strzeszewski Date: Wed, 17 Dec 2025 13:52:18 +0100 Subject: [PATCH 10/63] Add GetCoupons service and related elements, refactor controllers namespace for clarity --- Gemfile | 2 + Gemfile.lock | 48 ++++++++++ .../account_controller_decorator.rb | 81 ----------------- app/controllers/cart_controller_decorator.rb | 83 ----------------- .../checkout_controller_decorator.rb | 13 --- .../spree/api/account_controller_decorator.rb | 88 ++++++++++++++++++ .../spree/api/cart_controller_decorator.rb | 89 +++++++++++++++++++ .../api/checkout_controller_decorator.rb | 19 ++++ .../checkout_controller_decorator.rb | 20 +++++ app/overrides/add_coupons_in_checkout.rb | 10 +++ app/services/spl/get_coupons_service.rb | 13 +-- .../spree/checkout/_spl_coupons.html.erb | 6 ++ config/initializers/spree_spl.rb | 10 ++- .../checkout_controller_decorator_spec.rb | 38 ++++++++ spec/rails_helper.rb | 1 + 15 files changed, 335 insertions(+), 186 deletions(-) delete mode 100644 app/controllers/account_controller_decorator.rb delete mode 100644 app/controllers/cart_controller_decorator.rb delete mode 100644 app/controllers/checkout_controller_decorator.rb create mode 100644 app/controllers/spl/spree/api/account_controller_decorator.rb create mode 100644 app/controllers/spl/spree/api/cart_controller_decorator.rb create mode 100644 app/controllers/spl/spree/api/checkout_controller_decorator.rb create mode 100644 app/controllers/spl/spree/storefront/checkout_controller_decorator.rb create mode 100644 app/overrides/add_coupons_in_checkout.rb create mode 100644 app/views/spree/checkout/_spl_coupons.html.erb create mode 100644 spec/controllers/checkout_controller_decorator_spec.rb diff --git a/Gemfile b/Gemfile index e451ca8..16ba8fc 100644 --- a/Gemfile +++ b/Gemfile @@ -21,6 +21,8 @@ group :test do gem 'ffaker' gem 'observer' gem 'spree', '> 5.0' + gem 'spree_storefront' + gem "rails-controller-testing" end gem 'sprockets-rails', '~> 3.5' diff --git a/Gemfile.lock b/Gemfile.lock index 4139642..b64b540 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -65,6 +65,9 @@ GEM erubi (~> 1.11) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) + active_link_to (1.0.5) + actionpack + addressable active_storage_validations (1.3.0) activejob (>= 6.1.4) activemodel (>= 6.1.4) @@ -149,6 +152,8 @@ GEM builder (3.3.0) byebug (12.0.0) cancancan (3.6.1) + canonical-rails (0.2.17) + actionview (>= 4.1, < 8.1) capybara (3.40.0) addressable matrix @@ -252,6 +257,8 @@ GEM rake (>= 10.0) globalid (1.3.0) activesupport (>= 6.1) + heroicon (1.0.0) + rails (>= 5.2) highline (3.1.2) reline i18n (1.14.7) @@ -271,6 +278,13 @@ GEM image_processing (1.14.0) mini_magick (>= 4.9.5, < 6) ruby-vips (>= 2.0.17, < 3) + importmap-rails (2.2.2) + actionpack (>= 6.0.0) + activesupport (>= 6.0.0) + railties (>= 6.0.0) + inline_svg (1.10.0) + activesupport (>= 3.0) + nokogiri (>= 1.6) io-console (0.8.1) io-endpoint (0.15.2) io-event (1.14.2) @@ -303,6 +317,7 @@ GEM childprocess (~> 5.0) logger (~> 1.6) lint_roller (1.1.0) + local_time (3.0.3) logger (1.7.0) loofah (2.24.1) crass (~> 1.0.2) @@ -313,6 +328,9 @@ GEM net-imap net-pop net-smtp + mail_form (1.11.0) + actionpack (>= 7.0) + activemodel (>= 7.0) marcel (1.1.0) matrix (0.4.3) method_source (1.1.0) @@ -625,6 +643,19 @@ GEM spree_extension (0.1.0) activerecord (>= 4.2) spree_core + spree_storefront (5.2.0) + active_link_to + canonical-rails (~> 0.2.14) + heroicon + importmap-rails + inline_svg (~> 1.10) + local_time (~> 3.0) + mail_form + spree_core (>= 5.2.0) + stimulus-rails + tailwindcss-rails + tailwindcss-ruby + turbo-rails sprockets (4.2.2) concurrent-ruby (~> 1.0) logger @@ -648,8 +679,20 @@ GEM state_machines-activerecord (0.100.0) activerecord (>= 7.2) state_machines-activemodel (>= 0.100.0) + stimulus-rails (1.3.4) + railties (>= 6.0.0) stringex (2.8.6) stringio (3.1.8) + tailwindcss-rails (4.4.0) + railties (>= 7.0.0) + tailwindcss-ruby (~> 4.0) + tailwindcss-ruby (4.1.16) + tailwindcss-ruby (4.1.16-aarch64-linux-gnu) + tailwindcss-ruby (4.1.16-aarch64-linux-musl) + tailwindcss-ruby (4.1.16-arm64-darwin) + tailwindcss-ruby (4.1.16-x86_64-darwin) + tailwindcss-ruby (4.1.16-x86_64-linux-gnu) + tailwindcss-ruby (4.1.16-x86_64-linux-musl) terminal-table (4.0.0) unicode-display_width (>= 1.1.1, < 4) thor (1.4.0) @@ -660,6 +703,9 @@ GEM activesupport (>= 4.2.5) json (>= 1.8.3) tsort (0.2.0) + turbo-rails (2.0.20) + actionpack (>= 7.1.0) + railties (>= 7.1.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) unaccent (0.4.0) @@ -703,12 +749,14 @@ DEPENDENCIES factory_bot_rails ffaker observer + rails-controller-testing rake (~> 13.0) rubocop (~> 1.79, >= 1.79.2) rubocop-rails (~> 2.33, >= 2.33.3) rubocop-rails-omakase spree (> 5.0) spree-spl! + spree_storefront sprockets-rails (~> 3.5) sqlite3 (>= 2.0) diff --git a/app/controllers/account_controller_decorator.rb b/app/controllers/account_controller_decorator.rb deleted file mode 100644 index c30637f..0000000 --- a/app/controllers/account_controller_decorator.rb +++ /dev/null @@ -1,81 +0,0 @@ -# frozen_string_literal: true - -# Account decorator to validate spl card no -module AccountControllerDecorator - def self.prepended(base) - base.before_action :validate_spl_no_card, only: :update - end - - def connect_loyalty_account - spree_authorize! :update, spree_current_user - assign_card_number(spree_current_user, current_store, params) - spree_current_user.reload - render_serialized_payload { serialize_resource(spree_current_user) } - rescue Spl::LoginAccountService::SplLoginAccountError, AssignSpartaCardNumberService::AssignSpartaCardNumberError, - Spl::MeService::SplMeError => e - render json: { error: e }, status: :bad_request - end - - def register_loyalty_account - spree_authorize! :update, spree_current_user - Spl::RegisterAccountService.new(spree_current_user, current_store, params).call - spree_current_user.reload - render_serialized_payload { serialize_resource(spree_current_user) } - rescue Spl::RegisterAccountService::SplRegisterAccountError, Spl::OauthTokenService::OauthTokenError => e - render json: { error: e }, status: :bad_request - end - - def registration_code - Spl::RequestOtpService.new(DateTime.current, current_store, params).call - head 204 - rescue Spl::RequestOtpService::SplRequestOtpError, Spl::OauthTokenService::OauthTokenError => e - render json: { error: e }, status: :bad_request - end - - def login_code - spree_authorize! :update, spree_current_user - Spl::SendOtpService.new(DateTime.current, params[:mobile_country], params[:phone_number], current_store).call - head 204 - rescue Spl::SendOtpService::SplSendOtpError => e - render json: { error: e }, status: :bad_request - end - - private - - def assign_card_number(user, store, params) - Spl::LoginAccountService.new(user, store, params).call - AssignSpartaCardNumberService.new(user, store).call - end - - def validate_spl_no_card # rubocop:disable Metrics/AbcSize - return unless user_update_params[:public_metadata].present? - return if disactivated_card - return unless user_update_params[:public_metadata][:spl_no_card].present? - - Spl::ValidateCardService.new(user_update_params[:public_metadata][:spl_no_card], - spree_current_user, - current_store).call - rescue Spl::ValidateCardService::SplCardValidationError => e - update_order - render json: { error: e.message }, status: :bad_request - end - - def update_order(spl_card: nil, active: false) - current_order = spree_current_user.orders.last - return unless %w[cart address delivery payment].include?(current_order.state) - - current_order.update( - public_metadata: current_order.public_metadata.merge( - { - 'spl_no_card' => spl_card, - 'spl_card_active' => active - } - ) - ) - end - - def disactivated_card - user_update_params[:public_metadata][:spl_card_active].present? && - !user_update_params[:public_metadata][:spl_card_active] - end -end diff --git a/app/controllers/cart_controller_decorator.rb b/app/controllers/cart_controller_decorator.rb deleted file mode 100644 index 834a0d8..0000000 --- a/app/controllers/cart_controller_decorator.rb +++ /dev/null @@ -1,83 +0,0 @@ -# frozen_string_literal: true - -module CartControllerDecorator - def create - add_spl_discount_params_to_order(spree_current_user) - - super - end - - def show - promotion_switcher(spree_current_order, true) - - super - end - - def update_spl_card_activate # rubocop:disable Metrics/AbcSize,Metrics/MethodLength - spree_authorize! :update, spree_current_order, order_token - - if params.dig('public_metadata', 'spl_card_active').nil? - spree_current_order.errors.add(:base, I18n.t('order.loyalty_card_missing')) - render_error_payload(spree_current_order.errors) - else - active_param = cast_boolean(params.dig('public_metadata', 'spl_card_active')) - public_metadata = spree_current_order.public_metadata - update_spl_card_active_param(spree_current_order, active_param, public_metadata) - spree_current_order.reload - promotion_switcher(spree_current_order, true) - - render_serialized_payload { serialized_current_order } - end - end - - private - - def promotion_switcher(order, check_only) - PromotionSwitcherService.new(order, check_only).call - end - - def update_spl_card_active_param(order, active_param, public_metadata) - order.update(public_metadata: public_metadata.merge('spl_card_active' => active_param)) - order.promotions.destroy_all if order.promotions.any? - end - - def add_spl_discount_params_to_order(user) - return unless user.present? - return unless user.public_metadata.key?(:spl_no_card) && user.public_metadata.key?(:spl_card_active) - - spl_card_active = cast_boolean(user.public_metadata['spl_card_active']) - params[:public_metadata].merge!(spl_card_active: spl_card_active, - spl_no_card: user.public_metadata['spl_no_card']) - end - - def switch_spl_active_param(order, check_only) - return unless order.public_metadata.key?(:spl_card_active) - - if order.public_metadata[:spl_card_active] == true - order.update(public_metadata: order.public_metadata.merge(spl_card_active: false)) - else - order.update(public_metadata: order.public_metadata.merge(spl_card_active: true)) - end - - promotion_switcher(order, check_only) - end - - def assign_spl_active_param(order, user) - return unless user.public_metadata.key?(:spl_card_active) - - active_param = cast_boolean(user.public_metadata[:spl_card_active]) - spl_card = user.public_metadata[:spl_no_card] - order.update(public_metadata: order.public_metadata.merge(spl_card_active: active_param, spl_no_card: spl_card)) - end - - def maintain_spl_adjustments(order) - return unless order.public_metadata.key?(:spl_card_active) - - order.update(public_metadata: order.public_metadata.merge(spl_card_active: true)) - promotion_switcher(order, true) - end - - def cast_boolean(value) - ActiveModel::Type::Boolean.new.cast(value) - end -end diff --git a/app/controllers/checkout_controller_decorator.rb b/app/controllers/checkout_controller_decorator.rb deleted file mode 100644 index 81b665c..0000000 --- a/app/controllers/checkout_controller_decorator.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -module CheckoutControllerDecorator - private - - def promotions_and_spl_adjustment_present?(order) - order.promotions.any? && order.line_items.map { |li| li.adjustments.where(source_type: 'SPL') }.present? - end - - def promotion_switcher(order, check_only) - PromotionSwitcherService.new(order, check_only).call - end -end diff --git a/app/controllers/spl/spree/api/account_controller_decorator.rb b/app/controllers/spl/spree/api/account_controller_decorator.rb new file mode 100644 index 0000000..fe96393 --- /dev/null +++ b/app/controllers/spl/spree/api/account_controller_decorator.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +module Spl + module Spree + module Api + # Account decorator to validate spl card no + module AccountControllerDecorator + def self.prepended(base) + base.before_action :validate_spl_no_card, only: :update + end + + def connect_loyalty_account + spree_authorize! :update, spree_current_user + assign_card_number(spree_current_user, current_store, params) + spree_current_user.reload + render_serialized_payload { serialize_resource(spree_current_user) } + rescue Spl::LoginAccountService::SplLoginAccountError, AssignSpartaCardNumberService::AssignSpartaCardNumberError, + Spl::MeService::SplMeError => e + render json: { error: e }, status: :bad_request + end + + def register_loyalty_account + spree_authorize! :update, spree_current_user + Spl::RegisterAccountService.new(spree_current_user, current_store, params).call + spree_current_user.reload + render_serialized_payload { serialize_resource(spree_current_user) } + rescue Spl::RegisterAccountService::SplRegisterAccountError, Spl::OauthTokenService::OauthTokenError => e + render json: { error: e }, status: :bad_request + end + + def registration_code + Spl::RequestOtpService.new(DateTime.current, current_store, params).call + head 204 + rescue Spl::RequestOtpService::SplRequestOtpError, Spl::OauthTokenService::OauthTokenError => e + render json: { error: e }, status: :bad_request + end + + def login_code + spree_authorize! :update, spree_current_user + Spl::SendOtpService.new(DateTime.current, params[:mobile_country], params[:phone_number], current_store).call + head 204 + rescue Spl::SendOtpService::SplSendOtpError => e + render json: { error: e }, status: :bad_request + end + + private + + def assign_card_number(user, store, params) + Spl::LoginAccountService.new(user, store, params).call + AssignSpartaCardNumberService.new(user, store).call + end + + def validate_spl_no_card # rubocop:disable Metrics/AbcSize + return unless user_update_params[:public_metadata].present? + return if disactivated_card + return unless user_update_params[:public_metadata][:spl_no_card].present? + + Spl::ValidateCardService.new(user_update_params[:public_metadata][:spl_no_card], + spree_current_user, + current_store).call + rescue Spl::ValidateCardService::SplCardValidationError => e + update_order + render json: { error: e.message }, status: :bad_request + end + + def update_order(spl_card: nil, active: false) + current_order = spree_current_user.orders.last + return unless %w[cart address delivery payment].include?(current_order.state) + + current_order.update( + public_metadata: current_order.public_metadata.merge( + { + 'spl_no_card' => spl_card, + 'spl_card_active' => active + } + ) + ) + end + + def disactivated_card + user_update_params[:public_metadata][:spl_card_active].present? && + !user_update_params[:public_metadata][:spl_card_active] + end + end + end + end + +end \ No newline at end of file diff --git a/app/controllers/spl/spree/api/cart_controller_decorator.rb b/app/controllers/spl/spree/api/cart_controller_decorator.rb new file mode 100644 index 0000000..b47136d --- /dev/null +++ b/app/controllers/spl/spree/api/cart_controller_decorator.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +module Spl + module Spree + module Api + module CartControllerDecorator + def create + add_spl_discount_params_to_order(spree_current_user) + + super + end + + def show + promotion_switcher(spree_current_order, true) + + super + end + + def update_spl_card_activate # rubocop:disable Metrics/AbcSize,Metrics/MethodLength + spree_authorize! :update, spree_current_order, order_token + + if params.dig('public_metadata', 'spl_card_active').nil? + spree_current_order.errors.add(:base, I18n.t('order.loyalty_card_missing')) + render_error_payload(spree_current_order.errors) + else + active_param = cast_boolean(params.dig('public_metadata', 'spl_card_active')) + public_metadata = spree_current_order.public_metadata + update_spl_card_active_param(spree_current_order, active_param, public_metadata) + spree_current_order.reload + promotion_switcher(spree_current_order, true) + + render_serialized_payload { serialized_current_order } + end + end + + private + + def promotion_switcher(order, check_only) + PromotionSwitcherService.new(order, check_only).call + end + + def update_spl_card_active_param(order, active_param, public_metadata) + order.update(public_metadata: public_metadata.merge('spl_card_active' => active_param)) + order.promotions.destroy_all if order.promotions.any? + end + + def add_spl_discount_params_to_order(user) + return unless user.present? + return unless user.public_metadata.key?(:spl_no_card) && user.public_metadata.key?(:spl_card_active) + + spl_card_active = cast_boolean(user.public_metadata['spl_card_active']) + params[:public_metadata].merge!(spl_card_active: spl_card_active, + spl_no_card: user.public_metadata['spl_no_card']) + end + + def switch_spl_active_param(order, check_only) + return unless order.public_metadata.key?(:spl_card_active) + + if order.public_metadata[:spl_card_active] == true + order.update(public_metadata: order.public_metadata.merge(spl_card_active: false)) + else + order.update(public_metadata: order.public_metadata.merge(spl_card_active: true)) + end + + promotion_switcher(order, check_only) + end + + def assign_spl_active_param(order, user) + return unless user.public_metadata.key?(:spl_card_active) + + active_param = cast_boolean(user.public_metadata[:spl_card_active]) + spl_card = user.public_metadata[:spl_no_card] + order.update(public_metadata: order.public_metadata.merge(spl_card_active: active_param, spl_no_card: spl_card)) + end + + def maintain_spl_adjustments(order) + return unless order.public_metadata.key?(:spl_card_active) + + order.update(public_metadata: order.public_metadata.merge(spl_card_active: true)) + promotion_switcher(order, true) + end + + def cast_boolean(value) + ActiveModel::Type::Boolean.new.cast(value) + end + end + end + end +end diff --git a/app/controllers/spl/spree/api/checkout_controller_decorator.rb b/app/controllers/spl/spree/api/checkout_controller_decorator.rb new file mode 100644 index 0000000..34f1484 --- /dev/null +++ b/app/controllers/spl/spree/api/checkout_controller_decorator.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Spl + module Spree + module Api + module CheckoutControllerDecorator + private + + def promotions_and_spl_adjustment_present?(order) + order.promotions.any? && order.line_items.map { |li| li.adjustments.where(source_type: 'SPL') }.present? + end + + def promotion_switcher(order, check_only) + PromotionSwitcherService.new(order, check_only).call + end + end + end + end +end diff --git a/app/controllers/spl/spree/storefront/checkout_controller_decorator.rb b/app/controllers/spl/spree/storefront/checkout_controller_decorator.rb new file mode 100644 index 0000000..ef2ddb5 --- /dev/null +++ b/app/controllers/spl/spree/storefront/checkout_controller_decorator.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + + +module Spl + module Spree + module Storefront + module CheckoutControllerDecorator + def self.prepended(base) + base.before_action :load_user_coupons + end + + private + + def load_user_coupons + @coupons = Spl::GetCouponsService.new(@order.user, @order.store).call + end + end + end + end +end \ No newline at end of file diff --git a/app/overrides/add_coupons_in_checkout.rb b/app/overrides/add_coupons_in_checkout.rb new file mode 100644 index 0000000..020b7d3 --- /dev/null +++ b/app/overrides/add_coupons_in_checkout.rb @@ -0,0 +1,10 @@ +class AddCouponsInCheckout + Deface::Override.new( + virtual_path: 'spree/checkout/_sidebar', + name: 'add_coupons_in_checkout', + insert_before: '.summary-content', + text: <<-ERB + <%= render partial: 'spree/checkout/spl_coupons' %> + ERB + ) +end \ No newline at end of file diff --git a/app/services/spl/get_coupons_service.rb b/app/services/spl/get_coupons_service.rb index 143a0b8..a3e1abf 100644 --- a/app/services/spl/get_coupons_service.rb +++ b/app/services/spl/get_coupons_service.rb @@ -6,19 +6,20 @@ module Spl class GetCouponsService class SplGetCouponError < StandardError; end - def initialize(user) - @get_coupons_url = URI.parse(Spl::UrlCreatorService.new.coupon_find) + def initialize(user, store) + @store = store + @find_coupons_url = URI.parse(Spl::UrlCreatorService.new(store.private_metadata['spl_url']).coupon_find) @user = user end def call body = prepare_me_body - response = send_request(@get_coupons_url, body) + response = send_request(@find_coupons_url, body) response_body = JSON.parse(response.body) Rails.logger.debug response_body - raise SplMeError, response_body['msg'] if response_body['errorCode'] != '0' + raise SplGetCouponError, response_body['msg'] if response_body['errorCode'] != '0' - response_body + response_body['response'] end private @@ -30,7 +31,7 @@ def send_request(url, body) def prepare_me_body { context: { - prgCode: ENV.fetch('SPL_PRG_CODE'), + prgCode: @store.private_metadata['spl_prg_code'], oauthToken: @user.private_metadata['spl_access_token'] } } diff --git a/app/views/spree/checkout/_spl_coupons.html.erb b/app/views/spree/checkout/_spl_coupons.html.erb new file mode 100644 index 0000000..448f3c8 --- /dev/null +++ b/app/views/spree/checkout/_spl_coupons.html.erb @@ -0,0 +1,6 @@ +
+

Your SPL coupons:

+ <% @coupons&.each do |coupon| %> + <%= coupon["typeCustomerName"] %> + <% end %> +
\ No newline at end of file diff --git a/config/initializers/spree_spl.rb b/config/initializers/spree_spl.rb index 0bbd3ab..f877a71 100644 --- a/config/initializers/spree_spl.rb +++ b/config/initializers/spree_spl.rb @@ -2,7 +2,7 @@ Rails.application.config.to_prepare do # rubocop:disable Metrics/BlockLength ::Spree::Api::V2::Storefront::CartController.prepend( - CartControllerDecorator + Spl::Spree::Api::CartControllerDecorator ) ::Spree::V2::Storefront::CartSerializer.prepend( @@ -10,11 +10,15 @@ ) ::Spree::Api::V2::Storefront::CheckoutController.prepend( - CheckoutControllerDecorator + Spl::Spree::Api::CheckoutControllerDecorator ) ::Spree::Api::V2::Storefront::AccountController.prepend( - AccountControllerDecorator + Spl::Spree::Api::AccountControllerDecorator + ) + + ::Spree::CheckoutController.prepend( + Spl::Spree::Storefront::CheckoutControllerDecorator ) ::Spree::Adjustable::AdjustmentsUpdater.prepend( diff --git a/spec/controllers/checkout_controller_decorator_spec.rb b/spec/controllers/checkout_controller_decorator_spec.rb new file mode 100644 index 0000000..b33b553 --- /dev/null +++ b/spec/controllers/checkout_controller_decorator_spec.rb @@ -0,0 +1,38 @@ +# spec/controllers/checkout_controller_decorator_spec.rb +require 'rails_helper' + +RSpec.describe Spree::CheckoutController, type: :controller do + routes { Spree::Core::Engine.routes } + + describe 'load_user_coupons before_action' do + let(:order) { create(:order) } + let(:service) { instance_double(Spl::GetCouponsService) } + let(:coupons) { %w[COUP1 COUP2] } + + before do + Spree::CheckoutController.prepend(Spl::Spree::Storefront::CheckoutControllerDecorator) + allow(controller).to receive(:order).and_return(order) + controller.instance_variable_set(:@order, order) + + allow(Spl::GetCouponsService).to receive(:new) + .with(order.user, order.store) + .and_return(service) + + allow(service).to receive(:call).and_return(coupons) + end + + it 'sets @coupons' do + # + # Spree 5 checkout requires BOTH :token and :state + # + get :edit, + params: { + token: 'asdasd', # REQUIRED + state: "address" # REQUIRED + }, + format: :json + + expect(assigns(:coupons)).to eq(coupons) + end + end +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 6e848dd..e9a259f 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -97,4 +97,5 @@ config.filter_rails_from_backtrace! # arbitrary gems may also be filtered via: # config.filter_gems_from_backtrace("gem name") + Rails.application.reload_routes! end From f9a47ea6ae13ad15ef46981399c7659ef4ac2ebb Mon Sep 17 00:00:00 2001 From: Tomasz Strzeszewski Date: Wed, 17 Dec 2025 17:35:51 +0100 Subject: [PATCH 11/63] Rubocop corrections --- Gemfile | 2 +- .../spree/api/account_controller_decorator.rb | 17 +++++++------ .../spree/api/cart_controller_decorator.rb | 5 ++-- .../checkout_controller_decorator.rb | 3 +-- app/overrides/add_coupons_in_checkout.rb | 4 +++- .../checkout_controller_decorator_spec.rb | 24 ++++++++++--------- spec/rails_helper.rb | 1 + spec/services/spl/get_coupon_service_spec.rb | 13 ++++++---- 8 files changed, 38 insertions(+), 31 deletions(-) diff --git a/Gemfile b/Gemfile index 16ba8fc..4c5d94f 100644 --- a/Gemfile +++ b/Gemfile @@ -20,9 +20,9 @@ group :test do gem 'factory_bot_rails' gem 'ffaker' gem 'observer' + gem 'rails-controller-testing' gem 'spree', '> 5.0' gem 'spree_storefront' - gem "rails-controller-testing" end gem 'sprockets-rails', '~> 3.5' diff --git a/app/controllers/spl/spree/api/account_controller_decorator.rb b/app/controllers/spl/spree/api/account_controller_decorator.rb index fe96393..599ea1a 100644 --- a/app/controllers/spl/spree/api/account_controller_decorator.rb +++ b/app/controllers/spl/spree/api/account_controller_decorator.rb @@ -15,7 +15,7 @@ def connect_loyalty_account spree_current_user.reload render_serialized_payload { serialize_resource(spree_current_user) } rescue Spl::LoginAccountService::SplLoginAccountError, AssignSpartaCardNumberService::AssignSpartaCardNumberError, - Spl::MeService::SplMeError => e + Spl::MeService::SplMeError => e render json: { error: e }, status: :bad_request end @@ -30,7 +30,7 @@ def register_loyalty_account def registration_code Spl::RequestOtpService.new(DateTime.current, current_store, params).call - head 204 + head :no_content rescue Spl::RequestOtpService::SplRequestOtpError, Spl::OauthTokenService::OauthTokenError => e render json: { error: e }, status: :bad_request end @@ -38,7 +38,7 @@ def registration_code def login_code spree_authorize! :update, spree_current_user Spl::SendOtpService.new(DateTime.current, params[:mobile_country], params[:phone_number], current_store).call - head 204 + head :no_content rescue Spl::SendOtpService::SplSendOtpError => e render json: { error: e }, status: :bad_request end @@ -51,9 +51,9 @@ def assign_card_number(user, store, params) end def validate_spl_no_card # rubocop:disable Metrics/AbcSize - return unless user_update_params[:public_metadata].present? - return if disactivated_card - return unless user_update_params[:public_metadata][:spl_no_card].present? + return if user_update_params[:public_metadata].blank? + return if disactivated_card? + return if user_update_params[:public_metadata][:spl_no_card].blank? Spl::ValidateCardService.new(user_update_params[:public_metadata][:spl_no_card], spree_current_user, @@ -77,12 +77,11 @@ def update_order(spl_card: nil, active: false) ) end - def disactivated_card + def disactivated_card? user_update_params[:public_metadata][:spl_card_active].present? && !user_update_params[:public_metadata][:spl_card_active] end end end end - -end \ No newline at end of file +end diff --git a/app/controllers/spl/spree/api/cart_controller_decorator.rb b/app/controllers/spl/spree/api/cart_controller_decorator.rb index b47136d..f32b793 100644 --- a/app/controllers/spl/spree/api/cart_controller_decorator.rb +++ b/app/controllers/spl/spree/api/cart_controller_decorator.rb @@ -45,7 +45,7 @@ def update_spl_card_active_param(order, active_param, public_metadata) end def add_spl_discount_params_to_order(user) - return unless user.present? + return if user.blank? return unless user.public_metadata.key?(:spl_no_card) && user.public_metadata.key?(:spl_card_active) spl_card_active = cast_boolean(user.public_metadata['spl_card_active']) @@ -70,7 +70,8 @@ def assign_spl_active_param(order, user) active_param = cast_boolean(user.public_metadata[:spl_card_active]) spl_card = user.public_metadata[:spl_no_card] - order.update(public_metadata: order.public_metadata.merge(spl_card_active: active_param, spl_no_card: spl_card)) + order.update(public_metadata: order.public_metadata.merge(spl_card_active: active_param, + spl_no_card: spl_card)) end def maintain_spl_adjustments(order) diff --git a/app/controllers/spl/spree/storefront/checkout_controller_decorator.rb b/app/controllers/spl/spree/storefront/checkout_controller_decorator.rb index ef2ddb5..1944d8c 100644 --- a/app/controllers/spl/spree/storefront/checkout_controller_decorator.rb +++ b/app/controllers/spl/spree/storefront/checkout_controller_decorator.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true - module Spl module Spree module Storefront @@ -17,4 +16,4 @@ def load_user_coupons end end end -end \ No newline at end of file +end diff --git a/app/overrides/add_coupons_in_checkout.rb b/app/overrides/add_coupons_in_checkout.rb index 020b7d3..382f65e 100644 --- a/app/overrides/add_coupons_in_checkout.rb +++ b/app/overrides/add_coupons_in_checkout.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AddCouponsInCheckout Deface::Override.new( virtual_path: 'spree/checkout/_sidebar', @@ -7,4 +9,4 @@ class AddCouponsInCheckout <%= render partial: 'spree/checkout/spl_coupons' %> ERB ) -end \ No newline at end of file +end diff --git a/spec/controllers/checkout_controller_decorator_spec.rb b/spec/controllers/checkout_controller_decorator_spec.rb index 3c5f301..cd13dfb 100644 --- a/spec/controllers/checkout_controller_decorator_spec.rb +++ b/spec/controllers/checkout_controller_decorator_spec.rb @@ -1,7 +1,9 @@ +# frozen_string_literal: true + require 'rails_helper' RSpec.describe Spree::CheckoutController, type: :controller do - describe "#load_user_coupons" do + describe '#load_user_coupons' do let!(:order) { create(:order) } let!(:service) { instance_double(Spl::GetCouponsService) } @@ -9,37 +11,37 @@ allow(controller).to receive(:order).and_return(order) controller.instance_variable_set(:@order, order) allow(Spl::GetCouponsService).to receive(:new) - .with(order.user, order.store) - .and_return(service) + .with(order.user, order.store) + .and_return(service) end subject(:load_user_coupons) { controller.send(:load_user_coupons) } - context "when the service returns coupons" do + context 'when the service returns coupons' do let(:coupons) { %w[COUP1 COUP2] } before { allow(service).to receive(:call).and_return(coupons) } - it "sets @coupons" do + it 'sets @coupons' do load_user_coupons expect(assigns(:coupons)).to eq(coupons) end end - context "when the service returns an empty array" do + context 'when the service returns an empty array' do before { allow(service).to receive(:call).and_return([]) } - it "sets @coupons to an empty array" do + it 'sets @coupons to an empty array' do load_user_coupons expect(assigns(:coupons)).to eq([]) end end - context "when the service raises an error" do - before { allow(service).to receive(:call).and_raise(StandardError.new("fail")) } + context 'when the service raises an error' do + before { allow(service).to receive(:call).and_raise(StandardError.new('fail')) } - it "propagates the error" do - expect { load_user_coupons }.to raise_error(StandardError, "fail") + it 'propagates the error' do + expect { load_user_coupons }.to raise_error(StandardError, 'fail') end end end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index e9a259f..63546da 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -98,4 +98,5 @@ # arbitrary gems may also be filtered via: # config.filter_gems_from_backtrace("gem name") Rails.application.reload_routes! + Money.default_currency = 'PLN' end diff --git a/spec/services/spl/get_coupon_service_spec.rb b/spec/services/spl/get_coupon_service_spec.rb index 4e9794d..7a73738 100644 --- a/spec/services/spl/get_coupon_service_spec.rb +++ b/spec/services/spl/get_coupon_service_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_helper' RSpec.describe Spl::GetCouponsService do @@ -21,8 +23,8 @@ before do store.update(private_metadata: { 'spl_url' => 'https://example.com', 'spl_prg_code' => 'prg-2' }) allow(Spl::SendRequestService).to receive(:new) - .with(request_url, prepared_body) - .and_return(request_service) + .with(request_url, prepared_body) + .and_return(request_service) allow(request_service).to receive(:call).and_return(response_double) end @@ -100,7 +102,8 @@ context 'when service returns an error' do let(:error_response_body) do - {"errorCode" => "TOKEN_EXPIRED", "validationMessages" => nil, "fieldValidationMessages" => nil, "response" => nil, "msg" => "Token expired"} + { 'errorCode' => 'TOKEN_EXPIRED', 'validationMessages' => nil, 'fieldValidationMessages' => nil, + 'response' => nil, 'msg' => 'Token expired' } end before do @@ -110,8 +113,8 @@ it 'raises SplGetCouponError with message' do expect { service.call }.to raise_error( Spl::GetCouponsService::SplGetCouponError, - 'Something went wrong' - ) + 'Token expired' + ) end end From f0ab80a7a34b869958d05f1f9b8cbcade2e7fe71 Mon Sep 17 00:00:00 2001 From: Tomasz Strzeszewski Date: Wed, 17 Dec 2025 17:46:30 +0100 Subject: [PATCH 12/63] Delete app/services/spl/.DS_Store idk what is this file doing here --- app/services/spl/.DS_Store | Bin 6148 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 app/services/spl/.DS_Store diff --git a/app/services/spl/.DS_Store b/app/services/spl/.DS_Store deleted file mode 100644 index 5008ddfcf53c02e82d7eee2e57c38e5672ef89f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0 Date: Wed, 17 Dec 2025 20:39:23 +0100 Subject: [PATCH 13/63] Add test for send otp service and send request service --- spec/services/spl/send_otp_service_spec.rb | 95 +++++++++++++++++++ .../services/spl/send_request_service_spec.rb | 44 +++++++++ 2 files changed, 139 insertions(+) create mode 100644 spec/services/spl/send_otp_service_spec.rb create mode 100644 spec/services/spl/send_request_service_spec.rb diff --git a/spec/services/spl/send_otp_service_spec.rb b/spec/services/spl/send_otp_service_spec.rb new file mode 100644 index 0000000..d47628f --- /dev/null +++ b/spec/services/spl/send_otp_service_spec.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Spl::SendOtpService do + let(:store) do + instance_double( + Spree::Store, + private_metadata: { 'spl_url' => 'https://spl.test' } + ) + end + + let(:env) do + { + 'spl_prg_code' => 'PRG', + 'spl_api_user' => 'user', + 'spl_api_token' => 'token', + 'spl_signature_seed' => 'seed' + } + end + + let(:date) { Time.zone.parse('2025-12-16 12:00:00 UTC') } + let(:date_ms) { date.to_i * 1000 } + let(:mobile_country) { '+48' } + let(:phone_number) { '500600700' } + let(:send_otp_url) { 'https://spl.test/api/send-otp' } + let(:uri) { URI.parse(send_otp_url) } + let(:signature) { 'computed-signature' } + let(:url_creator) { instance_double('Spl::UrlCreatorService', send_otp: send_otp_url) } + let(:signature_service) { instance_double('Spl::ClientSignatureService', call: signature) } + let(:http_response) { instance_double('Net::HTTPResponse', body: response_body_json) } + + before do + allow(Spl::StorePrivateMetadataService).to receive(:all).with(store).and_return(env) + allow(Spl::UrlCreatorService).to receive(:new).with('https://spl.test').and_return(url_creator) + allow(Spl::ClientSignatureService).to receive(:new) + .with(date_ms, env['spl_api_token'], env['spl_signature_seed']) + .and_return(signature_service) + allow(Spl::SendRequestService).to receive(:new) + .and_return(instance_double('Spl::SendRequestService', call: http_response)) + allow(Rails.logger).to receive(:debug) + end + + subject(:service) { described_class.new(date, mobile_country, phone_number, store) } + + describe '#call' do + context "when API returns success (errorCode == '0')" do + let(:response_body_json) { { 'errorCode' => '0', 'msg' => nil, 'response' => { 'ok' => true } }.to_json } + + it 'returns parsed response hash' do + result = service.call + expect(result).to be_a(Hash) + expect(result['errorCode']).to eq('0') + expect(result['response']).to eq({ 'ok' => true }) + end + + it 'sends request with correct url and body' do + expected_body = { + context: { prgCode: 'PRG' }, + apiUser: 'user', + apiToken: 'token', + signatureSeed: 'seed', + date: date_ms, + mobileCountry: '+48', + mobile: '500600700', + signature: signature + } + + expect(Spl::SendRequestService).to receive(:new) do |passed_url, passed_body| + expect(passed_url).to eq(uri) + expect(passed_body).to eq(expected_body) + end.and_return(instance_double('Spl::SendRequestService', call: http_response)) + + service.call + end + end + + context "when API returns an error (errorCode != '0')" do + let(:response_body_json) { { 'errorCode' => 'PERSON_NOT_FOUND', 'msg' => 'Person not found' }.to_json } + + it 'raises SplSendOtpError with msg' do + expect { service.call } + .to raise_error(Spl::SendOtpService::SplSendOtpError, 'Person not found') + end + end + + context 'when API returns errorCode but msg is missing' do + let(:response_body_json) { { 'errorCode' => 'SOMETHING_BAD' }.to_json } + + it 'raises SplSendOtpError (message can be nil/blank depending on payload)' do + expect { service.call }.to raise_error(Spl::SendOtpService::SplSendOtpError) + end + end + end +end diff --git a/spec/services/spl/send_request_service_spec.rb b/spec/services/spl/send_request_service_spec.rb new file mode 100644 index 0000000..952d534 --- /dev/null +++ b/spec/services/spl/send_request_service_spec.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe Spl::SendRequestService do + let(:url) { URI.parse("https://spl.test/api/send-otp") } + let(:body) { { foo: "bar", nested: { a: 1 } } } + + let(:http) { instance_double(Net::HTTP) } + let(:response) { instance_double(Net::HTTPResponse) } + + before do + allow(Rails.logger).to receive(:debug) + end + + describe "#call" do + it "builds a POST request with JSON content-type and sends it over SSL" do + expect(Net::HTTP).to receive(:new).with(url.host, url.port).and_return(http) + expect(http).to receive(:use_ssl=).with(true) + + # We want to assert on the actual request object created + expect(http).to receive(:request) do |request| + expect(request).to be_a(Net::HTTP::Post) + expect(request["Content-Type"]).to eq("application/json") + expect(request.body).to eq(body.to_json) + response + end.and_return(response) + + result = described_class.new(url, body).call + expect(result).to eq(response) + end + + it "logs the request body (debug)" do + allow(Net::HTTP).to receive(:new).and_return(http) + allow(http).to receive(:use_ssl=) + + expect(Rails.logger).to receive(:debug).with(body.to_json.inspect) + + allow(http).to receive(:request).and_return(response) + + described_class.new(url, body).call + end + end +end From 179df5bff8834f96d84b3356ce2a8eac5d441ddb Mon Sep 17 00:00:00 2001 From: Tomasz Strzeszewski Date: Thu, 18 Dec 2025 15:13:07 +0100 Subject: [PATCH 14/63] Add translations, little refactoring --- app/services/spl/get_coupons_service.rb | 4 ++-- app/views/spree/checkout/_spl_coupons.html.erb | 4 ++-- config/locales/cs.yml | 3 +++ config/locales/en.yml | 3 +++ config/locales/sk.yml | 3 +++ 5 files changed, 13 insertions(+), 4 deletions(-) diff --git a/app/services/spl/get_coupons_service.rb b/app/services/spl/get_coupons_service.rb index a3e1abf..cab472f 100644 --- a/app/services/spl/get_coupons_service.rb +++ b/app/services/spl/get_coupons_service.rb @@ -13,7 +13,7 @@ def initialize(user, store) end def call - body = prepare_me_body + body = prepare_body response = send_request(@find_coupons_url, body) response_body = JSON.parse(response.body) Rails.logger.debug response_body @@ -28,7 +28,7 @@ def send_request(url, body) Spl::SendRequestService.new(url, body).call end - def prepare_me_body + def prepare_body { context: { prgCode: @store.private_metadata['spl_prg_code'], diff --git a/app/views/spree/checkout/_spl_coupons.html.erb b/app/views/spree/checkout/_spl_coupons.html.erb index 448f3c8..3586707 100644 --- a/app/views/spree/checkout/_spl_coupons.html.erb +++ b/app/views/spree/checkout/_spl_coupons.html.erb @@ -1,5 +1,5 @@ -
-

Your SPL coupons:

+
+

<%= Spree.t('spl.coupons.find.checkout_list') if @coupons.present? %>

<% @coupons&.each do |coupon| %> <%= coupon["typeCustomerName"] %> <% end %> diff --git a/config/locales/cs.yml b/config/locales/cs.yml index 739c2ab..8061e3e 100644 --- a/config/locales/cs.yml +++ b/config/locales/cs.yml @@ -7,3 +7,6 @@ cs: card_not_active: Karta není aktivní card_not_match: Karta neodpovídá číslu karty uživatele wrong_owner: Karta je přiřazena jinému uživateli + coupons: + find: + checkout_list: Your SPL coupons diff --git a/config/locales/en.yml b/config/locales/en.yml index cc93ec2..3d79fbc 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -7,3 +7,6 @@ en: card_not_active: Card is not active card_not_match: Card doesnt match user's card number wrong_owner: Card assigned to a different user + coupons: + find: + checkout_list: Your SPL coupons diff --git a/config/locales/sk.yml b/config/locales/sk.yml index dd62daf..df44431 100644 --- a/config/locales/sk.yml +++ b/config/locales/sk.yml @@ -7,3 +7,6 @@ sk: card_not_active: Karta nie je aktívna card_not_match: Karta nezodpovedá číslu karty používateľa wrong_owner: Karta je priradená inému používateľovi + coupons: + find: + checkout_list: Your SPL coupons From f68d16cd33ed32f51f4a8557052f85ef99abc4d4 Mon Sep 17 00:00:00 2001 From: Tomasz Strzeszewski Date: Thu, 18 Dec 2025 15:24:35 +0100 Subject: [PATCH 15/63] Replace spree.t with i18n.t --- app/views/spree/checkout/_spl_coupons.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/spree/checkout/_spl_coupons.html.erb b/app/views/spree/checkout/_spl_coupons.html.erb index 3586707..f163963 100644 --- a/app/views/spree/checkout/_spl_coupons.html.erb +++ b/app/views/spree/checkout/_spl_coupons.html.erb @@ -1,5 +1,5 @@
-

<%= Spree.t('spl.coupons.find.checkout_list') if @coupons.present? %>

+

<%= I18n.t('spl.coupons.find.checkout_list') if @coupons.present? %>

<% @coupons&.each do |coupon| %> <%= coupon["typeCustomerName"] %> <% end %> From 27c7155f16b9764edc8280b2de2b982b8d04697b Mon Sep 17 00:00:00 2001 From: nika-piotrowska Date: Thu, 18 Dec 2025 16:03:17 +0100 Subject: [PATCH 16/63] Add connect loyalty account method --- Gemfile | 1 - Gemfile.lock | 9 ----- .../profile_controller_decorator.rb | 35 +++++++++++++++---- app/services/spl/login_account_service.rb | 16 ++++----- app/services/spl/me_service.rb | 4 +-- app/services/spl/oauth_token_service.rb | 18 +++++----- .../spl/_loyalty_connect_button.html.erb | 21 +++++------ app/views/spl/_loyalty_connect_form.html.erb | 4 +-- app/views/spl/_otp_code_form.html.erb | 16 +++++++++ config/routes.rb | 1 + spec/rails_helper.rb | 2 -- ...=> store_private_metadata_service_spec.rb} | 0 12 files changed, 75 insertions(+), 52 deletions(-) create mode 100644 app/views/spl/_otp_code_form.html.erb rename spec/services/spl/{store_private_metadata_service_pec.rb => store_private_metadata_service_spec.rb} (100%) diff --git a/Gemfile b/Gemfile index c0c3c59..90ec793 100644 --- a/Gemfile +++ b/Gemfile @@ -23,7 +23,6 @@ group :test do gem 'rails-controller-testing' gem 'spree', '> 5.0' gem 'spree_storefront', '> 5.0' - gem 'webmock' end gem 'sprockets-rails', '~> 3.5' diff --git a/Gemfile.lock b/Gemfile.lock index e0b7b96..9aea972 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -180,9 +180,6 @@ GEM json countries (8.0.4) unaccent (~> 0.3) - crack (1.0.1) - bigdecimal - rexml crass (1.0.6) csv (3.3.5) database_cleaner (2.1.0) @@ -260,7 +257,6 @@ GEM rake (>= 10.0) globalid (1.3.0) activesupport (>= 6.1) - hashdiff (1.2.1) heroicon (1.0.0) rails (>= 5.2) highline (3.1.2) @@ -727,10 +723,6 @@ GEM nokogiri (~> 1.6) rubyzip (>= 1.3.0) selenium-webdriver (~> 4.0, < 4.11) - webmock (3.26.1) - addressable (>= 2.8.0) - crack (>= 0.3.2) - hashdiff (>= 0.4.0, < 2.0.0) websocket (1.2.11) websocket-driver (0.8.0) base64 @@ -767,7 +759,6 @@ DEPENDENCIES spree_storefront (> 5.0) sprockets-rails (~> 3.5) sqlite3 (>= 2.0) - webmock BUNDLED WITH 2.6.2 diff --git a/app/controllers/profile_controller_decorator.rb b/app/controllers/profile_controller_decorator.rb index 851d007..c7caaf6 100644 --- a/app/controllers/profile_controller_decorator.rb +++ b/app/controllers/profile_controller_decorator.rb @@ -8,21 +8,32 @@ def self.prepended(base) def login_code phone = phone_parser Spl::SendOtpService.new(DateTime.current, phone.country_code, phone.national_number, current_store).call - try_spree_current_user.update!(public_metadata: (try_spree_current_user.public_metadata || {}).merge('accept_yc_terms' => true)) + try_spree_current_user.update!(phone: login_code_params[:phone], public_metadata: (try_spree_current_user.public_metadata || {}).merge('accept_yc_terms' => true)) render_login_code_success(phone) rescue Spl::SendOtpService::SplSendOtpError => e - clear_login_code_errors - try_spree_current_user.errors.add(:base, e.message.presence || I18n.t('spl.user.errors.otp_send_failed')) + clear_errors + try_spree_current_user.errors.add(:base, e.message.presence || I18n.t('spl.errors.otp_send_failed')) render_login_code_error end - def connect_loyalty_account; end + def connect_loyalty_account + Spl::LoginAccountService.new(try_spree_current_user, current_store, params).call + AssignSpartaCardNumberService.new(try_spree_current_user, current_store).call + redirect_to spree.edit_account_profile_path, notice: Spree.t(:successfully_updated, resource: Spree.t(:account)) + rescue Spl::LoginAccountService::SplLoginAccountError, AssignSpartaCardNumberService::AssignSpartaCardNumberError, + Spl::MeService::SplMeError => e + payload = e.respond_to?(:payload) ? e.payload : (e.message.is_a?(Hash) ? e.message : nil) + msg = payload ? Spl::ErrorTranslator.translate(payload) : I18n.t('spl.errors.generic') + clear_errors + try_spree_current_user.errors.add(:base, msg) + render_connect_loyalty_account_error(try_spree_current_user.phone) + end private def validate_login_code_request - clear_login_code_errors + clear_errors validate_yc_terms validate_phone @@ -49,7 +60,7 @@ def phone_parser PhoneParserService.new(login_code_params[:phone]) end - def clear_login_code_errors + def clear_errors try_spree_current_user.errors.clear end @@ -74,6 +85,18 @@ def render_login_code_success(phone) status: :ok end + def render_connect_loyalty_account_error(phone) + render turbo_stream: turbo_stream.replace( + 'otp_code_form', + partial: 'spl/otp_code_form', + locals: { + user: try_spree_current_user, + phone_e164: phone.respond_to?(:e164) ? phone.e164 : nil + } + ), + status: :unprocessable_entity + end + def login_code_params params.require(:user).permit(:phone, :accept_yc_terms) end diff --git a/app/services/spl/login_account_service.rb b/app/services/spl/login_account_service.rb index 3f985d9..6a5fae5 100644 --- a/app/services/spl/login_account_service.rb +++ b/app/services/spl/login_account_service.rb @@ -10,10 +10,10 @@ def initialize(user, store, params) @login_url = URI.parse(Spl::UrlCreatorService.new(store.private_metadata['spl_url']).login) @user = user @store = store - @mobile_country = params.dig('user', 'public_metadata', 'mobile_country') - @phone_number = params.dig('user', 'public_metadata', 'phone_number') - @card_number = params.dig('user', 'public_metadata', 'card_number') - @otp_code = params.dig('user', 'public_metadata', 'spl_auth_code') + @phone_number = user.phone + @card_number = params.dig('user', 'card_number') + @otp_code = params.dig('user', 'spl_auth_code') + @env = Spl::StorePrivateMetadataService.all(store) end def call @@ -36,9 +36,9 @@ def send_request(url, body) def prepare_login_body # rubocop:disable Metrics/MethodLength { context: { - prgCode: @store.private_metadata['spl_prg_code'] + prgCode: @env['spl_prg_code'] }, - apiUser: @store.private_metadata['spl_api_user'], + apiUser: @env['spl_api_user'], scope: ['spl_cwp'], responseType: 'code', login: generate_login, @@ -52,8 +52,8 @@ def encrypted_otp end def generate_login - login = "#{@mobile_country}#{@phone_number}" if @card_number.nil? - login = @card_number if @mobile_country.nil? || @phone_number.nil? + login = @phone_number if @card_number.nil? + login = @card_number if @phone_number.nil? login end diff --git a/app/services/spl/me_service.rb b/app/services/spl/me_service.rb index b38a6d0..ba076d1 100644 --- a/app/services/spl/me_service.rb +++ b/app/services/spl/me_service.rb @@ -9,7 +9,7 @@ class SplMeError < StandardError; end def initialize(user, store) @me_url = URI.parse(Spl::UrlCreatorService.new(store.private_metadata['spl_url']).me) @user = user - @store = store + @env = Spl::StorePrivateMetadataService.all(store) end def call @@ -31,7 +31,7 @@ def send_request(url, body) def prepare_me_body { context: { - prgCode: @store.private_metadata['spl_prg_code'], + prgCode: @env['spl_prg_code'], oauthToken: @user.private_metadata['spl_access_token'] } } diff --git a/app/services/spl/oauth_token_service.rb b/app/services/spl/oauth_token_service.rb index d36b983..f59b63f 100644 --- a/app/services/spl/oauth_token_service.rb +++ b/app/services/spl/oauth_token_service.rb @@ -8,7 +8,7 @@ class OauthTokenError < StandardError; end def initialize(date, store) @date = date.to_i * 1000 - @store = store + @env = Spl::StorePrivateMetadataService.all(store) @token_url = URI.parse(Spl::UrlCreatorService.new(store.private_metadata['spl_url']).oauth_token) end @@ -41,10 +41,10 @@ def send_request(url, body) def prepare_oauth_token_body_with_signature { context: { - prgCode: @store.private_metadata['spl_prg_code'] + prgCode: @env['spl_prg_code'] }, - apiUser: @store.private_metadata['spl_api_user'], - apiToken: @store.private_metadata['spl_api_token'], + apiUser: @env['spl_api_user'], + apiToken: @env['spl_api_token'], signature: generate_signature, date: @date, grantType: 'signature' @@ -54,10 +54,10 @@ def prepare_oauth_token_body_with_signature def prepare_oauth_token_body_with_oauth_code(auth_code) { context: { - prgCode: @store.private_metadata['spl_prg_code'] + prgCode: @env['spl_prg_code'] }, - apiUser: @store.private_metadata['spl_api_user'], - apiToken: @store.private_metadata['spl_api_token'], + apiUser: @env['spl_api_user'], + apiToken: @env['spl_api_token'], oauthCode: auth_code, grantType: 'authorization_code' } @@ -65,8 +65,8 @@ def prepare_oauth_token_body_with_oauth_code(auth_code) def generate_signature Spl::ClientSignatureService.new(@date, - @store.private_metadata['spl_api_token'], - @store.private_metadata['spl_signature_seed']).call + @env['spl_api_token'], + @env['spl_signature_seed']).call end end end diff --git a/app/views/spl/_loyalty_connect_button.html.erb b/app/views/spl/_loyalty_connect_button.html.erb index 35662eb..cf3f4d2 100644 --- a/app/views/spl/_loyalty_connect_button.html.erb +++ b/app/views/spl/_loyalty_connect_button.html.erb @@ -1,26 +1,21 @@ -
- <%= f.submit Spree.t('loyalty.send_code_button'), class: 'btn-primary w-full' %> + <%= f.submit I18n.t('buttons.send_code'), class: 'btn-primary w-full' %>
<% end %> <% end %> diff --git a/app/views/spl/_otp_code_form.html.erb b/app/views/spl/_otp_code_form.html.erb new file mode 100644 index 0000000..a1d83de --- /dev/null +++ b/app/views/spl/_otp_code_form.html.erb @@ -0,0 +1,16 @@ +<%= turbo_frame_tag "otp_code_form" do %> + <%= render partial: "spree/shared/error_messages", locals: { target: user } %> + <%= form_with scope: :user, url: connect_loyalty_account_account_profile_path, method: :post, data: { turbo_frame: "otp_code_form" } do |f| %> + +
+ <%= I18n.t("spl.otp.sent_to", phone: phone_e164) if phone_e164.present? %> +
+ +
+ <%= f.label :spl_auth_code, I18n.t("spl.otp.code_label"), class: "text-xs text-neutral-600 block mb-2" %> + <%= f.text_field :spl_auth_code, class: "text-input w-full" %> +
+ + <%= f.submit I18n.t("buttons.verify_button"), class: "btn-primary w-full" %> + <% end %> +<% end %> diff --git a/config/routes.rb b/config/routes.rb index 341aea9..26cb018 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -19,6 +19,7 @@ namespace :account do resource :profile, controller: :profile, only: [:edit, :update] do patch :login_code, to: 'profile#login_code' + post :connect_loyalty_account, to: 'profile#connect_loyalty_account' end end end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index cfbfa20..6e848dd 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -34,8 +34,6 @@ require 'spree/core/controller_helpers/strong_parameters' require 'database_cleaner' -require 'webmock/rspec' -WebMock.disable_net_connect!(allow_localhost: true) # Requires supporting ruby files with custom matchers and macros, etc, in # spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are # run as spec files by default. This means that files in spec/support that end diff --git a/spec/services/spl/store_private_metadata_service_pec.rb b/spec/services/spl/store_private_metadata_service_spec.rb similarity index 100% rename from spec/services/spl/store_private_metadata_service_pec.rb rename to spec/services/spl/store_private_metadata_service_spec.rb From a28131d46c4f9494dd2c24528e0161040f32ef9d Mon Sep 17 00:00:00 2001 From: nika-piotrowska Date: Thu, 18 Dec 2025 21:56:31 +0100 Subject: [PATCH 17/63] Add error translator to handle errors comes from spl --- .../profile_controller_decorator.rb | 8 ++-- app/services/spl/error_payload_parser.rb | 42 +++++++++++++++++++ app/services/spl/error_translator.rb | 25 +++++++++++ app/services/spl/login_account_service.rb | 2 +- app/services/spl/me_service.rb | 2 +- app/services/spl/send_otp_service.rb | 2 +- config/locales/cs.yml | 10 +++++ config/locales/en.yml | 10 +++++ config/locales/pl.yml | 10 +++++ config/locales/sk.yml | 10 +++++ lib/spree/spl/configuration.rb | 20 +++++++++ 11 files changed, 135 insertions(+), 6 deletions(-) create mode 100644 app/services/spl/error_payload_parser.rb create mode 100644 app/services/spl/error_translator.rb create mode 100644 lib/spree/spl/configuration.rb diff --git a/app/controllers/profile_controller_decorator.rb b/app/controllers/profile_controller_decorator.rb index c7caaf6..2dfecfe 100644 --- a/app/controllers/profile_controller_decorator.rb +++ b/app/controllers/profile_controller_decorator.rb @@ -11,8 +11,10 @@ def login_code try_spree_current_user.update!(phone: login_code_params[:phone], public_metadata: (try_spree_current_user.public_metadata || {}).merge('accept_yc_terms' => true)) render_login_code_success(phone) rescue Spl::SendOtpService::SplSendOtpError => e + payload = Spl::ErrorPayloadParser.parse(e.message) || {} + msg = Spl::ErrorTranslator.translate(payload) clear_errors - try_spree_current_user.errors.add(:base, e.message.presence || I18n.t('spl.errors.otp_send_failed')) + try_spree_current_user.errors.add(:base, msg) render_login_code_error end @@ -23,8 +25,8 @@ def connect_loyalty_account redirect_to spree.edit_account_profile_path, notice: Spree.t(:successfully_updated, resource: Spree.t(:account)) rescue Spl::LoginAccountService::SplLoginAccountError, AssignSpartaCardNumberService::AssignSpartaCardNumberError, Spl::MeService::SplMeError => e - payload = e.respond_to?(:payload) ? e.payload : (e.message.is_a?(Hash) ? e.message : nil) - msg = payload ? Spl::ErrorTranslator.translate(payload) : I18n.t('spl.errors.generic') + payload = Spl::ErrorPayloadParser.parse(e.message) || {} + msg = Spl::ErrorTranslator.translate(payload) clear_errors try_spree_current_user.errors.add(:base, msg) render_connect_loyalty_account_error(try_spree_current_user.phone) diff --git a/app/services/spl/error_payload_parser.rb b/app/services/spl/error_payload_parser.rb new file mode 100644 index 0000000..ac8b089 --- /dev/null +++ b/app/services/spl/error_payload_parser.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module Spl + class ErrorPayloadParser + # Parses strings like: + # "{\"errorCode\" => \"TEMPORARY_BLOCKED\", \"validationMessages\" => nil, ... }" + # Returns a Hash with string keys, or nil if it can't parse. + def self.parse(message) + str = message.to_s.strip + return nil unless str.start_with?("{") && str.end_with?("}") + + inner = str[1..-2] # remove { } + result = {} + + # Split into key/value pairs. This assumes values are quoted and don't contain unescaped commas. + inner.split(/,\s+/).each do |pair| + k, v = pair.split(/\s*=>\s*/, 2) + next unless k && v + + key = unquote(k.strip) + value = parse_value(v.strip) + result[key] = value + end + + result + rescue StandardError + nil + end + + def self.parse_value(v) + return nil if v == "nil" + return unquote(v) if v.start_with?("\"") && v.end_with?("\"") + v + end + + def self.unquote(s) + s = s.strip + s = s[1..-2] if s.start_with?("\"") && s.end_with?("\"") + s.gsub('\"', '"').gsub("\\\\", "\\") + end + end +end diff --git a/app/services/spl/error_translator.rb b/app/services/spl/error_translator.rb new file mode 100644 index 0000000..7f053e0 --- /dev/null +++ b/app/services/spl/error_translator.rb @@ -0,0 +1,25 @@ +module Spl + class ErrorTranslator + I18N_BASE = "spl.errors".freeze + + def self.translate(payload, locale: I18n.locale) + code = payload["errorCode"].to_s.presence + # Example key: spl.errors.auth_temporary_blocked + key = "#{I18N_BASE}.#{code&.downcase}" + + if code && I18n.exists?(key, locale) + I18n.t(key, locale:) + else + # report unknown code for later translations + Spree::Spl.report_error( + "Unknown Sparta error code", + error_code: code, + raw_msg: payload["msg"], + payload: payload + ) + + I18n.t("#{I18N_BASE}.generic", locale:) + end + end + end +end diff --git a/app/services/spl/login_account_service.rb b/app/services/spl/login_account_service.rb index 6a5fae5..53c0825 100644 --- a/app/services/spl/login_account_service.rb +++ b/app/services/spl/login_account_service.rb @@ -21,7 +21,7 @@ def call response = send_request(@login_url, body) response_body = JSON.parse(response.body) Rails.logger.debug response_body - raise SplLoginAccountError, response_body['msg'] if response_body['errorCode'] != '0' + raise SplLoginAccountError, response_body if response_body['errorCode'] != '0' access_token = response_body.dig('response', 'oauthCode') get_access_token(access_token) diff --git a/app/services/spl/me_service.rb b/app/services/spl/me_service.rb index ba076d1..d1c12ba 100644 --- a/app/services/spl/me_service.rb +++ b/app/services/spl/me_service.rb @@ -17,7 +17,7 @@ def call response = send_request(@me_url, body) response_body = JSON.parse(response.body) Rails.logger.debug response_body - raise SplMeError, response_body['msg'] if response_body['errorCode'] != '0' + raise SplMeError, response_body if response_body['errorCode'] != '0' response_body end diff --git a/app/services/spl/send_otp_service.rb b/app/services/spl/send_otp_service.rb index f546498..50d5a39 100644 --- a/app/services/spl/send_otp_service.rb +++ b/app/services/spl/send_otp_service.rb @@ -19,7 +19,7 @@ def call response = send_request(@send_otp_url, body) response_body = JSON.parse(response.body) Rails.logger.debug response_body - raise SplSendOtpError, response_body['msg'] if response_body['errorCode'] != '0' + raise SplSendOtpError, response_body if response_body['errorCode'] != '0' response_body end diff --git a/config/locales/cs.yml b/config/locales/cs.yml index 7988456..92fa482 100644 --- a/config/locales/cs.yml +++ b/config/locales/cs.yml @@ -13,3 +13,13 @@ cs: wrong_owner: Karta je přiřazena jinému uživateli profile: connect_button: Connect to your loyalty account + errors: + generic: We couldn't connect your loyalty account. Please try again. + auth_temporary_blocked: Too many failed attempts. Please try again later. + auth_failed: Authentication failed. + token_expired: Your session has expired. Please sign in again. + not_authorized: You are not authorized to perform this action. + coupon_cannot_be_modified: This coupon cannot be modified. + coupon_expired: This coupon has expired. + coupon_is_not_valid: This coupon is not valid. + coupon_already_used: This coupon has already been used. diff --git a/config/locales/en.yml b/config/locales/en.yml index 4b923ee..bf9ab40 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -13,3 +13,13 @@ en: wrong_owner: Card assigned to a different user profile: connect_button: Connect to your loyalty account + errors: + generic: We couldn't connect your loyalty account. Please try again. + auth_temporary_blocked: Too many failed attempts. Please try again later. + auth_failed: Authentication failed. + token_expired: Your session has expired. Please sign in again. + not_authorized: You are not authorized to perform this action. + coupon_cannot_be_modified: This coupon cannot be modified. + coupon_expired: This coupon has expired. + coupon_is_not_valid: This coupon is not valid. + coupon_already_used: This coupon has already been used. diff --git a/config/locales/pl.yml b/config/locales/pl.yml index 69362f0..be0569e 100644 --- a/config/locales/pl.yml +++ b/config/locales/pl.yml @@ -13,3 +13,13 @@ pl: wrong_owner: Karta przypisana innemu użytkownikowi profile: connect_button: Połącz z kontem lojalnościowym + errors: + generic: Nie udało się połączyć konta lojalnościowego. Spróbuj ponownie. + auth_temporary_blocked: Zbyt wiele nieudanych prób. Spróbuj ponownie później. + auth_failed: Uwierzytelnienie nie powiodło się. + token_expired: Twoja sesja wygasła. Zaloguj się ponownie. + not_authorized: Nie masz uprawnień do wykonania tej operacji. + coupon_cannot_be_modified: Ten kupon nie może zostać zmodyfikowany. + coupon_expired: Ten kupon wygasł. + coupon_is_not_valid: Ten kupon jest nieprawidłowy. + coupon_already_used: Ten kupon został już wykorzystany. diff --git a/config/locales/sk.yml b/config/locales/sk.yml index f91a2bb..ba6fe67 100644 --- a/config/locales/sk.yml +++ b/config/locales/sk.yml @@ -13,3 +13,13 @@ sk: wrong_owner: Karta je priradená inému používateľovi profile: connect_button: Connect to your loyalty account + errors: + generic: We couldn't connect your loyalty account. Please try again. + auth_temporary_blocked: Too many failed attempts. Please try again later. + auth_failed: Authentication failed. + token_expired: Your session has expired. Please sign in again. + not_authorized: You are not authorized to perform this action. + coupon_cannot_be_modified: This coupon cannot be modified. + coupon_expired: This coupon has expired. + coupon_is_not_valid: This coupon is not valid. + coupon_already_used: This coupon has already been used. diff --git a/lib/spree/spl/configuration.rb b/lib/spree/spl/configuration.rb new file mode 100644 index 0000000..7eeb7d8 --- /dev/null +++ b/lib/spree/spl/configuration.rb @@ -0,0 +1,20 @@ +module Spree + module Spl + class Configuration + attr_accessor :error_reporter + + def initialize + @error_reporter = ->(message, extra = {}) { Rails.logger.warn("[SPL] #{message} #{extra.inspect}") } + end + end + + def self.config = (@config ||= Configuration.new) + def self.configure = yield(config) + + def self.report_error(message, extra = {}) + config.error_reporter&.call(message, extra) + rescue => ex + Rails.logger.error("[SPL] error_reporter failed: #{ex.class}: #{ex.message}") + end + end +end From 5e11548146eb443715d535b0bc27bc51621ff970 Mon Sep 17 00:00:00 2001 From: nika-piotrowska Date: Fri, 19 Dec 2025 01:45:17 +0100 Subject: [PATCH 18/63] Improve of nesting profile controller decorator --- .../profile_controller_decorator.rb | 105 --------------- .../profile_controller_decorator.rb | 124 ++++++++++++++++++ config/initializers/spree_spl.rb | 2 +- 3 files changed, 125 insertions(+), 106 deletions(-) delete mode 100644 app/controllers/profile_controller_decorator.rb create mode 100644 app/controllers/spl/spree/storefront/profile_controller_decorator.rb diff --git a/app/controllers/profile_controller_decorator.rb b/app/controllers/profile_controller_decorator.rb deleted file mode 100644 index 2dfecfe..0000000 --- a/app/controllers/profile_controller_decorator.rb +++ /dev/null @@ -1,105 +0,0 @@ -# frozen_string_literal: true - -module ProfileControllerDecorator - def self.prepended(base) - base.before_action :validate_login_code_request, only: :login_code - end - - def login_code - phone = phone_parser - Spl::SendOtpService.new(DateTime.current, phone.country_code, phone.national_number, current_store).call - try_spree_current_user.update!(phone: login_code_params[:phone], public_metadata: (try_spree_current_user.public_metadata || {}).merge('accept_yc_terms' => true)) - render_login_code_success(phone) - rescue Spl::SendOtpService::SplSendOtpError => e - payload = Spl::ErrorPayloadParser.parse(e.message) || {} - msg = Spl::ErrorTranslator.translate(payload) - clear_errors - try_spree_current_user.errors.add(:base, msg) - - render_login_code_error - end - - def connect_loyalty_account - Spl::LoginAccountService.new(try_spree_current_user, current_store, params).call - AssignSpartaCardNumberService.new(try_spree_current_user, current_store).call - redirect_to spree.edit_account_profile_path, notice: Spree.t(:successfully_updated, resource: Spree.t(:account)) - rescue Spl::LoginAccountService::SplLoginAccountError, AssignSpartaCardNumberService::AssignSpartaCardNumberError, - Spl::MeService::SplMeError => e - payload = Spl::ErrorPayloadParser.parse(e.message) || {} - msg = Spl::ErrorTranslator.translate(payload) - clear_errors - try_spree_current_user.errors.add(:base, msg) - render_connect_loyalty_account_error(try_spree_current_user.phone) - end - - private - - def validate_login_code_request - clear_errors - validate_yc_terms - validate_phone - - render_login_code_error if try_spree_current_user.errors.any? - end - - def validate_yc_terms - return if yc_terms_accepted? - - try_spree_current_user.errors.add(:base, I18n.t('spl.user.errors.must_accept_yc_terms')) - end - - def validate_phone - return if phone_parser.valid? - - try_spree_current_user.errors.add(:phone, I18n.t('spl.user.errors.invalid_phone')) - end - - def yc_terms_accepted? - ActiveModel::Type::Boolean.new.cast(login_code_params[:accept_yc_terms]) - end - - def phone_parser - PhoneParserService.new(login_code_params[:phone]) - end - - def clear_errors - try_spree_current_user.errors.clear - end - - def render_login_code_error - render turbo_stream: turbo_stream.replace( - 'loyalty_connect_form', - partial: 'spl/loyalty_connect_form', - locals: { user: try_spree_current_user } - ), - status: :unprocessable_entity - end - - def render_login_code_success(phone) - render turbo_stream: turbo_stream.replace( - 'loyalty_connect_form', - partial: 'spl/otp_code_form', - locals: { - user: try_spree_current_user, - phone_e164: phone.respond_to?(:e164) ? phone.e164 : nil - } - ), - status: :ok - end - - def render_connect_loyalty_account_error(phone) - render turbo_stream: turbo_stream.replace( - 'otp_code_form', - partial: 'spl/otp_code_form', - locals: { - user: try_spree_current_user, - phone_e164: phone.respond_to?(:e164) ? phone.e164 : nil - } - ), - status: :unprocessable_entity - end - - def login_code_params - params.require(:user).permit(:phone, :accept_yc_terms) - end -end diff --git a/app/controllers/spl/spree/storefront/profile_controller_decorator.rb b/app/controllers/spl/spree/storefront/profile_controller_decorator.rb new file mode 100644 index 0000000..b0a6813 --- /dev/null +++ b/app/controllers/spl/spree/storefront/profile_controller_decorator.rb @@ -0,0 +1,124 @@ +# frozen_string_literal: true + +module Spl + module Spree + module Storefront + module ProfileControllerDecorator + def self.prepended(base) + base.before_action :validate_login_code_request, only: :login_code + end + + def login_code + phone = phone_parser + Spl::SendOtpService.new(DateTime.current, phone.country_code, phone.national_number, current_store).call + update_user_after_otp_request + render_login_code_success(phone) + rescue Spl::SendOtpService::SplSendOtpError => e + handle_spl_error(e) + render_login_code_error + end + + def connect_loyalty_account + assign_card_number(try_spree_current_user, current_store, params) + redirect_to spree.edit_account_profile_path, + notice: ::Spree.t(:successfully_updated, resource: ::Spree.t(:account)) + rescue Spl::LoginAccountService::SplLoginAccountError, AssignSpartaCardNumberService::AssignSpartaCardNumberError, + Spl::MeService::SplMeError => e + handle_spl_error(e) + render_connect_loyalty_account_error(try_spree_current_user.phone) + end + + private + + def validate_login_code_request + clear_errors + validate_yc_terms + validate_phone + + render_login_code_error if try_spree_current_user.errors.any? + end + + def validate_yc_terms + return if yc_terms_accepted? + + try_spree_current_user.errors.add(:base, I18n.t('spl.user.errors.must_accept_yc_terms')) + end + + def validate_phone + return if phone_parser.valid? + + try_spree_current_user.errors.add(:phone, I18n.t('spl.user.errors.invalid_phone')) + end + + def yc_terms_accepted? + ActiveModel::Type::Boolean.new.cast(login_code_params[:accept_yc_terms]) + end + + def phone_parser + PhoneParserService.new(login_code_params[:phone]) + end + + def clear_errors + try_spree_current_user.errors.clear + end + + def render_login_code_error + render turbo_stream: turbo_stream.replace( + 'loyalty_connect_form', + partial: 'spl/loyalty_connect_form', + locals: { user: try_spree_current_user } + ), + status: :unprocessable_entity + end + + def render_login_code_success(phone) + render turbo_stream: turbo_stream.replace( + 'loyalty_connect_form', + partial: 'spl/otp_code_form', + locals: { + user: try_spree_current_user, + phone_e164: phone.respond_to?(:e164) ? phone.e164 : nil + } + ), + status: :ok + end + + def render_connect_loyalty_account_error(phone) + render turbo_stream: turbo_stream.replace( + 'otp_code_form', + partial: 'spl/otp_code_form', + locals: { + user: try_spree_current_user, + phone_e164: phone.respond_to?(:e164) ? phone.e164 : nil + } + ), + status: :unprocessable_entity + end + + def update_user_after_otp_request + try_spree_current_user.update!( + phone: login_code_params[:phone], + public_metadata: (try_spree_current_user.public_metadata || {}).merge('accept_yc_terms' => true) + ) + end + + def handle_spl_error(error) + payload = Spl::ErrorPayloadParser.parse(error.message) || error + msg = Spl::ErrorTranslator.translate(payload) + + clear_errors + try_spree_current_user.errors.add(:base, msg) + end + + def assign_card_number(user, store, params) + Spl::LoginAccountService.new(user, store, params).call + AssignSpartaCardNumberService.new(user, store).call + end + + def login_code_params + params.require(:user).permit(:phone, :accept_yc_terms) + end + end + end + end +end diff --git a/config/initializers/spree_spl.rb b/config/initializers/spree_spl.rb index 881c84e..6b53d54 100644 --- a/config/initializers/spree_spl.rb +++ b/config/initializers/spree_spl.rb @@ -38,6 +38,6 @@ ) ::Spree::Account::ProfileController.prepend( - ProfileControllerDecorator + Spl::Spree::Storefront::ProfileControllerDecorator ) end From 7038b4822415a669a44093ae4296d90ca8d09579 Mon Sep 17 00:00:00 2001 From: nika-piotrowska Date: Fri, 19 Dec 2025 01:45:53 +0100 Subject: [PATCH 19/63] Add translations --- app/services/spl/error_payload_parser.rb | 19 ++-- app/services/spl/error_translator.rb | 27 +++-- app/views/spl/_loyalty_connect_form.html.erb | 2 +- app/views/spl/_otp_code_form.html.erb | 4 +- config/locales/cs.yml | 9 ++ config/locales/en.yml | 9 ++ config/locales/pl.yml | 11 +- config/locales/sk.yml | 9 ++ .../services/spl/error_payload_parser_spec.rb | 101 ++++++++++++++++++ spec/services/spl/error_translator_spec.rb | 81 ++++++++++++++ 10 files changed, 247 insertions(+), 25 deletions(-) create mode 100644 spec/services/spl/error_payload_parser_spec.rb create mode 100644 spec/services/spl/error_translator_spec.rb diff --git a/app/services/spl/error_payload_parser.rb b/app/services/spl/error_payload_parser.rb index ac8b089..ea138cb 100644 --- a/app/services/spl/error_payload_parser.rb +++ b/app/services/spl/error_payload_parser.rb @@ -2,17 +2,12 @@ module Spl class ErrorPayloadParser - # Parses strings like: - # "{\"errorCode\" => \"TEMPORARY_BLOCKED\", \"validationMessages\" => nil, ... }" - # Returns a Hash with string keys, or nil if it can't parse. def self.parse(message) str = message.to_s.strip - return nil unless str.start_with?("{") && str.end_with?("}") + return nil unless str.start_with?('{') && str.end_with?('}') - inner = str[1..-2] # remove { } + inner = str[1..-2] result = {} - - # Split into key/value pairs. This assumes values are quoted and don't contain unescaped commas. inner.split(/,\s+/).each do |pair| k, v = pair.split(/\s*=>\s*/, 2) next unless k && v @@ -21,22 +16,22 @@ def self.parse(message) value = parse_value(v.strip) result[key] = value end - result rescue StandardError nil end def self.parse_value(v) - return nil if v == "nil" - return unquote(v) if v.start_with?("\"") && v.end_with?("\"") + return nil if v == 'nil' + return unquote(v) if v.start_with?('"') && v.end_with?('"') + v end def self.unquote(s) s = s.strip - s = s[1..-2] if s.start_with?("\"") && s.end_with?("\"") - s.gsub('\"', '"').gsub("\\\\", "\\") + s = s[1..-2] if s.start_with?('"') && s.end_with?('"') + s.gsub('\"', '"').gsub('\\\\', '\\') end end end diff --git a/app/services/spl/error_translator.rb b/app/services/spl/error_translator.rb index 7f053e0..cb99160 100644 --- a/app/services/spl/error_translator.rb +++ b/app/services/spl/error_translator.rb @@ -1,25 +1,36 @@ +# frozen_string_literal: true + module Spl class ErrorTranslator - I18N_BASE = "spl.errors".freeze + I18N_BASE = 'spl.errors' def self.translate(payload, locale: I18n.locale) - code = payload["errorCode"].to_s.presence - # Example key: spl.errors.auth_temporary_blocked - key = "#{I18N_BASE}.#{code&.downcase}" + return translate_hash_payload(payload, locale) if payload.is_a?(Hash) + + translate_error_payload(payload) + end + def self.translate_hash_payload(payload, locale) + code = payload['errorCode'].to_s.presence + key = "#{I18N_BASE}.#{code&.downcase}" if code && I18n.exists?(key, locale) I18n.t(key, locale:) else - # report unknown code for later translations - Spree::Spl.report_error( - "Unknown Sparta error code", + ::Spree::Spl.report_error( + 'Unknown Sparta error code', error_code: code, - raw_msg: payload["msg"], + raw_msg: payload['msg'], payload: payload ) I18n.t("#{I18N_BASE}.generic", locale:) end end + + def self.translate_error_payload(payload) + payload.message if payload.respond_to?(:message) + end + + private_class_method :translate_hash_payload, :translate_error_payload end end diff --git a/app/views/spl/_loyalty_connect_form.html.erb b/app/views/spl/_loyalty_connect_form.html.erb index 585cb23..87eb779 100644 --- a/app/views/spl/_loyalty_connect_form.html.erb +++ b/app/views/spl/_loyalty_connect_form.html.erb @@ -21,7 +21,7 @@ }, 'true', 'false' %> - <%= f.label :accept_yc_terms, I18n.t('loyalty.accept_yc_terms_label'), class: 'text-sm text-neutral-700' %> + <%= f.label :accept_yc_terms, I18n.t('spl.user.accept_yc_terms_label'), class: 'text-sm text-neutral-700' %>
diff --git a/app/views/spl/_otp_code_form.html.erb b/app/views/spl/_otp_code_form.html.erb index a1d83de..5a7e44a 100644 --- a/app/views/spl/_otp_code_form.html.erb +++ b/app/views/spl/_otp_code_form.html.erb @@ -3,11 +3,11 @@ <%= form_with scope: :user, url: connect_loyalty_account_account_profile_path, method: :post, data: { turbo_frame: "otp_code_form" } do |f| %>
- <%= I18n.t("spl.otp.sent_to", phone: phone_e164) if phone_e164.present? %> + <%= I18n.t("spl.user.sent_to", phone: phone_e164) if phone_e164.present? %>
- <%= f.label :spl_auth_code, I18n.t("spl.otp.code_label"), class: "text-xs text-neutral-600 block mb-2" %> + <%= f.label :spl_auth_code, I18n.t("spl.user.code_label"), class: "text-xs text-neutral-600 block mb-2" %> <%= f.text_field :spl_auth_code, class: "text-input w-full" %>
diff --git a/config/locales/cs.yml b/config/locales/cs.yml index 92fa482..b3d4625 100644 --- a/config/locales/cs.yml +++ b/config/locales/cs.yml @@ -3,6 +3,9 @@ cs: loyalty_card_missing: K objednávce není přiřazena žádná věrnostní karta spl: user: + accept_yc_terms_label: I accept the terms and conditions + sent_to: The code was sent to %{phone} + code_label: One-time code errors: must_accept_yc_terms: You must accept the loyalty club terms and conditions to continue invalid_phone: Podaj poprawny numer telefonu @@ -23,3 +26,9 @@ cs: coupon_expired: This coupon has expired. coupon_is_not_valid: This coupon is not valid. coupon_already_used: This coupon has already been used. + temporary_blocked: Temporarily blocked due to too many attempts. Please try again later. + person_not_found: Person not found. + buttons: + connect_account: Connect loyalty account + send_code: Send one-time code + verify_button: Verify phone number diff --git a/config/locales/en.yml b/config/locales/en.yml index bf9ab40..449fa8c 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -3,6 +3,9 @@ en: loyalty_card_missing: There is no loyalty card assigned to order spl: user: + accept_yc_terms_label: I accept the terms and conditions + sent_to: The code was sent to %{phone} + code_label: One-time code errors: must_accept_yc_terms: You must accept the loyalty club terms and conditions to continue invalid_phone: Podaj poprawny numer telefonu @@ -23,3 +26,9 @@ en: coupon_expired: This coupon has expired. coupon_is_not_valid: This coupon is not valid. coupon_already_used: This coupon has already been used. + temporary_blocked: Temporarily blocked due to too many attempts. Please try again later. + person_not_found: Person not found. + buttons: + connect_account: Connect loyalty account + send_code: Send one-time code + verify_button: Verify phone number diff --git a/config/locales/pl.yml b/config/locales/pl.yml index be0569e..bea256b 100644 --- a/config/locales/pl.yml +++ b/config/locales/pl.yml @@ -3,6 +3,9 @@ pl: loyalty_card_missing: Do zamówienia nie przypisano karty lojalnościowej spl: user: + accept_yc_terms_label: Akceptuje regulamin + sent_to: Wysłano kod na numer %{phone} + code_label: Jednorazowy kod errors: must_accept_yc_terms: Musisz zaakceptować regulamin klubu lojalnościowego invalid_phone: Podaj poprawny numer telefonu @@ -11,8 +14,6 @@ pl: card_not_active: Karta nie jest aktywna card_not_match: Karta nie jest zgodna z numerem karty użytkownika wrong_owner: Karta przypisana innemu użytkownikowi - profile: - connect_button: Połącz z kontem lojalnościowym errors: generic: Nie udało się połączyć konta lojalnościowego. Spróbuj ponownie. auth_temporary_blocked: Zbyt wiele nieudanych prób. Spróbuj ponownie później. @@ -23,3 +24,9 @@ pl: coupon_expired: Ten kupon wygasł. coupon_is_not_valid: Ten kupon jest nieprawidłowy. coupon_already_used: Ten kupon został już wykorzystany. + temporary_blocked: Tymczasowo zablokowano z powodu zbyt wielu prób. Spróbuj ponownie później. + person_not_found: Nie znaleziono użytkownika + buttons: + connect_account: Połącz z kontem lojalnościowym + send_code: Wyślij jednorazowy kod + verify_button: Zweryfikuj numer telefonu diff --git a/config/locales/sk.yml b/config/locales/sk.yml index ba6fe67..5b2c650 100644 --- a/config/locales/sk.yml +++ b/config/locales/sk.yml @@ -3,6 +3,9 @@ sk: loyalty_card_missing: K objednávke nie je priradená žiadna vernostná karta spl: user: + accept_yc_terms_label: I accept the terms and conditions + sent_to: The code was sent to %{phone} + code_label: One-time code errors: must_accept_yc_terms: You must accept the loyalty club terms and conditions to continue invalid_phone: Podaj poprawny numer telefonu @@ -23,3 +26,9 @@ sk: coupon_expired: This coupon has expired. coupon_is_not_valid: This coupon is not valid. coupon_already_used: This coupon has already been used. + temporary_blocked: Temporarily blocked due to too many attempts. Please try again later. + person_not_found: Person not found. + buttons: + connect_account: Connect loyalty account + send_code: Send one-time code + verify_button: Verify phone number diff --git a/spec/services/spl/error_payload_parser_spec.rb b/spec/services/spl/error_payload_parser_spec.rb new file mode 100644 index 0000000..3474c78 --- /dev/null +++ b/spec/services/spl/error_payload_parser_spec.rb @@ -0,0 +1,101 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Spl::ErrorPayloadParser do + describe '.parse' do + subject(:parse) { described_class.parse(message) } + + context 'when message is a ruby-hash-like string' do + let(:message) do + %({"errorCode" => "TEMPORARY_BLOCKED", "validationMessages" => nil, "fieldValidationMessages" => nil, "response" => nil, "msg" => "Temporarily blocked (too much attempts)"}) + end + + it 'parses into a hash with string keys and values' do + expect(parse).to eq( + 'errorCode' => 'TEMPORARY_BLOCKED', + 'validationMessages' => nil, + 'fieldValidationMessages' => nil, + 'response' => nil, + 'msg' => 'Temporarily blocked (too much attempts)' + ) + end + end + + context 'when message is not wrapped in braces' do + let(:message) { %("errorCode" => "TEMPORARY_BLOCKED") } + + it 'returns nil' do + expect(parse).to be_nil + end + end + + context 'when message is blank' do + let(:message) { ' ' } + + it 'returns nil' do + expect(parse).to be_nil + end + end + + context 'when message contains escaped quotes and backslashes' do + let(:message) do + %({"msg" => "He said \\"hi\\"", "path" => "C:\\\\Temp\\\\file"}) + end + + it 'unescapes values correctly' do + expect(parse).to eq( + 'msg' => %(He said "hi"), + 'path' => %q(C:\Temp\file) + ) + end + end + + context 'when message is malformed' do + let(:message) { %({this is not => valid,) } + + it 'returns nil (does not raise)' do + expect(parse).to be_nil + end + end + + context 'when message includes unquoted values' do + let(:message) { %({"errorCode" => 123, "msg" => "ok"}) } + + it 'keeps unquoted values as strings (best-effort parsing)' do + expect(parse).to eq( + 'errorCode' => '123', + 'msg' => 'ok' + ) + end + end + end + + describe '.parse_value' do + it "returns nil for 'nil'" do + expect(described_class.parse_value('nil')).to be_nil + end + + it 'unquotes quoted strings' do + expect(described_class.parse_value(%("abc"))).to eq('abc') + end + + it 'returns raw value for non-quoted strings' do + expect(described_class.parse_value('SOME_CODE')).to eq('SOME_CODE') + end + end + + describe '.unquote' do + it 'removes surrounding quotes' do + expect(described_class.unquote(%("hello"))).to eq('hello') + end + + it 'unescapes escaped quotes' do + expect(described_class.unquote(%("He said \\"hi\\""))).to eq(%(He said "hi")) + end + + it 'unescapes backslashes' do + expect(described_class.unquote(%("C:\\\\Temp\\\\file"))).to eq(%q(C:\Temp\file)) + end + end +end diff --git a/spec/services/spl/error_translator_spec.rb b/spec/services/spl/error_translator_spec.rb new file mode 100644 index 0000000..ec2d710 --- /dev/null +++ b/spec/services/spl/error_translator_spec.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Spl::ErrorTranslator do + describe '.translate' do + let(:locale) { :en } + + before do + stub_const('Spree::Spl', Module.new) unless defined?(Spree::Spl) + allow(Spree::Spl).to receive(:report_error).and_return(nil) + end + + context 'when errorCode is present and translation key exists' do + let(:temporary_blocked_payload) do + { 'errorCode' => 'TEMPORARY_BLOCKED', 'msg' => 'Temporarily blocked' } + end + let(:temporary_blocked_key) { 'spl.errors.temporary_blocked' } + + it 'returns the translated message and does not report error' do + expect(I18n).to receive(:exists?).with(temporary_blocked_key, locale).and_return(true) + expect(I18n).to receive(:t).with(temporary_blocked_key, locale: locale).and_return('Translated message') + expect(Spree::Spl).not_to receive(:report_error) + expect(described_class.translate(temporary_blocked_payload, locale: locale)).to eq('Translated message') + end + end + + context 'when translation key does not exist' do + let(:unknown_code_payload) do + { 'errorCode' => 'SOME_NEW_CODE', 'msg' => 'Raw message' } + end + let(:some_new_code_key) { 'spl.errors.some_new_code' } + it 'reports error and returns generic translation' do + expect(I18n).to receive(:exists?).with(some_new_code_key, locale).and_return(false) + expect(Spree::Spl).to receive(:report_error).with( + 'Unknown Sparta error code', + hash_including( + error_code: 'SOME_NEW_CODE', + raw_msg: 'Raw message', + payload: unknown_code_payload + ) + ) + expect(I18n).to receive(:t).with('spl.errors.generic', locale: locale).and_return('Generic message') + expect(described_class.translate(unknown_code_payload, locale: locale)).to eq('Generic message') + end + end + + context 'when errorCode is nil/blank' do + let(:nil_code_payload) do + { 'errorCode' => nil, 'msg' => 'Raw message' } + end + it 'reports error and returns generic translation' do + expect(I18n).not_to receive(:exists?) + expect(Spree::Spl).to receive(:report_error).with( + 'Unknown Sparta error code', + hash_including( + error_code: nil, + raw_msg: 'Raw message', + payload: nil_code_payload + ) + ) + expect(I18n).to receive(:t).with('spl.errors.generic', locale: locale).and_return('Generic message') + + expect(described_class.translate(nil_code_payload, locale: locale)).to eq('Generic message') + end + end + + context 'when locale is passed explicitly' do + it 'uses the provided locale for lookup and translation' do + payload = { 'errorCode' => 'TEMPORARY_BLOCKED' } + pl = :pl + key = 'spl.errors.temporary_blocked' + + expect(I18n).to receive(:exists?).with(key, pl).and_return(true) + expect(I18n).to receive(:t).with(key, locale: pl).and_return('PL message') + + expect(described_class.translate(payload, locale: pl)).to eq('PL message') + end + end + end +end From 113945d317c1cdf82f3c007be3b8f551c2b3dfcb Mon Sep 17 00:00:00 2001 From: nika-piotrowska Date: Fri, 19 Dec 2025 01:46:12 +0100 Subject: [PATCH 20/63] Add tests troughout the app --- .../assign_sparta_card_number_service.rb | 10 +- lib/spree/spl/configuration.rb | 6 +- .../profile_controller_decorator_spec.rb | 96 ++++++++++- .../apply_sparta_discount_service_spec.rb | 2 +- .../assign_sparta_card_number_service_spec.rb | 17 +- spec/services/phone_parser_service_spec.rb | 2 +- .../promotion_switcher_service_spec.rb | 2 +- .../remove_sparta_discount_service_spec.rb | 2 +- .../spl/login_account_service_spec.rb | 159 ++++++++++++++++++ spec/services/spl/me_service_spec.rb | 68 ++++++++ spec/services/spl/send_otp_service_spec.rb | 4 +- .../services/spl/send_request_service_spec.rb | 16 +- .../spl/sparta_loyalty_service_spec.rb | 2 +- .../store_private_metadata_service_spec.rb | 2 +- spec/services/spl/url_creator_service_spec.rb | 77 +++++++++ 15 files changed, 427 insertions(+), 38 deletions(-) rename spec/controllers/{ => spl/spree/storefront}/profile_controller_decorator_spec.rb (52%) create mode 100644 spec/services/spl/login_account_service_spec.rb create mode 100644 spec/services/spl/me_service_spec.rb create mode 100644 spec/services/spl/url_creator_service_spec.rb diff --git a/app/services/assign_sparta_card_number_service.rb b/app/services/assign_sparta_card_number_service.rb index 66a4014..659d4b2 100644 --- a/app/services/assign_sparta_card_number_service.rb +++ b/app/services/assign_sparta_card_number_service.rb @@ -23,7 +23,7 @@ def add_card_number_to_user(card_data) if card_data['status'] != 'A' raise AssignSpartaCardNumberError, I18n.t('spl.card_validation.errors.card_not_active') end - unless cards_assigned_user(card_data['no']) + if card_assigned_to_other_user?(card_data['no']) raise AssignSpartaCardNumberError, I18n.t('spl.card_validation.errors.wrong_owner') end @@ -31,7 +31,11 @@ def add_card_number_to_user(card_data) spl_card_active: true)) end - def cards_assigned_user(card_number) - Spree::User.exists?(["id = ? AND public_metadata ->> 'spl_no_card' = ?", @user.id, card_number]) + def card_assigned_to_other_user?(card_number) + Spree::User.where( + "id != ? AND public_metadata ->> 'spl_no_card' = ?", + @user.id, + card_number + ).exists? end end diff --git a/lib/spree/spl/configuration.rb b/lib/spree/spl/configuration.rb index 7eeb7d8..4b58aa0 100644 --- a/lib/spree/spl/configuration.rb +++ b/lib/spree/spl/configuration.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Spree module Spl class Configuration @@ -13,8 +15,8 @@ def self.configure = yield(config) def self.report_error(message, extra = {}) config.error_reporter&.call(message, extra) - rescue => ex - Rails.logger.error("[SPL] error_reporter failed: #{ex.class}: #{ex.message}") + rescue StandardError => e + Rails.logger.error("[SPL] error_reporter failed: #{e.class}: #{e.message}") end end end diff --git a/spec/controllers/profile_controller_decorator_spec.rb b/spec/controllers/spl/spree/storefront/profile_controller_decorator_spec.rb similarity index 52% rename from spec/controllers/profile_controller_decorator_spec.rb rename to spec/controllers/spl/spree/storefront/profile_controller_decorator_spec.rb index ef515f6..1eeca06 100644 --- a/spec/controllers/profile_controller_decorator_spec.rb +++ b/spec/controllers/spl/spree/storefront/profile_controller_decorator_spec.rb @@ -9,7 +9,8 @@ end end - let(:store) { instance_double(Spree::Store) } + let(:country) { create(:country) } + let(:store) { create(:store, default_country: country) } let!(:user) { create(:user, public_metadata: {}) } let(:terms_accepted) { 'true' } @@ -37,7 +38,7 @@ allow(controller).to receive(:phone_parser).and_return(phone_double) allow(controller).to receive(:turbo_stream).and_return(turbo_stream_helper) allow(turbo_stream_helper).to receive(:replace).and_return('') - + allow(Spree::Spl).to receive(:report_error).and_return(nil) user.errors.clear end @@ -75,17 +76,29 @@ describe '#login_code (service + success/rescue)' do context 'when OTP service raises SplSendOtpError' do - it 'renders 422 and shows service message as base error' do + it 'renders 422 and shows translated message as base error' do service_instance = instance_double('Spl::SendOtpService') + payload = { + 'errorCode' => 'TEMPORARY_BLOCKED', + 'validationMessages' => nil, + 'fieldValidationMessages' => nil, + 'response' => nil, + 'msg' => 'Temporarily blocked (too much attempts)' + } + expect(Spl::SendOtpService).to receive(:new) .with(kind_of(DateTime), '+48', '500600700', store) .and_return(service_instance) - expect(service_instance).to receive(:call) - .and_raise(Spl::SendOtpService::SplSendOtpError.new('Person not found')) + + expect(service_instance).to receive(:call).and_raise(Spl::SendOtpService::SplSendOtpError.new(payload.inspect)) + expect(controller).to receive(:render).with(hash_including(status: :unprocessable_entity)) + controller.login_code - expect(user.errors.full_messages.join(' ')).to include('Person not found') + + expect(user.errors.full_messages.join(' ')) + .to include(I18n.t('spl.errors.temporary_blocked')) end end @@ -103,4 +116,75 @@ end end end + + describe '#connect_loyalty_account (service + success/rescue)' do + let(:service_params) { { user: { spl_auth_code: '123456' } } } + + before do + allow(controller).to receive(:params).and_return(service_params) + end + + context 'when account is connected successfully' do + it 'calls LoginAccountService and AssignSpartaCardNumberService, then redirects with notice' do + login_service = instance_double(Spl::LoginAccountService) + assign_service = instance_double(AssignSpartaCardNumberService) + + expect(Spl::LoginAccountService).to receive(:new) + .with(user, store, service_params) + .and_return(login_service) + expect(login_service).to receive(:call) + + expect(AssignSpartaCardNumberService).to receive(:new) + .with(user, store) + .and_return(assign_service) + expect(assign_service).to receive(:call) + + expect(controller).to receive(:redirect_to).with( + spree.edit_account_profile_path, + hash_including(notice: Spree.t(:successfully_updated, resource: Spree.t(:account))) + ) + + controller.connect_loyalty_account + end + end + + shared_examples 'renders connect error' do |error_class| + it "renders 422 and adds translated base error for #{error_class}" do + payload = { + 'errorCode' => 'TEMPORARY_BLOCKED', + 'validationMessages' => nil, + 'fieldValidationMessages' => nil, + 'response' => nil, + 'msg' => 'Temporarily blocked (too much attempts)' + } + + phone_obj = double('Phone', e164: '+48500600700') + allow(user).to receive(:phone).and_return(phone_obj) + + login_service = instance_double(Spl::LoginAccountService) + allow(Spl::LoginAccountService).to receive(:new).and_return(login_service) + allow(login_service).to receive(:call) + .and_raise(error_class.new(payload.inspect)) + + expect(controller).to receive(:render).with(hash_including(status: :unprocessable_entity)) + + controller.connect_loyalty_account + + expect(user.errors.full_messages.join(' ')) + .to include(I18n.t('spl.errors.temporary_blocked')) + end + end + + context 'when LoginAccountService raises' do + include_examples 'renders connect error', Spl::LoginAccountService::SplLoginAccountError + end + + context 'when AssignSpartaCardNumberService raises' do + include_examples 'renders connect error', AssignSpartaCardNumberService::AssignSpartaCardNumberError + end + + context 'when MeService raises' do + include_examples 'renders connect error', Spl::MeService::SplMeError + end + end end diff --git a/spec/services/apply_sparta_discount_service_spec.rb b/spec/services/apply_sparta_discount_service_spec.rb index 58d0122..e893f1f 100644 --- a/spec/services/apply_sparta_discount_service_spec.rb +++ b/spec/services/apply_sparta_discount_service_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe ApplySpartaDiscountService do +RSpec.describe ApplySpartaDiscountService, type: :service do let(:country) { create(:country) } let(:store) { create(:store, default_country: country) } let(:order) { create(:order, store: store) } diff --git a/spec/services/assign_sparta_card_number_service_spec.rb b/spec/services/assign_sparta_card_number_service_spec.rb index 8f2f72f..c0769a9 100644 --- a/spec/services/assign_sparta_card_number_service_spec.rb +++ b/spec/services/assign_sparta_card_number_service_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe AssignSpartaCardNumberService do +RSpec.describe AssignSpartaCardNumberService, type: :service do let(:country) { create(:country) } let(:store) { create(:store, default_country: country) } let(:user) { create(:user, public_metadata: initial_public_metadata) } @@ -47,18 +47,13 @@ describe '#call' do context 'when main card is active and belongs to the user' do - let(:initial_public_metadata) do - { 'spl_no_card' => '5100179585157' } - end + let(:initial_public_metadata) { { 'spl_no_card' => '5100179585157' } } it 'assigns card number and marks card as active in public_metadata' do - expect do - service.call - end.to change { user.reload.public_metadata }.from( - { 'spl_no_card' => '5100179585157' } - ).to( - { 'spl_no_card' => '5100179585157', 'spl_card_active' => true } - ) + expect { service.call } + .to change { user.reload.public_metadata } + .from({ 'spl_no_card' => '5100179585157' }) + .to({ 'spl_no_card' => '5100179585157', 'spl_card_active' => true }) end end diff --git a/spec/services/phone_parser_service_spec.rb b/spec/services/phone_parser_service_spec.rb index bcd0088..ffb3611 100644 --- a/spec/services/phone_parser_service_spec.rb +++ b/spec/services/phone_parser_service_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe PhoneParserService do +RSpec.describe PhoneParserService, type: :service do describe '#initialize' do it 'parses the raw phone with Phonelib and stores it as #phone' do phone_double = instance_double('Phonelib::Phone') diff --git a/spec/services/promotion_switcher_service_spec.rb b/spec/services/promotion_switcher_service_spec.rb index 5c363df..08c7a94 100644 --- a/spec/services/promotion_switcher_service_spec.rb +++ b/spec/services/promotion_switcher_service_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe PromotionSwitcherService do +RSpec.describe PromotionSwitcherService, type: :service do let(:country) { create(:country) } let(:store) { create(:store, default_country: country) } let(:order) do diff --git a/spec/services/remove_sparta_discount_service_spec.rb b/spec/services/remove_sparta_discount_service_spec.rb index 4da6ccb..beddb33 100644 --- a/spec/services/remove_sparta_discount_service_spec.rb +++ b/spec/services/remove_sparta_discount_service_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe RemoveSpartaDiscountService do +RSpec.describe RemoveSpartaDiscountService, type: :service do let(:country) { create(:country) } let(:store) { create(:store, default_country: country) } let(:order) { create(:order, store: store) } diff --git a/spec/services/spl/login_account_service_spec.rb b/spec/services/spl/login_account_service_spec.rb new file mode 100644 index 0000000..047126f --- /dev/null +++ b/spec/services/spl/login_account_service_spec.rb @@ -0,0 +1,159 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'json' +require 'digest' + +RSpec.describe Spl::LoginAccountService, type: :service do + let(:country) { create(:country) } + let(:user) { create(:user, phone: phone) } + let(:store) { create(:store, default_country: country, private_metadata: { 'spl_url' => 'https://spl.test' }) } + + let(:env) do + { + 'spl_prg_code' => 'PRG', + 'spl_api_user' => 'user' + } + end + + let(:login_url) { URI.parse('https://spl.test/login') } + + let(:phone) { '+48123123123' } + + let(:card_number) { nil } + let(:otp_code) { '123456' } + + let(:params) do + { + 'user' => { + 'card_number' => card_number, + 'spl_auth_code' => otp_code + } + } + end + + subject(:service) { described_class.new(user, store, params) } + + let(:send_request_service) { instance_double(Spl::SendRequestService) } + let(:oauth_service) { instance_double(Spl::OauthTokenService) } + + let(:login_response_body) do + { + 'errorCode' => '0', + 'response' => { 'oauthCode' => 'OAUTH_CODE_123' } + } + end + + let(:login_response) do + instance_double(Net::HTTPResponse, body: login_response_body.to_json) + end + + let(:token_body) do + { 'accessToken' => 'ACCESS_TOKEN', 'refreshToken' => 'REFRESH_TOKEN' } + end + + before do + allow(Spl::UrlCreatorService).to receive(:new) + .with('https://spl.test') + .and_return(double(login: login_url.to_s)) + + allow(Spl::StorePrivateMetadataService).to receive(:all) + .with(store) + .and_return(env) + + allow(Spl::SendRequestService).to receive(:new) + .and_return(send_request_service) + + allow(send_request_service).to receive(:call).and_return(login_response) + + allow(Spl::OauthTokenService).to receive(:new) + .and_return(oauth_service) + + allow(oauth_service).to receive(:authorization_code_token) + .with('OAUTH_CODE_123') + .and_return(token_body) + end + + describe '#call' do + context 'when SPL login succeeds' do + it 'updates user tokens from oauth token service' do + service.call + + user.reload + expect(user.private_metadata['spl_access_token']).to eq('ACCESS_TOKEN') + expect(user.private_metadata['spl_refresh_token']).to eq('REFRESH_TOKEN') + end + + it 'sends request to login URL with expected body (hashed OTP when length is 6)' do + expected_password = Digest::SHA256.hexdigest(otp_code) + + expect(Spl::SendRequestService).to receive(:new) do |url, body| + expect(url).to eq(login_url) + + # core body assertions + expect(body).to include( + apiUser: 'user', + scope: ['spl_cwp'], + responseType: 'code', + method: 'OTP' + ) + expect(body[:context]).to eq(prgCode: 'PRG') + expect(body[:password]).to eq(expected_password) + + # login should be phone when card_number nil + expect(body[:login]).to eq(phone) + + send_request_service + end + + service.call + end + end + + context 'when SPL returns an error' do + let(:login_response_body) do + { 'errorCode' => '123', 'msg' => 'Bad OTP' } + end + + it 'raises SplLoginAccountError' do + expect { service.call } + .to raise_error(Spl::LoginAccountService::SplLoginAccountError) + end + + it 'does not update user tokens' do + expect do + service.call + rescue StandardError + nil + end.not_to(change { user.reload.private_metadata }) + end + end + + context 'when OTP code is not 6 characters' do + let(:otp_code) { 'ALREADY_ENCRYPTED_OR_LONGER' } + + it 'uses OTP as-is (no hashing)' do + expect(Spl::SendRequestService).to receive(:new) do |_url, body| + expect(body[:password]).to eq(otp_code) + send_request_service + end + + service.call + end + end + + context 'when card number is provided and phone is nil' do + let(:phone) { nil } + let(:card_number) { '1234567890123' } + + it 'uses card number as login' do + expect(Spl::SendRequestService).to receive(:new) do |_url, body| + expect(body[:login]).to eq(card_number) + send_request_service + end + + service.call + end + end + end +end diff --git a/spec/services/spl/me_service_spec.rb b/spec/services/spl/me_service_spec.rb new file mode 100644 index 0000000..ca3595d --- /dev/null +++ b/spec/services/spl/me_service_spec.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Spl::MeService, type: :service do + subject(:service) { described_class.new(user, store) } + + let(:country) { create(:country) } + let(:user) { create(:user, private_metadata: { 'spl_access_token' => 'access-token-123' }) } + let(:store) { create(:store, default_country: country, private_metadata: { 'spl_url' => 'https://api.example.test' }) } + let(:env) { { 'spl_prg_code' => 'PRG001' } } + let(:me_url) { 'https://api.example.test/me' } + + before do + allow(Spl::StorePrivateMetadataService).to receive(:all).with(store).and_return(env) + + url_creator = instance_double(Spl::UrlCreatorService, me: me_url) + allow(Spl::UrlCreatorService).to receive(:new).with('https://api.example.test').and_return(url_creator) + end + + describe '#call' do + context "when API returns success (errorCode == '0')" do + let(:expected_body) do + { + context: { + prgCode: 'PRG001', + oauthToken: 'access-token-123' + } + } + end + let(:success_response_hash) do + { 'errorCode' => '0', 'data' => { 'foo' => 'bar' } } + end + + it 'sends request with correct body and returns parsed response body' do + response = double('Response', body: success_response_hash.to_json) + request_service = instance_double(Spl::SendRequestService) + + expect(Spl::SendRequestService).to receive(:new) + .with(URI.parse(me_url), expected_body) + .and_return(request_service) + expect(request_service).to receive(:call).and_return(response) + expect(service.call).to eq(success_response_hash) + end + end + + context "when API returns an error (errorCode != '0')" do + let(:error_response_hash) do + { + 'errorCode' => 'TEMPORARY_BLOCKED', + 'msg' => 'Temporarily blocked' + } + end + + before do + response = double('Response', body: error_response_hash.to_json) + request_service = instance_double(Spl::SendRequestService) + allow(Spl::SendRequestService).to receive(:new).and_return(request_service) + allow(request_service).to receive(:call).and_return(response) + end + + it 'raises SplMeError' do + expect { service.call } + .to raise_error(Spl::MeService::SplMeError, error_response_hash.to_s) + end + end + end +end diff --git a/spec/services/spl/send_otp_service_spec.rb b/spec/services/spl/send_otp_service_spec.rb index d47628f..155103b 100644 --- a/spec/services/spl/send_otp_service_spec.rb +++ b/spec/services/spl/send_otp_service_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Spl::SendOtpService do +RSpec.describe Spl::SendOtpService, type: :service do let(:store) do instance_double( Spree::Store, @@ -80,7 +80,7 @@ it 'raises SplSendOtpError with msg' do expect { service.call } - .to raise_error(Spl::SendOtpService::SplSendOtpError, 'Person not found') + .to raise_error(Spl::SendOtpService::SplSendOtpError, /Person not found/) end end diff --git a/spec/services/spl/send_request_service_spec.rb b/spec/services/spl/send_request_service_spec.rb index 952d534..1c4e837 100644 --- a/spec/services/spl/send_request_service_spec.rb +++ b/spec/services/spl/send_request_service_spec.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true -require "rails_helper" +require 'rails_helper' -RSpec.describe Spl::SendRequestService do - let(:url) { URI.parse("https://spl.test/api/send-otp") } - let(:body) { { foo: "bar", nested: { a: 1 } } } +RSpec.describe Spl::SendRequestService, type: :service do + let(:url) { URI.parse('https://spl.test/api/send-otp') } + let(:body) { { foo: 'bar', nested: { a: 1 } } } let(:http) { instance_double(Net::HTTP) } let(:response) { instance_double(Net::HTTPResponse) } @@ -13,15 +13,15 @@ allow(Rails.logger).to receive(:debug) end - describe "#call" do - it "builds a POST request with JSON content-type and sends it over SSL" do + describe '#call' do + it 'builds a POST request with JSON content-type and sends it over SSL' do expect(Net::HTTP).to receive(:new).with(url.host, url.port).and_return(http) expect(http).to receive(:use_ssl=).with(true) # We want to assert on the actual request object created expect(http).to receive(:request) do |request| expect(request).to be_a(Net::HTTP::Post) - expect(request["Content-Type"]).to eq("application/json") + expect(request['Content-Type']).to eq('application/json') expect(request.body).to eq(body.to_json) response end.and_return(response) @@ -30,7 +30,7 @@ expect(result).to eq(response) end - it "logs the request body (debug)" do + it 'logs the request body (debug)' do allow(Net::HTTP).to receive(:new).and_return(http) allow(http).to receive(:use_ssl=) diff --git a/spec/services/spl/sparta_loyalty_service_spec.rb b/spec/services/spl/sparta_loyalty_service_spec.rb index 80caf1a..c4be6d6 100644 --- a/spec/services/spl/sparta_loyalty_service_spec.rb +++ b/spec/services/spl/sparta_loyalty_service_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Spl::SpartaLoyaltyService do +RSpec.describe Spl::SpartaLoyaltyService, type: :service do let(:country) { create(:country) } let(:store) do diff --git a/spec/services/spl/store_private_metadata_service_spec.rb b/spec/services/spl/store_private_metadata_service_spec.rb index a995389..0538580 100644 --- a/spec/services/spl/store_private_metadata_service_spec.rb +++ b/spec/services/spl/store_private_metadata_service_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Spl::StorePrivateMetadataService do +RSpec.describe Spl::StorePrivateMetadataService, type: :service do let(:private_metadata) do { 'spl_url' => 'https://spl.test', diff --git a/spec/services/spl/url_creator_service_spec.rb b/spec/services/spl/url_creator_service_spec.rb new file mode 100644 index 0000000..1f30830 --- /dev/null +++ b/spec/services/spl/url_creator_service_spec.rb @@ -0,0 +1,77 @@ +# spec/services/spl/url_creator_service_spec.rb +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Spl::UrlCreatorService, type: :service do + subject(:service) { described_class.new(base_url) } + let(:base_url) { 'https://example.test' } + + describe '#check_card' do + it 'builds the checkCard URL' do + expect(service.check_card).to eq('https://example.test/api/tx/checkCard') + end + end + + describe '#find' do + it 'builds the find URL' do + expect(service.find).to eq('https://example.test/api/tx/find') + end + end + + describe '#sale' do + it 'builds the sale URL' do + expect(service.sale).to eq('https://example.test/api/tx/sale') + end + end + + describe '#sale_refund' do + it 'builds the saleRefund URL' do + expect(service.sale_refund).to eq('https://example.test/api/tx/saleRefund') + end + end + + describe '#me' do + it 'builds the me URL' do + expect(service.me).to eq('https://example.test/api/cwp/customer/me') + end + end + + describe '#register' do + it 'builds the register URL' do + expect(service.register).to eq('https://example.test/api/cwp/customer/register') + end + end + + describe '#request_otp' do + it 'builds the requestOTP URL' do + expect(service.request_otp).to eq('https://example.test/api/cwp/customer/requestOTP') + end + end + + describe '#login' do + it 'builds the login URL' do + expect(service.login).to eq('https://example.test/api/oauth/login') + end + end + + describe '#send_otp' do + it 'builds the sendOTP URL' do + expect(service.send_otp).to eq('https://example.test/api/oauth/sendOTP') + end + end + + describe '#oauth_token' do + it 'builds the token URL' do + expect(service.oauth_token).to eq('https://example.test/api/oauth/token') + end + end + + context 'when base url has a trailing slash' do + let(:base_url) { 'https://example.test/' } + + it 'keeps the double slash behavior (document current behavior)' do + expect(service.me).to eq('https://example.test//api/cwp/customer/me') + end + end +end From 56c8359c2b5bd2bd18fc9a6e9b274733c0082719 Mon Sep 17 00:00:00 2001 From: nika-piotrowska Date: Fri, 19 Dec 2025 01:57:50 +0100 Subject: [PATCH 21/63] Fix rubocop offenses --- .rubocop_todo.yml | 50 +++++++++++++++++-- app/controllers/cart_controller_decorator.rb | 2 +- app/models/order_updater_decorator.rb | 2 +- app/services/apply_sparta_discount_service.rb | 4 +- app/services/spl/login_account_service.rb | 2 +- app/services/spl/send_otp_service.rb | 2 +- 6 files changed, 53 insertions(+), 9 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index fa38b3d..a91cb97 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,25 +1,46 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2025-12-11 14:39:51 UTC using RuboCop version 1.79.2. +# on 2025-12-19 00:57:04 UTC using RuboCop version 1.79.2. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. # Offense count: 5 +# This cop supports safe autocorrection (--autocorrect). +Lint/RedundantCopDisableDirective: + Exclude: + - 'app/jobs/update_sparta_state_job.rb' + - 'app/services/spl/sparta_loyalty_service.rb' + - 'app/services/spl/validate_card_service.rb' + - 'spec/services/promotion_switcher_service_spec.rb' + - 'spec/services/remove_sparta_discount_service_spec.rb' + +# Offense count: 37 # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. # AllowedMethods: refine Metrics/BlockLength: - Max: 261 + Max: 254 + +# Offense count: 2 +# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. +Metrics/MethodLength: + Max: 14 # Offense count: 2 +# Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames. +# AllowedNames: as, at, by, cc, db, id, if, in, io, ip, of, on, os, pp, to +Naming/MethodParameterName: + Exclude: + - 'app/services/spl/error_payload_parser.rb' + +# Offense count: 1 # Configuration parameters: Mode, AllowedMethods, AllowedPatterns, AllowBangMethods, WaywardPredicates. # AllowedMethods: call # WaywardPredicates: nonzero? Naming/PredicateMethod: Exclude: - 'app/controllers/account_controller_decorator.rb' - - 'app/services/assign_sparta_card_number_service.rb' # Offense count: 1 # This cop supports unsafe autocorrection (--autocorrect-all). @@ -56,3 +77,26 @@ Rails/SkipsModelValidations: Exclude: - 'app/models/spree/adjustable/adjustments_updater_decorator.rb' - 'app/services/remove_sparta_discount_service.rb' + +# Offense count: 1 +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: exists, where +Rails/WhereExists: + Exclude: + - 'app/services/assign_sparta_card_number_service.rb' + +# Offense count: 4 +# This cop supports safe autocorrection (--autocorrect). +Style/IfUnlessModifier: + Exclude: + - 'app/services/assign_sparta_card_number_service.rb' + - 'app/services/spl/validate_card_service.rb' + - 'spec/controllers/spl/spree/storefront/profile_controller_decorator_spec.rb' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, IgnoreCopDirectives, AllowedPatterns, SplitStrings. +# URISchemes: http, https +Layout/LineLength: + Max: 181 diff --git a/app/controllers/cart_controller_decorator.rb b/app/controllers/cart_controller_decorator.rb index 834a0d8..8407677 100644 --- a/app/controllers/cart_controller_decorator.rb +++ b/app/controllers/cart_controller_decorator.rb @@ -13,7 +13,7 @@ def show super end - def update_spl_card_activate # rubocop:disable Metrics/AbcSize,Metrics/MethodLength + def update_spl_card_activate # rubocop:disable Metrics/AbcSize spree_authorize! :update, spree_current_order, order_token if params.dig('public_metadata', 'spl_card_active').nil? diff --git a/app/models/order_updater_decorator.rb b/app/models/order_updater_decorator.rb index 5a1f864..ef2cb25 100644 --- a/app/models/order_updater_decorator.rb +++ b/app/models/order_updater_decorator.rb @@ -10,7 +10,7 @@ def update # rubocop:disable Metrics/AbcSize private - def check_spl_adjustments # rubocop:disable Metrics/MethodLength + def check_spl_adjustments if order.public_metadata['spl_card_active'] == true updated_any_adjustment = false order.adjustments.each do |adjustment| diff --git a/app/services/apply_sparta_discount_service.rb b/app/services/apply_sparta_discount_service.rb index 8d7f6a2..15d5b43 100644 --- a/app/services/apply_sparta_discount_service.rb +++ b/app/services/apply_sparta_discount_service.rb @@ -8,7 +8,7 @@ def initialize(response, order) @response = response end - def call # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength + def call # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity return unless response_valid? line_items.each do |line_item| @@ -49,7 +49,7 @@ def discounts_present?(line_item, label) RemoveSpartaDiscountService.destroy_inactive_adjustments(adjustments, line_item, order) end - def create_sparta_adjustment(order, amount, label, line_item) # rubocop:disable Metrics/MethodLength + def create_sparta_adjustment(order, amount, label, line_item) return if amount.zero? || line_item.adjustments.find_by(label: label, amount: amount).present? remove_spree_promotions_adjustments(line_item) diff --git a/app/services/spl/login_account_service.rb b/app/services/spl/login_account_service.rb index 53c0825..0e156da 100644 --- a/app/services/spl/login_account_service.rb +++ b/app/services/spl/login_account_service.rb @@ -33,7 +33,7 @@ def send_request(url, body) Spl::SendRequestService.new(url, body).call end - def prepare_login_body # rubocop:disable Metrics/MethodLength + def prepare_login_body { context: { prgCode: @env['spl_prg_code'] diff --git a/app/services/spl/send_otp_service.rb b/app/services/spl/send_otp_service.rb index 50d5a39..a2f5fda 100644 --- a/app/services/spl/send_otp_service.rb +++ b/app/services/spl/send_otp_service.rb @@ -30,7 +30,7 @@ def send_request(url, body) Spl::SendRequestService.new(url, body).call end - def prepare_sms_otp_body # rubocop:disable Metrics/MethodLength + def prepare_sms_otp_body { context: { prgCode: @env['spl_prg_code'] From d80159e56d04394252a366f40699a4fbece67352 Mon Sep 17 00:00:00 2001 From: Tomasz Strzeszewski Date: Fri, 19 Dec 2025 13:13:06 +0100 Subject: [PATCH 22/63] part 1 --- app/controllers/spl/coupon_controller.rb | 15 +++++++ app/helpers/spl_service_helper.rb | 5 +++ app/services/spl/base_spl_service.rb | 8 ++++ .../spl/coupons/activate_coupon_service.rb | 41 +++++++++++++++++++ app/services/spl/url_creator_service.rb | 8 ++++ 5 files changed, 77 insertions(+) create mode 100644 app/controllers/spl/coupon_controller.rb create mode 100644 app/helpers/spl_service_helper.rb create mode 100644 app/services/spl/base_spl_service.rb create mode 100644 app/services/spl/coupons/activate_coupon_service.rb diff --git a/app/controllers/spl/coupon_controller.rb b/app/controllers/spl/coupon_controller.rb new file mode 100644 index 0000000..028036f --- /dev/null +++ b/app/controllers/spl/coupon_controller.rb @@ -0,0 +1,15 @@ +module Spl + class CouponController < Spree::Api::V2::BaseController + before_action do + spree_authorize! :update, spree_current_user + end + + def activate_coupon;end + + def deactivate_coupon; end + + private + + + end +end \ No newline at end of file diff --git a/app/helpers/spl_service_helper.rb b/app/helpers/spl_service_helper.rb new file mode 100644 index 0000000..32ef4f9 --- /dev/null +++ b/app/helpers/spl_service_helper.rb @@ -0,0 +1,5 @@ +module SplServiceHelper + def send_request(url, body) + Spl::SendRequestService.new(url, body).call + end +end \ No newline at end of file diff --git a/app/services/spl/base_spl_service.rb b/app/services/spl/base_spl_service.rb new file mode 100644 index 0000000..b74ed97 --- /dev/null +++ b/app/services/spl/base_spl_service.rb @@ -0,0 +1,8 @@ +module Spl + class BaseSplService + def initialize;end + def send_request(url, body) + Spl::SendRequestService.new(url, body).call + end + end +end \ No newline at end of file diff --git a/app/services/spl/coupons/activate_coupon_service.rb b/app/services/spl/coupons/activate_coupon_service.rb new file mode 100644 index 0000000..4f96449 --- /dev/null +++ b/app/services/spl/coupons/activate_coupon_service.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require 'json' + +module Spl + module Coupons + class ActivateCouponService + class ActivateCouponServiceError < StandardError; end + include SplServiceHelper + + def initialize(user, store, params) + @store = store + @activate_coupons_url = URI.parse(Spl::UrlCreatorService.new(store.private_metadata['spl_url']).coupon_activate) + @user = user + @coupon_code = params[:coupon_code] + end + + def call + body = prepare_body + response = send_request(@activate_coupons_url, body) + response_body = JSON.parse(response.body) + Rails.logger.debug response_body + raise ActivateCouponServiceError, response_body['msg'] if response_body['errorCode'] != '0' + + response_body['response'] + end + + private + + def prepare_body + { + context: { + prgCode: @store.private_metadata['spl_prg_code'], + oauthToken: @user.private_metadata['spl_access_token'] + }, + coupon_code: @coupon_code + } + end + end + end +end diff --git a/app/services/spl/url_creator_service.rb b/app/services/spl/url_creator_service.rb index af94517..6f0195d 100644 --- a/app/services/spl/url_creator_service.rb +++ b/app/services/spl/url_creator_service.rb @@ -30,6 +30,14 @@ def coupon_find "#{coupon_base}/find" end + def coupon_activate + "#{coupon_base}/activate" + end + + def coupon_deactivate + "#{coupon_base}/deactivate" + end + ## Authentication and Authorization def check_card From 248505ecc8b3b6e1404db19f2055ed76a887f3c8 Mon Sep 17 00:00:00 2001 From: nika-piotrowska Date: Fri, 19 Dec 2025 20:26:25 +0100 Subject: [PATCH 23/63] Changes after review --- .../profile_controller_decorator.rb | 13 ++- app/services/spl/error_payload_parser.rb | 2 + config/initializers/spree_spl.rb | 2 +- .../profile_controller_decorator_spec.rb | 4 +- spec/services/phone_parser_service_spec.rb | 9 +- .../services/spl/error_payload_parser_spec.rb | 40 +++----- spec/services/spl/error_translator_spec.rb | 38 +++++--- .../spl/login_account_service_spec.rb | 4 - spec/services/spl/me_service_spec.rb | 97 ++++++++++++++++++- spec/services/spl/send_otp_service_spec.rb | 2 +- .../spl/sparta_loyalty_service_spec.rb | 2 +- .../store_private_metadata_service_spec.rb | 4 +- 12 files changed, 149 insertions(+), 68 deletions(-) rename app/controllers/spl/spree/{storefront => account}/profile_controller_decorator.rb (92%) rename spec/controllers/spl/spree/{storefront => account}/profile_controller_decorator_spec.rb (97%) diff --git a/app/controllers/spl/spree/storefront/profile_controller_decorator.rb b/app/controllers/spl/spree/account/profile_controller_decorator.rb similarity index 92% rename from app/controllers/spl/spree/storefront/profile_controller_decorator.rb rename to app/controllers/spl/spree/account/profile_controller_decorator.rb index b0a6813..5d53711 100644 --- a/app/controllers/spl/spree/storefront/profile_controller_decorator.rb +++ b/app/controllers/spl/spree/account/profile_controller_decorator.rb @@ -2,17 +2,16 @@ module Spl module Spree - module Storefront + module Account module ProfileControllerDecorator def self.prepended(base) base.before_action :validate_login_code_request, only: :login_code end def login_code - phone = phone_parser - Spl::SendOtpService.new(DateTime.current, phone.country_code, phone.national_number, current_store).call + send_otp(phone_parser, current_store) update_user_after_otp_request - render_login_code_success(phone) + render_login_code_success(phone_parser) rescue Spl::SendOtpService::SplSendOtpError => e handle_spl_error(e) render_login_code_error @@ -55,7 +54,7 @@ def yc_terms_accepted? end def phone_parser - PhoneParserService.new(login_code_params[:phone]) + @phone_parser ||= PhoneParserService.new(login_code_params[:phone]) end def clear_errors @@ -95,6 +94,10 @@ def render_connect_loyalty_account_error(phone) status: :unprocessable_entity end + def send_otp(phone, store) + Spl::SendOtpService.new(DateTime.current, phone.country_code, phone.national_number, store).call + end + def update_user_after_otp_request try_spree_current_user.update!( phone: login_code_params[:phone], diff --git a/app/services/spl/error_payload_parser.rb b/app/services/spl/error_payload_parser.rb index ea138cb..5172685 100644 --- a/app/services/spl/error_payload_parser.rb +++ b/app/services/spl/error_payload_parser.rb @@ -33,5 +33,7 @@ def self.unquote(s) s = s[1..-2] if s.start_with?('"') && s.end_with?('"') s.gsub('\"', '"').gsub('\\\\', '\\') end + + private_class_method :parse_value, :unquote end end diff --git a/config/initializers/spree_spl.rb b/config/initializers/spree_spl.rb index 6b53d54..07434b5 100644 --- a/config/initializers/spree_spl.rb +++ b/config/initializers/spree_spl.rb @@ -38,6 +38,6 @@ ) ::Spree::Account::ProfileController.prepend( - Spl::Spree::Storefront::ProfileControllerDecorator + Spl::Spree::Account::ProfileControllerDecorator ) end diff --git a/spec/controllers/spl/spree/storefront/profile_controller_decorator_spec.rb b/spec/controllers/spl/spree/account/profile_controller_decorator_spec.rb similarity index 97% rename from spec/controllers/spl/spree/storefront/profile_controller_decorator_spec.rb rename to spec/controllers/spl/spree/account/profile_controller_decorator_spec.rb index 1eeca06..1a3b0b8 100644 --- a/spec/controllers/spl/spree/storefront/profile_controller_decorator_spec.rb +++ b/spec/controllers/spl/spree/account/profile_controller_decorator_spec.rb @@ -4,9 +4,7 @@ RSpec.describe Spree::Account::ProfileController, type: :controller do before do - unless defined?(Spl::SendOtpService::SplSendOtpError) - stub_const('Spl::SendOtpService::SplSendOtpError', Class.new(StandardError)) - end + stub_const('Spl::SendOtpService::SplSendOtpError', Class.new(StandardError)) unless defined?(Spl::SendOtpService::SplSendOtpError) end let(:country) { create(:country) } diff --git a/spec/services/phone_parser_service_spec.rb b/spec/services/phone_parser_service_spec.rb index ffb3611..5c2bcdb 100644 --- a/spec/services/phone_parser_service_spec.rb +++ b/spec/services/phone_parser_service_spec.rb @@ -6,7 +6,6 @@ describe '#initialize' do it 'parses the raw phone with Phonelib and stores it as #phone' do phone_double = instance_double('Phonelib::Phone') - expect(Phonelib).to receive(:parse).with('+48500600700').and_return(phone_double) service = described_class.new('+48500600700') @@ -15,10 +14,12 @@ end describe '#valid?' do - it 'delegates to the parsed phone object' do + before do phone_double = instance_double('Phonelib::Phone', valid?: true) allow(Phonelib).to receive(:parse).and_return(phone_double) + end + it 'delegates to the parsed phone object' do expect(described_class.new('anything')).to be_valid end end @@ -36,10 +37,6 @@ ) end - before do - allow(Phonelib).to receive(:parse).with('+48500600700').and_return(phone_double) - end - describe '#country_code' do it "returns country code with leading '+'" do expect(service.country_code).to eq('+48') diff --git a/spec/services/spl/error_payload_parser_spec.rb b/spec/services/spl/error_payload_parser_spec.rb index 3474c78..58e04f4 100644 --- a/spec/services/spl/error_payload_parser_spec.rb +++ b/spec/services/spl/error_payload_parser_spec.rb @@ -51,11 +51,13 @@ end end - context 'when message is malformed' do - let(:message) { %({this is not => valid,) } + context 'when message contains nil values' do + let(:message) { %({"response" => nil}) } - it 'returns nil (does not raise)' do - expect(parse).to be_nil + it 'parses nil correctly' do + expect(parse).to eq( + 'response' => nil + ) end end @@ -69,33 +71,13 @@ ) end end - end - - describe '.parse_value' do - it "returns nil for 'nil'" do - expect(described_class.parse_value('nil')).to be_nil - end - - it 'unquotes quoted strings' do - expect(described_class.parse_value(%("abc"))).to eq('abc') - end - it 'returns raw value for non-quoted strings' do - expect(described_class.parse_value('SOME_CODE')).to eq('SOME_CODE') - end - end - - describe '.unquote' do - it 'removes surrounding quotes' do - expect(described_class.unquote(%("hello"))).to eq('hello') - end - - it 'unescapes escaped quotes' do - expect(described_class.unquote(%("He said \\"hi\\""))).to eq(%(He said "hi")) - end + context 'when message is malformed' do + let(:message) { %({this is not => valid,) } - it 'unescapes backslashes' do - expect(described_class.unquote(%("C:\\\\Temp\\\\file"))).to eq(%q(C:\Temp\file)) + it 'returns nil (does not raise)' do + expect(parse).to be_nil + end end end end diff --git a/spec/services/spl/error_translator_spec.rb b/spec/services/spl/error_translator_spec.rb index ec2d710..ba76527 100644 --- a/spec/services/spl/error_translator_spec.rb +++ b/spec/services/spl/error_translator_spec.rb @@ -11,17 +11,22 @@ allow(Spree::Spl).to receive(:report_error).and_return(nil) end + around do |example| + I18n.with_locale(locale) do + example.run + end + end + context 'when errorCode is present and translation key exists' do let(:temporary_blocked_payload) do { 'errorCode' => 'TEMPORARY_BLOCKED', 'msg' => 'Temporarily blocked' } end - let(:temporary_blocked_key) { 'spl.errors.temporary_blocked' } it 'returns the translated message and does not report error' do - expect(I18n).to receive(:exists?).with(temporary_blocked_key, locale).and_return(true) - expect(I18n).to receive(:t).with(temporary_blocked_key, locale: locale).and_return('Translated message') expect(Spree::Spl).not_to receive(:report_error) - expect(described_class.translate(temporary_blocked_payload, locale: locale)).to eq('Translated message') + + expected = I18n.t('spl.errors.temporary_blocked', locale: locale) + expect(described_class.translate(temporary_blocked_payload, locale: locale)).to eq(expected) end end @@ -29,9 +34,8 @@ let(:unknown_code_payload) do { 'errorCode' => 'SOME_NEW_CODE', 'msg' => 'Raw message' } end - let(:some_new_code_key) { 'spl.errors.some_new_code' } + it 'reports error and returns generic translation' do - expect(I18n).to receive(:exists?).with(some_new_code_key, locale).and_return(false) expect(Spree::Spl).to receive(:report_error).with( 'Unknown Sparta error code', hash_including( @@ -40,8 +44,9 @@ payload: unknown_code_payload ) ) - expect(I18n).to receive(:t).with('spl.errors.generic', locale: locale).and_return('Generic message') - expect(described_class.translate(unknown_code_payload, locale: locale)).to eq('Generic message') + + expected = I18n.t('spl.errors.generic', locale: locale) + expect(described_class.translate(unknown_code_payload, locale: locale)).to eq(expected) end end @@ -49,8 +54,8 @@ let(:nil_code_payload) do { 'errorCode' => nil, 'msg' => 'Raw message' } end + it 'reports error and returns generic translation' do - expect(I18n).not_to receive(:exists?) expect(Spree::Spl).to receive(:report_error).with( 'Unknown Sparta error code', hash_including( @@ -59,9 +64,9 @@ payload: nil_code_payload ) ) - expect(I18n).to receive(:t).with('spl.errors.generic', locale: locale).and_return('Generic message') - expect(described_class.translate(nil_code_payload, locale: locale)).to eq('Generic message') + expected = I18n.t('spl.errors.generic', locale: locale) + expect(described_class.translate(nil_code_payload, locale: locale)).to eq(expected) end end @@ -69,12 +74,15 @@ it 'uses the provided locale for lookup and translation' do payload = { 'errorCode' => 'TEMPORARY_BLOCKED' } pl = :pl - key = 'spl.errors.temporary_blocked' - expect(I18n).to receive(:exists?).with(key, pl).and_return(true) - expect(I18n).to receive(:t).with(key, locale: pl).and_return('PL message') + # jeśli masz pl.yml w appce, to wystarczy: + # expected = I18n.t('spl.errors.temporary_blocked', locale: pl) + + # jeśli NIE masz pl.yml, ale chcesz przetestować ścieżkę "locale param działa": + I18n.backend.store_translations(pl, spl: { errors: { temporary_blocked: 'PL message' } }) + expected = I18n.t('spl.errors.temporary_blocked', locale: pl) - expect(described_class.translate(payload, locale: pl)).to eq('PL message') + expect(described_class.translate(payload, locale: pl)).to eq(expected) end end end diff --git a/spec/services/spl/login_account_service_spec.rb b/spec/services/spl/login_account_service_spec.rb index 047126f..491d785 100644 --- a/spec/services/spl/login_account_service_spec.rb +++ b/spec/services/spl/login_account_service_spec.rb @@ -89,8 +89,6 @@ expect(Spl::SendRequestService).to receive(:new) do |url, body| expect(url).to eq(login_url) - - # core body assertions expect(body).to include( apiUser: 'user', scope: ['spl_cwp'], @@ -99,8 +97,6 @@ ) expect(body[:context]).to eq(prgCode: 'PRG') expect(body[:password]).to eq(expected_password) - - # login should be phone when card_number nil expect(body[:login]).to eq(phone) send_request_service diff --git a/spec/services/spl/me_service_spec.rb b/spec/services/spl/me_service_spec.rb index ca3595d..759239f 100644 --- a/spec/services/spl/me_service_spec.rb +++ b/spec/services/spl/me_service_spec.rb @@ -29,7 +29,102 @@ } end let(:success_response_hash) do - { 'errorCode' => '0', 'data' => { 'foo' => 'bar' } } + { + 'errorCode' => '0', + 'validationMessages' => nil, + 'fieldValidationMessages' => nil, + 'response' => { + 'person' => { + 'addonsList' => [], + 'address' => {}, + 'addresses' => nil, + 'assignedPlace' => { + 'id' => 'id123', + 'idAsDictLabel' => 'shop.pl' + }, + 'assignedPlaceManualId' => nil, + 'avatarUrl' => nil, + 'birthDate' => nil, + 'cards' => [ + { + 'burnEnabled' => true, + 'cardType' => { + 'id' => 'id234', + 'idAsDictLabel' => 'Shop - Default Virtual Card' + }, + 'depositBurnDisabled' => nil, + 'no' => '5100179585157', + 'status' => 'A', + 'statusAsDictLabel' => 'Active' + } + ], + 'company' => nil, + 'email' => 'name.surname@email.com', + 'emailVerified' => nil, + 'firstName' => 'name', + 'gender' => nil, + 'lastName' => 'surname', + 'marital' => nil, + 'middleName' => nil, + 'mobile' => '500600700', + 'mobileCountry' => '+48', + 'mobileVerified' => true, + 'nameDay' => nil, + 'passwordLastChanged' => nil, + 'permissions' => {}, + 'personId' => 'id345', + 'phone' => nil, + 'preReg' => nil, + 'preferredCommChannel' => nil, + 'pushNotificationsEnabled' => nil, + 'social' => [], + 'taxNo' => nil, + 'undefined' => false, + 'walletOwner' => true, + 'maritalAsDictLabel' => nil + }, + 'cards' => [ + { + 'burnEnabled' => true, + 'cardType' => { + 'id' => 'id456', + 'idAsDictLabel' => 'Shop - Default Virtual Card' + }, + 'depositBurnDisabled' => nil, + 'no' => '5100179585157', + 'status' => 'A', + 'statusAsDictLabel' => 'Active' + } + ], + 'wallet' => { + 'balances' => [ + { + 'balance' => 0.0, + 'nbBalance' => 0.0, + 'type' => 'P', + 'typeAsDictLabel' => 'Points' + } + ], + 'primaryBalance' => 0.0, + 'toBeExpired' => { 'primaryBalance' => {} }, + 'firstTx' => { 'date' => nil } + }, + 'badges' => [], + 'mainCard' => { + 'burnEnabled' => true, + 'cardType' => { + 'id' => 'id567', + 'idAsDictLabel' => 'Shop - Default Virtual Card' + }, + 'depositBurnDisabled' => nil, + 'no' => '5100179585157', + 'status' => 'A', + 'statusAsDictLabel' => 'Active' + }, + 'forcePasswordChange' => false + }, + 'msg' => 'OK' + } end it 'sends request with correct body and returns parsed response body' do diff --git a/spec/services/spl/send_otp_service_spec.rb b/spec/services/spl/send_otp_service_spec.rb index 155103b..7754707 100644 --- a/spec/services/spl/send_otp_service_spec.rb +++ b/spec/services/spl/send_otp_service_spec.rb @@ -19,7 +19,7 @@ } end - let(:date) { Time.zone.parse('2025-12-16 12:00:00 UTC') } + let(:date) { DateTime.parse('2025-12-16 12:00:00 UTC') } let(:date_ms) { date.to_i * 1000 } let(:mobile_country) { '+48' } let(:phone_number) { '500600700' } diff --git a/spec/services/spl/sparta_loyalty_service_spec.rb b/spec/services/spl/sparta_loyalty_service_spec.rb index c4be6d6..86eabeb 100644 --- a/spec/services/spl/sparta_loyalty_service_spec.rb +++ b/spec/services/spl/sparta_loyalty_service_spec.rb @@ -25,7 +25,7 @@ let(:order_token) { 'ORDER123' } let(:card_number) { '5100179585157' } - let(:date) { Time.zone.parse('2025-01-01 12:00:00') } + let(:date) { DateTime.parse('2025-01-01 12:00:00') } let(:check_only) { true } let(:product1) { create(:product) } let(:variant1) do diff --git a/spec/services/spl/store_private_metadata_service_spec.rb b/spec/services/spl/store_private_metadata_service_spec.rb index 0538580..6a1f354 100644 --- a/spec/services/spl/store_private_metadata_service_spec.rb +++ b/spec/services/spl/store_private_metadata_service_spec.rb @@ -62,7 +62,7 @@ expect(described_class.fetch(nil, :spl_url, default: 'x')).to eq('x') end - it 'raises KeyError when key is missing and no default is provided' do + it 'returns nil when store is nil and no default is provided' do expect(described_class.fetch(nil, :spl_url)).to be_nil end end @@ -79,7 +79,7 @@ expect(described_class.fetch(store, :missing_key, default: 'fallback')).to eq('fallback') end - it 'raises KeyError when key is missing and no default is provided' do + it 'returns nil when key is missing and no default is provided' do expect(described_class.fetch(store, :missing_key)).to be_nil end From e87cf7eed744016c15f7997ac0f6da5a65da8d3c Mon Sep 17 00:00:00 2001 From: Tomasz Strzeszewski Date: Mon, 29 Dec 2025 14:26:24 +0100 Subject: [PATCH 24/63] Add translations, checks for logged in user --- app/helpers/login_check_helper.rb | 12 ++++++++++++ app/services/spl/get_coupons_service.rb | 8 ++++++++ app/services/spl/me_service.rb | 8 ++++++++ config/locales/pl.yml | 12 ++++++++++++ spec/services/spl/get_coupon_service_spec.rb | 8 ++++++++ 5 files changed, 48 insertions(+) create mode 100644 app/helpers/login_check_helper.rb create mode 100644 config/locales/pl.yml diff --git a/app/helpers/login_check_helper.rb b/app/helpers/login_check_helper.rb new file mode 100644 index 0000000..dd9811a --- /dev/null +++ b/app/helpers/login_check_helper.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module LoginCheckHelper + # Checks if user is logged to SPL basing on local information + # @param user [Spree::User] + # @return [true, false] + def self.logged?(user) + return false unless user&.private_metadata&.fetch('spl_access_token', nil) + + true + end +end diff --git a/app/services/spl/get_coupons_service.rb b/app/services/spl/get_coupons_service.rb index cab472f..e4bdceb 100644 --- a/app/services/spl/get_coupons_service.rb +++ b/app/services/spl/get_coupons_service.rb @@ -4,6 +4,8 @@ module Spl class GetCouponsService + include LoginCheckHelper + class SplGetCouponError < StandardError; end def initialize(user, store) @@ -13,6 +15,8 @@ def initialize(user, store) end def call + return unless logged_user? + body = prepare_body response = send_request(@find_coupons_url, body) response_body = JSON.parse(response.body) @@ -36,5 +40,9 @@ def prepare_body } } end + + def logged_user? + LoginCheckHelper.logged?(@user) + end end end diff --git a/app/services/spl/me_service.rb b/app/services/spl/me_service.rb index b38a6d0..87be70d 100644 --- a/app/services/spl/me_service.rb +++ b/app/services/spl/me_service.rb @@ -4,6 +4,8 @@ module Spl class MeService + include LoginCheckHelper + class SplMeError < StandardError; end def initialize(user, store) @@ -13,6 +15,8 @@ def initialize(user, store) end def call + return unless logged_user? + body = prepare_me_body response = send_request(@me_url, body) response_body = JSON.parse(response.body) @@ -36,5 +40,9 @@ def prepare_me_body } } end + + def logged_user? + LoginCheckHelper.logged?(@user) + end end end diff --git a/config/locales/pl.yml b/config/locales/pl.yml new file mode 100644 index 0000000..04d5285 --- /dev/null +++ b/config/locales/pl.yml @@ -0,0 +1,12 @@ +pl: + order: + loyalty_card_missing: Brak karty lojalnościowej przypisanej do klienta + spl: + card_validation: + errors: + card_not_active: Karta jest nieaktywna + card_not_match: Karta nie pasuje do numeru klienta + wrong_owner: Karta przypisana do innego użytkownika + coupons: + find: + checkout_list: Twoje kupony lojalnościowe diff --git a/spec/services/spl/get_coupon_service_spec.rb b/spec/services/spl/get_coupon_service_spec.rb index 7a73738..e2eb49c 100644 --- a/spec/services/spl/get_coupon_service_spec.rb +++ b/spec/services/spl/get_coupon_service_spec.rb @@ -118,6 +118,14 @@ end end + context 'when user is not logged-in ' do + let(:user) { create(:user, private_metadata: nil) } + + it 'does nothing' do + expect(service.call).to eq(nil) + end + end + it 'sends a request with the correct body and URL' do allow(response_double).to receive(:body).and_return({ 'errorCode' => '0', 'response' => [] }.to_json) service.call From de7622ae943e37289135d5ccb11b81b01eb7baa8 Mon Sep 17 00:00:00 2001 From: vi4me Date: Tue, 30 Dec 2025 10:15:10 +0100 Subject: [PATCH 25/63] EK-683 Rejestracja konta loyalty przez nr telefonu --- .../account/profile_controller_decorator.rb | 149 ++++++++++++++++++ app/services/spl/register_account_service.rb | 28 ++-- app/services/spl/request_otp_service.rb | 10 +- app/views/spl/_loyalty_connect_form.html.erb | 89 +++++++++++ app/views/spl/_otp_registration_form.html.erb | 18 +++ config/initializers/spree_spl.rb | 4 + config/locales/cs.yml | 7 + config/locales/en.yml | 7 + config/locales/pl.yml | 9 ++ config/locales/sk.yml | 7 + config/routes.rb | 6 + 11 files changed, 315 insertions(+), 19 deletions(-) create mode 100644 app/controllers/spl/spree/account/profile_controller_decorator.rb create mode 100644 app/views/spl/_loyalty_connect_form.html.erb create mode 100644 app/views/spl/_otp_registration_form.html.erb create mode 100644 config/locales/pl.yml diff --git a/app/controllers/spl/spree/account/profile_controller_decorator.rb b/app/controllers/spl/spree/account/profile_controller_decorator.rb new file mode 100644 index 0000000..cb1e661 --- /dev/null +++ b/app/controllers/spl/spree/account/profile_controller_decorator.rb @@ -0,0 +1,149 @@ +# frozen_string_literal: true + +module Spl + module Spree + module Account + module ProfileControllerDecorator # rubocop:disable Metrics/ModuleLength + def self.prepended(base) + base.before_action :validate_login_code_request, only: %i[login_code registration_code] + end + + def login_code + send_otp(phone_parser, current_store) + update_user_after_otp_request + render_login_code_success(phone_parser, 'otp_code_form') + rescue Spl::SendOtpService::SplSendOtpError => e + handle_spl_error(e) + render_login_code_error + end + + def connect_loyalty_account + assign_card_number(try_spree_current_user, current_store, params) + redirect_to spree.edit_account_profile_path, + notice: ::Spree.t(:successfully_updated, resource: ::Spree.t(:account)) + rescue Spl::LoginAccountService::SplLoginAccountError, AssignSpartaCardNumberService::AssignSpartaCardNumberError, + Spl::MeService::SplMeError => e + handle_spl_error(e) + render_connect_loyalty_account_error(try_spree_current_user.phone, 'otp_code_form') + end + + def registration_code + request_otp(phone_parser, current_store) + update_user_after_otp_request + render_login_code_success(phone_parser, 'otp_registration_form') + rescue Spl::RequestOtpService::SplRequestOtpError, Spl::OauthTokenService::OauthTokenError => e + handle_spl_error(e) + render_login_code_error + end + + def register_loyalty_account + Spl::RegisterAccountService.new(spree_current_user, current_store, params['user']['spl_auth_code']).call + redirect_to spree.edit_account_profile_path, + notice: ::Spree.t(:successfully_updated, resource: ::Spree.t(:account)) + rescue Spl::RegisterAccountService::SplRegisterAccountError, Spl::OauthTokenService::OauthTokenError => e + handle_spl_error(e) + render_connect_loyalty_account_error(try_spree_current_user.phone, 'otp_registration_form') + end + + private + + def validate_login_code_request + clear_errors + validate_yc_terms + validate_phone + + render_login_code_error if try_spree_current_user.errors.any? + end + + def validate_yc_terms + return if yc_terms_accepted? + + try_spree_current_user.errors.add(:base, I18n.t('spl.user.errors.must_accept_yc_terms')) + end + + def validate_phone + return if phone_parser.valid? + + try_spree_current_user.errors.add(:phone, I18n.t('spl.user.errors.invalid_phone')) + end + + def yc_terms_accepted? + ActiveModel::Type::Boolean.new.cast(login_code_params[:accept_yc_terms]) + end + + def phone_parser + @phone_parser ||= PhoneParserService.new(login_code_params[:phone]) + end + + def clear_errors + try_spree_current_user.errors.clear + end + + def render_login_code_error + render turbo_stream: turbo_stream.replace( + 'loyalty_connect_form', + partial: 'spl/loyalty_connect_form', + locals: { user: try_spree_current_user } + ), + status: :unprocessable_entity + end + + def render_login_code_success(phone, partial) + render turbo_stream: turbo_stream.replace( + 'loyalty_connect_form', + partial: "spl/#{partial}", + locals: { + user: try_spree_current_user, + phone_e164: phone.respond_to?(:e164) ? phone.e164 : nil + } + ), + status: :ok + end + + def render_connect_loyalty_account_error(phone, partial) + render turbo_stream: turbo_stream.replace( + partial, + partial: "spl/#{partial}", + locals: { + user: try_spree_current_user, + phone_e164: phone.respond_to?(:e164) ? phone.e164 : nil + } + ), + status: :unprocessable_entity + end + + def send_otp(phone, store) + Spl::SendOtpService.new(DateTime.current, phone.country_code, phone.national_number, store).call + end + + def request_otp(phone, store) + Spl::RequestOtpService.new(DateTime.current, phone.country_code, phone.national_number, store).call + end + + def update_user_after_otp_request + try_spree_current_user.update!( + phone: login_code_params[:phone], + public_metadata: (try_spree_current_user.public_metadata || {}).merge('accept_yc_terms' => true) + ) + end + + def handle_spl_error(error) + payload = Spl::ErrorPayloadParser.parse(error.message) || error + msg = Spl::ErrorTranslator.translate(payload) + + clear_errors + try_spree_current_user.errors.add(:base, msg) + end + + def assign_card_number(user, store, params) + Spl::LoginAccountService.new(user, store, params).call + AssignSpartaCardNumberService.new(user, store).call + end + + def login_code_params + params.require(:user).permit(:phone, :accept_yc_terms) + end + end + end + end +end diff --git a/app/services/spl/register_account_service.rb b/app/services/spl/register_account_service.rb index bea3e1a..f4ab4fa 100644 --- a/app/services/spl/register_account_service.rb +++ b/app/services/spl/register_account_service.rb @@ -6,11 +6,12 @@ module Spl class RegisterAccountService class SplRegisterAccountError < StandardError; end - def initialize(user, store, params) + def initialize(user, store, spl_auth_code) @register_url = URI.parse(Spl::UrlCreatorService.new(store.private_metadata['spl_url']).register) @user = user @store = store - @params = params + @spl_auth_code = spl_auth_code + @phone = PhoneParserService.new(user.phone) end def call @@ -24,7 +25,7 @@ def call raise SplRegisterAccountError, register_response_body['msg'] if register_response_body['errorCode'] != '0' spl_card = register_response_body.dig('response', 'cardNo') - update_account(spl_card, @params) + update_account(spl_card) end private @@ -34,6 +35,7 @@ def send_request(url, body) end def prepare_registration_body(access_token) # rubocop:disable Metrics/MethodLength + accept_yc_terms = @user.public_metadata['accept_yc_terms'] { context: { oauthToken: access_token, @@ -41,28 +43,26 @@ def prepare_registration_body(access_token) # rubocop:disable Metrics/MethodLeng }, person: { firstName: @user[:first_name], - lastName: @user[:first_name], + lastName: @user[:last_name], email: @user[:email], - mobileCountry: @params.dig('user', 'public_metadata', 'mobileCountry'), - mobile: @params.dig('user', 'public_metadata', 'phone_number'), + mobileCountry: @phone.country_code, + mobile: @phone.national_number, permissions: { - processData: @params.dig('user', 'public_metadata', 'splProcessData'), - operationalSms: @params.dig('user', 'public_metadata', 'operationalSms') + processData: accept_yc_terms, + operationalSms: accept_yc_terms } }, - authCode: @params.dig('user', 'public_metadata', 'splAuthCode'), + authCode: @spl_auth_code, partnerCode: @store.private_metadata['spl_partner_code'], placeCode: @store.private_metadata['spl_place_code'] } end - def update_account(card_number, params) - mobile_country = params.dig('user', 'public_metadata', 'mobileCountry') - mobile_phone = params.dig('user', 'public_metadata', 'phone_number') + def update_account(card_number) @user.update(public_metadata: @user.public_metadata.merge(spl_no_card: card_number, spl_card_active: true, - mobile_country: mobile_country, - phone_number: mobile_phone)) + mobile_country: @phone.country_code, + phone_number: @phone.national_number)) end end end diff --git a/app/services/spl/request_otp_service.rb b/app/services/spl/request_otp_service.rb index c41303f..555f705 100644 --- a/app/services/spl/request_otp_service.rb +++ b/app/services/spl/request_otp_service.rb @@ -6,13 +6,13 @@ module Spl class RequestOtpService class SplRequestOtpError < StandardError; end - def initialize(date, store, params) + def initialize(date, mobile_country, phone_number, store) @date = date.to_i * 1000 @store = store @request_otp_url = URI.parse(Spl::UrlCreatorService.new(store.private_metadata['spl_url']).request_otp) - @mobile_country = params[:mobile_country] - @phone_number = params[:phone_number] - @email = params[:email] + @mobile_country = mobile_country + @phone_number = phone_number + @email = nil end def call @@ -23,7 +23,7 @@ def call request_otp_response = send_request(@request_otp_url, request_otp_body) request_otp_response_body = JSON.parse(request_otp_response.body) Rails.logger.debug request_otp_response_body - raise SplSendOtpCodeError, request_otp_response_body['msg'] if request_otp_response_body['errorCode'] != '0' + raise SplRequestOtpError, request_otp_response_body['msg'] if request_otp_response_body['errorCode'] != '0' request_otp_response_body end diff --git a/app/views/spl/_loyalty_connect_form.html.erb b/app/views/spl/_loyalty_connect_form.html.erb new file mode 100644 index 0000000..50979a8 --- /dev/null +++ b/app/views/spl/_loyalty_connect_form.html.erb @@ -0,0 +1,89 @@ +<%= turbo_frame_tag "loyalty_connect_form" do %> + <%= render partial: 'spree/shared/error_messages', locals: { target: @user } %> + +
+ <%= check_box_tag :has_account, '1', true, id: 'has_account_checkbox', class: 'mt-1' %> + <%= label_tag :has_account, I18n.t('spl.user.have_account'), class: 'text-sm text-neutral-700' %> +
+ +
+ <%= form_with scope: :user, url: login_code_account_profile_path, method: :patch, data: { turbo_frame: "loyalty_connect_form" } do |f| %> +
+ <%= f.label :phone, + Spree.t(:phone), + class: 'text-xs text-neutral-600 block mb-2' %> + <%= f.telephone_field :phone, + class: 'text-input w-full', + placeholder: Spree.t('loyalty.phone_label'), + value: try_spree_current_user&.phone %> +
+ +
+ <%= f.check_box :accept_yc_terms, + { + class: 'mt-1', + checked: ActiveModel::Type::Boolean.new.cast( + try_spree_current_user&.public_metadata&.fetch('accept_yc_terms', false) + ) + }, + 'true', + 'false' %> + <%= f.label :accept_yc_terms, I18n.t('spl.user.accept_yc_terms_label'), class: 'text-sm text-neutral-700' %> +
+ +
+ <%= f.submit I18n.t('buttons.send_log_in_code'), class: 'btn-primary w-full' %> +
+ <% end %> +
+ + +<% end %> + + diff --git a/app/views/spl/_otp_registration_form.html.erb b/app/views/spl/_otp_registration_form.html.erb new file mode 100644 index 0000000..6ac44ee --- /dev/null +++ b/app/views/spl/_otp_registration_form.html.erb @@ -0,0 +1,18 @@ +<%= turbo_frame_tag "otp_registration_form" do %> + <%= render partial: "spree/shared/error_messages", locals: { target: user } %> + <%= form_with scope: :user, url: register_loyalty_account_account_profile_path, method: :post, data: { turbo_frame: "otp_registration_form" } do |f| %> + +
+ <%= I18n.t("spl.user.sent_to", phone: phone_e164) if phone_e164.present? %> +
+ +
+ <%= f.label :spl_auth_code, I18n.t("spl.user.code_label"), class: "text-xs text-neutral-600 block mb-2" %> + <%= f.text_field :spl_auth_code, class: "text-input w-full" %> +
+ + <%= hidden_field_tag user.phone %> + + <%= f.submit I18n.t("buttons.verify_button"), class: "btn-primary w-full" %> + <% end %> +<% end %> diff --git a/config/initializers/spree_spl.rb b/config/initializers/spree_spl.rb index 0bbd3ab..07434b5 100644 --- a/config/initializers/spree_spl.rb +++ b/config/initializers/spree_spl.rb @@ -36,4 +36,8 @@ ::Spree::Payment.prepend( PaymentDecorator ) + + ::Spree::Account::ProfileController.prepend( + Spl::Spree::Account::ProfileControllerDecorator + ) end diff --git a/config/locales/cs.yml b/config/locales/cs.yml index 739c2ab..0082705 100644 --- a/config/locales/cs.yml +++ b/config/locales/cs.yml @@ -2,8 +2,15 @@ cs: order: loyalty_card_missing: K objednávce není přiřazena žádná věrnostní karta spl: + user: + have_account: Do you have loyalty club account? card_validation: errors: card_not_active: Karta není aktivní card_not_match: Karta neodpovídá číslu karty uživatele wrong_owner: Karta je přiřazena jinému uživateli + buttons: + connect_account: Connect loyalty account + send_log_in_code: Send one-time code for log in + send_registration_code: Send one-time code for registration + verify_button: Verify phone number diff --git a/config/locales/en.yml b/config/locales/en.yml index cc93ec2..3858d61 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -2,8 +2,15 @@ en: order: loyalty_card_missing: There is no loyalty card assigned to order spl: + user: + have_account: Do you have loyalty club account? card_validation: errors: card_not_active: Card is not active card_not_match: Card doesnt match user's card number wrong_owner: Card assigned to a different user + buttons: + connect_account: Connect loyalty account + send_log_in_code: Send one-time code for log in + send_registration_code: Send one-time code for registration + verify_button: Verify phone number diff --git a/config/locales/pl.yml b/config/locales/pl.yml new file mode 100644 index 0000000..a3cd5cd --- /dev/null +++ b/config/locales/pl.yml @@ -0,0 +1,9 @@ +pl: + spl: + user: + have_account: Czy masz konto w klubie lojalnościowym? + buttons: + connect_account: Połącz z kontem lojalnościowym + send_log_in_code: Wyślij jednorazowy kod do logowania + send_registration_code: Wyślij jednorazowy kod do rejestracji + verify_button: Zweryfikuj numer telefonu diff --git a/config/locales/sk.yml b/config/locales/sk.yml index dd62daf..ab80d4a 100644 --- a/config/locales/sk.yml +++ b/config/locales/sk.yml @@ -2,8 +2,15 @@ sk: order: loyalty_card_missing: K objednávke nie je priradená žiadna vernostná karta spl: + user: + have_account: Do you have loyalty club account? card_validation: errors: card_not_active: Karta nie je aktívna card_not_match: Karta nezodpovedá číslu karty používateľa wrong_owner: Karta je priradená inému používateľovi + buttons: + connect_account: Connect loyalty account + send_log_in_code: Send one-time code for log in + send_registration_code: Send one-time code for registration + verify_button: Verify phone number diff --git a/config/routes.rb b/config/routes.rb index 1f06ae8..7607eaa 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -16,4 +16,10 @@ end end end + namespace :account do + resource :profile, controller: :profile, only: [:edit, :update] do + patch :registration_code, to: 'profile#registration_code' + post :register_loyalty_account, to: 'profile#register_loyalty_account' + end + end end From 0beeed5c49b70edd7ebd2e2537ad81e05268d5b7 Mon Sep 17 00:00:00 2001 From: vi4me Date: Tue, 30 Dec 2025 13:31:11 +0100 Subject: [PATCH 26/63] EK-683 Add tests --- Gemfile | 2 + Gemfile.lock | 48 ++++++ .../profile_controller_decorator_spec.rb | 157 ++++++++++++++++++ .../spl/register_account_service_spec.rb | 122 ++++++++++++++ spec/services/spl/request_otp_service_spec.rb | 62 +++++++ 5 files changed, 391 insertions(+) create mode 100644 spec/controllers/spl/spree/account/profile_controller_decorator_spec.rb create mode 100644 spec/services/spl/register_account_service_spec.rb create mode 100644 spec/services/spl/request_otp_service_spec.rb diff --git a/Gemfile b/Gemfile index e451ca8..90ec793 100644 --- a/Gemfile +++ b/Gemfile @@ -20,7 +20,9 @@ group :test do gem 'factory_bot_rails' gem 'ffaker' gem 'observer' + gem 'rails-controller-testing' gem 'spree', '> 5.0' + gem 'spree_storefront', '> 5.0' end gem 'sprockets-rails', '~> 3.5' diff --git a/Gemfile.lock b/Gemfile.lock index 4139642..9ebaa3a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -65,6 +65,9 @@ GEM erubi (~> 1.11) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) + active_link_to (1.0.5) + actionpack + addressable active_storage_validations (1.3.0) activejob (>= 6.1.4) activemodel (>= 6.1.4) @@ -149,6 +152,8 @@ GEM builder (3.3.0) byebug (12.0.0) cancancan (3.6.1) + canonical-rails (0.2.17) + actionview (>= 4.1, < 8.1) capybara (3.40.0) addressable matrix @@ -252,6 +257,8 @@ GEM rake (>= 10.0) globalid (1.3.0) activesupport (>= 6.1) + heroicon (1.0.0) + rails (>= 5.2) highline (3.1.2) reline i18n (1.14.7) @@ -271,6 +278,13 @@ GEM image_processing (1.14.0) mini_magick (>= 4.9.5, < 6) ruby-vips (>= 2.0.17, < 3) + importmap-rails (2.2.2) + actionpack (>= 6.0.0) + activesupport (>= 6.0.0) + railties (>= 6.0.0) + inline_svg (1.10.0) + activesupport (>= 3.0) + nokogiri (>= 1.6) io-console (0.8.1) io-endpoint (0.15.2) io-event (1.14.2) @@ -303,6 +317,7 @@ GEM childprocess (~> 5.0) logger (~> 1.6) lint_roller (1.1.0) + local_time (3.0.3) logger (1.7.0) loofah (2.24.1) crass (~> 1.0.2) @@ -313,6 +328,9 @@ GEM net-imap net-pop net-smtp + mail_form (1.11.0) + actionpack (>= 7.0) + activemodel (>= 7.0) marcel (1.1.0) matrix (0.4.3) method_source (1.1.0) @@ -625,6 +643,19 @@ GEM spree_extension (0.1.0) activerecord (>= 4.2) spree_core + spree_storefront (5.2.0) + active_link_to + canonical-rails (~> 0.2.14) + heroicon + importmap-rails + inline_svg (~> 1.10) + local_time (~> 3.0) + mail_form + spree_core (>= 5.2.0) + stimulus-rails + tailwindcss-rails + tailwindcss-ruby + turbo-rails sprockets (4.2.2) concurrent-ruby (~> 1.0) logger @@ -648,8 +679,20 @@ GEM state_machines-activerecord (0.100.0) activerecord (>= 7.2) state_machines-activemodel (>= 0.100.0) + stimulus-rails (1.3.4) + railties (>= 6.0.0) stringex (2.8.6) stringio (3.1.8) + tailwindcss-rails (4.4.0) + railties (>= 7.0.0) + tailwindcss-ruby (~> 4.0) + tailwindcss-ruby (4.1.18) + tailwindcss-ruby (4.1.18-aarch64-linux-gnu) + tailwindcss-ruby (4.1.18-aarch64-linux-musl) + tailwindcss-ruby (4.1.18-arm64-darwin) + tailwindcss-ruby (4.1.18-x86_64-darwin) + tailwindcss-ruby (4.1.18-x86_64-linux-gnu) + tailwindcss-ruby (4.1.18-x86_64-linux-musl) terminal-table (4.0.0) unicode-display_width (>= 1.1.1, < 4) thor (1.4.0) @@ -660,6 +703,9 @@ GEM activesupport (>= 4.2.5) json (>= 1.8.3) tsort (0.2.0) + turbo-rails (2.0.20) + actionpack (>= 7.1.0) + railties (>= 7.1.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) unaccent (0.4.0) @@ -703,12 +749,14 @@ DEPENDENCIES factory_bot_rails ffaker observer + rails-controller-testing rake (~> 13.0) rubocop (~> 1.79, >= 1.79.2) rubocop-rails (~> 2.33, >= 2.33.3) rubocop-rails-omakase spree (> 5.0) spree-spl! + spree_storefront (> 5.0) sprockets-rails (~> 3.5) sqlite3 (>= 2.0) diff --git a/spec/controllers/spl/spree/account/profile_controller_decorator_spec.rb b/spec/controllers/spl/spree/account/profile_controller_decorator_spec.rb new file mode 100644 index 0000000..5aeaf0a --- /dev/null +++ b/spec/controllers/spl/spree/account/profile_controller_decorator_spec.rb @@ -0,0 +1,157 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Spree::Account::ProfileController, type: :controller do + before do + unless defined?(Spl::SendOtpService::SplSendOtpError) + stub_const('Spl::SendOtpService::SplSendOtpError', Class.new(StandardError)) + end + end + + let(:country) { create(:country) } + let(:store) { create(:store, default_country: country) } + let!(:user) { create(:user, public_metadata: {}) } + + let(:terms_accepted) { 'true' } + let(:phone_valid) { true } + let(:phone_value) { '+48500600700' } + + let(:phone_double) do + instance_double( + 'PhoneParserService', + valid?: phone_valid, + country_code: '+48', + national_number: '500600700', + e164: '+48500600700' + ) + end + + let(:turbo_stream_helper) { instance_double('TurboStreamHelper') } + + before do + allow(controller).to receive(:current_store).and_return(store) + allow(controller).to receive(:try_spree_current_user).and_return(user) + allow(controller).to receive(:login_code_params).and_return( + ActionController::Parameters.new(phone: phone_value, accept_yc_terms: terms_accepted).permit! + ) + allow(controller).to receive(:phone_parser).and_return(phone_double) + allow(controller).to receive(:turbo_stream).and_return(turbo_stream_helper) + allow(turbo_stream_helper).to receive(:replace).and_return('') + allow(Spree::Spl).to receive(:report_error).and_return(nil) + user.errors.clear + end + + describe '#registration_code (service + success/rescue)' do + before do + allow(controller).to receive(:login_code_params).and_return( + ActionController::Parameters.new(phone: phone_value, accept_yc_terms: terms_accepted).permit! + ) + end + + context 'when OTP request succeeds' do + xit 'calls RequestOtpService, updates user, and renders 200' do + service_instance = instance_double(Spl::RequestOtpService) + + expect(Spl::RequestOtpService).to receive(:new) + .with(kind_of(DateTime), '+48', '500600700', store) + .and_return(service_instance) + + expect(service_instance).to receive(:call) + expect(controller).to receive(:render).with(hash_including(status: :ok)) + + controller.registration_code + + expect(user.reload.phone).to eq(phone_value) + expect(user.public_metadata['accept_yc_terms']).to eq(true) + end + end + + shared_examples 'registration_code error' do |error_class| + xit "renders 422 and adds translated base error for #{error_class}" do + payload = { + 'errorCode' => 'TEMPORARY_BLOCKED', + 'msg' => 'Temporarily blocked' + } + + service_instance = instance_double(Spl::RequestOtpService) + + allow(Spl::RequestOtpService).to receive(:new).and_return(service_instance) + allow(service_instance).to receive(:call).and_raise(error_class.new(payload.inspect)) + + expect(controller).to receive(:render).with(hash_including(status: :unprocessable_entity)) + + controller.registration_code + + expect(user.errors.full_messages.join(' ')).to include(I18n.t('spl.errors.temporary_blocked')) + end + end + + context 'when RequestOtpService raises error' do + include_examples 'registration_code error', Spl::RequestOtpService::SplRequestOtpError + end + + context 'when OauthTokenService raises error' do + include_examples 'registration_code error', Spl::OauthTokenService::OauthTokenError + end + end + + describe '#register_loyalty_account (service + success/rescue)' do + let(:params_hash) { { 'user' => { 'spl_auth_code' => '123456' } } } + + before do + allow(controller).to receive(:params).and_return(params_hash) + allow(controller).to receive(:spree_current_user).and_return(user) + end + + context 'when registration succeeds' do + xit 'calls RegisterAccountService and redirects with notice' do + service_instance = instance_double(Spl::RegisterAccountService) + + expect(Spl::RegisterAccountService).to receive(:new) + .with(user, store, '123456') + .and_return(service_instance) + + expect(service_instance).to receive(:call) + + expect(controller).to receive(:redirect_to).with( + spree.edit_account_profile_path, + hash_including(notice: Spree.t(:successfully_updated, resource: Spree.t(:account))) + ) + + controller.register_loyalty_account + end + end + + shared_examples 'register_loyalty_account error' do |error_class| + xit "renders 422 and adds translated base error for #{error_class}" do + payload = { + 'errorCode' => 'TEMPORARY_BLOCKED', + 'msg' => 'Temporarily blocked' + } + + phone_obj = double('Phone', e164: '+48500600700') + allow(user).to receive(:phone).and_return(phone_obj) + + service_instance = instance_double(Spl::RegisterAccountService) + + allow(Spl::RegisterAccountService).to receive(:new).and_return(service_instance) + allow(service_instance).to receive(:call).and_raise(error_class.new(payload.inspect)) + + expect(controller).to receive(:render).with(hash_including(status: :unprocessable_entity)) + + controller.register_loyalty_account + + expect(user.errors.full_messages.join(' ')).to include(I18n.t('spl.errors.temporary_blocked')) + end + end + + context 'when RegisterAccountService raises error' do + include_examples 'register_loyalty_account error', Spl::RegisterAccountService::SplRegisterAccountError + end + + context 'when OauthTokenService raises error' do + include_examples 'register_loyalty_account error', Spl::OauthTokenService::OauthTokenError + end + end +end diff --git a/spec/services/spl/register_account_service_spec.rb b/spec/services/spl/register_account_service_spec.rb new file mode 100644 index 0000000..5473efa --- /dev/null +++ b/spec/services/spl/register_account_service_spec.rb @@ -0,0 +1,122 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Spl::RegisterAccountService do + subject(:service) { described_class.new(user, store, spl_auth_code) } + + let(:store) do + create( + :store, + default_country: create(:country), + private_metadata: { + 'spl_url' => 'https://spl.test', + 'spl_prg_code' => 'PRG', + 'spl_partner_code' => 'PARTNER', + 'spl_place_code' => 'PLACE' + } + ) + end + + let(:user) do + create( + :user, + phone: '+48500600700', + first_name: 'John', + last_name: 'Doe', + email: 'john@example.com', + public_metadata: { 'accept_yc_terms' => true } + ) + end + + let(:spl_auth_code) { '123456' } + let(:phone_parser) { instance_double(PhoneParserService, country_code: '+48', national_number: '500600700') } + let(:oauth_response) { { 'response' => { 'accessToken' => 'ACCESS_TOKEN' } } } + let(:register_response_body) { { 'errorCode' => '0', 'response' => { 'cardNo' => 'CARD123' } } } + let(:http_response) { instance_double(Net::HTTPResponse, body: register_response_body.to_json) } + let(:send_request_service) { instance_double(Spl::SendRequestService, call: http_response) } + + before do + allow(Spl::OauthTokenService).to receive(:new).and_return( + instance_double(Spl::OauthTokenService, annonymus_token: oauth_response) + ) + allow(Spl::SendRequestService).to receive(:new).and_return(send_request_service) + end + + describe '#call' do + context 'when registration succeeds' do + xit 'updates user public_metadata with SPL card data' do + service.call + + expect(user.public_metadata).to include( + 'spl_no_card' => 'CARD123', + 'spl_card_active' => true, + 'mobile_country' => '+48', + 'phone_number' => '500600700' + ) + end + + xit 'sends correct payload to SendRequestService' do + expect(Spl::SendRequestService).to receive(:new) do |url, body| + expect(url.to_s).to include('register') + expect(body).to include(authCode: spl_auth_code, partnerCode: 'PARTNER', placeCode: 'PLACE') + expect(body[:context]).to include(oauthToken: 'ACCESS_TOKEN', prgCode: 'PRG') + expect(body[:person][:permissions]).to eq(processData: true, operationalSms: true) + expect(body[:person]).to include( + firstName: 'John', + lastName: 'Doe', + email: 'john@example.com', + mobileCountry: '+48', + mobile: '500600700' + ) + end.and_return(send_request_service) + + service.call + end + end + + context 'when SPL returns errorCode != 0' do + let(:register_response_body) { { 'errorCode' => 'TEMPORARY_BLOCKED', 'msg' => 'Temporarily blocked' } } + + xit 'raises SplRegisterAccountError with SPL message' do + expect do + service.call + end.to raise_error(Spl::RegisterAccountService::SplRegisterAccountError, 'Temporarily blocked') + end + + xit 'does not update user public_metadata' do + expect do + service.call + rescue StandardError + nil + end.not_to(change { user.reload.public_metadata }) + end + end + + context 'when OTP code is not 6 characters' do + let(:spl_auth_code) { '123' } + + xit 'still sends request to SPL (no local validation)' do + expect(Spl::SendRequestService).to receive(:new).and_return(send_request_service) + + service.call + end + end + + context 'when card number is provided and phone is nil' do + before do + user.update!(phone: nil, public_metadata: user.public_metadata.merge('spl_no_card' => 'CARD123')) + allow(PhoneParserService).to receive(:new).with(nil).and_raise(StandardError, 'Phone missing') + end + + xit 'raises error before sending request' do + expect { service.call }.to raise_error(StandardError, 'Phone missing') + end + + xit 'does not send request to SPL' do + expect(Spl::SendRequestService).not_to receive(:new) + expect { service.call }.to raise_error(StandardError) + end + end + end +end diff --git a/spec/services/spl/request_otp_service_spec.rb b/spec/services/spl/request_otp_service_spec.rb new file mode 100644 index 0000000..ef6d38d --- /dev/null +++ b/spec/services/spl/request_otp_service_spec.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Spl::RequestOtpService do + subject(:service) { described_class.new(date, mobile_country, phone_number, store) } + + let(:store) do + create(:store, + default_country: create(:country), + private_metadata: { + 'spl_url' => 'https://spl.test', + 'spl_prg_code' => 'PRG' + }) + end + + let(:date) { DateTime.current } + let(:mobile_country) { '+48' } + let(:phone_number) { '500600700' } + let(:oauth_response) { { 'response' => { 'accessToken' => 'ACCESS_TOKEN' } } } + let(:success_response_body) { { 'errorCode' => '0', 'response' => { 'status' => 'OK' } } } + let(:error_response_body) { { 'errorCode' => 'TEMPORARY_BLOCKED', 'msg' => 'Temporarily blocked' } } + let(:http_response) { instance_double(Net::HTTPResponse, body: response_body.to_json) } + let(:send_request_service) { instance_double(Spl::SendRequestService, call: http_response) } + + before do + allow(Spl::OauthTokenService).to receive(:new).and_return( + instance_double(Spl::OauthTokenService, annonymus_token: oauth_response) + ) + allow(Spl::SendRequestService).to receive(:new).and_return(send_request_service) + end + + describe '#call' do + context 'when OTP request succeeds (SMS)' do + let(:response_body) { success_response_body } + + it 'returns parsed response body' do + result = service.call + + expect(result).to eq(success_response_body) + end + + it 'sends correct SMS payload to SendRequestService' do + expect(Spl::SendRequestService).to receive(:new) do |url, body| + expect(url.to_s).to include('requestOTP') + expect(body).to include(channel: 'S', mobileCountry: '+48', mobile: '500600700') + expect(body[:context]).to include(oauthToken: 'ACCESS_TOKEN', prgCode: 'PRG') + end.and_return(send_request_service) + + service.call + end + end + + context 'when SPL returns errorCode != 0' do + let(:response_body) { error_response_body } + + it 'raises SplRequestOtpError with SPL message' do + expect { service.call }.to raise_error(Spl::RequestOtpService::SplRequestOtpError, 'Temporarily blocked') + end + end + end +end From a7d6b4142c52232a0d730335a01153a876e40b5b Mon Sep 17 00:00:00 2001 From: Tomasz Strzeszewski Date: Tue, 30 Dec 2025 16:09:53 +0100 Subject: [PATCH 27/63] small refactor, activate coupon mechanism --- .rubocop.yml | 4 ++++ app/controllers/spl/coupon_controller.rb | 15 -------------- .../checkout_controller_decorator.rb | 20 ++++++++++++++++++- app/helpers/spl_service_helper.rb | 4 +++- app/services/spl/base_spl_service.rb | 6 ++++-- .../spl/coupons/activate_coupon_service.rb | 3 +-- app/services/spl/get_coupons_service.rb | 9 +++------ app/services/spl/login_account_service.rb | 6 +----- app/services/spl/me_service.rb | 6 +----- app/services/spl/oauth_token_service.rb | 6 +----- app/services/spl/register_account_service.rb | 6 +----- app/services/spl/request_otp_service.rb | 8 ++------ app/services/spl/send_otp_service.rb | 6 +----- app/services/spl/sparta_loyalty_service.rb | 6 +----- app/services/spl/validate_card_service.rb | 6 +----- .../spree/checkout/_spl_coupons.html.erb | 13 +++++++++++- config/routes.rb | 5 +++++ 17 files changed, 60 insertions(+), 69 deletions(-) delete mode 100644 app/controllers/spl/coupon_controller.rb diff --git a/.rubocop.yml b/.rubocop.yml index 68d5cbd..c60f039 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -17,3 +17,7 @@ Style/Documentation: Metrics/ParameterLists: Max: 8 + +Lint/MissingSuper: + Exclude: + - app/services/**/* diff --git a/app/controllers/spl/coupon_controller.rb b/app/controllers/spl/coupon_controller.rb deleted file mode 100644 index 028036f..0000000 --- a/app/controllers/spl/coupon_controller.rb +++ /dev/null @@ -1,15 +0,0 @@ -module Spl - class CouponController < Spree::Api::V2::BaseController - before_action do - spree_authorize! :update, spree_current_user - end - - def activate_coupon;end - - def deactivate_coupon; end - - private - - - end -end \ No newline at end of file diff --git a/app/controllers/spl/spree/storefront/checkout_controller_decorator.rb b/app/controllers/spl/spree/storefront/checkout_controller_decorator.rb index 1944d8c..53485df 100644 --- a/app/controllers/spl/spree/storefront/checkout_controller_decorator.rb +++ b/app/controllers/spl/spree/storefront/checkout_controller_decorator.rb @@ -8,10 +8,28 @@ def self.prepended(base) base.before_action :load_user_coupons end + def activate_coupon + debugger + Spl::Coupons::ActivateCouponService.new(current_user, current_store, params).call + + respond_to do |format| + format.turbo_stream + format.html { redirect_back fallback_location: checkout_path, notice: "Coupon activated" } + end + end + + def deactivate_coupon; end + private def load_user_coupons - @coupons = Spl::GetCouponsService.new(@order.user, @order.store).call + @coupons = Spl::GetCouponsService.new(@order.user, @order.store).call&.filter do |coupon| + active?(coupon) + end + end + + def active?(coupon) + coupon['used'] != true && coupon['usageTemporaryBlocked'] != true && Time.new(coupon['expirationDate']).future? end end end diff --git a/app/helpers/spl_service_helper.rb b/app/helpers/spl_service_helper.rb index 32ef4f9..ba677ba 100644 --- a/app/helpers/spl_service_helper.rb +++ b/app/helpers/spl_service_helper.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + module SplServiceHelper def send_request(url, body) Spl::SendRequestService.new(url, body).call end -end \ No newline at end of file +end diff --git a/app/services/spl/base_spl_service.rb b/app/services/spl/base_spl_service.rb index b74ed97..1c01cf3 100644 --- a/app/services/spl/base_spl_service.rb +++ b/app/services/spl/base_spl_service.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + module Spl + # Base service accumulating shared methods to DRY subclasses class BaseSplService - def initialize;end def send_request(url, body) Spl::SendRequestService.new(url, body).call end end -end \ No newline at end of file +end diff --git a/app/services/spl/coupons/activate_coupon_service.rb b/app/services/spl/coupons/activate_coupon_service.rb index 4f96449..203f871 100644 --- a/app/services/spl/coupons/activate_coupon_service.rb +++ b/app/services/spl/coupons/activate_coupon_service.rb @@ -21,7 +21,6 @@ def call response_body = JSON.parse(response.body) Rails.logger.debug response_body raise ActivateCouponServiceError, response_body['msg'] if response_body['errorCode'] != '0' - response_body['response'] end @@ -33,7 +32,7 @@ def prepare_body prgCode: @store.private_metadata['spl_prg_code'], oauthToken: @user.private_metadata['spl_access_token'] }, - coupon_code: @coupon_code + couponCode: @coupon_code } end end diff --git a/app/services/spl/get_coupons_service.rb b/app/services/spl/get_coupons_service.rb index cab472f..eeb0721 100644 --- a/app/services/spl/get_coupons_service.rb +++ b/app/services/spl/get_coupons_service.rb @@ -3,7 +3,7 @@ require 'json' module Spl - class GetCouponsService + class GetCouponsService < BaseSplService class SplGetCouponError < StandardError; end def initialize(user, store) @@ -24,16 +24,13 @@ def call private - def send_request(url, body) - Spl::SendRequestService.new(url, body).call - end - def prepare_body { context: { prgCode: @store.private_metadata['spl_prg_code'], oauthToken: @user.private_metadata['spl_access_token'] - } + }, + withArchival: true } end end diff --git a/app/services/spl/login_account_service.rb b/app/services/spl/login_account_service.rb index 3f985d9..a9e8385 100644 --- a/app/services/spl/login_account_service.rb +++ b/app/services/spl/login_account_service.rb @@ -3,7 +3,7 @@ require 'json' module Spl - class LoginAccountService + class LoginAccountService < BaseSplService class SplLoginAccountError < StandardError; end def initialize(user, store, params) @@ -29,10 +29,6 @@ def call private - def send_request(url, body) - Spl::SendRequestService.new(url, body).call - end - def prepare_login_body # rubocop:disable Metrics/MethodLength { context: { diff --git a/app/services/spl/me_service.rb b/app/services/spl/me_service.rb index b38a6d0..7f7453b 100644 --- a/app/services/spl/me_service.rb +++ b/app/services/spl/me_service.rb @@ -3,7 +3,7 @@ require 'json' module Spl - class MeService + class MeService < BaseSplService class SplMeError < StandardError; end def initialize(user, store) @@ -24,10 +24,6 @@ def call private - def send_request(url, body) - Spl::SendRequestService.new(url, body).call - end - def prepare_me_body { context: { diff --git a/app/services/spl/oauth_token_service.rb b/app/services/spl/oauth_token_service.rb index d36b983..c75ab9d 100644 --- a/app/services/spl/oauth_token_service.rb +++ b/app/services/spl/oauth_token_service.rb @@ -3,7 +3,7 @@ require 'json' module Spl - class OauthTokenService + class OauthTokenService < BaseSplService class OauthTokenError < StandardError; end def initialize(date, store) @@ -34,10 +34,6 @@ def authorization_code_token(auth_code) private - def send_request(url, body) - Spl::SendRequestService.new(url, body).call - end - def prepare_oauth_token_body_with_signature { context: { diff --git a/app/services/spl/register_account_service.rb b/app/services/spl/register_account_service.rb index bea3e1a..1fc4939 100644 --- a/app/services/spl/register_account_service.rb +++ b/app/services/spl/register_account_service.rb @@ -3,7 +3,7 @@ require 'json' module Spl - class RegisterAccountService + class RegisterAccountService < BaseSplService class SplRegisterAccountError < StandardError; end def initialize(user, store, params) @@ -29,10 +29,6 @@ def call private - def send_request(url, body) - Spl::SendRequestService.new(url, body).call - end - def prepare_registration_body(access_token) # rubocop:disable Metrics/MethodLength { context: { diff --git a/app/services/spl/request_otp_service.rb b/app/services/spl/request_otp_service.rb index c41303f..6699ff1 100644 --- a/app/services/spl/request_otp_service.rb +++ b/app/services/spl/request_otp_service.rb @@ -3,7 +3,7 @@ require 'json' module Spl - class RequestOtpService + class RequestOtpService < BaseSplService class SplRequestOtpError < StandardError; end def initialize(date, store, params) @@ -23,17 +23,13 @@ def call request_otp_response = send_request(@request_otp_url, request_otp_body) request_otp_response_body = JSON.parse(request_otp_response.body) Rails.logger.debug request_otp_response_body - raise SplSendOtpCodeError, request_otp_response_body['msg'] if request_otp_response_body['errorCode'] != '0' + raise SplRequestOtpError, request_otp_response_body['msg'] if request_otp_response_body['errorCode'] != '0' request_otp_response_body end private - def send_request(url, body) - Spl::SendRequestService.new(url, body).call - end - def prepare_sms_otp_body(access_token) { context: { diff --git a/app/services/spl/send_otp_service.rb b/app/services/spl/send_otp_service.rb index a3be995..bab224e 100644 --- a/app/services/spl/send_otp_service.rb +++ b/app/services/spl/send_otp_service.rb @@ -3,7 +3,7 @@ require 'json' module Spl - class SendOtpService + class SendOtpService < BaseSplService class SplSendOtpError < StandardError; end def initialize(date, mobile_country, phone_number, store) @@ -26,10 +26,6 @@ def call private - def send_request(url, body) - Spl::SendRequestService.new(url, body).call - end - def prepare_sms_otp_body # rubocop:disable Metrics/MethodLength { context: { diff --git a/app/services/spl/sparta_loyalty_service.rb b/app/services/spl/sparta_loyalty_service.rb index a765a15..4c5faa3 100644 --- a/app/services/spl/sparta_loyalty_service.rb +++ b/app/services/spl/sparta_loyalty_service.rb @@ -5,7 +5,7 @@ require 'json' module Spl - class SpartaLoyaltyService + class SpartaLoyaltyService < BaseSplService class SplSendRequestError < StandardError; end def initialize(order_token, card_number, line_items, date, products, check_only, store) @@ -35,10 +35,6 @@ def call private - def send_request(body) - Spl::SendRequestService.new(@url, body).call - end - def prepare_basket_body # rubocop:disable Metrics/MethodLength { ver: 4, diff --git a/app/services/spl/validate_card_service.rb b/app/services/spl/validate_card_service.rb index 57ad9df..47954da 100644 --- a/app/services/spl/validate_card_service.rb +++ b/app/services/spl/validate_card_service.rb @@ -6,7 +6,7 @@ module Spl # Validates SPL card number - class ValidateCardService + class ValidateCardService < BaseSplService class SplCardValidationError < StandardError; end def initialize(card_number, user, store) @@ -50,10 +50,6 @@ def verify_card_request Spl::SendRequestService.new(url, body).call end - def send_request(url, body) - Spl::SendRequestService.new(url, body).call - end - def body(card_number, date) # rubocop: disable Metrics/MethodLength date_in_ms = date.to_i * 1000 uuid = SecureRandom.uuid diff --git a/app/views/spree/checkout/_spl_coupons.html.erb b/app/views/spree/checkout/_spl_coupons.html.erb index f163963..e46f734 100644 --- a/app/views/spree/checkout/_spl_coupons.html.erb +++ b/app/views/spree/checkout/_spl_coupons.html.erb @@ -2,5 +2,16 @@

<%= I18n.t('spl.coupons.find.checkout_list') if @coupons.present? %>

<% @coupons&.each do |coupon| %> <%= coupon["typeCustomerName"] %> + <% if coupon["couponType"]['autoLoad'] == 'A'%> + <% if coupon['autoLoad'] %> +

dezaktywuj

+ <% else %> + <%= button_to "Activate", + activate_coupon_asd_path(order: @order, token: @order.token, coupon_code: coupon['code']), + data: { turbo_method: :post }, + class: "min-h-full ml-3 btn-primary !px-4 !py-3" %> + <% end %> + + <% end %> <% end %> -
\ No newline at end of file +
diff --git a/config/routes.rb b/config/routes.rb index 1f06ae8..039ccb8 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -16,4 +16,9 @@ end end end + + resource :checkout, as: 'asd', controller: :checkout, only: %i[show create update] do + post :activate_coupon, to: 'checkout#activate_coupon' + post :deactivate_coupon, to: 'checkout#deactivate_coupon' + end end From dae543e9c2f5b4c3c77ba9f321ec1b1cad8b5d01 Mon Sep 17 00:00:00 2001 From: Tomasz Strzeszewski Date: Wed, 31 Dec 2025 12:39:22 +0100 Subject: [PATCH 28/63] Add coupon deactivation, refactor --- .../checkout_controller_decorator.rb | 32 ++++++++++--- .../spl/coupons/deactivate_coupon_service.rb | 41 +++++++++++++++++ .../spree/checkout/_spl_coupons.html.erb | 46 +++++++++++++------ .../checkout/activate_coupon.turbo_stream.erb | 4 ++ .../deactivate_coupon.turbo_stream.erb | 4 ++ 5 files changed, 106 insertions(+), 21 deletions(-) create mode 100644 app/services/spl/coupons/deactivate_coupon_service.rb create mode 100644 app/views/spree/checkout/activate_coupon.turbo_stream.erb create mode 100644 app/views/spree/checkout/deactivate_coupon.turbo_stream.erb diff --git a/app/controllers/spl/spree/storefront/checkout_controller_decorator.rb b/app/controllers/spl/spree/storefront/checkout_controller_decorator.rb index 53485df..ec76e4e 100644 --- a/app/controllers/spl/spree/storefront/checkout_controller_decorator.rb +++ b/app/controllers/spl/spree/storefront/checkout_controller_decorator.rb @@ -5,20 +5,34 @@ module Spree module Storefront module CheckoutControllerDecorator def self.prepended(base) - base.before_action :load_user_coupons + base.before_action :load_user_coupons, except: [:activate_coupon, :deactivate_coupon] end def activate_coupon - debugger - Spl::Coupons::ActivateCouponService.new(current_user, current_store, params).call + Spl::Coupons::ActivateCouponService + .new(current_user, current_store, params) + .call + + load_user_coupons respond_to do |format| format.turbo_stream - format.html { redirect_back fallback_location: checkout_path, notice: "Coupon activated" } + format.html { redirect_to checkout_path } end end - def deactivate_coupon; end + def deactivate_coupon + Spl::Coupons::DeactivateCouponService + .new(current_user, current_store, params) + .call + + load_user_coupons + + respond_to do |format| + format.turbo_stream + format.html { redirect_to checkout_path } + end + end private @@ -29,7 +43,13 @@ def load_user_coupons end def active?(coupon) - coupon['used'] != true && coupon['usageTemporaryBlocked'] != true && Time.new(coupon['expirationDate']).future? + if coupon['used'] != true && coupon['usageTemporaryBlocked'] != true && coupon['expirationDate'].nil? + return true + end + + Time.new(coupon['expirationDate']).future? + rescue StandardError + false end end end diff --git a/app/services/spl/coupons/deactivate_coupon_service.rb b/app/services/spl/coupons/deactivate_coupon_service.rb new file mode 100644 index 0000000..0429aa2 --- /dev/null +++ b/app/services/spl/coupons/deactivate_coupon_service.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require 'json' + +module Spl + module Coupons + class DeactivateCouponService + class DeactivateCouponServiceError < StandardError; end + include SplServiceHelper + + def initialize(user, store, params) + @store = store + @deactivate_coupons_url = URI.parse(Spl::UrlCreatorService.new(store.private_metadata['spl_url']).coupon_deactivate) + @user = user + @coupon_code = params[:coupon_code] + end + + def call + body = prepare_body + response = send_request(@deactivate_coupons_url, body) + response_body = JSON.parse(response.body) + Rails.logger.debug response_body + raise DeactivateCouponServiceError, response_body['msg'] if response_body['errorCode'] != '0' + + response_body['response'] + end + + private + + def prepare_body + { + context: { + prgCode: @store.private_metadata['spl_prg_code'], + oauthToken: @user.private_metadata['spl_access_token'] + }, + couponCode: @coupon_code + } + end + end + end +end diff --git a/app/views/spree/checkout/_spl_coupons.html.erb b/app/views/spree/checkout/_spl_coupons.html.erb index e46f734..7d5f581 100644 --- a/app/views/spree/checkout/_spl_coupons.html.erb +++ b/app/views/spree/checkout/_spl_coupons.html.erb @@ -1,17 +1,33 @@ -
-

<%= I18n.t('spl.coupons.find.checkout_list') if @coupons.present? %>

- <% @coupons&.each do |coupon| %> - <%= coupon["typeCustomerName"] %> - <% if coupon["couponType"]['autoLoad'] == 'A'%> - <% if coupon['autoLoad'] %> -

dezaktywuj

- <% else %> - <%= button_to "Activate", - activate_coupon_asd_path(order: @order, token: @order.token, coupon_code: coupon['code']), - data: { turbo_method: :post }, - class: "min-h-full ml-3 btn-primary !px-4 !py-3" %> - <% end %> + +
+

<%= I18n.t('spl.coupons.find.checkout_list') if @coupons.present? %>

+ + <% @coupons&.each do |coupon| %> + <%= coupon["typeCustomerName"] %> + <% if coupon.dig("couponType", "autoLoad") == "A" %> + <% if coupon["autoLoad"] %> + <%= button_to "Deactivate", + deactivate_coupon_asd_path( + order: @order, + token: @order.token, + coupon_code: coupon["code"] + ), + method: :post, + data: { turbo: true }, + class: "min-h-full ml-3 btn-primary !px-4 !py-3" %> + <% else %> + <%= button_to "Activate", + activate_coupon_asd_path( + order: @order, + token: @order.token, + coupon_code: coupon["code"] + ), + method: :post, + data: { turbo: true }, + class: "min-h-full ml-3 btn-primary !px-4 !py-3" %> + <% end %> + <% end %> <% end %> - <% end %> -
+
+ diff --git a/app/views/spree/checkout/activate_coupon.turbo_stream.erb b/app/views/spree/checkout/activate_coupon.turbo_stream.erb new file mode 100644 index 0000000..aa53e8b --- /dev/null +++ b/app/views/spree/checkout/activate_coupon.turbo_stream.erb @@ -0,0 +1,4 @@ +<%= turbo_stream.replace "checkout_coupons" do %> + <%= render partial: "spree/checkout/spl_coupons", + locals: { coupons: @coupons, order: @order } %> +<% end %> \ No newline at end of file diff --git a/app/views/spree/checkout/deactivate_coupon.turbo_stream.erb b/app/views/spree/checkout/deactivate_coupon.turbo_stream.erb new file mode 100644 index 0000000..03de1fa --- /dev/null +++ b/app/views/spree/checkout/deactivate_coupon.turbo_stream.erb @@ -0,0 +1,4 @@ +<%= turbo_stream.replace "checkout_coupons" do %> + <%= render partial: "spree/checkout/spl_coupons", + locals: { coupons: @coupons, order: @order } %> +<% end %> From 0d96df757c9ccb856d336b40becdbb4bf25f66ab Mon Sep 17 00:00:00 2001 From: Tomasz Strzeszewski Date: Wed, 31 Dec 2025 12:39:53 +0100 Subject: [PATCH 29/63] Rubocop offences --- .../spl/spree/storefront/checkout_controller_decorator.rb | 6 +++--- app/services/spl/coupons/activate_coupon_service.rb | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/controllers/spl/spree/storefront/checkout_controller_decorator.rb b/app/controllers/spl/spree/storefront/checkout_controller_decorator.rb index ec76e4e..189efeb 100644 --- a/app/controllers/spl/spree/storefront/checkout_controller_decorator.rb +++ b/app/controllers/spl/spree/storefront/checkout_controller_decorator.rb @@ -5,7 +5,7 @@ module Spree module Storefront module CheckoutControllerDecorator def self.prepended(base) - base.before_action :load_user_coupons, except: [:activate_coupon, :deactivate_coupon] + base.before_action :load_user_coupons, except: %i[activate_coupon deactivate_coupon] end def activate_coupon @@ -46,8 +46,8 @@ def active?(coupon) if coupon['used'] != true && coupon['usageTemporaryBlocked'] != true && coupon['expirationDate'].nil? return true end - - Time.new(coupon['expirationDate']).future? + + Time.zone.local(coupon['expirationDate']).future? rescue StandardError false end diff --git a/app/services/spl/coupons/activate_coupon_service.rb b/app/services/spl/coupons/activate_coupon_service.rb index 203f871..5391994 100644 --- a/app/services/spl/coupons/activate_coupon_service.rb +++ b/app/services/spl/coupons/activate_coupon_service.rb @@ -21,6 +21,7 @@ def call response_body = JSON.parse(response.body) Rails.logger.debug response_body raise ActivateCouponServiceError, response_body['msg'] if response_body['errorCode'] != '0' + response_body['response'] end From 47709699c6c5a53cc4306a4ef3ef56ea712f3994 Mon Sep 17 00:00:00 2001 From: nika-piotrowska Date: Sun, 4 Jan 2026 20:45:35 +0100 Subject: [PATCH 30/63] Fix rubocop offenses --- .rubocop_todo.yml | 16 ++++++---------- app/services/spl/validate_card_service.rb | 4 ++-- spec/services/promotion_switcher_service_spec.rb | 2 +- .../remove_sparta_discount_service_spec.rb | 2 +- 4 files changed, 10 insertions(+), 14 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index a91cb97..67b7a94 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,28 +1,25 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2025-12-19 00:57:04 UTC using RuboCop version 1.79.2. +# on 2026-01-04 19:44:56 UTC using RuboCop version 1.79.2. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. -# Offense count: 5 +# Offense count: 2 # This cop supports safe autocorrection (--autocorrect). Lint/RedundantCopDisableDirective: Exclude: - 'app/jobs/update_sparta_state_job.rb' - 'app/services/spl/sparta_loyalty_service.rb' - - 'app/services/spl/validate_card_service.rb' - - 'spec/services/promotion_switcher_service_spec.rb' - - 'spec/services/remove_sparta_discount_service_spec.rb' -# Offense count: 37 +# Offense count: 39 # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. # AllowedMethods: refine Metrics/BlockLength: Max: 254 -# Offense count: 2 +# Offense count: 9 # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. Metrics/MethodLength: Max: 14 @@ -86,15 +83,14 @@ Rails/WhereExists: Exclude: - 'app/services/assign_sparta_card_number_service.rb' -# Offense count: 4 +# Offense count: 3 # This cop supports safe autocorrection (--autocorrect). Style/IfUnlessModifier: Exclude: - 'app/services/assign_sparta_card_number_service.rb' - 'app/services/spl/validate_card_service.rb' - - 'spec/controllers/spl/spree/storefront/profile_controller_decorator_spec.rb' -# Offense count: 1 +# Offense count: 4 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, IgnoreCopDirectives, AllowedPatterns, SplitStrings. # URISchemes: http, https diff --git a/app/services/spl/validate_card_service.rb b/app/services/spl/validate_card_service.rb index 57ad9df..8f5b1d1 100644 --- a/app/services/spl/validate_card_service.rb +++ b/app/services/spl/validate_card_service.rb @@ -54,7 +54,7 @@ def send_request(url, body) Spl::SendRequestService.new(url, body).call end - def body(card_number, date) # rubocop: disable Metrics/MethodLength + def body(card_number, date) date_in_ms = date.to_i * 1000 uuid = SecureRandom.uuid { @@ -72,7 +72,7 @@ def body(card_number, date) # rubocop: disable Metrics/MethodLength end def signature(date, card_number) - data = "#{@store.private_metadata['spl_partner_code']}#{@store.private_metadata['spl_place_code']}#{date}#{card_number}" # rubocop:disable Layout/LineLength + data = "#{@store.private_metadata['spl_partner_code']}#{@store.private_metadata['spl_place_code']}#{date}#{card_number}" Rails.logger.debug data.inspect signature_base = Digest::SHA256.hexdigest(data) Digest::SHA256.hexdigest(signature_base + @store.private_metadata['spl_pos_key']) diff --git a/spec/services/promotion_switcher_service_spec.rb b/spec/services/promotion_switcher_service_spec.rb index 08c7a94..7a5b664 100644 --- a/spec/services/promotion_switcher_service_spec.rb +++ b/spec/services/promotion_switcher_service_spec.rb @@ -99,7 +99,7 @@ sparta_service_double = instance_double(Spl::SpartaLoyaltyService) apply_service_double = instance_double(ApplySpartaDiscountService) - expect(Spl::SpartaLoyaltyService).to receive(:new) do |token, card_no, line_items, date, products, chk, store_arg| # rubocop:disable Layout/LineLength + expect(Spl::SpartaLoyaltyService).to receive(:new) do |token, card_no, line_items, date, products, chk, store_arg| expect(token).to eq(order.token) expect(card_no).to eq('5100179585157') expect(line_items).to match_array(order.line_items) diff --git a/spec/services/remove_sparta_discount_service_spec.rb b/spec/services/remove_sparta_discount_service_spec.rb index beddb33..5cd7822 100644 --- a/spec/services/remove_sparta_discount_service_spec.rb +++ b/spec/services/remove_sparta_discount_service_spec.rb @@ -106,7 +106,7 @@ def self.call(order:, line_item: nil); end let(:relation) { line_item1.adjustments.where(source_type: 'SPL') } - it 'marks eligible SPL adjustments as ineligible & closed, recalculates cart, then destroys all given adjustments' do # rubocop:disable Layout/LineLength + it 'marks eligible SPL adjustments as ineligible & closed, recalculates cart, then destroys all given adjustments' do expect(line_item1.adjustments.where(source_type: 'SPL').count).to eq(2) expect(FakeCartRecalculateService).to receive(:call).with(order: order, line_item: line_item1).twice From 2e2c180bb3c112fd9c28145f7e190f0aa27356a0 Mon Sep 17 00:00:00 2001 From: vi4me Date: Mon, 5 Jan 2026 17:44:00 +0100 Subject: [PATCH 31/63] EK-683 Update after merge and services params --- .rubocop_todo.yml | 34 +--- .../account/profile_controller_decorator.rb | 149 ---------------- .../profile_controller_decorator.rb | 136 +++++++++++++++ app/services/spl/register_account_service.rb | 17 +- app/services/spl/request_otp_service.rb | 8 +- .../profile_controller_decorator_spec.rb | 6 +- .../profile_controller_decorator_spec.rb | 159 +++++++++++++++++- .../spl/register_account_service_spec.rb | 21 ++- spec/services/spl/request_otp_service_spec.rb | 35 ++-- 9 files changed, 354 insertions(+), 211 deletions(-) delete mode 100644 app/controllers/spl/spree/account/profile_controller_decorator.rb diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 72a56b3..9a0fc92 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,30 +1,28 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2025-12-17 20:33:32 UTC using RuboCop version 1.79.2. +# on 2026-01-05 16:41:49 UTC using RuboCop version 1.81.7. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. -# Offense count: 5 +# Offense count: 1 # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. # AllowedMethods: refine Metrics/BlockLength: - Max: 261 + Max: 29 -# Offense count: 2 -# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. -# AllowedMethods: refine -Metrics/BlockLength: - Max: 103 +# Offense count: 1 +# Configuration parameters: CountComments, CountAsOne. +Metrics/ModuleLength: + Max: 154 -# Offense count: 2 +# Offense count: 1 # Configuration parameters: Mode, AllowedMethods, AllowedPatterns, AllowBangMethods, WaywardPredicates. # AllowedMethods: call # WaywardPredicates: nonzero? Naming/PredicateMethod: Exclude: - - 'app/controllers/account_controller_decorator.rb' - 'app/services/assign_sparta_card_number_service.rb' # Offense count: 1 @@ -33,22 +31,6 @@ Rails/ApplicationJob: Exclude: - 'app/jobs/update_sparta_state_job.rb' -# Offense count: 3 -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: NilOrEmpty, NotPresent, UnlessPresent. -Rails/Blank: - Exclude: - - 'app/controllers/account_controller_decorator.rb' - - 'app/controllers/cart_controller_decorator.rb' - -# Offense count: 2 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: numeric, symbolic -Rails/HttpStatus: - Exclude: - - 'app/controllers/account_controller_decorator.rb' - # Offense count: 1 # This cop supports unsafe autocorrection (--autocorrect-all). Rails/NegateInclude: diff --git a/app/controllers/spl/spree/account/profile_controller_decorator.rb b/app/controllers/spl/spree/account/profile_controller_decorator.rb deleted file mode 100644 index cb1e661..0000000 --- a/app/controllers/spl/spree/account/profile_controller_decorator.rb +++ /dev/null @@ -1,149 +0,0 @@ -# frozen_string_literal: true - -module Spl - module Spree - module Account - module ProfileControllerDecorator # rubocop:disable Metrics/ModuleLength - def self.prepended(base) - base.before_action :validate_login_code_request, only: %i[login_code registration_code] - end - - def login_code - send_otp(phone_parser, current_store) - update_user_after_otp_request - render_login_code_success(phone_parser, 'otp_code_form') - rescue Spl::SendOtpService::SplSendOtpError => e - handle_spl_error(e) - render_login_code_error - end - - def connect_loyalty_account - assign_card_number(try_spree_current_user, current_store, params) - redirect_to spree.edit_account_profile_path, - notice: ::Spree.t(:successfully_updated, resource: ::Spree.t(:account)) - rescue Spl::LoginAccountService::SplLoginAccountError, AssignSpartaCardNumberService::AssignSpartaCardNumberError, - Spl::MeService::SplMeError => e - handle_spl_error(e) - render_connect_loyalty_account_error(try_spree_current_user.phone, 'otp_code_form') - end - - def registration_code - request_otp(phone_parser, current_store) - update_user_after_otp_request - render_login_code_success(phone_parser, 'otp_registration_form') - rescue Spl::RequestOtpService::SplRequestOtpError, Spl::OauthTokenService::OauthTokenError => e - handle_spl_error(e) - render_login_code_error - end - - def register_loyalty_account - Spl::RegisterAccountService.new(spree_current_user, current_store, params['user']['spl_auth_code']).call - redirect_to spree.edit_account_profile_path, - notice: ::Spree.t(:successfully_updated, resource: ::Spree.t(:account)) - rescue Spl::RegisterAccountService::SplRegisterAccountError, Spl::OauthTokenService::OauthTokenError => e - handle_spl_error(e) - render_connect_loyalty_account_error(try_spree_current_user.phone, 'otp_registration_form') - end - - private - - def validate_login_code_request - clear_errors - validate_yc_terms - validate_phone - - render_login_code_error if try_spree_current_user.errors.any? - end - - def validate_yc_terms - return if yc_terms_accepted? - - try_spree_current_user.errors.add(:base, I18n.t('spl.user.errors.must_accept_yc_terms')) - end - - def validate_phone - return if phone_parser.valid? - - try_spree_current_user.errors.add(:phone, I18n.t('spl.user.errors.invalid_phone')) - end - - def yc_terms_accepted? - ActiveModel::Type::Boolean.new.cast(login_code_params[:accept_yc_terms]) - end - - def phone_parser - @phone_parser ||= PhoneParserService.new(login_code_params[:phone]) - end - - def clear_errors - try_spree_current_user.errors.clear - end - - def render_login_code_error - render turbo_stream: turbo_stream.replace( - 'loyalty_connect_form', - partial: 'spl/loyalty_connect_form', - locals: { user: try_spree_current_user } - ), - status: :unprocessable_entity - end - - def render_login_code_success(phone, partial) - render turbo_stream: turbo_stream.replace( - 'loyalty_connect_form', - partial: "spl/#{partial}", - locals: { - user: try_spree_current_user, - phone_e164: phone.respond_to?(:e164) ? phone.e164 : nil - } - ), - status: :ok - end - - def render_connect_loyalty_account_error(phone, partial) - render turbo_stream: turbo_stream.replace( - partial, - partial: "spl/#{partial}", - locals: { - user: try_spree_current_user, - phone_e164: phone.respond_to?(:e164) ? phone.e164 : nil - } - ), - status: :unprocessable_entity - end - - def send_otp(phone, store) - Spl::SendOtpService.new(DateTime.current, phone.country_code, phone.national_number, store).call - end - - def request_otp(phone, store) - Spl::RequestOtpService.new(DateTime.current, phone.country_code, phone.national_number, store).call - end - - def update_user_after_otp_request - try_spree_current_user.update!( - phone: login_code_params[:phone], - public_metadata: (try_spree_current_user.public_metadata || {}).merge('accept_yc_terms' => true) - ) - end - - def handle_spl_error(error) - payload = Spl::ErrorPayloadParser.parse(error.message) || error - msg = Spl::ErrorTranslator.translate(payload) - - clear_errors - try_spree_current_user.errors.add(:base, msg) - end - - def assign_card_number(user, store, params) - Spl::LoginAccountService.new(user, store, params).call - AssignSpartaCardNumberService.new(user, store).call - end - - def login_code_params - params.require(:user).permit(:phone, :accept_yc_terms) - end - end - end - end -end diff --git a/app/controllers/spl/spree/storefront/profile_controller_decorator.rb b/app/controllers/spl/spree/storefront/profile_controller_decorator.rb index a8156a4..7340ddf 100644 --- a/app/controllers/spl/spree/storefront/profile_controller_decorator.rb +++ b/app/controllers/spl/spree/storefront/profile_controller_decorator.rb @@ -8,10 +8,146 @@ module ProfileControllerDecorator def self.prepended(base) base.before_action :validate_spl_no_card, only: :update + base.before_action :validate_login_code_request, only: %i[login_code registration_code] + end + + def login_code + send_otp(phone_parser, current_store) + update_user_after_otp_request + render_login_code_success(phone_parser, 'otp_code_form') + rescue Spl::SendOtpService::SplSendOtpError => e + handle_spl_error(e) + render_login_code_error + end + + def connect_loyalty_account + assign_card_number(try_spree_current_user, current_store, params) + redirect_to spree.edit_account_profile_path, + notice: ::Spree.t(:successfully_updated, resource: ::Spree.t(:account)) + rescue Spl::LoginAccountService::SplLoginAccountError, AssignSpartaCardNumberService::AssignSpartaCardNumberError, + Spl::MeService::SplMeError => e + handle_spl_error(e) + render_connect_loyalty_account_error(try_spree_current_user.phone, 'otp_code_form') + end + + def registration_code + request_otp(phone_parser, current_store, params['user']) + update_user_after_otp_request + render_login_code_success(phone_parser, 'otp_registration_form') + rescue Spl::RequestOtpService::SplRequestOtpError, Spl::OauthTokenService::OauthTokenError => e + handle_spl_error(e) + render_login_code_error + end + + def register_loyalty_account + Spl::RegisterAccountService.new(spree_current_user, current_store, params['user']['spl_auth_code']).call + redirect_to spree.edit_account_profile_path, + notice: ::Spree.t(:successfully_updated, resource: ::Spree.t(:account)) + rescue Spl::RegisterAccountService::SplRegisterAccountError, Spl::OauthTokenService::OauthTokenError => e + handle_spl_error(e) + render_connect_loyalty_account_error(try_spree_current_user.phone, 'otp_registration_form') end private + def validate_login_code_request + clear_errors + validate_yc_terms + validate_phone + + render_login_code_error if try_spree_current_user.errors.any? + end + + def validate_yc_terms + return if yc_terms_accepted? + + try_spree_current_user.errors.add(:base, I18n.t('spl.user.errors.must_accept_yc_terms')) + end + + def validate_phone + return if phone_parser.valid? + + try_spree_current_user.errors.add(:phone, I18n.t('spl.user.errors.invalid_phone')) + end + + def yc_terms_accepted? + ActiveModel::Type::Boolean.new.cast(login_code_params[:accept_yc_terms]) + end + + def phone_parser + @phone_parser ||= PhoneParserService.new(login_code_params[:phone]) + end + + def clear_errors + try_spree_current_user.errors.clear + end + + def render_login_code_error + render turbo_stream: turbo_stream.replace( + 'loyalty_connect_form', + partial: 'spl/loyalty_connect_form', + locals: { user: try_spree_current_user } + ), + status: :unprocessable_content + end + + def render_login_code_success(phone, partial) + render turbo_stream: turbo_stream.replace( + 'loyalty_connect_form', + partial: "spl/#{partial}", + locals: { + user: try_spree_current_user, + phone_e164: phone.respond_to?(:e164) ? phone.e164 : nil + } + ), + status: :ok + end + + def render_connect_loyalty_account_error(phone, partial) + render turbo_stream: turbo_stream.replace( + partial, + partial: "spl/#{partial}", + locals: { + user: try_spree_current_user, + phone_e164: phone.respond_to?(:e164) ? phone.e164 : nil + } + ), + status: :unprocessable_content + end + + def send_otp(phone, store) + Spl::SendOtpService.new(DateTime.current, phone.country_code, phone.national_number, store).call + end + + def request_otp(phone, store, params) + params.merge!(mobile_country: phone.country_code, phone_number: phone.national_number) + Spl::RequestOtpService.new(DateTime.current, store, params).call + end + + def update_user_after_otp_request + try_spree_current_user.update!( + phone: login_code_params[:phone], + public_metadata: (try_spree_current_user.public_metadata || {}).merge('accept_yc_terms' => true) + ) + end + + def handle_spl_error(error) + payload = Spl::ErrorPayloadParser.parse(error.message) || error + msg = Spl::ErrorTranslator.translate(payload) + + clear_errors + try_spree_current_user.errors.add(:base, msg) + end + + def assign_card_number(user, store, params) + Spl::LoginAccountService.new(user, store, params).call + AssignSpartaCardNumberService.new(user, store).call + end + + def login_code_params + params.require(:user).permit(:phone, :accept_yc_terms) + end + def user_params params.require(:user).permit(:first_name, :last_name, :phone, :email, public_metadata: %i[spl_card_active spl_no_card]) diff --git a/app/services/spl/register_account_service.rb b/app/services/spl/register_account_service.rb index f4ab4fa..02f6c73 100644 --- a/app/services/spl/register_account_service.rb +++ b/app/services/spl/register_account_service.rb @@ -6,11 +6,11 @@ module Spl class RegisterAccountService class SplRegisterAccountError < StandardError; end - def initialize(user, store, spl_auth_code) + def initialize(user, store, params) @register_url = URI.parse(Spl::UrlCreatorService.new(store.private_metadata['spl_url']).register) @user = user @store = store - @spl_auth_code = spl_auth_code + @params = params @phone = PhoneParserService.new(user.phone) end @@ -25,7 +25,7 @@ def call raise SplRegisterAccountError, register_response_body['msg'] if register_response_body['errorCode'] != '0' spl_card = register_response_body.dig('response', 'cardNo') - update_account(spl_card) + update_account(spl_card, oauth_response_body) end private @@ -52,17 +52,22 @@ def prepare_registration_body(access_token) # rubocop:disable Metrics/MethodLeng operationalSms: accept_yc_terms } }, - authCode: @spl_auth_code, + authCode: @params, partnerCode: @store.private_metadata['spl_partner_code'], placeCode: @store.private_metadata['spl_place_code'] } end - def update_account(card_number) + def update_account(card_number, oauth_response_body) + @user.private_metadata ||= {} if @user.private_metadata.blank? @user.update(public_metadata: @user.public_metadata.merge(spl_no_card: card_number, spl_card_active: true, mobile_country: @phone.country_code, - phone_number: @phone.national_number)) + phone_number: @phone.national_number), + private_metadata: @user.private_metadata.merge( + accessToken: oauth_response_body.dig('response', 'accessToken'), + refreshToken: oauth_response_body.dig('response', 'refreshToken') + )) end end end diff --git a/app/services/spl/request_otp_service.rb b/app/services/spl/request_otp_service.rb index 555f705..a210b8c 100644 --- a/app/services/spl/request_otp_service.rb +++ b/app/services/spl/request_otp_service.rb @@ -6,13 +6,13 @@ module Spl class RequestOtpService class SplRequestOtpError < StandardError; end - def initialize(date, mobile_country, phone_number, store) + def initialize(date, store, params) @date = date.to_i * 1000 @store = store @request_otp_url = URI.parse(Spl::UrlCreatorService.new(store.private_metadata['spl_url']).request_otp) - @mobile_country = mobile_country - @phone_number = phone_number - @email = nil + @mobile_country = params[:mobile_country] + @phone_number = params[:phone_number] + @email = params[:email] end def call diff --git a/spec/controllers/profile_controller_decorator_spec.rb b/spec/controllers/profile_controller_decorator_spec.rb index 82aee92..f90b564 100644 --- a/spec/controllers/profile_controller_decorator_spec.rb +++ b/spec/controllers/profile_controller_decorator_spec.rb @@ -33,7 +33,7 @@ end context 'when spl_no_card exists and card is active' do - it 'calls ValidateCardService with correct parameters' do + xit 'calls ValidateCardService with correct parameters' do put :update, params: { user: { public_metadata: { spl_no_card: '1234567890123', spl_card_active: true } } } expect(service_double).to have_received(:call) @@ -46,14 +46,14 @@ allow(service_double).to receive(:call).and_raise(Spl::ValidateCardService::SplCardValidationError.new('Invalid')) end - it 'renders edit with status 422' do + xit 'renders edit with status 422' do put :update, params: { user: { public_metadata: { spl_no_card: '1234567890123', spl_card_active: 'true' } } } expect(response).to have_http_status(:unprocessable_content) expect(response).to render_template(:edit) end - it 'sets flash error message' do + xit 'sets flash error message' do put :update, params: { user: { public_metadata: { spl_no_card: '1234567890123', spl_card_active: 'true' } } } expect(flash[:error]).to eq('Invalid') diff --git a/spec/controllers/spl/spree/account/profile_controller_decorator_spec.rb b/spec/controllers/spl/spree/account/profile_controller_decorator_spec.rb index 5aeaf0a..78abc7e 100644 --- a/spec/controllers/spl/spree/account/profile_controller_decorator_spec.rb +++ b/spec/controllers/spl/spree/account/profile_controller_decorator_spec.rb @@ -42,10 +42,156 @@ user.errors.clear end + describe '#validate_login_code_request (before_action logic)' do + context 'when YC terms are not accepted' do + let(:terms_accepted) { 'false' } + + xit 'adds error and renders 422 (does not touch OTP service)' do + expect(Spl::SendOtpService).not_to receive(:new) + expect(controller).to receive(:render).with(hash_including(status: :unprocessable_entity)) + controller.send(:validate_login_code_request) + expect(user.errors.full_messages.join(' ')).to include(I18n.t('spl.user.errors.must_accept_yc_terms')) + end + end + + context 'when phone is invalid' do + let(:phone_valid) { false } + + xit 'adds phone error and renders 422 (does not touch OTP service)' do + expect(Spl::SendOtpService).not_to receive(:new) + expect(controller).to receive(:render).with(hash_including(status: :unprocessable_entity)) + controller.send(:validate_login_code_request) + expect(user.errors.full_messages.join(' ')).to include(I18n.t('spl.user.errors.invalid_phone')) + end + end + + context 'when everything is valid' do + it 'does not render and does not add errors' do + expect(controller).not_to receive(:render) + controller.send(:validate_login_code_request) + expect(user.errors).to be_empty + end + end + end + + describe '#login_code (service + success/rescue)' do + context 'when OTP service raises SplSendOtpError' do + xit 'renders 422 and shows translated message as base error' do + service_instance = instance_double('Spl::SendOtpService') + + payload = { + 'errorCode' => 'TEMPORARY_BLOCKED', + 'validationMessages' => nil, + 'fieldValidationMessages' => nil, + 'response' => nil, + 'msg' => 'Temporarily blocked (too much attempts)' + } + + expect(Spl::SendOtpService).to receive(:new) + .with(kind_of(DateTime), '+48', '500600700', store) + .and_return(service_instance) + + expect(service_instance).to receive(:call).and_raise(Spl::SendOtpService::SplSendOtpError.new(payload.inspect)) + + expect(controller).to receive(:render).with(hash_including(status: :unprocessable_entity)) + + controller.login_code + + expect(user.errors.full_messages.join(' ')) + .to include(I18n.t('spl.errors.temporary_blocked')) + end + end + + context 'when OTP is sent successfully' do + it 'calls OTP service, updates accept_yc_terms in public_metadata, and renders 200' do + service_instance = instance_double('Spl::SendOtpService') + + expect(Spl::SendOtpService).to receive(:new) + .with(kind_of(DateTime), '+48', '500600700', store) + .and_return(service_instance) + expect(service_instance).to receive(:call) + expect(controller).to receive(:render).with(hash_including(status: :ok)) + controller.login_code + expect(user.reload.public_metadata['accept_yc_terms']).to eq(true) + end + end + end + + describe '#connect_loyalty_account (service + success/rescue)' do + let(:service_params) { { user: { spl_auth_code: '123456' } } } + + before do + allow(controller).to receive(:params).and_return(service_params) + end + + context 'when account is connected successfully' do + xit 'calls LoginAccountService and AssignSpartaCardNumberService, then redirects with notice' do + login_service = instance_double(Spl::LoginAccountService) + assign_service = instance_double(AssignSpartaCardNumberService) + + expect(Spl::LoginAccountService).to receive(:new) + .with(user, store, service_params) + .and_return(login_service) + expect(login_service).to receive(:call) + + expect(AssignSpartaCardNumberService).to receive(:new) + .with(user, store) + .and_return(assign_service) + expect(assign_service).to receive(:call) + + expect(controller).to receive(:redirect_to).with( + spree.edit_account_profile_path, + hash_including(notice: Spree.t(:successfully_updated, resource: Spree.t(:account))) + ) + + controller.connect_loyalty_account + end + end + + shared_examples 'renders connect error' do |error_class| + xit "renders 422 and adds translated base error for #{error_class}" do + payload = { + 'errorCode' => 'TEMPORARY_BLOCKED', + 'validationMessages' => nil, + 'fieldValidationMessages' => nil, + 'response' => nil, + 'msg' => 'Temporarily blocked (too much attempts)' + } + + phone_obj = double('Phone', e164: '+48500600700') + allow(user).to receive(:phone).and_return(phone_obj) + + login_service = instance_double(Spl::LoginAccountService) + allow(Spl::LoginAccountService).to receive(:new).and_return(login_service) + allow(login_service).to receive(:call) + .and_raise(error_class.new(payload.inspect)) + + expect(controller).to receive(:render).with(hash_including(status: :unprocessable_entity)) + + controller.connect_loyalty_account + + expect(user.errors.full_messages.join(' ')) + .to include(I18n.t('spl.errors.temporary_blocked')) + end + end + + context 'when LoginAccountService raises' do + include_examples 'renders connect error', Spl::LoginAccountService::SplLoginAccountError + end + + context 'when AssignSpartaCardNumberService raises' do + include_examples 'renders connect error', AssignSpartaCardNumberService::AssignSpartaCardNumberError + end + + context 'when MeService raises' do + include_examples 'renders connect error', Spl::MeService::SplMeError + end + end + describe '#registration_code (service + success/rescue)' do before do - allow(controller).to receive(:login_code_params).and_return( - ActionController::Parameters.new(phone: phone_value, accept_yc_terms: terms_accepted).permit! + allow(controller).to receive(:params).and_return( + ActionController::Parameters.new(user: { phone: phone_value, accept_yc_terms: terms_accepted }) ) end @@ -53,9 +199,12 @@ xit 'calls RequestOtpService, updates user, and renders 200' do service_instance = instance_double(Spl::RequestOtpService) - expect(Spl::RequestOtpService).to receive(:new) - .with(kind_of(DateTime), '+48', '500600700', store) - .and_return(service_instance) + expect(Spl::RequestOtpService).to receive(:new) do |date, passed_store, passed_params| + expect(date).to be_a(DateTime) + expect(passed_store).to eq(store) + expect(passed_params['mobile_country']).to eq('+48') + expect(passed_params['phone_number']).to eq('500600700') + end.and_return(service_instance) expect(service_instance).to receive(:call) expect(controller).to receive(:render).with(hash_including(status: :ok)) diff --git a/spec/services/spl/register_account_service_spec.rb b/spec/services/spl/register_account_service_spec.rb index 5473efa..12f9c70 100644 --- a/spec/services/spl/register_account_service_spec.rb +++ b/spec/services/spl/register_account_service_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' RSpec.describe Spl::RegisterAccountService do - subject(:service) { described_class.new(user, store, spl_auth_code) } + subject(:service) { described_class.new(user, store, params) } let(:store) do create( @@ -25,18 +25,20 @@ first_name: 'John', last_name: 'Doe', email: 'john@example.com', - public_metadata: { 'accept_yc_terms' => true } + public_metadata: { 'accept_yc_terms' => true }, + private_metadata: {} ) end - let(:spl_auth_code) { '123456' } + let(:params) { '123456' } let(:phone_parser) { instance_double(PhoneParserService, country_code: '+48', national_number: '500600700') } - let(:oauth_response) { { 'response' => { 'accessToken' => 'ACCESS_TOKEN' } } } + let(:oauth_response) { { 'response' => { 'accessToken' => 'ACCESS_TOKEN', 'refreshToken' => 'REFRESH_TOKEN' } } } let(:register_response_body) { { 'errorCode' => '0', 'response' => { 'cardNo' => 'CARD123' } } } let(:http_response) { instance_double(Net::HTTPResponse, body: register_response_body.to_json) } let(:send_request_service) { instance_double(Spl::SendRequestService, call: http_response) } before do + allow(PhoneParserService).to receive(:new).and_return(phone_parser) allow(Spl::OauthTokenService).to receive(:new).and_return( instance_double(Spl::OauthTokenService, annonymus_token: oauth_response) ) @@ -56,10 +58,16 @@ ) end + xit 'updates user private_metadata with tokens' do + service.call + + expect(user.private_metadata).to include('accessToken' => 'ACCESS_TOKEN', 'refreshToken' => 'REFRESH_TOKEN') + end + xit 'sends correct payload to SendRequestService' do expect(Spl::SendRequestService).to receive(:new) do |url, body| expect(url.to_s).to include('register') - expect(body).to include(authCode: spl_auth_code, partnerCode: 'PARTNER', placeCode: 'PLACE') + expect(body).to include(authCode: params, partnerCode: 'PARTNER', placeCode: 'PLACE') expect(body[:context]).to include(oauthToken: 'ACCESS_TOKEN', prgCode: 'PRG') expect(body[:person][:permissions]).to eq(processData: true, operationalSms: true) expect(body[:person]).to include( @@ -69,6 +77,7 @@ mobileCountry: '+48', mobile: '500600700' ) + expect(body[:person][:permissions]).to eq(processData: true, operationalSms: true) end.and_return(send_request_service) service.call @@ -94,7 +103,7 @@ end context 'when OTP code is not 6 characters' do - let(:spl_auth_code) { '123' } + let(:params) { '123' } xit 'still sends request to SPL (no local validation)' do expect(Spl::SendRequestService).to receive(:new).and_return(send_request_service) diff --git a/spec/services/spl/request_otp_service_spec.rb b/spec/services/spl/request_otp_service_spec.rb index ef6d38d..da6b48e 100644 --- a/spec/services/spl/request_otp_service_spec.rb +++ b/spec/services/spl/request_otp_service_spec.rb @@ -3,24 +3,22 @@ require 'rails_helper' RSpec.describe Spl::RequestOtpService do - subject(:service) { described_class.new(date, mobile_country, phone_number, store) } + subject(:service) { described_class.new(date, store, params) } let(:store) do - create(:store, - default_country: create(:country), - private_metadata: { + create(:store, default_country: create(:country), private_metadata: { 'spl_url' => 'https://spl.test', 'spl_prg_code' => 'PRG' }) end let(:date) { DateTime.current } - let(:mobile_country) { '+48' } - let(:phone_number) { '500600700' } let(:oauth_response) { { 'response' => { 'accessToken' => 'ACCESS_TOKEN' } } } let(:success_response_body) { { 'errorCode' => '0', 'response' => { 'status' => 'OK' } } } let(:error_response_body) { { 'errorCode' => 'TEMPORARY_BLOCKED', 'msg' => 'Temporarily blocked' } } + let(:response_body) { success_response_body } let(:http_response) { instance_double(Net::HTTPResponse, body: response_body.to_json) } + let(:send_request_service) { instance_double(Spl::SendRequestService, call: http_response) } before do @@ -31,13 +29,11 @@ end describe '#call' do - context 'when OTP request succeeds (SMS)' do - let(:response_body) { success_response_body } + context 'when requesting OTP via SMS' do + let(:params) { { mobile_country: '+48', phone_number: '500600700' } } it 'returns parsed response body' do - result = service.call - - expect(result).to eq(success_response_body) + expect(service.call).to eq(success_response_body) end it 'sends correct SMS payload to SendRequestService' do @@ -51,11 +47,26 @@ end end + context 'when requesting OTP via email' do + let(:params) { { email: 'test@example.com' } } + + it 'sends email OTP payload' do + expect(Spl::SendRequestService).to receive(:new) do |_url, body| + expect(body).to include(channel: 'E', email: 'test@example.com') + end.and_return(send_request_service) + + service.call + end + end + context 'when SPL returns errorCode != 0' do + let(:params) { { mobile_country: '+48', phone_number: '500600700' } } let(:response_body) { error_response_body } it 'raises SplRequestOtpError with SPL message' do - expect { service.call }.to raise_error(Spl::RequestOtpService::SplRequestOtpError, 'Temporarily blocked') + expect do + service.call + end.to raise_error(Spl::RequestOtpService::SplRequestOtpError, 'Temporarily blocked') end end end From 240cf5db8758a6e16a208baf2e9b576f62838a44 Mon Sep 17 00:00:00 2001 From: vi4me Date: Wed, 7 Jan 2026 14:01:42 +0100 Subject: [PATCH 32/63] EK-683 Remove phone from user public_metadata --- app/services/spl/register_account_service.rb | 5 +---- spec/services/spl/register_account_service_spec.rb | 4 +--- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/app/services/spl/register_account_service.rb b/app/services/spl/register_account_service.rb index 02f6c73..a236d13 100644 --- a/app/services/spl/register_account_service.rb +++ b/app/services/spl/register_account_service.rb @@ -60,10 +60,7 @@ def prepare_registration_body(access_token) # rubocop:disable Metrics/MethodLeng def update_account(card_number, oauth_response_body) @user.private_metadata ||= {} if @user.private_metadata.blank? - @user.update(public_metadata: @user.public_metadata.merge(spl_no_card: card_number, - spl_card_active: true, - mobile_country: @phone.country_code, - phone_number: @phone.national_number), + @user.update(public_metadata: @user.public_metadata.merge(spl_no_card: card_number, spl_card_active: true), private_metadata: @user.private_metadata.merge( accessToken: oauth_response_body.dig('response', 'accessToken'), refreshToken: oauth_response_body.dig('response', 'refreshToken') diff --git a/spec/services/spl/register_account_service_spec.rb b/spec/services/spl/register_account_service_spec.rb index 12f9c70..c4757e1 100644 --- a/spec/services/spl/register_account_service_spec.rb +++ b/spec/services/spl/register_account_service_spec.rb @@ -52,9 +52,7 @@ expect(user.public_metadata).to include( 'spl_no_card' => 'CARD123', - 'spl_card_active' => true, - 'mobile_country' => '+48', - 'phone_number' => '500600700' + 'spl_card_active' => true ) end From f798f683e13fc663c4e286446203d919ede17ad0 Mon Sep 17 00:00:00 2001 From: nika-piotrowska Date: Wed, 7 Jan 2026 14:09:29 +0100 Subject: [PATCH 33/63] Fix rubocop offenses --- .rubocop_todo.yml | 22 ++++++------------- .../spree/api/cart_controller_decorator.rb | 2 +- .../profile_controller_decorator.rb | 4 ++-- app/models/order_updater_decorator.rb | 2 +- 4 files changed, 11 insertions(+), 19 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 6136bcd..4a2c61e 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2026-01-07 12:56:25 UTC using RuboCop version 1.82.1. +# on 2026-01-07 13:09:12 UTC using RuboCop version 1.82.1. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new @@ -19,11 +19,16 @@ Lint/RedundantCopDisableDirective: Metrics/BlockLength: Max: 29 -# Offense count: 7 +# Offense count: 8 # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. Metrics/MethodLength: Max: 14 +# Offense count: 1 +# Configuration parameters: CountComments, CountAsOne. +Metrics/ModuleLength: + Max: 134 + # Offense count: 2 # Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames. # AllowedNames: as, at, by, cc, db, id, if, in, io, ip, of, on, os, pp, to @@ -37,19 +42,6 @@ Rails/ApplicationJob: Exclude: - 'app/jobs/update_sparta_state_job.rb' -# Offense count: 1 -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: NilOrEmpty, NotPresent, UnlessPresent. -Rails/Blank: - Exclude: - - 'app/controllers/cart_controller_decorator.rb' - -# Offense count: 2 -# This cop supports safe autocorrection (--autocorrect). -Rails/HttpStatusNameConsistency: - Exclude: - - 'app/controllers/spl/spree/account/profile_controller_decorator.rb' - # Offense count: 1 # This cop supports unsafe autocorrection (--autocorrect-all). Rails/NegateInclude: diff --git a/app/controllers/spl/spree/api/cart_controller_decorator.rb b/app/controllers/spl/spree/api/cart_controller_decorator.rb index d7c931c..77d223a 100644 --- a/app/controllers/spl/spree/api/cart_controller_decorator.rb +++ b/app/controllers/spl/spree/api/cart_controller_decorator.rb @@ -18,7 +18,7 @@ def show super end - def update_spl_card_activate # rubocop:disable Metrics/AbcSize,Metrics/MethodLength + def update_spl_card_activate # rubocop:disable Metrics/AbcSize spree_authorize! :update, spree_current_order, order_token if params.dig('public_metadata', 'spl_card_active').nil? diff --git a/app/controllers/spl/spree/storefront/profile_controller_decorator.rb b/app/controllers/spl/spree/storefront/profile_controller_decorator.rb index f5e5a38..7a72e85 100644 --- a/app/controllers/spl/spree/storefront/profile_controller_decorator.rb +++ b/app/controllers/spl/spree/storefront/profile_controller_decorator.rb @@ -115,7 +115,7 @@ def render_login_code_error partial: 'spl/loyalty_connect_form', locals: { user: try_spree_current_user } ), - status: :unprocessable_entity + status: :unprocessable_content end def render_login_code_success(phone) @@ -139,7 +139,7 @@ def render_connect_loyalty_account_error(phone) phone_e164: phone.respond_to?(:e164) ? phone.e164 : nil } ), - status: :unprocessable_entity + status: :unprocessable_content end def send_otp(phone, store) diff --git a/app/models/order_updater_decorator.rb b/app/models/order_updater_decorator.rb index cbe6fd4..0547058 100644 --- a/app/models/order_updater_decorator.rb +++ b/app/models/order_updater_decorator.rb @@ -8,7 +8,7 @@ def preform_update_sparta_state_job # rubocop:disable Metrics/AbcSize UpdateSpartaStateJob.perform_later(order.token, 'C', order.number, order.store) if order.state == 'canceled' end - def check_spl_adjustments # rubocop:disable Metrics/MethodLength + def check_spl_adjustments if order.public_metadata['spl_card_active'] == true updated_any_adjustment = false order.adjustments.each do |adjustment| From 7fc0e8dab1396164df697e602c309a21ce4608b8 Mon Sep 17 00:00:00 2001 From: vi4me Date: Wed, 7 Jan 2026 17:06:31 +0100 Subject: [PATCH 34/63] EK-683 Remove ProfileControllerDecorator methods and xits --- .rubocop_todo.yml | 4 +- .../profile_controller_decorator.rb | 167 ++++-------------- .../profile_controller_decorator_spec.rb | 6 +- .../profile_controller_decorator_spec.rb | 20 +-- .../spl/register_account_service_spec.rb | 16 +- 5 files changed, 60 insertions(+), 153 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 4a2c61e..87f0671 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2026-01-07 13:09:12 UTC using RuboCop version 1.82.1. +# on 2026-01-07 15:20:46 UTC using RuboCop version 1.82.1. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new @@ -27,7 +27,7 @@ Metrics/MethodLength: # Offense count: 1 # Configuration parameters: CountComments, CountAsOne. Metrics/ModuleLength: - Max: 134 + Max: 154 # Offense count: 2 # Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames. diff --git a/app/controllers/spl/spree/storefront/profile_controller_decorator.rb b/app/controllers/spl/spree/storefront/profile_controller_decorator.rb index e01dcdb..0afb430 100644 --- a/app/controllers/spl/spree/storefront/profile_controller_decorator.rb +++ b/app/controllers/spl/spree/storefront/profile_controller_decorator.rb @@ -50,6 +50,16 @@ def register_loyalty_account private + def validate_spl_no_card + metadata = user_params[:public_metadata] + return unless metadata&.[](:spl_no_card) + + update_order(metadata[:spl_no_card], metadata[:spl_card_active]) + validate_card(metadata) + rescue ::Spl::ValidateCardService::SplCardValidationError => e + handle_validation_error(e) + end + def validate_login_code_request clear_errors validate_yc_terms @@ -58,63 +68,6 @@ def validate_login_code_request render_login_code_error if try_spree_current_user.errors.any? end - def validate_yc_terms - return if yc_terms_accepted? - - try_spree_current_user.errors.add(:base, I18n.t('spl.user.errors.must_accept_yc_terms')) - end - - def validate_phone - return if phone_parser.valid? - - try_spree_current_user.errors.add(:phone, I18n.t('spl.user.errors.invalid_phone')) - end - - def yc_terms_accepted? - ActiveModel::Type::Boolean.new.cast(login_code_params[:accept_yc_terms]) - end - - def phone_parser - @phone_parser ||= PhoneParserService.new(login_code_params[:phone]) - end - - def clear_errors - try_spree_current_user.errors.clear - end - - def render_login_code_error - render turbo_stream: turbo_stream.replace( - 'loyalty_connect_form', - partial: 'spl/loyalty_connect_form', - locals: { user: try_spree_current_user } - ), - status: :unprocessable_content - end - - def render_login_code_success(phone, partial) - render turbo_stream: turbo_stream.replace( - 'loyalty_connect_form', - partial: "spl/#{partial}", - locals: { - user: try_spree_current_user, - phone_e164: phone.respond_to?(:e164) ? phone.e164 : nil - } - ), - status: :ok - end - - def render_connect_loyalty_account_error(phone, partial) - render turbo_stream: turbo_stream.replace( - partial, - partial: "spl/#{partial}", - locals: { - user: try_spree_current_user, - phone_e164: phone.respond_to?(:e164) ? phone.e164 : nil - } - ), - status: :unprocessable_content - end - def send_otp(phone, store) Spl::SendOtpService.new(DateTime.current, phone.country_code, phone.national_number, store).call end @@ -124,45 +77,6 @@ def request_otp(phone, store, params) Spl::RequestOtpService.new(DateTime.current, store, params).call end - def update_user_after_otp_request - try_spree_current_user.update!( - phone: login_code_params[:phone], - public_metadata: (try_spree_current_user.public_metadata || {}).merge('accept_yc_terms' => true) - ) - end - - def handle_spl_error(error) - payload = Spl::ErrorPayloadParser.parse(error.message) || error - msg = Spl::ErrorTranslator.translate(payload) - - clear_errors - try_spree_current_user.errors.add(:base, msg) - end - - def assign_card_number(user, store, params) - Spl::LoginAccountService.new(user, store, params).call - AssignSpartaCardNumberService.new(user, store).call - end - - def login_code_params - params.require(:user).permit(:phone, :accept_yc_terms) - end - - def user_params - params.require(:user).permit(:first_name, :last_name, :phone, :email, - public_metadata: %i[spl_card_active spl_no_card]) - end - - def validate_spl_no_card - metadata = user_params[:public_metadata] - return unless metadata&.[](:spl_no_card) - - update_order(metadata[:spl_no_card], metadata[:spl_card_active]) - validate_card(metadata) - rescue ::Spl::ValidateCardService::SplCardValidationError => e - handle_validation_error(e) - end - def validate_card(metadata) return unless disactivated_card? @@ -193,14 +107,6 @@ def disactivated_card? value.present? && cast_boolean(value) end - def validate_login_code_request - clear_errors - validate_yc_terms - validate_phone - - render_login_code_error if try_spree_current_user.errors.any? - end - def validate_yc_terms return if yc_terms_accepted? @@ -225,19 +131,22 @@ def clear_errors try_spree_current_user.errors.clear end - def render_login_code_error - render turbo_stream: turbo_stream.replace( - 'loyalty_connect_form', - partial: 'spl/loyalty_connect_form', - locals: { user: try_spree_current_user } - ), - status: :unprocessable_content + def update_user_after_otp_request + try_spree_current_user.update!( + phone: login_code_params[:phone], + public_metadata: (try_spree_current_user.public_metadata || {}).merge('accept_yc_terms' => true) + ) + end + + def assign_card_number(user, store, params) + Spl::LoginAccountService.new(user, store, params).call + AssignSpartaCardNumberService.new(user, store).call end - def render_login_code_success(phone) + def render_login_code_success(phone, partial) render turbo_stream: turbo_stream.replace( 'loyalty_connect_form', - partial: 'spl/otp_code_form', + partial: "spl/#{partial}", locals: { user: try_spree_current_user, phone_e164: phone.respond_to?(:e164) ? phone.e164 : nil @@ -246,10 +155,10 @@ def render_login_code_success(phone) status: :ok end - def render_connect_loyalty_account_error(phone) + def render_connect_loyalty_account_error(phone, partial) render turbo_stream: turbo_stream.replace( - 'otp_code_form', - partial: 'spl/otp_code_form', + partial, + partial: "spl/#{partial}", locals: { user: try_spree_current_user, phone_e164: phone.respond_to?(:e164) ? phone.e164 : nil @@ -258,17 +167,6 @@ def render_connect_loyalty_account_error(phone) status: :unprocessable_content end - def send_otp(phone, store) - Spl::SendOtpService.new(DateTime.current, phone.country_code, phone.national_number, store).call - end - - def update_user_after_otp_request - try_spree_current_user.update!( - phone: login_code_params[:phone], - public_metadata: (try_spree_current_user.public_metadata || {}).merge('accept_yc_terms' => true) - ) - end - def handle_spl_error(error) payload = Spl::ErrorPayloadParser.parse(error.message) || error msg = Spl::ErrorTranslator.translate(payload) @@ -277,14 +175,23 @@ def handle_spl_error(error) try_spree_current_user.errors.add(:base, msg) end - def assign_card_number(user, store, params) - Spl::LoginAccountService.new(user, store, params).call - AssignSpartaCardNumberService.new(user, store).call + def render_login_code_error + render turbo_stream: turbo_stream.replace( + 'loyalty_connect_form', + partial: 'spl/loyalty_connect_form', + locals: { user: try_spree_current_user } + ), + status: :unprocessable_content end def login_code_params params.require(:user).permit(:phone, :accept_yc_terms) end + + def user_params + params.require(:user).permit(:first_name, :last_name, :phone, :email, + public_metadata: %i[spl_card_active spl_no_card]) + end end end end diff --git a/spec/controllers/profile_controller_decorator_spec.rb b/spec/controllers/profile_controller_decorator_spec.rb index f90b564..82aee92 100644 --- a/spec/controllers/profile_controller_decorator_spec.rb +++ b/spec/controllers/profile_controller_decorator_spec.rb @@ -33,7 +33,7 @@ end context 'when spl_no_card exists and card is active' do - xit 'calls ValidateCardService with correct parameters' do + it 'calls ValidateCardService with correct parameters' do put :update, params: { user: { public_metadata: { spl_no_card: '1234567890123', spl_card_active: true } } } expect(service_double).to have_received(:call) @@ -46,14 +46,14 @@ allow(service_double).to receive(:call).and_raise(Spl::ValidateCardService::SplCardValidationError.new('Invalid')) end - xit 'renders edit with status 422' do + it 'renders edit with status 422' do put :update, params: { user: { public_metadata: { spl_no_card: '1234567890123', spl_card_active: 'true' } } } expect(response).to have_http_status(:unprocessable_content) expect(response).to render_template(:edit) end - xit 'sets flash error message' do + it 'sets flash error message' do put :update, params: { user: { public_metadata: { spl_no_card: '1234567890123', spl_card_active: 'true' } } } expect(flash[:error]).to eq('Invalid') diff --git a/spec/controllers/spl/spree/account/profile_controller_decorator_spec.rb b/spec/controllers/spl/spree/account/profile_controller_decorator_spec.rb index 1721f12..51b8d69 100644 --- a/spec/controllers/spl/spree/account/profile_controller_decorator_spec.rb +++ b/spec/controllers/spl/spree/account/profile_controller_decorator_spec.rb @@ -46,7 +46,7 @@ it 'adds error and renders 422 (does not touch OTP service)' do expect(Spl::SendOtpService).not_to receive(:new) - expect(controller).to receive(:render).with(hash_including(status: :unprocessable_entity)) + expect(controller).to receive(:render).with(hash_including(status: :unprocessable_content)) controller.send(:validate_login_code_request) expect(user.errors.full_messages.join(' ')).to include(I18n.t('spl.user.errors.must_accept_yc_terms')) end @@ -57,7 +57,7 @@ it 'adds phone error and renders 422 (does not touch OTP service)' do expect(Spl::SendOtpService).not_to receive(:new) - expect(controller).to receive(:render).with(hash_including(status: :unprocessable_entity)) + expect(controller).to receive(:render).with(hash_including(status: :unprocessable_content)) controller.send(:validate_login_code_request) expect(user.errors.full_messages.join(' ')).to include(I18n.t('spl.user.errors.invalid_phone')) end @@ -91,7 +91,7 @@ expect(service_instance).to receive(:call).and_raise(Spl::SendOtpService::SplSendOtpError.new(payload.inspect)) - expect(controller).to receive(:render).with(hash_including(status: :unprocessable_entity)) + expect(controller).to receive(:render).with(hash_including(status: :unprocessable_content)) controller.login_code @@ -164,7 +164,7 @@ allow(login_service).to receive(:call) .and_raise(error_class.new(payload.inspect)) - expect(controller).to receive(:render).with(hash_including(status: :unprocessable_entity)) + expect(controller).to receive(:render).with(hash_including(status: :unprocessable_content)) controller.connect_loyalty_account @@ -194,7 +194,7 @@ end context 'when OTP request succeeds' do - xit 'calls RequestOtpService, updates user, and renders 200' do + it 'calls RequestOtpService, updates user, and renders 200' do service_instance = instance_double(Spl::RequestOtpService) expect(Spl::RequestOtpService).to receive(:new) do |date, passed_store, passed_params| @@ -215,7 +215,7 @@ end shared_examples 'registration_code error' do |error_class| - xit "renders 422 and adds translated base error for #{error_class}" do + it "renders 422 and adds translated base error for #{error_class}" do payload = { 'errorCode' => 'TEMPORARY_BLOCKED', 'msg' => 'Temporarily blocked' @@ -226,7 +226,7 @@ allow(Spl::RequestOtpService).to receive(:new).and_return(service_instance) allow(service_instance).to receive(:call).and_raise(error_class.new(payload.inspect)) - expect(controller).to receive(:render).with(hash_including(status: :unprocessable_entity)) + expect(controller).to receive(:render).with(hash_including(status: :unprocessable_content)) controller.registration_code @@ -252,7 +252,7 @@ end context 'when registration succeeds' do - xit 'calls RegisterAccountService and redirects with notice' do + it 'calls RegisterAccountService and redirects with notice' do service_instance = instance_double(Spl::RegisterAccountService) expect(Spl::RegisterAccountService).to receive(:new) @@ -271,7 +271,7 @@ end shared_examples 'register_loyalty_account error' do |error_class| - xit "renders 422 and adds translated base error for #{error_class}" do + it "renders 422 and adds translated base error for #{error_class}" do payload = { 'errorCode' => 'TEMPORARY_BLOCKED', 'msg' => 'Temporarily blocked' @@ -285,7 +285,7 @@ allow(Spl::RegisterAccountService).to receive(:new).and_return(service_instance) allow(service_instance).to receive(:call).and_raise(error_class.new(payload.inspect)) - expect(controller).to receive(:render).with(hash_including(status: :unprocessable_entity)) + expect(controller).to receive(:render).with(hash_including(status: :unprocessable_content)) controller.register_loyalty_account diff --git a/spec/services/spl/register_account_service_spec.rb b/spec/services/spl/register_account_service_spec.rb index c4757e1..8fcacbc 100644 --- a/spec/services/spl/register_account_service_spec.rb +++ b/spec/services/spl/register_account_service_spec.rb @@ -47,7 +47,7 @@ describe '#call' do context 'when registration succeeds' do - xit 'updates user public_metadata with SPL card data' do + it 'updates user public_metadata with SPL card data' do service.call expect(user.public_metadata).to include( @@ -56,13 +56,13 @@ ) end - xit 'updates user private_metadata with tokens' do + it 'updates user private_metadata with tokens' do service.call expect(user.private_metadata).to include('accessToken' => 'ACCESS_TOKEN', 'refreshToken' => 'REFRESH_TOKEN') end - xit 'sends correct payload to SendRequestService' do + it 'sends correct payload to SendRequestService' do expect(Spl::SendRequestService).to receive(:new) do |url, body| expect(url.to_s).to include('register') expect(body).to include(authCode: params, partnerCode: 'PARTNER', placeCode: 'PLACE') @@ -85,13 +85,13 @@ context 'when SPL returns errorCode != 0' do let(:register_response_body) { { 'errorCode' => 'TEMPORARY_BLOCKED', 'msg' => 'Temporarily blocked' } } - xit 'raises SplRegisterAccountError with SPL message' do + it 'raises SplRegisterAccountError with SPL message' do expect do service.call end.to raise_error(Spl::RegisterAccountService::SplRegisterAccountError, 'Temporarily blocked') end - xit 'does not update user public_metadata' do + it 'does not update user public_metadata' do expect do service.call rescue StandardError @@ -103,7 +103,7 @@ context 'when OTP code is not 6 characters' do let(:params) { '123' } - xit 'still sends request to SPL (no local validation)' do + it 'still sends request to SPL (no local validation)' do expect(Spl::SendRequestService).to receive(:new).and_return(send_request_service) service.call @@ -116,11 +116,11 @@ allow(PhoneParserService).to receive(:new).with(nil).and_raise(StandardError, 'Phone missing') end - xit 'raises error before sending request' do + it 'raises error before sending request' do expect { service.call }.to raise_error(StandardError, 'Phone missing') end - xit 'does not send request to SPL' do + it 'does not send request to SPL' do expect(Spl::SendRequestService).not_to receive(:new) expect { service.call }.to raise_error(StandardError) end From 0320346f01454015cc079a0b7c2a8bca60fc0875 Mon Sep 17 00:00:00 2001 From: Tomasz Strzeszewski Date: Wed, 7 Jan 2026 19:04:22 +0100 Subject: [PATCH 35/63] Refactor + unit tests --- .../checkout_controller_decorator.rb | 24 +- .../spl/coupons/activate_coupon_service.rb | 4 +- .../spl/coupons/deactivate_coupon_service.rb | 4 +- .../spl/coupons/get_coupons_service.rb | 49 ++++ app/services/spl/get_coupons_service.rb | 37 --- .../checkout_controller_decorator_spec.rb | 259 +++++++++++------- .../coupons/activate_coupon_service_spec.rb | 133 +++++++++ .../coupons/deactivate_coupon_service_spec.rb | 133 +++++++++ .../{ => coupons}/get_coupon_service_spec.rb | 9 +- 9 files changed, 482 insertions(+), 170 deletions(-) create mode 100644 app/services/spl/coupons/get_coupons_service.rb delete mode 100644 app/services/spl/get_coupons_service.rb create mode 100644 spec/services/spl/coupons/activate_coupon_service_spec.rb create mode 100644 spec/services/spl/coupons/deactivate_coupon_service_spec.rb rename spec/services/spl/{ => coupons}/get_coupon_service_spec.rb (95%) diff --git a/app/controllers/spl/spree/storefront/checkout_controller_decorator.rb b/app/controllers/spl/spree/storefront/checkout_controller_decorator.rb index ab77e58..8744212 100644 --- a/app/controllers/spl/spree/storefront/checkout_controller_decorator.rb +++ b/app/controllers/spl/spree/storefront/checkout_controller_decorator.rb @@ -10,12 +10,10 @@ def self.prepended(base) end def activate_coupon - Spl::Coupons::ActivateCouponService - .new(current_user, current_store, params) - .call - + Spl::Coupons::ActivateCouponService.new(@order.user, @order.store, params[:coupon_code]).call load_user_coupons + ensure respond_to do |format| format.turbo_stream format.html { redirect_to checkout_path } @@ -24,11 +22,11 @@ def activate_coupon def deactivate_coupon Spl::Coupons::DeactivateCouponService - .new(current_user, current_store, params) + .new(@order.user, @order.store, params[:coupon_code]) .call - load_user_coupons + ensure respond_to do |format| format.turbo_stream format.html { redirect_to checkout_path } @@ -42,19 +40,7 @@ def promotion_switcher end def load_user_coupons - @coupons = Spl::GetCouponsService.new(@order.user, @order.store).call&.filter do |coupon| - active?(coupon) - end - end - - def active?(coupon) - if coupon['used'] != true && coupon['usageTemporaryBlocked'] != true && coupon['expirationDate'].nil? - return true - end - - Time.zone.local(coupon['expirationDate']).future? - rescue StandardError - false + @coupons = Spl::Coupons::GetCouponsService.new(@order.user, @order.store).call end end end diff --git a/app/services/spl/coupons/activate_coupon_service.rb b/app/services/spl/coupons/activate_coupon_service.rb index 5391994..3994372 100644 --- a/app/services/spl/coupons/activate_coupon_service.rb +++ b/app/services/spl/coupons/activate_coupon_service.rb @@ -8,11 +8,11 @@ class ActivateCouponService class ActivateCouponServiceError < StandardError; end include SplServiceHelper - def initialize(user, store, params) + def initialize(user, store, coupon_code) @store = store @activate_coupons_url = URI.parse(Spl::UrlCreatorService.new(store.private_metadata['spl_url']).coupon_activate) @user = user - @coupon_code = params[:coupon_code] + @coupon_code = coupon_code end def call diff --git a/app/services/spl/coupons/deactivate_coupon_service.rb b/app/services/spl/coupons/deactivate_coupon_service.rb index 0429aa2..f2a961e 100644 --- a/app/services/spl/coupons/deactivate_coupon_service.rb +++ b/app/services/spl/coupons/deactivate_coupon_service.rb @@ -8,11 +8,11 @@ class DeactivateCouponService class DeactivateCouponServiceError < StandardError; end include SplServiceHelper - def initialize(user, store, params) + def initialize(user, store, coupon_code) @store = store @deactivate_coupons_url = URI.parse(Spl::UrlCreatorService.new(store.private_metadata['spl_url']).coupon_deactivate) @user = user - @coupon_code = params[:coupon_code] + @coupon_code = coupon_code end def call diff --git a/app/services/spl/coupons/get_coupons_service.rb b/app/services/spl/coupons/get_coupons_service.rb new file mode 100644 index 0000000..ea309f3 --- /dev/null +++ b/app/services/spl/coupons/get_coupons_service.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require 'json' + +module Spl + module Coupons + class GetCouponsService < BaseSplService + class SplGetCouponError < StandardError; end + + def initialize(user, store) + @store = store + @find_coupons_url = URI.parse(Spl::UrlCreatorService.new(store.private_metadata['spl_url']).coupon_find) + @user = user + end + + def call + body = prepare_body + response = send_request(@find_coupons_url, body) + response_body = JSON.parse(response.body) + Rails.logger.debug response_body + raise SplGetCouponError, response_body['msg'] if response_body['errorCode'] != '0' + + response_body['response']&.filter do |coupon| + active?(coupon) + end + end + + private + + def prepare_body + { + context: { + prgCode: @store.private_metadata['spl_prg_code'], + oauthToken: @user.private_metadata['spl_access_token'] + }, + withArchival: true + } + end + + def active?(coupon) + return true if coupon['used'] != true && coupon['usageTemporaryBlocked'] != true && coupon['expirationDate'].nil? + + Time.zone.local(coupon['expirationDate']).future? + rescue StandardError + false + end + end + end +end diff --git a/app/services/spl/get_coupons_service.rb b/app/services/spl/get_coupons_service.rb deleted file mode 100644 index eeb0721..0000000 --- a/app/services/spl/get_coupons_service.rb +++ /dev/null @@ -1,37 +0,0 @@ -# frozen_string_literal: true - -require 'json' - -module Spl - class GetCouponsService < BaseSplService - class SplGetCouponError < StandardError; end - - def initialize(user, store) - @store = store - @find_coupons_url = URI.parse(Spl::UrlCreatorService.new(store.private_metadata['spl_url']).coupon_find) - @user = user - end - - def call - body = prepare_body - response = send_request(@find_coupons_url, body) - response_body = JSON.parse(response.body) - Rails.logger.debug response_body - raise SplGetCouponError, response_body['msg'] if response_body['errorCode'] != '0' - - response_body['response'] - end - - private - - def prepare_body - { - context: { - prgCode: @store.private_metadata['spl_prg_code'], - oauthToken: @user.private_metadata['spl_access_token'] - }, - withArchival: true - } - end - end -end diff --git a/spec/controllers/checkout_controller_decorator_spec.rb b/spec/controllers/checkout_controller_decorator_spec.rb index 8dd7f41..36245af 100644 --- a/spec/controllers/checkout_controller_decorator_spec.rb +++ b/spec/controllers/checkout_controller_decorator_spec.rb @@ -1,128 +1,175 @@ # frozen_string_literal: true -require 'rails_helper' - -describe Spree::CheckoutController, type: :controller do - let(:store) do - create(:store, private_metadata: { - 'spl_url' => 'https://fake-spl.example.com', - 'spl_mode' => 'test', - 'spl_pos_key' => 'fake-pos-key', - 'spl_api_user' => 'fake-api-user', - 'spl_prg_code' => 'fake-prg-code', - 'spl_sale_url' => 'https://fake-spl.example.com/sale', - 'spl_api_token' => 'fake-token', - 'spl_place_code' => 'fake-place', - 'spl_partner_code' => 'fake-partner', - 'spl_signature_seed' => 'fake-seed' - }) - end +require "rails_helper" + +RSpec.describe Spl::Spree::Storefront::CheckoutControllerDecorator, type: :controller do + controller(ActionController::Base) do + before_action :set_order + + def checkout_path + "/checkout" + end + + def show + render plain: "ok" + end - let!(:state) { create(:state, country: create(:country_us), name: 'Gdansk', abbr: 'GDA') } - let(:user) { create(:user, public_metadata: { 'spl_no_card' => '1234567890123', 'spl_card_active' => 'true' }) } - - let(:order) do - create( - :order_with_totals, - store: store, - user: user, - email: 'test@example.com', - public_metadata: { - 'spl_no_card' => '1234567890123', - 'spl_card_active' => 'true' - } - ) + # Provided by decorator; defined here so Rails can dispatch + def activate_coupon = super + def deactivate_coupon = super + + private + + def set_order + @order = ::Spree::Order.first + end end before do - allow(controller).to receive_messages(current_store: store, try_spree_current_user: user, spree_current_user: user, - spree_signup_path: '/signup', spree_login_path: '/login') - - stub_request(:any, /fake-spl\.example\.com/).to_return( - status: 200, - body: { - 'errorCode' => '0', - 'response' => { - 'basket' => order.line_items.map do |li| - { - 'pos' => li.id, - 'discounts' => [ - { - 'name' => 'TEST' - } - ], - 'discountGross' => 5.0 - } - end - } - }.to_json, - headers: { 'Content-Type' => 'application/json' } - ) - - allow_any_instance_of(Spree::Order).to receive(:recalculate).and_return(true) + controller.class.prepend described_class + routes.draw do + get "/checkout", to: "anonymous#show" + post "/checkout/activate_coupon", to: "anonymous#activate_coupon" + post "/checkout/deactivate_coupon", to: "anonymous#deactivate_coupon" + end + allow(request).to receive(:url).and_return("http://test.host/checkout") end - describe '#promotion_switcher' do - context 'creates a new SPL adjustment' do - it 'with SPL source_type' do - post :update, params: { state: 'address', token: order.token } + let(:country){ create(:country) } + let!(:store) { create(:store, default_country: country)} - expect(order.line_items.last.adjustments.last.source_type).to eq('SPL') - end + let!(:user) { create(:user) } + let!(:order) { create( :order, + store: store, + user: user) } - it 'with amount from discountGross' do - post :update, params: { state: 'address', token: order.token } + describe "#promotion_switcher" do + it "calls PromotionSwitcherService with confirm=false when URL does not include 'confirm'" do + switcher = instance_double("PromotionSwitcherService", call: true) + expect(PromotionSwitcherService).to receive(:new).with(order, false).and_return(switcher) - expect(order.line_items.last.adjustments.last.amount).to eq(-5.0) - end + allow(Spl::Coupons::GetCouponsService) + .to receive(:new).with(user, store) + .and_return(instance_double("Spl::Coupons::GetCouponsService", call: [])) + + get :show + expect(response).to have_http_status(:ok) end - context 'when SPL API response has no line_items (unhappy path)' do - before do - stub_request(:any, /fake-spl\.example\.com/).to_return( - status: 200, - body: { - 'errorCode' => '0', - 'response' => { - 'basket' => [] - } - }.to_json, - headers: { 'Content-Type' => 'application/json' } - ) - end + it "calls PromotionSwitcherService with confirm=true when URL includes 'confirm'" do + allow_any_instance_of(ActionDispatch::Request) + .to receive(:url) + .and_return("http://test.host/checkout/confirm") - it 'does NOT create SPL adjustments' do - expect do - post :update, params: { state: 'address', token: order.token } - end.not_to(change { order.line_items.first.adjustments.count }) - end + switcher = instance_double("PromotionSwitcherService", call: true) + expect(PromotionSwitcherService).to receive(:new).with(order, true).and_return(switcher) + + allow(Spl::Coupons::GetCouponsService) + .to receive(:new).with(user, store) + .and_return(instance_double("Spl::Coupons::GetCouponsService", call: [])) + + get :show + expect(response).to have_http_status(:ok) + end + end + + describe "#load_user_coupons" do + it "loads coupons for user/store and assigns @coupons" do + allow(PromotionSwitcherService) + .to receive(:new) + .and_return(instance_double("PromotionSwitcherService", call: true)) + + coupons_service = instance_double("Spl::Coupons::GetCouponsService", call: %w[c1 c2]) + expect(Spl::Coupons::GetCouponsService) + .to receive(:new).with(user, store).and_return(coupons_service) + + get :show + + expect(response).to have_http_status(:ok) + expect(assigns(:coupons)).to eq(%w[c1 c2]) end - context 'when SPL API response has no discounts (unhappy path)' do - before do - stub_request(:any, /fake-spl\.example\.com/).to_return( - status: 200, - body: { - 'errorCode' => '0', - 'response' => { - 'basket' => order.line_items.map do |li| - { - 'pos' => li.id, - 'discounts' => [], - 'discountGross' => 0.0 - } - end - } - }.to_json, - headers: { 'Content-Type' => 'application/json' } - ) + it "does not run load_user_coupons as a before_action for activate_coupon (it is called inside the action instead)" do + allow(PromotionSwitcherService) + .to receive(:new) + .and_return(instance_double("PromotionSwitcherService", call: true)) + + # This will still be called (inside #activate_coupon), but we can ensure it is not called twice. + coupons_service = instance_double("Spl::Coupons::GetCouponsService", call: []) + expect(Spl::Coupons::GetCouponsService).to receive(:new).with(user, store).once.and_return(coupons_service) + + allow(Spl::Coupons::ActivateCouponService) + .to receive(:new) + .and_return(instance_double("Spl::Coupons::ActivateCouponService", call: true)) + + post :activate_coupon, params: { coupon_code: "ABC123" }, format: :html + end + end + + describe "#activate_coupon" do + let(:activate) { instance_double(Spl::Coupons::ActivateCouponService, call: true)} + context "when coupon is valid" do + it "calls ActivateCouponService, reloads coupons, and redirects (HTML)" do + expect(Spl::Coupons::ActivateCouponService) + .to receive(:new).with(user, store, "YOLO_90").and_return(activate) + + coupons_service = instance_double("Spl::Coupons::GetCouponsService", call: %w[YOLO_90]) + expect(Spl::Coupons::GetCouponsService) + .to receive(:new).with(user, store).and_return(coupons_service) + + post :activate_coupon, params: { coupon_code: "YOLO_90" }, format: :html + + expect(assigns(:coupons)).to eq(%w[YOLO_90]) + expect(response).to have_http_status(:found) + expect(response.location).to include("/checkout") end + end + + context 'when coupon is invalid' do + before { allow(activate).to receive(:call).and_raise(Spl::Coupons::ActivateCouponService::ActivateCouponServiceError) } + + it "still redirects even if ActivateCouponService raises" do + expect(Spl::Coupons::ActivateCouponService).to receive(:new).with(user, store, "YOLO_90").and_return(activate) + expect(activate).to receive(:call).and_raise(Spl::Coupons::ActivateCouponService::ActivateCouponServiceError, "some error") - it 'does NOT create SPL adjustments' do - expect do - post :update, params: { state: 'address', token: order.token } - end.not_to(change { order.line_items.first.adjustments.count }) + expect { + post :activate_coupon, params: { coupon_code: "YOLO_90" }, format: :html + }.to raise_error('some error') + + expect(response).to have_http_status(:found) + expect(response.location).to include("/checkout") end end end + + describe "#deactivate_coupon" do + it "calls DeactivateCouponService, reloads coupons, and redirects (HTML)" do + deactivate = instance_double("Spl::Coupons::DeactivateCouponService", call: true) + expect(Spl::Coupons::DeactivateCouponService) + .to receive(:new).with(user, store, "YOLO_90").and_return(deactivate) + + coupons_service = instance_double("Spl::Coupons::GetCouponsService", call: %w[YOLO_90]) + expect(Spl::Coupons::GetCouponsService) + .to receive(:new).with(user, store).and_return(coupons_service) + + post :deactivate_coupon, params: { coupon_code: "YOLO_90" }, format: :html + + expect(assigns(:coupons)).to eq(%w[YOLO_90]) + expect(response).to have_http_status(:found) + expect(response.location).to include("/checkout") + end + + it "still redirects even if DeactivateCouponService raises" do + deactivate = instance_double("Spl::Coupons::DeactivateCouponService") + expect(Spl::Coupons::DeactivateCouponService).to receive(:new).with(user, store, "YOLO_90").and_return(deactivate) + expect(deactivate).to receive(:call).and_raise(Spl::Coupons::DeactivateCouponService::DeactivateCouponServiceError, "some error") + + expect { + post :deactivate_coupon, params: { coupon_code: "YOLO_90" }, format: :html + }.to raise_error('some error') + + expect(response).to have_http_status(:found) + expect(response.location).to include("/checkout") + end + end end diff --git a/spec/services/spl/coupons/activate_coupon_service_spec.rb b/spec/services/spl/coupons/activate_coupon_service_spec.rb new file mode 100644 index 0000000..01146dc --- /dev/null +++ b/spec/services/spl/coupons/activate_coupon_service_spec.rb @@ -0,0 +1,133 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe Spl::Coupons::ActivateCouponService do + subject(:service) { described_class.new(user, store, params) } + + # Spree factories where possible + let(:store) { build_stubbed(:store) } + let(:user) { build_stubbed(:user) } + + let(:spl_url) { "https://spl.example.test" } + let(:prg_code) { "PRG123" } + let(:access_token) { "token_abc" } + + let(:coupon_code) { "ABC123" } + let(:params) { coupon_code } + + let(:activate_url_string) { "https://api.spl.example.test/coupons/activate" } + let(:activate_uri) { URI.parse(activate_url_string) } + + let(:success_body_raw) do + { + "errorCode" => "0", + "validationMessages" => nil, + "fieldValidationMessages" => nil, + "response" => { "returned" => nil }, + "msg" => "OK" + }.to_json + end + + let(:response) { instance_double("Net::HTTPResponse", body: success_body_raw) } + + before do + allow(store).to receive(:private_metadata).and_return( + "spl_url" => spl_url, + "spl_prg_code" => prg_code + ) + allow(user).to receive(:private_metadata).and_return( + "spl_access_token" => access_token + ) + + url_creator = instance_double("Spl::UrlCreatorService", coupon_activate: activate_url_string) + allow(Spl::UrlCreatorService).to receive(:new).with(spl_url).and_return(url_creator) + + allow(service).to receive(:send_request).and_return(response) + end + + describe "#call" do + context "success (errorCode == '0')" do + it "sends request to coupon_activate URI with expected body and returns parsed 'response' hash" do + expected_body = { + context: { + prgCode: prg_code, + oauthToken: access_token + }, + couponCode: coupon_code + } + + expect(service).to receive(:send_request).with(activate_uri, expected_body).and_return(response) + + expect(service.call).to eq({ "returned" => nil }) + end + + it "succeeds even if validation messages are present (still errorCode '0')" do + body = { + "errorCode" => "0", + "validationMessages" => ["some warning"], + "fieldValidationMessages" => { "couponCode" => ["format warning"] }, + "response" => { "returned" => nil }, + "msg" => "OK" + }.to_json + + allow(service).to receive(:send_request).and_return(instance_double("Net::HTTPResponse", body: body)) + + expect(service.call).to eq({ "returned" => nil }) + end + + it "returns nil if 'response' key is missing" do + body = { "errorCode" => "0", "msg" => "OK" }.to_json + allow(service).to receive(:send_request).and_return(instance_double("Net::HTTPResponse", body: body)) + + expect(service.call).to be_nil + end + + it "returns nil if 'response' is null" do + body = { "errorCode" => "0", "response" => nil, "msg" => "OK" }.to_json + allow(service).to receive(:send_request).and_return(instance_double("Net::HTTPResponse", body: body)) + + expect(service.call).to be_nil + end + end + + context "API failure (errorCode != '0')" do + it "raises ActivateCouponServiceError with msg" do + body = { "errorCode" => "COUPON_CANNOT_BE_MODIFIED", "msg" => "Coupon invalid", "response" => nil }.to_json + allow(service).to receive(:send_request).and_return(instance_double("Net::HTTPResponse", body: body)) + + expect { service.call } + .to raise_error(described_class::ActivateCouponServiceError, "Coupon invalid") + end + + it "raises even if msg is missing" do + body = { "errorCode" => "COUPON_CANNOT_BE_MODIFIED" }.to_json + allow(service).to receive(:send_request).and_return(instance_double("Net::HTTPResponse", body: body)) + + expect { service.call }.to raise_error(described_class::ActivateCouponServiceError) + end + end + + context "bad/empty HTTP response" do + it "raises JSON::ParserError for invalid JSON" do + allow(service).to receive(:send_request).and_return(instance_double("Net::HTTPResponse", body: "not-json")) + expect { service.call }.to raise_error(JSON::ParserError) + end + + it "raises TypeError when response.body is nil (JSON.parse(nil))" do + allow(service).to receive(:send_request).and_return(instance_double("Net::HTTPResponse", body: nil)) + expect { service.call }.to raise_error(TypeError) + end + + it "raises NoMethodError when send_request returns nil (nil.body)" do + allow(service).to receive(:send_request).and_return(nil) + expect { service.call }.to raise_error(NoMethodError) + end + + it "bubbles exceptions from send_request (e.g., timeouts)" do + allow(service).to receive(:send_request).and_raise(Timeout::Error) + expect { service.call }.to raise_error(Timeout::Error) + end + end + end +end diff --git a/spec/services/spl/coupons/deactivate_coupon_service_spec.rb b/spec/services/spl/coupons/deactivate_coupon_service_spec.rb new file mode 100644 index 0000000..15f4dbc --- /dev/null +++ b/spec/services/spl/coupons/deactivate_coupon_service_spec.rb @@ -0,0 +1,133 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe Spl::Coupons::DeactivateCouponService do + subject(:service) { described_class.new(user, store, params) } + + # Spree factories where possible + let(:store) { build_stubbed(:store) } + let(:user) { build_stubbed(:user) } + + let(:spl_url) { "https://spl.example.test" } + let(:prg_code) { "prg2" } + let(:access_token) { "token_abc" } + + let(:coupon_code) { "COUPON" } + let(:params) { coupon_code } + + let(:deactivate_url_string) { "https://api.spl.example.test/coupons/deactivate" } + let(:deactivate_uri) { URI.parse(deactivate_url_string) } + + let(:success_body_raw) do + { + "errorCode" => "0", + "validationMessages" => nil, + "fieldValidationMessages" => nil, + "response" => { "returned" => nil }, + "msg" => "OK" + }.to_json + end + + let(:response) { instance_double("Net::HTTPResponse", body: success_body_raw) } + + before do + allow(store).to receive(:private_metadata).and_return( + "spl_url" => spl_url, + "spl_prg_code" => prg_code + ) + allow(user).to receive(:private_metadata).and_return( + "spl_access_token" => access_token + ) + + url_creator = instance_double("Spl::UrlCreatorService", coupon_deactivate: deactivate_url_string) + allow(Spl::UrlCreatorService).to receive(:new).with(spl_url).and_return(url_creator) + + allow(service).to receive(:send_request).and_return(response) + end + + describe "#call" do + context "success (errorCode == '0')" do + it "sends request to coupon_deactivate URI with expected body and returns parsed 'response' hash" do + expected_body = { + context: { + prgCode: prg_code, + oauthToken: access_token + }, + couponCode: coupon_code + } + + expect(service).to receive(:send_request).with(deactivate_uri, expected_body).and_return(response) + + expect(service.call).to eq({ "returned" => nil }) + end + + it "succeeds even if validation messages are present (still errorCode '0')" do + body = { + "errorCode" => "0", + "validationMessages" => ["some warning"], + "fieldValidationMessages" => { "couponCode" => ["format warning"] }, + "response" => { "returned" => nil }, + "msg" => "OK" + }.to_json + + allow(service).to receive(:send_request).and_return(instance_double("Net::HTTPResponse", body: body)) + + expect(service.call).to eq({ "returned" => nil }) + end + + it "returns nil if 'response' key is missing" do + body = { "errorCode" => "0", "msg" => "OK" }.to_json + allow(service).to receive(:send_request).and_return(instance_double("Net::HTTPResponse", body: body)) + + expect(service.call).to be_nil + end + + it "returns nil if 'response' is null" do + body = { "errorCode" => "0", "response" => nil, "msg" => "OK" }.to_json + allow(service).to receive(:send_request).and_return(instance_double("Net::HTTPResponse", body: body)) + + expect(service.call).to be_nil + end + end + + context "API failure (errorCode != '0')" do + it "raises DeactivateCouponServiceError with msg" do + body = { "errorCode" => "COUPON_CANNOT_BE_MODIFIED", "msg" => "Coupon invalid", "response" => nil }.to_json + allow(service).to receive(:send_request).and_return(instance_double("Net::HTTPResponse", body: body)) + + expect { service.call } + .to raise_error(described_class::DeactivateCouponServiceError, "Coupon invalid") + end + + it "raises even if msg is missing" do + body = { "errorCode" => "COUPON_CANNOT_BE_MODIFIED" }.to_json + allow(service).to receive(:send_request).and_return(instance_double("Net::HTTPResponse", body: body)) + + expect { service.call }.to raise_error(described_class::DeactivateCouponServiceError) + end + end + + context "bad/empty HTTP response" do + it "raises JSON::ParserError for invalid JSON" do + allow(service).to receive(:send_request).and_return(instance_double("Net::HTTPResponse", body: "not-json")) + expect { service.call }.to raise_error(JSON::ParserError) + end + + it "raises TypeError when response.body is nil (JSON.parse(nil))" do + allow(service).to receive(:send_request).and_return(instance_double("Net::HTTPResponse", body: nil)) + expect { service.call }.to raise_error(TypeError) + end + + it "raises NoMethodError when send_request returns nil (nil.body)" do + allow(service).to receive(:send_request).and_return(nil) + expect { service.call }.to raise_error(NoMethodError) + end + + it "bubbles exceptions from send_request (e.g., timeouts)" do + allow(service).to receive(:send_request).and_raise(Timeout::Error) + expect { service.call }.to raise_error(Timeout::Error) + end + end + end +end diff --git a/spec/services/spl/get_coupon_service_spec.rb b/spec/services/spl/coupons/get_coupon_service_spec.rb similarity index 95% rename from spec/services/spl/get_coupon_service_spec.rb rename to spec/services/spl/coupons/get_coupon_service_spec.rb index 7a73738..433bff1 100644 --- a/spec/services/spl/get_coupon_service_spec.rb +++ b/spec/services/spl/coupons/get_coupon_service_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Spl::GetCouponsService do +RSpec.describe Spl::Coupons::GetCouponsService do let(:user) { create(:user, private_metadata: { 'spl_access_token' => 'LPUXLUZYZ9JN8XLXSSCTZA4Y5LEFEX' }) } let(:store) { Spree::Store.default } let(:service) { described_class.new(user, store) } @@ -14,7 +14,8 @@ context: { prgCode: 'prg-2', oauthToken: 'LPUXLUZYZ9JN8XLXSSCTZA4Y5LEFEX' # real exammple token - } + }, + withArchival: true } end let(:request_service) { instance_double(Spl::SendRequestService) } @@ -38,7 +39,7 @@ 'response' => [ { 'code' => '9004850879237', - 'expirationDate' => '2026-01-01T23:59:59.999+01:00', + 'expirationDate' => nil, 'valid' => true, 'type' => '90_CRAZY', 'typeId' => '67583a525012cbcf734c281a', @@ -112,7 +113,7 @@ it 'raises SplGetCouponError with message' do expect { service.call }.to raise_error( - Spl::GetCouponsService::SplGetCouponError, + Spl::Coupons::GetCouponsService::SplGetCouponError, 'Token expired' ) end From 7fa24cfd7422af5087369a14733ad483ad90218a Mon Sep 17 00:00:00 2001 From: Tomasz Strzeszewski Date: Wed, 7 Jan 2026 19:06:33 +0100 Subject: [PATCH 36/63] rubocop offences --- .../checkout_controller_decorator.rb | 2 - .../spl/coupons/get_coupons_service.rb | 4 +- .../checkout_controller_decorator_spec.rb | 123 +++++++++--------- .../coupons/activate_coupon_service_spec.rb | 86 ++++++------ .../coupons/deactivate_coupon_service_spec.rb | 86 ++++++------ 5 files changed, 152 insertions(+), 149 deletions(-) diff --git a/app/controllers/spl/spree/storefront/checkout_controller_decorator.rb b/app/controllers/spl/spree/storefront/checkout_controller_decorator.rb index 8744212..49bcc4d 100644 --- a/app/controllers/spl/spree/storefront/checkout_controller_decorator.rb +++ b/app/controllers/spl/spree/storefront/checkout_controller_decorator.rb @@ -12,7 +12,6 @@ def self.prepended(base) def activate_coupon Spl::Coupons::ActivateCouponService.new(@order.user, @order.store, params[:coupon_code]).call load_user_coupons - ensure respond_to do |format| format.turbo_stream @@ -25,7 +24,6 @@ def deactivate_coupon .new(@order.user, @order.store, params[:coupon_code]) .call load_user_coupons - ensure respond_to do |format| format.turbo_stream diff --git a/app/services/spl/coupons/get_coupons_service.rb b/app/services/spl/coupons/get_coupons_service.rb index ea309f3..21446ae 100644 --- a/app/services/spl/coupons/get_coupons_service.rb +++ b/app/services/spl/coupons/get_coupons_service.rb @@ -38,7 +38,9 @@ def prepare_body end def active?(coupon) - return true if coupon['used'] != true && coupon['usageTemporaryBlocked'] != true && coupon['expirationDate'].nil? + if coupon['used'] != true && coupon['usageTemporaryBlocked'] != true && coupon['expirationDate'].nil? + return true + end Time.zone.local(coupon['expirationDate']).future? rescue StandardError diff --git a/spec/controllers/checkout_controller_decorator_spec.rb b/spec/controllers/checkout_controller_decorator_spec.rb index 36245af..e5815a4 100644 --- a/spec/controllers/checkout_controller_decorator_spec.rb +++ b/spec/controllers/checkout_controller_decorator_spec.rb @@ -1,56 +1,56 @@ # frozen_string_literal: true -require "rails_helper" +require 'rails_helper' RSpec.describe Spl::Spree::Storefront::CheckoutControllerDecorator, type: :controller do controller(ActionController::Base) do before_action :set_order def checkout_path - "/checkout" + '/checkout' end def show - render plain: "ok" + render plain: 'ok' end # Provided by decorator; defined here so Rails can dispatch - def activate_coupon = super - def deactivate_coupon = super private def set_order - @order = ::Spree::Order.first + @order = Spree::Order.first end end before do controller.class.prepend described_class routes.draw do - get "/checkout", to: "anonymous#show" - post "/checkout/activate_coupon", to: "anonymous#activate_coupon" - post "/checkout/deactivate_coupon", to: "anonymous#deactivate_coupon" + get '/checkout', to: 'anonymous#show' + post '/checkout/activate_coupon', to: 'anonymous#activate_coupon' + post '/checkout/deactivate_coupon', to: 'anonymous#deactivate_coupon' end - allow(request).to receive(:url).and_return("http://test.host/checkout") + allow(request).to receive(:url).and_return('http://test.host/checkout') end - let(:country){ create(:country) } - let!(:store) { create(:store, default_country: country)} + let(:country) { create(:country) } + let!(:store) { create(:store, default_country: country) } let!(:user) { create(:user) } - let!(:order) { create( :order, - store: store, - user: user) } + let!(:order) do + create(:order, + store: store, + user: user) + end - describe "#promotion_switcher" do + describe '#promotion_switcher' do it "calls PromotionSwitcherService with confirm=false when URL does not include 'confirm'" do - switcher = instance_double("PromotionSwitcherService", call: true) + switcher = instance_double('PromotionSwitcherService', call: true) expect(PromotionSwitcherService).to receive(:new).with(order, false).and_return(switcher) allow(Spl::Coupons::GetCouponsService) .to receive(:new).with(user, store) - .and_return(instance_double("Spl::Coupons::GetCouponsService", call: [])) + .and_return(instance_double('Spl::Coupons::GetCouponsService', call: [])) get :show expect(response).to have_http_status(:ok) @@ -59,27 +59,27 @@ def set_order it "calls PromotionSwitcherService with confirm=true when URL includes 'confirm'" do allow_any_instance_of(ActionDispatch::Request) .to receive(:url) - .and_return("http://test.host/checkout/confirm") + .and_return('http://test.host/checkout/confirm') - switcher = instance_double("PromotionSwitcherService", call: true) + switcher = instance_double('PromotionSwitcherService', call: true) expect(PromotionSwitcherService).to receive(:new).with(order, true).and_return(switcher) allow(Spl::Coupons::GetCouponsService) .to receive(:new).with(user, store) - .and_return(instance_double("Spl::Coupons::GetCouponsService", call: [])) + .and_return(instance_double('Spl::Coupons::GetCouponsService', call: [])) get :show expect(response).to have_http_status(:ok) end end - describe "#load_user_coupons" do - it "loads coupons for user/store and assigns @coupons" do + describe '#load_user_coupons' do + it 'loads coupons for user/store and assigns @coupons' do allow(PromotionSwitcherService) .to receive(:new) - .and_return(instance_double("PromotionSwitcherService", call: true)) + .and_return(instance_double('PromotionSwitcherService', call: true)) - coupons_service = instance_double("Spl::Coupons::GetCouponsService", call: %w[c1 c2]) + coupons_service = instance_double('Spl::Coupons::GetCouponsService', call: %w[c1 c2]) expect(Spl::Coupons::GetCouponsService) .to receive(:new).with(user, store).and_return(coupons_service) @@ -89,87 +89,90 @@ def set_order expect(assigns(:coupons)).to eq(%w[c1 c2]) end - it "does not run load_user_coupons as a before_action for activate_coupon (it is called inside the action instead)" do + it 'does not run load_user_coupons as a before_action for activate_coupon' do allow(PromotionSwitcherService) .to receive(:new) - .and_return(instance_double("PromotionSwitcherService", call: true)) + .and_return(instance_double('PromotionSwitcherService', call: true)) # This will still be called (inside #activate_coupon), but we can ensure it is not called twice. - coupons_service = instance_double("Spl::Coupons::GetCouponsService", call: []) + coupons_service = instance_double('Spl::Coupons::GetCouponsService', call: []) expect(Spl::Coupons::GetCouponsService).to receive(:new).with(user, store).once.and_return(coupons_service) allow(Spl::Coupons::ActivateCouponService) .to receive(:new) - .and_return(instance_double("Spl::Coupons::ActivateCouponService", call: true)) + .and_return(instance_double('Spl::Coupons::ActivateCouponService', call: true)) - post :activate_coupon, params: { coupon_code: "ABC123" }, format: :html + post :activate_coupon, params: { coupon_code: 'YOLO_90' }, format: :html end end - describe "#activate_coupon" do - let(:activate) { instance_double(Spl::Coupons::ActivateCouponService, call: true)} - context "when coupon is valid" do - it "calls ActivateCouponService, reloads coupons, and redirects (HTML)" do + describe '#activate_coupon' do + let(:activate) { instance_double(Spl::Coupons::ActivateCouponService, call: true) } + context 'when coupon is valid' do + it 'calls ActivateCouponService, reloads coupons, and redirects (HTML)' do expect(Spl::Coupons::ActivateCouponService) - .to receive(:new).with(user, store, "YOLO_90").and_return(activate) + .to receive(:new).with(user, store, 'YOLO_90').and_return(activate) - coupons_service = instance_double("Spl::Coupons::GetCouponsService", call: %w[YOLO_90]) + coupons_service = instance_double('Spl::Coupons::GetCouponsService', call: %w[YOLO_90]) expect(Spl::Coupons::GetCouponsService) .to receive(:new).with(user, store).and_return(coupons_service) - post :activate_coupon, params: { coupon_code: "YOLO_90" }, format: :html + post :activate_coupon, params: { coupon_code: 'YOLO_90' }, format: :html expect(assigns(:coupons)).to eq(%w[YOLO_90]) expect(response).to have_http_status(:found) - expect(response.location).to include("/checkout") + expect(response.location).to include('/checkout') end end context 'when coupon is invalid' do before { allow(activate).to receive(:call).and_raise(Spl::Coupons::ActivateCouponService::ActivateCouponServiceError) } - it "still redirects even if ActivateCouponService raises" do - expect(Spl::Coupons::ActivateCouponService).to receive(:new).with(user, store, "YOLO_90").and_return(activate) - expect(activate).to receive(:call).and_raise(Spl::Coupons::ActivateCouponService::ActivateCouponServiceError, "some error") + it 'still redirects even if ActivateCouponService raises' do + expect(Spl::Coupons::ActivateCouponService).to receive(:new).with(user, store, 'YOLO_90').and_return(activate) + expect(activate).to receive(:call).and_raise(Spl::Coupons::ActivateCouponService::ActivateCouponServiceError, + 'some error') - expect { - post :activate_coupon, params: { coupon_code: "YOLO_90" }, format: :html - }.to raise_error('some error') + expect do + post :activate_coupon, params: { coupon_code: 'YOLO_90' }, format: :html + end.to raise_error('some error') expect(response).to have_http_status(:found) - expect(response.location).to include("/checkout") + expect(response.location).to include('/checkout') end end end - describe "#deactivate_coupon" do - it "calls DeactivateCouponService, reloads coupons, and redirects (HTML)" do - deactivate = instance_double("Spl::Coupons::DeactivateCouponService", call: true) + describe '#deactivate_coupon' do + it 'calls DeactivateCouponService, reloads coupons, and redirects (HTML)' do + deactivate = instance_double('Spl::Coupons::DeactivateCouponService', call: true) expect(Spl::Coupons::DeactivateCouponService) - .to receive(:new).with(user, store, "YOLO_90").and_return(deactivate) + .to receive(:new).with(user, store, 'YOLO_90').and_return(deactivate) - coupons_service = instance_double("Spl::Coupons::GetCouponsService", call: %w[YOLO_90]) + coupons_service = instance_double('Spl::Coupons::GetCouponsService', call: %w[YOLO_90]) expect(Spl::Coupons::GetCouponsService) .to receive(:new).with(user, store).and_return(coupons_service) - post :deactivate_coupon, params: { coupon_code: "YOLO_90" }, format: :html + post :deactivate_coupon, params: { coupon_code: 'YOLO_90' }, format: :html expect(assigns(:coupons)).to eq(%w[YOLO_90]) expect(response).to have_http_status(:found) - expect(response.location).to include("/checkout") + expect(response.location).to include('/checkout') end - it "still redirects even if DeactivateCouponService raises" do - deactivate = instance_double("Spl::Coupons::DeactivateCouponService") - expect(Spl::Coupons::DeactivateCouponService).to receive(:new).with(user, store, "YOLO_90").and_return(deactivate) - expect(deactivate).to receive(:call).and_raise(Spl::Coupons::DeactivateCouponService::DeactivateCouponServiceError, "some error") + it 'still redirects even if DeactivateCouponService raises' do + deactivate = instance_double('Spl::Coupons::DeactivateCouponService') + expect(Spl::Coupons::DeactivateCouponService).to receive(:new).with(user, store, 'YOLO_90').and_return(deactivate) + expect(deactivate).to receive(:call).and_raise( + Spl::Coupons::DeactivateCouponService::DeactivateCouponServiceError, 'some error' + ) - expect { - post :deactivate_coupon, params: { coupon_code: "YOLO_90" }, format: :html - }.to raise_error('some error') + expect do + post :deactivate_coupon, params: { coupon_code: 'YOLO_90' }, format: :html + end.to raise_error('some error') expect(response).to have_http_status(:found) - expect(response.location).to include("/checkout") + expect(response.location).to include('/checkout') end end end diff --git a/spec/services/spl/coupons/activate_coupon_service_spec.rb b/spec/services/spl/coupons/activate_coupon_service_spec.rb index 01146dc..def323a 100644 --- a/spec/services/spl/coupons/activate_coupon_service_spec.rb +++ b/spec/services/spl/coupons/activate_coupon_service_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require "rails_helper" +require 'rails_helper' RSpec.describe Spl::Coupons::ActivateCouponService do subject(:service) { described_class.new(user, store, params) } @@ -9,44 +9,44 @@ let(:store) { build_stubbed(:store) } let(:user) { build_stubbed(:user) } - let(:spl_url) { "https://spl.example.test" } - let(:prg_code) { "PRG123" } - let(:access_token) { "token_abc" } + let(:spl_url) { 'https://spl.example.test' } + let(:prg_code) { 'PRG123' } + let(:access_token) { 'token_abc' } - let(:coupon_code) { "ABC123" } + let(:coupon_code) { 'ABC123' } let(:params) { coupon_code } - let(:activate_url_string) { "https://api.spl.example.test/coupons/activate" } + let(:activate_url_string) { 'https://api.spl.example.test/coupons/activate' } let(:activate_uri) { URI.parse(activate_url_string) } let(:success_body_raw) do { - "errorCode" => "0", - "validationMessages" => nil, - "fieldValidationMessages" => nil, - "response" => { "returned" => nil }, - "msg" => "OK" + 'errorCode' => '0', + 'validationMessages' => nil, + 'fieldValidationMessages' => nil, + 'response' => { 'returned' => nil }, + 'msg' => 'OK' }.to_json end - let(:response) { instance_double("Net::HTTPResponse", body: success_body_raw) } + let(:response) { instance_double('Net::HTTPResponse', body: success_body_raw) } before do allow(store).to receive(:private_metadata).and_return( - "spl_url" => spl_url, - "spl_prg_code" => prg_code + 'spl_url' => spl_url, + 'spl_prg_code' => prg_code ) allow(user).to receive(:private_metadata).and_return( - "spl_access_token" => access_token + 'spl_access_token' => access_token ) - url_creator = instance_double("Spl::UrlCreatorService", coupon_activate: activate_url_string) + url_creator = instance_double('Spl::UrlCreatorService', coupon_activate: activate_url_string) allow(Spl::UrlCreatorService).to receive(:new).with(spl_url).and_return(url_creator) allow(service).to receive(:send_request).and_return(response) end - describe "#call" do + describe '#call' do context "success (errorCode == '0')" do it "sends request to coupon_activate URI with expected body and returns parsed 'response' hash" do expected_body = { @@ -59,72 +59,72 @@ expect(service).to receive(:send_request).with(activate_uri, expected_body).and_return(response) - expect(service.call).to eq({ "returned" => nil }) + expect(service.call).to eq({ 'returned' => nil }) end it "succeeds even if validation messages are present (still errorCode '0')" do body = { - "errorCode" => "0", - "validationMessages" => ["some warning"], - "fieldValidationMessages" => { "couponCode" => ["format warning"] }, - "response" => { "returned" => nil }, - "msg" => "OK" + 'errorCode' => '0', + 'validationMessages' => ['some warning'], + 'fieldValidationMessages' => { 'couponCode' => ['format warning'] }, + 'response' => { 'returned' => nil }, + 'msg' => 'OK' }.to_json - allow(service).to receive(:send_request).and_return(instance_double("Net::HTTPResponse", body: body)) + allow(service).to receive(:send_request).and_return(instance_double('Net::HTTPResponse', body: body)) - expect(service.call).to eq({ "returned" => nil }) + expect(service.call).to eq({ 'returned' => nil }) end it "returns nil if 'response' key is missing" do - body = { "errorCode" => "0", "msg" => "OK" }.to_json - allow(service).to receive(:send_request).and_return(instance_double("Net::HTTPResponse", body: body)) + body = { 'errorCode' => '0', 'msg' => 'OK' }.to_json + allow(service).to receive(:send_request).and_return(instance_double('Net::HTTPResponse', body: body)) expect(service.call).to be_nil end it "returns nil if 'response' is null" do - body = { "errorCode" => "0", "response" => nil, "msg" => "OK" }.to_json - allow(service).to receive(:send_request).and_return(instance_double("Net::HTTPResponse", body: body)) + body = { 'errorCode' => '0', 'response' => nil, 'msg' => 'OK' }.to_json + allow(service).to receive(:send_request).and_return(instance_double('Net::HTTPResponse', body: body)) expect(service.call).to be_nil end end context "API failure (errorCode != '0')" do - it "raises ActivateCouponServiceError with msg" do - body = { "errorCode" => "COUPON_CANNOT_BE_MODIFIED", "msg" => "Coupon invalid", "response" => nil }.to_json - allow(service).to receive(:send_request).and_return(instance_double("Net::HTTPResponse", body: body)) + it 'raises ActivateCouponServiceError with msg' do + body = { 'errorCode' => 'COUPON_CANNOT_BE_MODIFIED', 'msg' => 'Coupon invalid', 'response' => nil }.to_json + allow(service).to receive(:send_request).and_return(instance_double('Net::HTTPResponse', body: body)) expect { service.call } - .to raise_error(described_class::ActivateCouponServiceError, "Coupon invalid") + .to raise_error(described_class::ActivateCouponServiceError, 'Coupon invalid') end - it "raises even if msg is missing" do - body = { "errorCode" => "COUPON_CANNOT_BE_MODIFIED" }.to_json - allow(service).to receive(:send_request).and_return(instance_double("Net::HTTPResponse", body: body)) + it 'raises even if msg is missing' do + body = { 'errorCode' => 'COUPON_CANNOT_BE_MODIFIED' }.to_json + allow(service).to receive(:send_request).and_return(instance_double('Net::HTTPResponse', body: body)) expect { service.call }.to raise_error(described_class::ActivateCouponServiceError) end end - context "bad/empty HTTP response" do - it "raises JSON::ParserError for invalid JSON" do - allow(service).to receive(:send_request).and_return(instance_double("Net::HTTPResponse", body: "not-json")) + context 'bad/empty HTTP response' do + it 'raises JSON::ParserError for invalid JSON' do + allow(service).to receive(:send_request).and_return(instance_double('Net::HTTPResponse', body: 'not-json')) expect { service.call }.to raise_error(JSON::ParserError) end - it "raises TypeError when response.body is nil (JSON.parse(nil))" do - allow(service).to receive(:send_request).and_return(instance_double("Net::HTTPResponse", body: nil)) + it 'raises TypeError when response.body is nil (JSON.parse(nil))' do + allow(service).to receive(:send_request).and_return(instance_double('Net::HTTPResponse', body: nil)) expect { service.call }.to raise_error(TypeError) end - it "raises NoMethodError when send_request returns nil (nil.body)" do + it 'raises NoMethodError when send_request returns nil (nil.body)' do allow(service).to receive(:send_request).and_return(nil) expect { service.call }.to raise_error(NoMethodError) end - it "bubbles exceptions from send_request (e.g., timeouts)" do + it 'bubbles exceptions from send_request (e.g., timeouts)' do allow(service).to receive(:send_request).and_raise(Timeout::Error) expect { service.call }.to raise_error(Timeout::Error) end diff --git a/spec/services/spl/coupons/deactivate_coupon_service_spec.rb b/spec/services/spl/coupons/deactivate_coupon_service_spec.rb index 15f4dbc..cf9b722 100644 --- a/spec/services/spl/coupons/deactivate_coupon_service_spec.rb +++ b/spec/services/spl/coupons/deactivate_coupon_service_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require "rails_helper" +require 'rails_helper' RSpec.describe Spl::Coupons::DeactivateCouponService do subject(:service) { described_class.new(user, store, params) } @@ -9,44 +9,44 @@ let(:store) { build_stubbed(:store) } let(:user) { build_stubbed(:user) } - let(:spl_url) { "https://spl.example.test" } - let(:prg_code) { "prg2" } - let(:access_token) { "token_abc" } + let(:spl_url) { 'https://spl.example.test' } + let(:prg_code) { 'prg2' } + let(:access_token) { 'token_abc' } - let(:coupon_code) { "COUPON" } + let(:coupon_code) { 'COUPON' } let(:params) { coupon_code } - let(:deactivate_url_string) { "https://api.spl.example.test/coupons/deactivate" } + let(:deactivate_url_string) { 'https://api.spl.example.test/coupons/deactivate' } let(:deactivate_uri) { URI.parse(deactivate_url_string) } let(:success_body_raw) do { - "errorCode" => "0", - "validationMessages" => nil, - "fieldValidationMessages" => nil, - "response" => { "returned" => nil }, - "msg" => "OK" + 'errorCode' => '0', + 'validationMessages' => nil, + 'fieldValidationMessages' => nil, + 'response' => { 'returned' => nil }, + 'msg' => 'OK' }.to_json end - let(:response) { instance_double("Net::HTTPResponse", body: success_body_raw) } + let(:response) { instance_double('Net::HTTPResponse', body: success_body_raw) } before do allow(store).to receive(:private_metadata).and_return( - "spl_url" => spl_url, - "spl_prg_code" => prg_code + 'spl_url' => spl_url, + 'spl_prg_code' => prg_code ) allow(user).to receive(:private_metadata).and_return( - "spl_access_token" => access_token + 'spl_access_token' => access_token ) - url_creator = instance_double("Spl::UrlCreatorService", coupon_deactivate: deactivate_url_string) + url_creator = instance_double('Spl::UrlCreatorService', coupon_deactivate: deactivate_url_string) allow(Spl::UrlCreatorService).to receive(:new).with(spl_url).and_return(url_creator) allow(service).to receive(:send_request).and_return(response) end - describe "#call" do + describe '#call' do context "success (errorCode == '0')" do it "sends request to coupon_deactivate URI with expected body and returns parsed 'response' hash" do expected_body = { @@ -59,72 +59,72 @@ expect(service).to receive(:send_request).with(deactivate_uri, expected_body).and_return(response) - expect(service.call).to eq({ "returned" => nil }) + expect(service.call).to eq({ 'returned' => nil }) end it "succeeds even if validation messages are present (still errorCode '0')" do body = { - "errorCode" => "0", - "validationMessages" => ["some warning"], - "fieldValidationMessages" => { "couponCode" => ["format warning"] }, - "response" => { "returned" => nil }, - "msg" => "OK" + 'errorCode' => '0', + 'validationMessages' => ['some warning'], + 'fieldValidationMessages' => { 'couponCode' => ['format warning'] }, + 'response' => { 'returned' => nil }, + 'msg' => 'OK' }.to_json - allow(service).to receive(:send_request).and_return(instance_double("Net::HTTPResponse", body: body)) + allow(service).to receive(:send_request).and_return(instance_double('Net::HTTPResponse', body: body)) - expect(service.call).to eq({ "returned" => nil }) + expect(service.call).to eq({ 'returned' => nil }) end it "returns nil if 'response' key is missing" do - body = { "errorCode" => "0", "msg" => "OK" }.to_json - allow(service).to receive(:send_request).and_return(instance_double("Net::HTTPResponse", body: body)) + body = { 'errorCode' => '0', 'msg' => 'OK' }.to_json + allow(service).to receive(:send_request).and_return(instance_double('Net::HTTPResponse', body: body)) expect(service.call).to be_nil end it "returns nil if 'response' is null" do - body = { "errorCode" => "0", "response" => nil, "msg" => "OK" }.to_json - allow(service).to receive(:send_request).and_return(instance_double("Net::HTTPResponse", body: body)) + body = { 'errorCode' => '0', 'response' => nil, 'msg' => 'OK' }.to_json + allow(service).to receive(:send_request).and_return(instance_double('Net::HTTPResponse', body: body)) expect(service.call).to be_nil end end context "API failure (errorCode != '0')" do - it "raises DeactivateCouponServiceError with msg" do - body = { "errorCode" => "COUPON_CANNOT_BE_MODIFIED", "msg" => "Coupon invalid", "response" => nil }.to_json - allow(service).to receive(:send_request).and_return(instance_double("Net::HTTPResponse", body: body)) + it 'raises DeactivateCouponServiceError with msg' do + body = { 'errorCode' => 'COUPON_CANNOT_BE_MODIFIED', 'msg' => 'Coupon invalid', 'response' => nil }.to_json + allow(service).to receive(:send_request).and_return(instance_double('Net::HTTPResponse', body: body)) expect { service.call } - .to raise_error(described_class::DeactivateCouponServiceError, "Coupon invalid") + .to raise_error(described_class::DeactivateCouponServiceError, 'Coupon invalid') end - it "raises even if msg is missing" do - body = { "errorCode" => "COUPON_CANNOT_BE_MODIFIED" }.to_json - allow(service).to receive(:send_request).and_return(instance_double("Net::HTTPResponse", body: body)) + it 'raises even if msg is missing' do + body = { 'errorCode' => 'COUPON_CANNOT_BE_MODIFIED' }.to_json + allow(service).to receive(:send_request).and_return(instance_double('Net::HTTPResponse', body: body)) expect { service.call }.to raise_error(described_class::DeactivateCouponServiceError) end end - context "bad/empty HTTP response" do - it "raises JSON::ParserError for invalid JSON" do - allow(service).to receive(:send_request).and_return(instance_double("Net::HTTPResponse", body: "not-json")) + context 'bad/empty HTTP response' do + it 'raises JSON::ParserError for invalid JSON' do + allow(service).to receive(:send_request).and_return(instance_double('Net::HTTPResponse', body: 'not-json')) expect { service.call }.to raise_error(JSON::ParserError) end - it "raises TypeError when response.body is nil (JSON.parse(nil))" do - allow(service).to receive(:send_request).and_return(instance_double("Net::HTTPResponse", body: nil)) + it 'raises TypeError when response.body is nil (JSON.parse(nil))' do + allow(service).to receive(:send_request).and_return(instance_double('Net::HTTPResponse', body: nil)) expect { service.call }.to raise_error(TypeError) end - it "raises NoMethodError when send_request returns nil (nil.body)" do + it 'raises NoMethodError when send_request returns nil (nil.body)' do allow(service).to receive(:send_request).and_return(nil) expect { service.call }.to raise_error(NoMethodError) end - it "bubbles exceptions from send_request (e.g., timeouts)" do + it 'bubbles exceptions from send_request (e.g., timeouts)' do allow(service).to receive(:send_request).and_raise(Timeout::Error) expect { service.call }.to raise_error(Timeout::Error) end From 315ce5c5c363ad5ca51324ee061c4f83440f0004 Mon Sep 17 00:00:00 2001 From: Tomasz Strzeszewski Date: Wed, 7 Jan 2026 19:17:12 +0100 Subject: [PATCH 37/63] rubocop offences --- .../checkout_controller_decorator_spec.rb | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/spec/controllers/checkout_controller_decorator_spec.rb b/spec/controllers/checkout_controller_decorator_spec.rb index d242dd9..cc3cb13 100644 --- a/spec/controllers/checkout_controller_decorator_spec.rb +++ b/spec/controllers/checkout_controller_decorator_spec.rb @@ -5,17 +5,17 @@ RSpec.describe Spree::CheckoutController, type: :controller do let(:store) do create(:store, private_metadata: { - 'spl_url' => 'https://fake-spl.example.com', - 'spl_mode' => 'test', - 'spl_pos_key' => 'fake-pos-key', - 'spl_api_user' => 'fake-api-user', - 'spl_prg_code' => 'fake-prg-code', - 'spl_sale_url' => 'https://fake-spl.example.com/sale', - 'spl_api_token' => 'fake-token', - 'spl_place_code' => 'fake-place', - 'spl_partner_code' => 'fake-partner', - 'spl_signature_seed' => 'fake-seed' - }) + 'spl_url' => 'https://fake-spl.example.com', + 'spl_mode' => 'test', + 'spl_pos_key' => 'fake-pos-key', + 'spl_api_user' => 'fake-api-user', + 'spl_prg_code' => 'fake-prg-code', + 'spl_sale_url' => 'https://fake-spl.example.com/sale', + 'spl_api_token' => 'fake-token', + 'spl_place_code' => 'fake-place', + 'spl_partner_code' => 'fake-partner', + 'spl_signature_seed' => 'fake-seed' + }) end let!(:state) { create(:state, country: create(:country_us), name: 'Gdansk', abbr: 'GDA') } From f87b46fe41b3f51edcabfc7c9e3eabac0bdbdc5f Mon Sep 17 00:00:00 2001 From: vi4me Date: Thu, 8 Jan 2026 13:50:39 +0100 Subject: [PATCH 38/63] EK-683 Fix translations --- config/locales/cs.yml | 4 +++- config/locales/en.yml | 3 --- config/locales/pl.yml | 4 +--- config/locales/sk.yml | 2 ++ 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/config/locales/cs.yml b/config/locales/cs.yml index 236a4ac..cc6aaa0 100644 --- a/config/locales/cs.yml +++ b/config/locales/cs.yml @@ -3,10 +3,12 @@ cs: loyalty_card_missing: K objednávce není přiřazena žádná věrnostní karta spl: user: + card_number: The club card number has been assigned to the account + card_active: SPL card activation + have_account: Do you have loyalty club account? accept_yc_terms_label: I accept the terms and conditions sent_to: The code was sent to %{phone} code_label: One-time code - have_account: Do you have loyalty club account? errors: must_accept_yc_terms: You must accept the loyalty club terms and conditions to continue invalid_phone: Podaj poprawny numer telefonu diff --git a/config/locales/en.yml b/config/locales/en.yml index d32f127..479fe8c 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -36,6 +36,3 @@ en: send_log_in_code: Send one-time code for log in send_registration_code: Send one-time code for registration verify_button: Verify phone number - user: - card_number: The club card number has been assigned to the account - card_active: SPL card activation diff --git a/config/locales/pl.yml b/config/locales/pl.yml index 0d9fba0..1509e2f 100644 --- a/config/locales/pl.yml +++ b/config/locales/pl.yml @@ -34,6 +34,4 @@ pl: send_log_in_code: Wyślij jednorazowy kod do logowania send_registration_code: Wyślij jednorazowy kod do rejestracji verify_button: Zweryfikuj numer telefonu - user: - card_number: Numer karty klubowej został przypisany do konta - card_active: Aktywacja karty SPL + diff --git a/config/locales/sk.yml b/config/locales/sk.yml index e6678c4..978cd39 100644 --- a/config/locales/sk.yml +++ b/config/locales/sk.yml @@ -3,6 +3,8 @@ sk: loyalty_card_missing: K objednávke nie je priradená žiadna vernostná karta spl: user: + card_number: The club card number has been assigned to the account + card_active: SPL card activation have_account: Do you have loyalty club account? accept_yc_terms_label: I accept the terms and conditions sent_to: The code was sent to %{phone} From c39a599d56ac2117ed4ede425d17a087bfc99a28 Mon Sep 17 00:00:00 2001 From: Tomasz Strzeszewski Date: Mon, 12 Jan 2026 22:11:50 +0100 Subject: [PATCH 39/63] Refactor, fix datetime parsing bug --- app/services/spl/base_spl_service.rb | 10 ---------- app/services/spl/coupons/get_coupons_service.rb | 16 +++++++++------- app/services/spl/login_account_service.rb | 3 ++- app/services/spl/me_service.rb | 3 ++- app/services/spl/oauth_token_service.rb | 3 ++- app/services/spl/register_account_service.rb | 3 ++- app/services/spl/request_otp_service.rb | 3 ++- app/services/spl/send_otp_service.rb | 3 ++- app/services/spl/sparta_loyalty_service.rb | 5 +++-- app/services/spl/validate_card_service.rb | 3 ++- app/views/spree/checkout/_spl_coupons.html.erb | 5 ++--- config/routes.rb | 2 +- .../spl/coupons/activate_coupon_service_spec.rb | 7 +++---- .../coupons/deactivate_coupon_service_spec.rb | 7 +++---- 14 files changed, 35 insertions(+), 38 deletions(-) delete mode 100644 app/services/spl/base_spl_service.rb diff --git a/app/services/spl/base_spl_service.rb b/app/services/spl/base_spl_service.rb deleted file mode 100644 index 1c01cf3..0000000 --- a/app/services/spl/base_spl_service.rb +++ /dev/null @@ -1,10 +0,0 @@ -# frozen_string_literal: true - -module Spl - # Base service accumulating shared methods to DRY subclasses - class BaseSplService - def send_request(url, body) - Spl::SendRequestService.new(url, body).call - end - end -end diff --git a/app/services/spl/coupons/get_coupons_service.rb b/app/services/spl/coupons/get_coupons_service.rb index 21446ae..9f0c9aa 100644 --- a/app/services/spl/coupons/get_coupons_service.rb +++ b/app/services/spl/coupons/get_coupons_service.rb @@ -4,8 +4,9 @@ module Spl module Coupons - class GetCouponsService < BaseSplService + class GetCouponsService class SplGetCouponError < StandardError; end + include SplServiceHelper def initialize(user, store) @store = store @@ -38,13 +39,14 @@ def prepare_body end def active?(coupon) - if coupon['used'] != true && coupon['usageTemporaryBlocked'] != true && coupon['expirationDate'].nil? - return true - end + coupon['used'] != true && coupon['usageTemporaryBlocked'] != true && correct_time?(coupon['expirationDate']) && + coupon['usageDisabled'] != true + end + + def correct_time?(date) + return Time.zone.parse(date).future? unless date.nil? - Time.zone.local(coupon['expirationDate']).future? - rescue StandardError - false + true end end end diff --git a/app/services/spl/login_account_service.rb b/app/services/spl/login_account_service.rb index a9e8385..9a80f51 100644 --- a/app/services/spl/login_account_service.rb +++ b/app/services/spl/login_account_service.rb @@ -3,8 +3,9 @@ require 'json' module Spl - class LoginAccountService < BaseSplService + class LoginAccountService class SplLoginAccountError < StandardError; end + include SplServiceHelper def initialize(user, store, params) @login_url = URI.parse(Spl::UrlCreatorService.new(store.private_metadata['spl_url']).login) diff --git a/app/services/spl/me_service.rb b/app/services/spl/me_service.rb index 7f7453b..194503e 100644 --- a/app/services/spl/me_service.rb +++ b/app/services/spl/me_service.rb @@ -3,8 +3,9 @@ require 'json' module Spl - class MeService < BaseSplService + class MeService class SplMeError < StandardError; end + include SplServiceHelper def initialize(user, store) @me_url = URI.parse(Spl::UrlCreatorService.new(store.private_metadata['spl_url']).me) diff --git a/app/services/spl/oauth_token_service.rb b/app/services/spl/oauth_token_service.rb index c75ab9d..24679f5 100644 --- a/app/services/spl/oauth_token_service.rb +++ b/app/services/spl/oauth_token_service.rb @@ -3,8 +3,9 @@ require 'json' module Spl - class OauthTokenService < BaseSplService + class OauthTokenService class OauthTokenError < StandardError; end + include SplServiceHelper def initialize(date, store) @date = date.to_i * 1000 diff --git a/app/services/spl/register_account_service.rb b/app/services/spl/register_account_service.rb index 1fc4939..db45ccd 100644 --- a/app/services/spl/register_account_service.rb +++ b/app/services/spl/register_account_service.rb @@ -3,8 +3,9 @@ require 'json' module Spl - class RegisterAccountService < BaseSplService + class RegisterAccountService class SplRegisterAccountError < StandardError; end + include SplServiceHelper def initialize(user, store, params) @register_url = URI.parse(Spl::UrlCreatorService.new(store.private_metadata['spl_url']).register) diff --git a/app/services/spl/request_otp_service.rb b/app/services/spl/request_otp_service.rb index 6699ff1..d8aed32 100644 --- a/app/services/spl/request_otp_service.rb +++ b/app/services/spl/request_otp_service.rb @@ -3,8 +3,9 @@ require 'json' module Spl - class RequestOtpService < BaseSplService + class RequestOtpService class SplRequestOtpError < StandardError; end + include SplServiceHelper def initialize(date, store, params) @date = date.to_i * 1000 diff --git a/app/services/spl/send_otp_service.rb b/app/services/spl/send_otp_service.rb index bab224e..58e85ae 100644 --- a/app/services/spl/send_otp_service.rb +++ b/app/services/spl/send_otp_service.rb @@ -3,8 +3,9 @@ require 'json' module Spl - class SendOtpService < BaseSplService + class SendOtpService class SplSendOtpError < StandardError; end + include SplServiceHelper def initialize(date, mobile_country, phone_number, store) @date = date.to_i * 1000 diff --git a/app/services/spl/sparta_loyalty_service.rb b/app/services/spl/sparta_loyalty_service.rb index 4c5faa3..df86506 100644 --- a/app/services/spl/sparta_loyalty_service.rb +++ b/app/services/spl/sparta_loyalty_service.rb @@ -5,8 +5,9 @@ require 'json' module Spl - class SpartaLoyaltyService < BaseSplService + class SpartaLoyaltyService class SplSendRequestError < StandardError; end + include SplServiceHelper def initialize(order_token, card_number, line_items, date, products, check_only, store) @order_token = order_token @@ -22,7 +23,7 @@ def initialize(order_token, card_number, line_items, date, products, check_only, def call Rails.logger.debug 'SPL LOYALTY SERVICE START' basket_body = prepare_basket_body - response = send_request(basket_body) + response = send_request(@url, basket_body) return unless response.is_a?(Net::HTTPSuccess) response_body = JSON.parse(response.body) diff --git a/app/services/spl/validate_card_service.rb b/app/services/spl/validate_card_service.rb index f41b30e..411ee3b 100644 --- a/app/services/spl/validate_card_service.rb +++ b/app/services/spl/validate_card_service.rb @@ -6,8 +6,9 @@ module Spl # Validates SPL card number - class ValidateCardService < BaseSplService + class ValidateCardService class SplCardValidationError < StandardError; end + include SplServiceHelper def initialize(card_number, user, store) @card_number = card_number diff --git a/app/views/spree/checkout/_spl_coupons.html.erb b/app/views/spree/checkout/_spl_coupons.html.erb index 7d5f581..2cb3aa1 100644 --- a/app/views/spree/checkout/_spl_coupons.html.erb +++ b/app/views/spree/checkout/_spl_coupons.html.erb @@ -4,11 +4,10 @@ <% @coupons&.each do |coupon| %> <%= coupon["typeCustomerName"] %> - <% if coupon.dig("couponType", "autoLoad") == "A" %> <% if coupon["autoLoad"] %> <%= button_to "Deactivate", - deactivate_coupon_asd_path( + deactivate_coupon_coupons_path( order: @order, token: @order.token, coupon_code: coupon["code"] @@ -18,7 +17,7 @@ class: "min-h-full ml-3 btn-primary !px-4 !py-3" %> <% else %> <%= button_to "Activate", - activate_coupon_asd_path( + activate_coupon_coupons_path( order: @order, token: @order.token, coupon_code: coupon["code"] diff --git a/config/routes.rb b/config/routes.rb index 039ccb8..8203900 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -17,7 +17,7 @@ end end - resource :checkout, as: 'asd', controller: :checkout, only: %i[show create update] do + resource :checkout, as: 'coupons', controller: :checkout, only: %i[show create update] do post :activate_coupon, to: 'checkout#activate_coupon' post :deactivate_coupon, to: 'checkout#deactivate_coupon' end diff --git a/spec/services/spl/coupons/activate_coupon_service_spec.rb b/spec/services/spl/coupons/activate_coupon_service_spec.rb index def323a..0569557 100644 --- a/spec/services/spl/coupons/activate_coupon_service_spec.rb +++ b/spec/services/spl/coupons/activate_coupon_service_spec.rb @@ -5,13 +5,12 @@ RSpec.describe Spl::Coupons::ActivateCouponService do subject(:service) { described_class.new(user, store, params) } - # Spree factories where possible - let(:store) { build_stubbed(:store) } - let(:user) { build_stubbed(:user) } + let(:user) { create(:user) } + let(:store) { Spree::Store.default } let(:spl_url) { 'https://spl.example.test' } let(:prg_code) { 'PRG123' } - let(:access_token) { 'token_abc' } + let(:access_token) { 'LPUXLUZYZ9JN8XLXSSCTZA4Y5LEFEX' } let(:coupon_code) { 'ABC123' } let(:params) { coupon_code } diff --git a/spec/services/spl/coupons/deactivate_coupon_service_spec.rb b/spec/services/spl/coupons/deactivate_coupon_service_spec.rb index cf9b722..f9f0cd9 100644 --- a/spec/services/spl/coupons/deactivate_coupon_service_spec.rb +++ b/spec/services/spl/coupons/deactivate_coupon_service_spec.rb @@ -5,13 +5,12 @@ RSpec.describe Spl::Coupons::DeactivateCouponService do subject(:service) { described_class.new(user, store, params) } - # Spree factories where possible - let(:store) { build_stubbed(:store) } - let(:user) { build_stubbed(:user) } + let(:user) { create(:user) } + let(:store) { Spree::Store.default } let(:spl_url) { 'https://spl.example.test' } let(:prg_code) { 'prg2' } - let(:access_token) { 'token_abc' } + let(:access_token) { 'LPUXLUZYZ9JN8XLXSSCTZA4Y5LEFEX' } let(:coupon_code) { 'COUPON' } let(:params) { coupon_code } From 25151d9c2880dc928547e3e4a0dce4066524e508 Mon Sep 17 00:00:00 2001 From: vi4me Date: Tue, 13 Jan 2026 09:05:16 +0100 Subject: [PATCH 40/63] EK-683 Remove msg from register_response_body for pl translations --- app/services/spl/register_account_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/spl/register_account_service.rb b/app/services/spl/register_account_service.rb index a236d13..5682d80 100644 --- a/app/services/spl/register_account_service.rb +++ b/app/services/spl/register_account_service.rb @@ -22,7 +22,7 @@ def call register_response = send_request(@register_url, register_body) register_response_body = JSON.parse(register_response.body) Rails.logger.debug register_response_body - raise SplRegisterAccountError, register_response_body['msg'] if register_response_body['errorCode'] != '0' + raise SplRegisterAccountError, register_response_body if register_response_body['errorCode'] != '0' spl_card = register_response_body.dig('response', 'cardNo') update_account(spl_card, oauth_response_body) From 9d990bda2dcae75d7f5b9b7c7bbbf08ba8f047bd Mon Sep 17 00:00:00 2001 From: nika-piotrowska Date: Thu, 15 Jan 2026 17:40:15 +0100 Subject: [PATCH 41/63] [EK-680] Fix pl translations --- config/locales/pl.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config/locales/pl.yml b/config/locales/pl.yml index 818bb81..62a7ff6 100644 --- a/config/locales/pl.yml +++ b/config/locales/pl.yml @@ -27,9 +27,9 @@ pl: temporary_blocked: Tymczasowo zablokowano z powodu zbyt wielu prób. Spróbuj ponownie później. person_not_found: Nie znaleziono użytkownika buttons: - connect_account: Połącz z kontem lojalnościowym - send_code: Wyślij jednorazowy kod - verify_button: Zweryfikuj numer telefonu + connect_account: Połącz z kontem lojalnościowym + send_code: Wyślij jednorazowy kod + verify_button: Zweryfikuj numer telefonu user: card_number: Numer karty klubowej został przypisany do konta card_active: Aktywacja karty SPL From b7dd316c37d1907f901fbab6f019f9a9e2223d42 Mon Sep 17 00:00:00 2001 From: nika-piotrowska Date: Thu, 15 Jan 2026 17:40:57 +0100 Subject: [PATCH 42/63] [EK-680] Fix uptade user method in profile controller decorator --- .../storefront/profile_controller_decorator.rb | 2 +- app/services/phone_parser_service.rb | 17 +++++++++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/app/controllers/spl/spree/storefront/profile_controller_decorator.rb b/app/controllers/spl/spree/storefront/profile_controller_decorator.rb index 7a72e85..63afb3b 100644 --- a/app/controllers/spl/spree/storefront/profile_controller_decorator.rb +++ b/app/controllers/spl/spree/storefront/profile_controller_decorator.rb @@ -147,7 +147,7 @@ def send_otp(phone, store) end def update_user_after_otp_request - try_spree_current_user.update!( + try_spree_current_user.update( phone: login_code_params[:phone], public_metadata: (try_spree_current_user.public_metadata || {}).merge('accept_yc_terms' => true) ) diff --git a/app/services/phone_parser_service.rb b/app/services/phone_parser_service.rb index c02a539..353851c 100644 --- a/app/services/phone_parser_service.rb +++ b/app/services/phone_parser_service.rb @@ -1,13 +1,22 @@ # frozen_string_literal: true class PhoneParserService - attr_reader :phone + COUNTRY_CODE_REGEX = /\A\+\d{1,3}\d+\z/.freeze + + attr_reader :raw, :phone def initialize(raw_phone) - @phone = Phonelib.parse(raw_phone) + @raw = raw_phone.to_s.strip + @phone = Phonelib.parse(@raw) end - delegate :valid?, to: :phone + def valid? + has_country_code? && phone.valid? + end + + def has_country_code? + raw.match?(COUNTRY_CODE_REGEX) + end def country_code return unless valid? @@ -23,7 +32,7 @@ def national_number def e164 return unless valid? - + phone.e164 end From 0cf6b9d3262eac93fc366edfc30bea690b8040ee Mon Sep 17 00:00:00 2001 From: Tomasz Strzeszewski Date: Fri, 16 Jan 2026 12:51:09 +0100 Subject: [PATCH 43/63] rubocop --- app/services/phone_parser_service.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/services/phone_parser_service.rb b/app/services/phone_parser_service.rb index 353851c..e36da21 100644 --- a/app/services/phone_parser_service.rb +++ b/app/services/phone_parser_service.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class PhoneParserService - COUNTRY_CODE_REGEX = /\A\+\d{1,3}\d+\z/.freeze + COUNTRY_CODE_REGEX = /\A\+\d{1,3}\d+\z/ attr_reader :raw, :phone @@ -11,10 +11,10 @@ def initialize(raw_phone) end def valid? - has_country_code? && phone.valid? + country_code? && phone.valid? end - def has_country_code? + def country_code? raw.match?(COUNTRY_CODE_REGEX) end @@ -32,7 +32,7 @@ def national_number def e164 return unless valid? - + phone.e164 end From e4111c5fa809a55957c31b65d4a2ecd0b16bb8e1 Mon Sep 17 00:00:00 2001 From: Tomasz Strzeszewski Date: Fri, 16 Jan 2026 13:03:37 +0100 Subject: [PATCH 44/63] Add translations --- app/views/spree/checkout/_spl_coupons.html.erb | 4 ++-- config/locales/en.yml | 2 ++ config/locales/pl.yml | 5 +++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/app/views/spree/checkout/_spl_coupons.html.erb b/app/views/spree/checkout/_spl_coupons.html.erb index 2cb3aa1..f435677 100644 --- a/app/views/spree/checkout/_spl_coupons.html.erb +++ b/app/views/spree/checkout/_spl_coupons.html.erb @@ -6,7 +6,7 @@ <%= coupon["typeCustomerName"] %> <% if coupon.dig("couponType", "autoLoad") == "A" %> <% if coupon["autoLoad"] %> - <%= button_to "Deactivate", + <%= button_to I18n.t('spl.coupons.deactivate'), deactivate_coupon_coupons_path( order: @order, token: @order.token, @@ -16,7 +16,7 @@ data: { turbo: true }, class: "min-h-full ml-3 btn-primary !px-4 !py-3" %> <% else %> - <%= button_to "Activate", + <%= button_to I18n.t('spl.coupons.activate'), activate_coupon_coupons_path( order: @order, token: @order.token, diff --git a/config/locales/en.yml b/config/locales/en.yml index 57ab8b6..d49c4e5 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -10,6 +10,8 @@ en: coupons: find: checkout_list: Your SPL coupons + activate: Aktywuj + deactivate: Dezaktywuj user: card_number: The club card number has been assigned to the account card_active: SPL card activation diff --git a/config/locales/pl.yml b/config/locales/pl.yml index e3012c0..1530955 100644 --- a/config/locales/pl.yml +++ b/config/locales/pl.yml @@ -7,6 +7,11 @@ pl: card_not_active: Karta nie jest aktywna card_not_match: Karta nie jest zgodna z numerem karty użytkownika wrong_owner: Karta przypisana innemu użytkownikowi + coupons: + find: + checkout_list: Your SPL coupons + activate: Aktywuj + deactivate: Dezaktywuj user: card_number: Numer karty klubowej został przypisany do konta card_active: Aktywacja karty SPL From 14cde14d466ec88339a265bb6f21d78cdfdedca5 Mon Sep 17 00:00:00 2001 From: vi4me Date: Mon, 19 Jan 2026 10:28:27 +0100 Subject: [PATCH 45/63] Create ProfileControllerHelper, fix handle_spl_error, rebase JS --- .../profile_controller_decorator.rb | 62 +++---------------- app/helpers/profile_controller_helper.rb | 36 +++++++++++ app/views/spl/_loyalty_connect_form.html.erb | 23 ------- config/locales/cs.yml | 1 + config/locales/en.yml | 1 + config/locales/pl.yml | 1 + config/locales/sk.yml | 1 + config/routes.rb | 8 +-- 8 files changed, 54 insertions(+), 79 deletions(-) create mode 100644 app/helpers/profile_controller_helper.rb diff --git a/app/controllers/spl/spree/storefront/profile_controller_decorator.rb b/app/controllers/spl/spree/storefront/profile_controller_decorator.rb index fd1cffb..c289e8f 100644 --- a/app/controllers/spl/spree/storefront/profile_controller_decorator.rb +++ b/app/controllers/spl/spree/storefront/profile_controller_decorator.rb @@ -5,6 +5,7 @@ module Spree module Storefront module ProfileControllerDecorator include BooleanHelper + include ProfileControllerHelper def self.prepended(base) base.before_action :validate_spl_no_card, only: :update @@ -14,10 +15,10 @@ def self.prepended(base) def login_code send_otp(phone_parser, current_store) update_user_after_otp_request - render_login_code_success(phone_parser, 'otp_code_form') + render_login_code_success(try_spree_current_user, phone_parser, 'otp_code_form') rescue Spl::SendOtpService::SplSendOtpError => e handle_spl_error(e) - render_login_code_error + render_login_code_error(try_spree_current_user) end def connect_loyalty_account @@ -27,16 +28,16 @@ def connect_loyalty_account rescue Spl::LoginAccountService::SplLoginAccountError, AssignSpartaCardNumberService::AssignSpartaCardNumberError, Spl::MeService::SplMeError => e handle_spl_error(e) - render_connect_loyalty_account_error(try_spree_current_user.phone, 'otp_code_form') + render_connect_loyalty_account_error(try_spree_current_user, try_spree_current_user.phone, 'otp_code_form') end def registration_code request_otp(phone_parser, current_store, params['user']) update_user_after_otp_request - render_login_code_success(phone_parser, 'otp_registration_form') + render_login_code_success(try_spree_current_user, phone_parser, 'otp_registration_form') rescue Spl::RequestOtpService::SplRequestOtpError, Spl::OauthTokenService::OauthTokenError => e handle_spl_error(e) - render_login_code_error + render_login_code_error(try_spree_current_user) end def register_loyalty_account @@ -45,7 +46,8 @@ def register_loyalty_account notice: ::Spree.t(:successfully_updated, resource: ::Spree.t(:account)) rescue Spl::RegisterAccountService::SplRegisterAccountError, Spl::OauthTokenService::OauthTokenError => e handle_spl_error(e) - render_connect_loyalty_account_error(try_spree_current_user.phone, 'otp_registration_form') + user = try_spree_current_user + render_connect_loyalty_account_error(user, user.phone, 'otp_registration_form') end private @@ -65,11 +67,7 @@ def validate_login_code_request validate_yc_terms validate_phone - render_login_code_error if try_spree_current_user.errors.any? - end - - def send_otp(phone, store) - Spl::SendOtpService.new(DateTime.current, phone.country_code, phone.national_number, store).call + render_login_code_error(try_spree_current_user) if try_spree_current_user.errors.any? end def request_otp(phone, store, params) @@ -131,42 +129,11 @@ def clear_errors try_spree_current_user.errors.clear end - def update_user_after_otp_request - try_spree_current_user.update!( - phone: login_code_params[:phone], - public_metadata: (try_spree_current_user.public_metadata || {}).merge('accept_yc_terms' => true) - ) - end - def assign_card_number(user, store, params) Spl::LoginAccountService.new(user, store, params).call AssignSpartaCardNumberService.new(user, store).call end - def render_login_code_success(phone, partial) - render turbo_stream: turbo_stream.replace( - 'loyalty_connect_form', - partial: "spl/#{partial}", - locals: { - user: try_spree_current_user, - phone_e164: phone.respond_to?(:e164) ? phone.e164 : nil - } - ), - status: :ok - end - - def render_connect_loyalty_account_error(phone, partial) - render turbo_stream: turbo_stream.replace( - partial, - partial: "spl/#{partial}", - locals: { - user: try_spree_current_user, - phone_e164: phone.respond_to?(:e164) ? phone.e164 : nil - } - ), - status: :unprocessable_content - end - def send_otp(phone, store) Spl::SendOtpService.new(DateTime.current, phone.country_code, phone.national_number, store).call end @@ -180,21 +147,12 @@ def update_user_after_otp_request def handle_spl_error(error) payload = Spl::ErrorPayloadParser.parse(error.message) || error - msg = Spl::ErrorTranslator.translate(payload) + msg = Spl::ErrorTranslator.translate(payload || { errorCode: I18n.t('spl.errors.unknow_error') }) clear_errors try_spree_current_user.errors.add(:base, msg) end - def render_login_code_error - render turbo_stream: turbo_stream.replace( - 'loyalty_connect_form', - partial: 'spl/loyalty_connect_form', - locals: { user: try_spree_current_user } - ), - status: :unprocessable_content - end - def login_code_params params.require(:user).permit(:phone, :accept_yc_terms) end diff --git a/app/helpers/profile_controller_helper.rb b/app/helpers/profile_controller_helper.rb new file mode 100644 index 0000000..0308bfc --- /dev/null +++ b/app/helpers/profile_controller_helper.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module ProfileControllerHelper + def render_login_code_error(user) + render turbo_stream: turbo_stream.replace( + 'loyalty_connect_form', + partial: 'spl/loyalty_connect_form', + locals: { user: user } + ), + status: :unprocessable_content + end + + def render_login_code_success(user, phone, partial) + render turbo_stream: turbo_stream.replace( + 'loyalty_connect_form', + partial: "spl/#{partial}", + locals: { + user: user, + phone_e164: phone.respond_to?(:e164) ? phone.e164 : nil + } + ), + status: :ok + end + + def render_connect_loyalty_account_error(user, phone, partial) + render turbo_stream: turbo_stream.replace( + partial, + partial: "spl/#{partial}", + locals: { + user: user, + phone_e164: phone.respond_to?(:e164) ? phone.e164 : nil + } + ), + status: :unprocessable_content + end +end diff --git a/app/views/spl/_loyalty_connect_form.html.erb b/app/views/spl/_loyalty_connect_form.html.erb index 50979a8..8e78767 100644 --- a/app/views/spl/_loyalty_connect_form.html.erb +++ b/app/views/spl/_loyalty_connect_form.html.erb @@ -64,26 +64,3 @@ <% end %>
<% end %> - - diff --git a/config/locales/cs.yml b/config/locales/cs.yml index 53bece2..21aec17 100644 --- a/config/locales/cs.yml +++ b/config/locales/cs.yml @@ -34,6 +34,7 @@ cs: coupon_already_used: This coupon has already been used. temporary_blocked: Temporarily blocked due to too many attempts. Please try again later. person_not_found: Person not found. + unknow_error: NEZNÁMÁ_CHYBA buttons: connect_account: Connect loyalty account send_log_in_code: Send one-time code for log in diff --git a/config/locales/en.yml b/config/locales/en.yml index 7438402..46a8434 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -34,6 +34,7 @@ en: coupon_already_used: This coupon has already been used. temporary_blocked: Temporarily blocked due to too many attempts. Please try again later. person_not_found: Person not found. + unknow_error: UNKNOWN_ERROR buttons: connect_account: Connect loyalty account send_log_in_code: Send one-time code for log in diff --git a/config/locales/pl.yml b/config/locales/pl.yml index 63c14d6..ffa5a3d 100644 --- a/config/locales/pl.yml +++ b/config/locales/pl.yml @@ -32,6 +32,7 @@ pl: coupon_already_used: Ten kupon został już wykorzystany. temporary_blocked: Tymczasowo zablokowano z powodu zbyt wielu prób. Spróbuj ponownie później. person_not_found: Nie znaleziono użytkownika + unknow_error: NIEZNANY_BŁĄD buttons: connect_account: Połącz z kontem lojalnościowym send_log_in_code: Wyślij jednorazowy kod do logowania diff --git a/config/locales/sk.yml b/config/locales/sk.yml index 9cf3b5c..ddca18a 100644 --- a/config/locales/sk.yml +++ b/config/locales/sk.yml @@ -34,6 +34,7 @@ sk: coupon_already_used: This coupon has already been used. temporary_blocked: Temporarily blocked due to too many attempts. Please try again later. person_not_found: Person not found. + unknow_error: NEZNÁMA_CHYBA buttons: connect_account: Connect loyalty account send_log_in_code: Send one-time code for log in diff --git a/config/routes.rb b/config/routes.rb index bee9b17..48609ba 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -18,10 +18,10 @@ end namespace :account do resource :profile, controller: :profile, only: [:edit, :update] do - patch :registration_code, to: 'profile#registration_code' - post :register_loyalty_account, to: 'profile#register_loyalty_account' - patch :login_code, to: 'profile#login_code' - post :connect_loyalty_account, to: 'profile#connect_loyalty_account' + patch :registration_code + post :register_loyalty_account + patch :login_code + post :connect_loyalty_account end end end From 9780e9f6da10a558c69c2fd104ed91b8d2d1e579 Mon Sep 17 00:00:00 2001 From: Tomasz Strzeszewski Date: Mon, 19 Jan 2026 18:57:10 +0100 Subject: [PATCH 46/63] Add token refreshing mechanism and safety clauses for expired token --- Gemfile.lock | 361 ++++++++++++++++-- .../checkout_controller_decorator.rb | 6 + .../profile_controller_decorator.rb | 13 +- app/helpers/error_handling_helper.rb | 22 ++ app/helpers/spl_service_helper.rb | 13 + .../spl/coupons/activate_coupon_service.rb | 13 + .../spl/coupons/deactivate_coupon_service.rb | 13 + .../spl/coupons/get_coupons_service.rb | 13 + app/services/spl/login_account_service.rb | 4 +- app/services/spl/me_service.rb | 13 + app/services/spl/oauth_token_service.rb | 22 ++ .../checkout_controller_decorator_spec.rb | 11 +- .../profile_controller_decorator_spec.rb | 8 +- spec/services/phone_parser_service_spec.rb | 2 +- 14 files changed, 463 insertions(+), 51 deletions(-) create mode 100644 app/helpers/error_handling_helper.rb diff --git a/Gemfile.lock b/Gemfile.lock index 909dd67..e05004c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -116,20 +116,20 @@ GEM public_suffix (>= 2.0.2, < 8.0) any_ascii (0.3.3) ast (2.4.3) - async (2.35.1) + async (2.35.3) console (~> 1.29) fiber-annotation io-event (~> 1.11) metrics (~> 0.12) traces (~> 0.18) - async-http (0.92.1) + async-http (0.94.0) async (>= 2.10.2) async-pool (~> 0.11) io-endpoint (~> 0.14) io-stream (~> 0.6) metrics (~> 0.12) - protocol-http (~> 0.49) - protocol-http1 (~> 0.30) + protocol-http (~> 0.58) + protocol-http1 (~> 0.36) protocol-http2 (~> 0.22) protocol-url (~> 0.2) traces (~> 0.10) @@ -150,10 +150,9 @@ GEM brakeman (7.1.2) racc builder (3.3.0) - byebug (12.0.0) + byebug (13.0.0) + reline (>= 0.6.0) cancancan (3.6.1) - canonical-rails (0.2.17) - actionview (>= 4.1, < 8.1) capybara (3.40.0) addressable matrix @@ -228,7 +227,14 @@ GEM faraday-net_http (3.4.2) net-http (~> 0.5) ffaker (2.25.0) + ffi (1.17.3-aarch64-linux-gnu) + ffi (1.17.3-aarch64-linux-musl) + ffi (1.17.3-arm-linux-gnu) + ffi (1.17.3-arm-linux-musl) + ffi (1.17.3-arm64-darwin) ffi (1.17.3-x86_64-darwin) + ffi (1.17.3-x86_64-linux-gnu) + ffi (1.17.3-x86_64-linux-musl) fiber-annotation (0.2.0) fiber-local (1.1.0) fiber-storage @@ -275,7 +281,7 @@ GEM image_processing (1.14.0) mini_magick (>= 4.9.5, < 6) ruby-vips (>= 2.0.17, < 3) - importmap-rails (2.2.2) + importmap-rails (2.2.3) actionpack (>= 6.0.0) activesupport (>= 6.0.0) railties (>= 6.0.0) @@ -365,8 +371,22 @@ GEM net-smtp (0.5.1) net-protocol nio4r (2.7.5) + nokogiri (1.19.0-aarch64-linux-gnu) + racc (~> 1.4) + nokogiri (1.19.0-aarch64-linux-musl) + racc (~> 1.4) + nokogiri (1.19.0-arm-linux-gnu) + racc (~> 1.4) + nokogiri (1.19.0-arm-linux-musl) + racc (~> 1.4) + nokogiri (1.19.0-arm64-darwin) + racc (~> 1.4) nokogiri (1.19.0-x86_64-darwin) racc (~> 1.4) + nokogiri (1.19.0-x86_64-linux-gnu) + racc (~> 1.4) + nokogiri (1.19.0-x86_64-linux-musl) + racc (~> 1.4) observer (0.1.2) octokit (4.25.1) faraday (>= 1, < 3) @@ -377,7 +397,7 @@ GEM parallel (1.27.0) paranoia (3.1.0) activerecord (>= 7, < 8.2) - parser (3.3.10.0) + parser (3.3.10.1) ast (~> 2.4.1) racc phonelib (0.10.15) @@ -385,12 +405,12 @@ GEM pp (0.6.3) prettyprint prettyprint (0.2.0) - prism (1.7.0) + prism (1.8.0) protocol-hpack (1.5.1) - protocol-http (0.57.0) - protocol-http1 (0.35.2) - protocol-http (~> 0.22) - protocol-http2 (0.23.0) + protocol-http (0.58.0) + protocol-http1 (0.36.0) + protocol-http (~> 0.58) + protocol-http2 (0.24.0) protocol-hpack (~> 1.4) protocol-http (~> 0.47) protocol-url (0.4.0) @@ -457,9 +477,9 @@ GEM activerecord (>= 7.2) activesupport (>= 7.2) i18n - rbs (3.10.1) + rbs (3.10.2) logger - rdoc (7.0.3) + rdoc (7.1.0) erb psych (>= 4.0.0) tsort @@ -525,7 +545,7 @@ GEM rubocop (>= 1.72) rubocop-performance (>= 1.24) rubocop-rails (>= 2.30) - rubocop-rspec (3.8.0) + rubocop-rspec (3.9.0) lint_roller (~> 1.1) rubocop (~> 1.81) ruby-lsp (0.26.4) @@ -553,23 +573,23 @@ GEM simplecov_json_formatter (~> 0.1) simplecov-html (0.13.2) simplecov_json_formatter (0.1.4) - spree (5.2.4) - spree_api (= 5.2.4) - spree_cli (= 5.2.4) - spree_core (= 5.2.4) - spree_api (5.2.4) + spree (5.2.6) + spree_api (= 5.2.6) + spree_cli (= 5.2.6) + spree_core (= 5.2.6) + spree_api (5.2.6) bcrypt (~> 3.1) doorkeeper (~> 5.3) jsonapi-serializer (~> 2.1) - spree_core (= 5.2.4) + spree_core (= 5.2.6) spree_auth_devise (4.6.3) devise (~> 4.7) devise-encryptable (= 0.2.0) spree_core (>= 4.5.0) spree_extension - spree_cli (5.2.4) + spree_cli (5.2.6) thor (~> 1.0) - spree_core (5.2.4) + spree_core (5.2.6) active_storage_validations (= 1.3.0) activemerchant (~> 1.67) acts-as-taggable-on @@ -595,7 +615,7 @@ GEM ostruct paranoia (>= 2.4) phonelib (~> 0.10) - rails (>= 7.2, < 8.1) + rails (>= 7.2, < 8.2) ransack (>= 4.1) request_store (~> 1.7) rexml @@ -637,15 +657,14 @@ GEM spree_extension (0.1.0) activerecord (>= 4.2) spree_core - spree_storefront (5.2.4) + spree_storefront (5.2.6) active_link_to - canonical-rails (~> 0.2.14) heroicon importmap-rails inline_svg (~> 1.10) local_time (~> 3.0) mail_form - spree_core (>= 5.2.4) + spree_core (>= 5.2.6) stimulus-rails tailwindcss-rails tailwindcss-ruby @@ -658,7 +677,14 @@ GEM actionpack (>= 6.1) activesupport (>= 6.1) sprockets (>= 3.0.0) + sqlite3 (2.9.0-aarch64-linux-gnu) + sqlite3 (2.9.0-aarch64-linux-musl) + sqlite3 (2.9.0-arm-linux-gnu) + sqlite3 (2.9.0-arm-linux-musl) + sqlite3 (2.9.0-arm64-darwin) sqlite3 (2.9.0-x86_64-darwin) + sqlite3 (2.9.0-x86_64-linux-gnu) + sqlite3 (2.9.0-x86_64-linux-musl) state_machines (0.100.4) state_machines-activemodel (0.101.0) activemodel (>= 7.2) @@ -673,7 +699,13 @@ GEM tailwindcss-rails (4.4.0) railties (>= 7.0.0) tailwindcss-ruby (~> 4.0) + tailwindcss-ruby (4.1.18) + tailwindcss-ruby (4.1.18-aarch64-linux-gnu) + tailwindcss-ruby (4.1.18-aarch64-linux-musl) + tailwindcss-ruby (4.1.18-arm64-darwin) tailwindcss-ruby (4.1.18-x86_64-darwin) + tailwindcss-ruby (4.1.18-x86_64-linux-gnu) + tailwindcss-ruby (4.1.18-x86_64-linux-musl) terminal-table (4.0.0) unicode-display_width (>= 1.1.1, < 4) thor (1.5.0) @@ -746,5 +778,274 @@ DEPENDENCIES sqlite3 (>= 2.0) webmock +CHECKSUMS + abbrev (0.1.2) sha256=ad1b4eaaaed4cb722d5684d63949e4bde1d34f2a95e20db93aecfe7cbac74242 + actioncable (7.2.3) sha256=e15d17b245f1dfe7cafdda4a0c6f7ba8ebaab1af33884415e09cfef4e93ad4f9 + actionmailbox (7.2.3) sha256=16bbf0a7c330f2d08d52d5e3c1b03813a8ef60bfb0a48e89c0bf92b069cb4d5e + actionmailer (7.2.3) sha256=68d646b852a6d2b25d8834fc796c3dc10f76a4c7fd77b3251c3f4dd832ec8ab8 + actionpack (7.2.3) sha256=2a14e4c64695777041ea7aaf498462284cadd561f009654393daf9b2de7207cf + actiontext (7.2.3) sha256=a6ffd9efb7b7b4e26029e5c88e8a2ea9aae8d6cefdfed960be139772f1a94037 + actionview (7.2.3) sha256=1f427d7a41b43804d7250911535740451b9c32b6416239d87e6dab9d5948ecb2 + active_link_to (1.0.5) sha256=4830847b3d14589df1e9fc62038ceec015257fce975ec1c2a77836c461b139ba + active_storage_validations (1.3.0) sha256=788e4189d51c7b55049bf2825ede964baa2fe531e22a31c59f67a962ebf8d71e + activejob (7.2.3) sha256=e44964472de267b69e93752f088193c8ad2e56d2ef451d059dd7a53761e5ffb0 + activemerchant (1.137.0) sha256=fdf5e33f5c94d1981bd632d72e282c0a5afaf06bac6df3f933c08a95e98bd5ce + activemodel (7.2.3) sha256=bbaf66aeb93212e98ebf6ab900f8290f9a831645f0b235427f5acf0e074739db + activerecord (7.2.3) sha256=6facb7478ceb5f6baa9f0647daa50b4a3a43934997900f0011e6c667ff41a0d7 + activestorage (7.2.3) sha256=4c1422bbfaa60c89e7b43cc38ade7bd3b8dc81024c48a21c1ac56814cf34ca2f + activesupport (7.2.3) sha256=5675c9770dac93e371412684249f9dc3c8cec104efd0624362a520ae685c7b10 + acts-as-taggable-on (13.0.0) sha256=dca776c6ddebc458d175b57554ad692488e283f27047117e01ed871f1cba6135 + acts_as_list (1.2.6) sha256=8345380900b7bee620c07ad00991ccee59af3d8c9e8574f426e321da2865fdc8 + addressable (2.8.8) sha256=7c13b8f9536cf6364c03b9d417c19986019e28f7c00ac8132da4eb0fe393b057 + any_ascii (0.3.3) sha256=9bba37e4aadc7c57bc29e4bae520418416d1b69ea167d04f2aeaf9c07166f724 + ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383 + async (2.35.3) sha256=6bfc75ac10182d0485b1394a50803b827f65e73f9e3ebe0aeaa4ecd770238271 + async-http (0.94.0) sha256=479dc4a11c078aff7e054a729fc79fef7d0c28ca26bfe81a4d125672b28759ea + async-http-faraday (0.22.1) sha256=414c3197f32157bc0a7a187b1fb4c669097a8ef71832613eff2371c9808e9923 + async-pool (0.11.1) sha256=98e1583e199a75f7dc70f8e65fc8d0d3b28636c3f256595d43e206642ad8fbda + auto_strip_attributes (2.6.0) sha256=a7e2e0cf744de2bcd947fd68014220702bcc88c81274c1cd9ce6f7316aae39b0 + awesome_nested_set (3.9.0) sha256=3ce99e816550f97f4de118e621630070aacf24928b920fe4a68846578a8daaed + awesome_print (1.9.2) sha256=e99b32b704acff16d768b3468680793ced40bfdc4537eb07e06a4be11133786e + base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b + bcrypt (3.1.21) sha256=5964613d750a42c7ee5dc61f7b9336fb6caca429ba4ac9f2011609946e4a2dcf + benchmark (0.5.0) sha256=465df122341aedcb81a2a24b4d3bd19b6c67c1530713fd533f3ff034e419236c + bigdecimal (4.0.1) sha256=8b07d3d065a9f921c80ceaea7c9d4ae596697295b584c296fe599dd0ad01c4a7 + brakeman (7.1.2) sha256=6b04927710a2e7d13a72248b5d404c633188e02417f28f3d853e4b6370d26dce + builder (3.3.0) sha256=497918d2f9dca528fdca4b88d84e4ef4387256d984b8154e9d5d3fe5a9c8835f + byebug (13.0.0) sha256=d2263efe751941ca520fa29744b71972d39cbc41839496706f5d9b22e92ae05d + cancancan (3.6.1) sha256=975c1d5cbf58d5df48a9452a7f61ae3d254608cd87570402f5925a8864c56b62 + capybara (3.40.0) sha256=42dba720578ea1ca65fd7a41d163dd368502c191804558f6e0f71b391054aeef + capybara-screenshot (1.0.26) sha256=816b9370a07752097c82a05f568aaf5d3b7f45c3db5d3aab2014071e1b3c0c77 + carmen (1.1.3) sha256=970dbcb6a1743f0c7d765ebf4f0e5b59e99c4f96460baaa22e7306d5a2952e68 + cgi (0.5.1) sha256=e93fcafc69b8a934fe1e6146121fa35430efa8b4a4047c4893764067036f18e9 + childprocess (5.1.0) sha256=9a8d484be2fd4096a0e90a0cd3e449a05bc3aa33f8ac9e4d6dcef6ac1455b6ec + coderay (1.1.3) sha256=dc530018a4684512f8f38143cd2a096c9f02a1fc2459edcfe534787a7fc77d4b + concurrent-ruby (1.3.6) sha256=6b56837e1e7e5292f9864f34b69c5a2cbc75c0cf5338f1ce9903d10fa762d5ab + connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a + console (1.34.2) sha256=1c036abf606ccec83f9dc28f0c31710fe5936ffe7ba5d235ae2865590a482d58 + countries (8.1.0) sha256=4d6b318b8e906f1f769d5c021c13a418d33e917dc96ceb625a91d8e7ab2d192e + crack (1.0.1) sha256=ff4a10390cd31d66440b7524eb1841874db86201d5b70032028553130b6d4c7e + crass (1.0.6) sha256=dc516022a56e7b3b156099abc81b6d2b08ea1ed12676ac7a5657617f012bd45d + csv (3.3.5) sha256=6e5134ac3383ef728b7f02725d9872934f523cb40b961479f69cf3afa6c8e73f + database_cleaner (2.1.0) sha256=1dcba26e3b1576da692fc6bac10136a4744da5bcc293d248aae19640c65d89cd + database_cleaner-active_record (2.2.2) sha256=88296b9f3088c31f7c0d4fcec10f68e4b71c96698043916de59b04debec10388 + database_cleaner-core (2.0.1) sha256=8646574c32162e59ed7b5258a97a208d3c44551b854e510994f24683865d846c + date (3.5.1) sha256=750d06384d7b9c15d562c76291407d89e368dda4d4fff957eb94962d325a0dc0 + deface (1.9.0) sha256=b6da77da3618efa4ec33474debc7e95f8b4d84775f34b9d13799d97f6aef3806 + devise (4.9.4) sha256=920042fe5e704c548aa4eb65ebdd65980b83ffae67feb32c697206bfd975a7f8 + devise-encryptable (0.2.0) sha256=9860caed9484030c438cfaf05831355c16c6a9612dcd1e12e84805850f7d09b6 + diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962 + docile (1.4.1) sha256=96159be799bfa73cdb721b840e9802126e4e03dfc26863db73647204c727f21e + doorkeeper (5.8.2) sha256=a73d07aeaf590b1e7e2a35390446f23131c9f37bc0561653e514d3973f4d50d3 + dotenv (3.2.0) sha256=e375b83121ea7ca4ce20f214740076129ab8514cd81378161f11c03853fe619d + drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373 + erb (6.0.1) sha256=28ecdd99c5472aebd5674d6061e3c6b0a45c049578b071e5a52c2a7f13c197e5 + erubi (1.13.1) sha256=a082103b0885dbc5ecf1172fede897f9ebdb745a4b97a5e8dc63953db1ee4ad9 + factory_bot (6.5.6) sha256=12beb373214dccc086a7a63763d6718c49769d5606f0501e0a4442676917e077 + factory_bot_rails (6.5.1) sha256=d3cc4851eae4dea8a665ec4a4516895045e710554d2b5ac9e68b94d351bc6d68 + faraday (2.14.0) sha256=8699cfe5d97e55268f2596f9a9d5a43736808a943714e3d9a53e6110593941cd + faraday-http-cache (2.5.1) sha256=cbfc129a85e1ad8580130189832cf59e7e117d94fe277547d61fc531f7689483 + faraday-net_http (3.4.2) sha256=f147758260d3526939bf57ecf911682f94926a3666502e24c69992765875906c + ffaker (2.25.0) sha256=e485c5adf8195aac55662875b7f515469bca46d77b60d0e7d08db6861bcbec40 + ffi (1.17.3-aarch64-linux-gnu) sha256=28ad573df26560f0aedd8a90c3371279a0b2bd0b4e834b16a2baa10bd7a97068 + ffi (1.17.3-aarch64-linux-musl) sha256=020b33b76775b1abacc3b7d86b287cef3251f66d747092deec592c7f5df764b2 + ffi (1.17.3-arm-linux-gnu) sha256=5bd4cea83b68b5ec0037f99c57d5ce2dd5aa438f35decc5ef68a7d085c785668 + ffi (1.17.3-arm-linux-musl) sha256=0d7626bb96265f9af78afa33e267d71cfef9d9a8eb8f5525344f8da6c7d76053 + ffi (1.17.3-arm64-darwin) sha256=0c690555d4cee17a7f07c04d59df39b2fba74ec440b19da1f685c6579bb0717f + ffi (1.17.3-x86_64-darwin) sha256=1f211811eb5cfaa25998322cdd92ab104bfbd26d1c4c08471599c511f2c00bb5 + ffi (1.17.3-x86_64-linux-gnu) sha256=3746b01f677aae7b16dc1acb7cb3cc17b3e35bdae7676a3f568153fb0e2c887f + ffi (1.17.3-x86_64-linux-musl) sha256=086b221c3a68320b7564066f46fed23449a44f7a1935f1fe5a245bd89d9aea56 + fiber-annotation (0.2.0) sha256=7abfadf1d119f508867d4103bf231c0354d019cc39a5738945dec2edadaf6c03 + fiber-local (1.1.0) sha256=c885f94f210fb9b05737de65d511136ea602e00c5105953748aa0f8793489f06 + fiber-storage (1.0.1) sha256=f48e5b6d8b0be96dac486332b55cee82240057065dc761c1ea692b2e719240e1 + friendly_id (5.4.2) sha256=02bc2e9a0da1c4c70c428db49f21f95d1a21ef8d16027d051ff6367b35fe7d4d + friendly_id-mobility (1.0.4) sha256=ef4d61536e8d5d1ca9d90659a712045174221a6a8bba36a0318eb324de22eb10 + gem-release (2.2.4) sha256=2f11124c1580c811507c3b47e875e420cf3ed792a98105b49df11971e6e94db3 + geocoder (1.8.6) sha256=e0ca1554b499f466de9b003f7dff70f89a5888761c2ca68ed9f86b6e5e24e74c + github_changelog_generator (1.16.4) sha256=21d2dfe41d50cf3a7ffe896774779d9d61da6aaf08d390f8ddff0dacbdf8a611 + globalid (1.3.0) sha256=05c639ad6eb4594522a0b07983022f04aa7254626ab69445a0e493aa3786ff11 + hashdiff (1.2.1) sha256=9c079dbc513dfc8833ab59c0c2d8f230fa28499cc5efb4b8dd276cf931457cd1 + heroicon (1.0.0) sha256=e63a535a630d6fd3d99c0179d722753c8cd3478df1277eca7a4215c135b6e9cb + highline (3.1.2) sha256=67cbd34d19f6ef11a7ee1d82ffab5d36dfd5b3be861f450fc1716c7125f4bb4a + i18n (1.14.8) sha256=285778639134865c5e0f6269e0b818256017e8cde89993fdfcbfb64d088824a5 + i18n-tasks (1.1.2) sha256=4dcfba49e52a623f30661cb316cb80d84fbba5cb8c6d88ef5e02545fffa3637a + image_processing (1.14.0) sha256=754cc169c9c262980889bec6bfd325ed1dafad34f85242b5a07b60af004742fb + importmap-rails (2.2.3) sha256=7101be2a4dc97cf1558fb8f573a718404c5f6bcfe94f304bf1f39e444feeb16a + inline_svg (1.10.0) sha256=5b652934236fd9f8adc61f3fd6e208b7ca3282698b19f28659971da84bf9a10f + io-console (0.8.2) sha256=d6e3ae7a7cc7574f4b8893b4fca2162e57a825b223a177b7afa236c5ef9814cc + io-endpoint (0.16.0) sha256=b5517d9d2cde28e32ef0b0e13d4422d172a093760f50aad244d50282e990a114 + io-event (1.14.2) sha256=b0a069190eafe86005c22f7464f744971b5bd82f153740d34e6ab49548d4f613 + io-stream (0.11.1) sha256=fa5f551fcff99581c1757b9d1cee2c37b124f07d2ca4f40b756a05ab9bd21b87 + irb (1.16.0) sha256=2abe56c9ac947cdcb2f150572904ba798c1e93c890c256f8429981a7675b0806 + json (2.18.0) sha256=b10506aee4183f5cf49e0efc48073d7b75843ce3782c68dbeb763351c08fd505 + jsonapi-rspec (0.0.11) sha256=995f1adb59b386cc36e3f6823c828df575167586937e3e42601914c4ee650082 + jsonapi-serializer (2.2.0) sha256=f8141ac6f0c1e17e8513df68f8341afe2d7bffc285841d7090bc07f07efb0029 + kaminari (1.2.2) sha256=c4076ff9adccc6109408333f87b5c4abbda5e39dc464bd4c66d06d9f73442a3e + kaminari-actionview (1.2.2) sha256=1330f6fc8b59a4a4ef6a549ff8a224797289ebf7a3a503e8c1652535287cc909 + kaminari-activerecord (1.2.2) sha256=0dd3a67bab356a356f36b3b7236bcb81cef313095365befe8e98057dd2472430 + kaminari-core (1.2.2) sha256=3bd26fec7370645af40ca73b9426a448d09b8a8ba7afa9ba3c3e0d39cdbb83ff + language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc + launchy (3.1.1) sha256=72b847b5cc961589dde2c395af0108c86ff0119f42d4648d25b5440ebb10059e + lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87 + local_time (3.0.3) sha256=c69a8974d993fdf6e60db02977ed23c070f203dcb3a1ff0de52ad3d2393f8303 + logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203 + loofah (2.25.0) sha256=df5ed7ac3bac6a4ec802df3877ee5cc86d027299f8952e6243b3dac446b060e6 + mail (2.9.0) sha256=6fa6673ecd71c60c2d996260f9ee3dd387d4673b8169b502134659ece6d34941 + mail_form (1.11.0) sha256=54aaaf11b69e7e5bd4be220dfc80f2469bb809d6ed1f893b8d940f0b67232c5a + marcel (1.1.0) sha256=fdcfcfa33cc52e93c4308d40e4090a5d4ea279e160a7f6af988260fa970e0bee + matrix (0.4.3) sha256=a0d5ab7ddcc1973ff690ab361b67f359acbb16958d1dc072b8b956a286564c5b + method_source (1.1.0) sha256=181301c9c45b731b4769bc81e8860e72f9161ad7d66dd99103c9ab84f560f5c5 + metrics (0.15.0) sha256=61ded5bac95118e995b1bc9ed4a5f19bc9814928a312a85b200abbdac9039072 + mini_magick (5.3.1) sha256=29395dfd76badcabb6403ee5aff6f681e867074f8f28ce08d78661e9e4a351c4 + mini_mime (1.1.5) sha256=8681b7e2e4215f2a159f9400b5816d85e9d8c6c6b491e96a12797e798f8bccef + minitest (6.0.1) sha256=7854c74f48e2e975969062833adc4013f249a4b212f5e7b9d5c040bf838d54bb + mobility (1.3.2) sha256=32fbbb0e53118ef42de20daa6ac94dbb758c628874092eba311b968a1e1d757b + mobility-actiontext (1.1.1) sha256=98f8fb9f2d8aa49fce1814eefb2ad8060efdfb89167bc5bd864da0bdd858127d + mobility-ransack (1.2.2) sha256=8b480ce5bc4937abc37c17b9922d7e41b8a7064a3207b3a64e5aa71e0cb3332b + monetize (1.13.0) sha256=0c1f8ae28aa21bbf1a6d014138d9bb0a5ce756c1a99b8b7f7d409c258b01d3b3 + money (6.19.0) sha256=ec936fa1e42f2783719241ed9fd52725d0efa628f928feea1eb5c37d5de7daf3 + multi_json (1.19.1) sha256=7aefeff8f2c854bf739931a238e4aea64592845e0c0395c8a7d2eea7fdd631b7 + name_of_person (1.1.3) sha256=e12291250987ef79c413314f698f15d53e7a602c42231de99c276ef9585bc18c + net-http (0.9.1) sha256=25ba0b67c63e89df626ed8fac771d0ad24ad151a858af2cc8e6a716ca4336996 + net-imap (0.6.2) sha256=08caacad486853c61676cca0c0c47df93db02abc4a8239a8b67eb0981428acc6 + net-pop (0.1.2) sha256=848b4e982013c15b2f0382792268763b748cce91c9e91e36b0f27ed26420dff3 + net-protocol (0.2.2) sha256=aa73e0cba6a125369de9837b8d8ef82a61849360eba0521900e2c3713aa162a8 + net-smtp (0.5.1) sha256=ed96a0af63c524fceb4b29b0d352195c30d82dd916a42f03c62a3a70e5b70736 + nio4r (2.7.5) sha256=6c90168e48fb5f8e768419c93abb94ba2b892a1d0602cb06eef16d8b7df1dca1 + nokogiri (1.19.0-aarch64-linux-gnu) sha256=11a97ecc3c0e7e5edcf395720b10860ef493b768f6aa80c539573530bc933767 + nokogiri (1.19.0-aarch64-linux-musl) sha256=eb70507f5e01bc23dad9b8dbec2b36ad0e61d227b42d292835020ff754fb7ba9 + nokogiri (1.19.0-arm-linux-gnu) sha256=572a259026b2c8b7c161fdb6469fa2d0edd2b61cd599db4bbda93289abefbfe5 + nokogiri (1.19.0-arm-linux-musl) sha256=23ed90922f1a38aed555d3de4d058e90850c731c5b756d191b3dc8055948e73c + nokogiri (1.19.0-arm64-darwin) sha256=0811dfd936d5f6dd3f6d32ef790568bf29b2b7bead9ba68866847b33c9cf5810 + nokogiri (1.19.0-x86_64-darwin) sha256=1dad56220b603a8edb9750cd95798bffa2b8dd9dd9aa47f664009ee5b43e3067 + nokogiri (1.19.0-x86_64-linux-gnu) sha256=f482b95c713d60031d48c44ce14562f8d2ce31e3a9e8dd0ccb131e9e5a68b58c + nokogiri (1.19.0-x86_64-linux-musl) sha256=1c4ca6b381622420073ce6043443af1d321e8ed93cc18b08e2666e5bd02ffae4 + observer (0.1.2) sha256=d8a3107131ba661138d748e7be3dbafc0d82e732fffba9fccb3d7829880950ac + octokit (4.25.1) sha256=c02092ee82dcdfe84db0e0ea630a70d32becc54245a4f0bacfd21c010df09b96 + openssl (4.0.0) sha256=185711ed93d4e9c9a9db6efea7edb202dfe04f7d3692fbab988e3d84e498ee91 + orm_adapter (0.5.0) sha256=aa5d0be5d540cbb46d3a93e88061f4ece6a25f6e97d6a47122beb84fe595e9b9 + ostruct (0.6.3) sha256=95a2ed4a4bd1d190784e666b47b2d3f078e4a9efda2fccf18f84ddc6538ed912 + parallel (1.27.0) sha256=4ac151e1806b755fb4e2dc2332cbf0e54f2e24ba821ff2d3dcf86bf6dc4ae130 + paranoia (3.1.0) sha256=36574a46e8df663d0da7d9008e987b72a6a9eccee13c058b7094acc810e39f52 + parser (3.3.10.1) sha256=06f6a725d2cd91e5e7f2b7c32ba143631e1f7c8ae2fb918fc4cebec187e6a688 + phonelib (0.10.15) sha256=8e0c06acef2867632691d80b63d38438524cb0941b6c7da2551e00f91a5787b8 + polyglot (0.3.5) sha256=59d66ef5e3c166431c39cb8b7c1d02af419051352f27912f6a43981b3def16af + pp (0.6.3) sha256=2951d514450b93ccfeb1df7d021cae0da16e0a7f95ee1e2273719669d0ab9df6 + prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193 + prism (1.8.0) sha256=84453a16ef5530ea62c5f03ec16b52a459575ad4e7b9c2b360fd8ce2c39c1254 + protocol-hpack (1.5.1) sha256=6feca238b8078da1cd295677d6f306c6001af92d75fe0643d33e6956cbc3ad91 + protocol-http (0.58.0) sha256=b77c5799e7a7acd31afaa9fed5c3714fe91ab7746925d66e9e4d92a5c74555fa + protocol-http1 (0.36.0) sha256=057344d2509a59338d0b4bc8f092a52126bb9ed51b76adbb4365f132bbd4ef09 + protocol-http2 (0.24.0) sha256=65327a019b7e36d2774e94050bf57a43bb60212775d2fcf02ae1d2ed4f01ef28 + protocol-url (0.4.0) sha256=64d4c03b6b51ad815ac6fdaf77a1d91e5baf9220d26becb846c5459dacdea9e1 + pry (0.16.0) sha256=d76c69065698ed1f85e717bd33d7942c38a50868f6b0673c636192b3d1b6054e + psych (5.3.1) sha256=eb7a57cef10c9d70173ff74e739d843ac3b2c019a003de48447b2963d81b1974 + public_suffix (7.0.2) sha256=9114090c8e4e7135c1fd0e7acfea33afaab38101884320c65aaa0ffb8e26a857 + puma (7.1.0) sha256=e45c10cb124f224d448c98db653a75499794edbecadc440ad616cf50f2fd49dd + racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f + rack (3.2.4) sha256=5d74b6f75082a643f43c1e76b419c40f0e5527fcfee1e669ac1e6b73c0ccb6f6 + rack-session (2.1.1) sha256=0b6dc07dea7e4b583f58a48e8b806d4c9f1c6c9214ebc202ec94562cbea2e4e9 + rack-test (2.2.0) sha256=005a36692c306ac0b4a9350355ee080fd09ddef1148a5f8b2ac636c720f5c463 + rackup (2.3.1) sha256=6c79c26753778e90983761d677a48937ee3192b3ffef6bc963c0950f94688868 + rails (7.2.3) sha256=9a9812eb131189676e64665f6883fc9c4051f412cc87ef9e3fa242a09c609bff + rails-controller-testing (1.0.5) sha256=741448db59366073e86fc965ba403f881c636b79a2c39a48d0486f2607182e94 + rails-dom-testing (2.3.0) sha256=8acc7953a7b911ca44588bf08737bc16719f431a1cc3091a292bca7317925c1d + rails-html-sanitizer (1.6.2) sha256=35fce2ca8242da8775c83b6ba9c1bcaad6751d9eb73c1abaa8403475ab89a560 + rails-i18n (7.0.10) sha256=efae16e0ac28c0f42e98555c8db1327d69ab02058c8b535e0933cb106dd931ca + railties (7.2.3) sha256=6eb010a6bfe6f223e783f739ddfcbdb5b88b1f3a87f7739f0a0685e466250422 + rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a + rake (13.3.1) sha256=8c9e89d09f66a26a01264e7e3480ec0607f0c497a861ef16063604b1b08eb19c + ransack (4.4.1) sha256=6aeaac36fc19088570e10da1044e6cfd88c740e20f871b84566fd30e32b7a63d + rbs (3.10.2) sha256=bd8a5dc4c62f229f020146b61844a31f9c79e649449d212904a474eb79c846fc + rdoc (7.1.0) sha256=494899df0706c178596ca6e1d50f1b7eb285a9b2aae715be5abd742734f17363 + regexp_parser (2.11.3) sha256=ca13f381a173b7a93450e53459075c9b76a10433caadcb2f1180f2c741fc55a4 + reline (0.6.3) sha256=1198b04973565b36ec0f11542ab3f5cfeeec34823f4e54cebde90968092b1835 + request_store (1.7.0) sha256=e1b75d5346a315f452242a68c937ef8e48b215b9453a77a6c0acdca2934c88cb + responders (3.2.0) sha256=89c2d6ac0ae16f6458a11524cae4a8efdceba1a3baea164d28ee9046bd3df55a + rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142 + rspec-activemodel-mocks (1.3.0) sha256=b51b111b726370a20a2103a8a6851296783a70d2b0bd0287bfa24d293f58d76e + rspec-core (3.13.6) sha256=a8823c6411667b60a8bca135364351dda34cd55e44ff94c4be4633b37d828b2d + rspec-expectations (3.13.5) sha256=33a4d3a1d95060aea4c94e9f237030a8f9eae5615e9bd85718fe3a09e4b58836 + rspec-mocks (3.13.7) sha256=0979034e64b1d7a838aaaddf12bf065ea4dc40ef3d4c39f01f93ae2c66c62b1c + rspec-rails (8.0.2) sha256=113139a53f5d068d4f48d1c29ad5f982013ed9b0daa69d7f7b266eda5d433ace + rspec-retry (0.6.2) sha256=6101ba23a38809811ae3484acde4ab481c54d846ac66d5037ccb40131a60d858 + rspec-support (3.13.6) sha256=2e8de3702427eab064c9352fe74488cc12a1bfae887ad8b91cba480ec9f8afb2 + rspec_junit_formatter (0.6.0) sha256=40dde674e6ae4e6cc0ff560da25497677e34fefd2338cc467a8972f602b62b15 + rubocop (1.82.1) sha256=09f1a6a654a960eda767aebea33e47603080f8e9c9a3f019bf9b94c9cab5e273 + rubocop-ast (1.49.0) sha256=49c3676d3123a0923d333e20c6c2dbaaae2d2287b475273fddee0c61da9f71fd + rubocop-performance (1.26.1) sha256=cd19b936ff196df85829d264b522fd4f98b6c89ad271fa52744a8c11b8f71834 + rubocop-rails (2.34.3) sha256=10d37989024865ecda8199f311f3faca990143fbac967de943f88aca11eb9ad2 + rubocop-rails-omakase (1.1.0) sha256=2af73ac8ee5852de2919abbd2618af9c15c19b512c4cfc1f9a5d3b6ef009109d + rubocop-rspec (3.9.0) sha256=8fa70a3619408237d789aeecfb9beef40576acc855173e60939d63332fdb55e2 + ruby-lsp (0.26.4) sha256=1cb3046a066c8f1983dfe5f0cd3baa76034d6ba156e8ab460b15ab129d37a9f7 + ruby-oembed (0.18.1) sha256=877b87b3d98e8137599044277fdb82a5536973bd9df68a93129163b781ae3a5f + ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33 + ruby-vips (2.3.0) sha256=e685ec02c13969912debbd98019e50492e12989282da5f37d05f5471442f5374 + rubyzip (2.4.1) sha256=8577c88edc1fde8935eb91064c5cb1aef9ad5494b940cf19c775ee833e075615 + safely_block (0.5.0) sha256=782c342bc400a79c1233c40cf8c379da6116c6cfd1d7c1f17f0ef2ef11090103 + sawyer (0.9.3) sha256=0d0f19298408047037638639fe62f4794483fb04320269169bd41af2bdcf5e41 + securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1 + selenium-webdriver (4.10.0) sha256=a322af38f9a8966a6c3ab85c365b08c5556cdef72f577905b2b42469faee9d43 + simplecov (0.22.0) sha256=fe2622c7834ff23b98066bb0a854284b2729a569ac659f82621fc22ef36213a5 + simplecov-html (0.13.2) sha256=bd0b8e54e7c2d7685927e8d6286466359b6f16b18cb0df47b508e8d73c777246 + simplecov_json_formatter (0.1.4) sha256=529418fbe8de1713ac2b2d612aa3daa56d316975d307244399fa4838c601b428 + spree (5.2.6) sha256=04ac44abd2e6adea5d13b2aed0289d7025ee08fa0e8e06287b298f25c33dcd18 + spree-spl (2.0.0) + spree_api (5.2.6) sha256=a0bb2892bd32e9c4d53ece74e3906be8d1653b59fd7739de3b2c2277c7cec70b + spree_auth_devise (4.6.3) sha256=5eeada77570f5ab335a9c6c150d7cdaee39203ce43b70ac24d2ffb195952ed6e + spree_cli (5.2.6) sha256=8c86d35c2eab2bc19a4dc6220a73c52c877dd90a0fa47f2fe97bdc206bda7aab + spree_core (5.2.6) sha256=3f4d99e1ea5256d0f0aa5767aeedfff121d32498791a1b94c9a5738c99214358 + spree_dev_tools (0.5.2) sha256=faa8bda07eb785e066032e5527663292bbe0dfab653fc06c61d56798dd4deea7 + spree_extension (0.1.0) sha256=b648327f4787295e051be4ea0c0a47baec47c2e245bca9c93ecf73605fc3a051 + spree_storefront (5.2.6) sha256=da704b62e870ae3bac382635d57c03905bf51551ea600ee3e8716d2f6ce4fbfa + sprockets (4.2.2) sha256=761e5a49f1c288704763f73139763564c845a8f856d52fba013458f8af1b59b1 + sprockets-rails (3.5.2) sha256=a9e88e6ce9f8c912d349aa5401509165ec42326baf9e942a85de4b76dbc4119e + sqlite3 (2.9.0-aarch64-linux-gnu) sha256=cfe1e0216f46d7483839719bf827129151e6c680317b99d7b8fc1597a3e13473 + sqlite3 (2.9.0-aarch64-linux-musl) sha256=56a35cb2d70779afc2ac191baf2c2148242285ecfed72f9b021218c5c4917913 + sqlite3 (2.9.0-arm-linux-gnu) sha256=a19a21504b0d7c8c825fbbf37b358ae316b6bd0d0134c619874060b2eef05435 + sqlite3 (2.9.0-arm-linux-musl) sha256=fca5b26197c70e3363115d3faaea34d7b2ad9c7f5fa8d8312e31b64e7556ee07 + sqlite3 (2.9.0-arm64-darwin) sha256=a917bd9b84285766ff3300b7d79cd583f5a067594c8c1263e6441618c04a6ed3 + sqlite3 (2.9.0-x86_64-darwin) sha256=59fe51baa3cb33c36d27ce78b4ed9360cd33ccca09498c2ae63850c97c0a6026 + sqlite3 (2.9.0-x86_64-linux-gnu) sha256=72fff9bd750070ba3af695511ba5f0e0a2d8a9206f84869640b3e99dfaf3d5a5 + sqlite3 (2.9.0-x86_64-linux-musl) sha256=ef716ba7a66d7deb1ccc402ac3a6d7343da17fac862793b7f0be3d2917253c90 + state_machines (0.100.4) sha256=dd4e555ebb061569dd8fe18a5f39b51d10be6306b89a0c315a9697671d752565 + state_machines-activemodel (0.101.0) sha256=6798c6bf5aa4be2b1a00ca633949c999ebee7a148bd4ea8f6858b189e2cdf72d + state_machines-activerecord (0.100.0) sha256=b2213f74f3f32c6d92de5139e8898bc64a0ffb3f048df7d950ce27e28384beb5 + stimulus-rails (1.3.4) sha256=765676ffa1f33af64ce026d26b48e8ffb2e0b94e0f50e9119e11d6107d67cb06 + stringex (2.8.6) sha256=c7b382d2b2a47a1e1646f256df201c48d487d6296fbb289d76802f67f5e929c4 + stringio (3.2.0) sha256=c37cb2e58b4ffbd33fe5cd948c05934af997b36e0b6ca6fdf43afa234cf222e1 + tailwindcss-rails (4.4.0) sha256=efa2961351a52acebe616e645a81a30bb4f27fde46cc06ce7688d1cd1131e916 + tailwindcss-ruby (4.1.18) sha256=b62fad5b00494e92987ee319dfb5c5ad272f0ed93649963d62f08d2ba0f03fa7 + tailwindcss-ruby (4.1.18-aarch64-linux-gnu) sha256=e10f9560bccddbb4955fd535b3bcc8c7071a7df07404dd473a23fa791ec4e46b + tailwindcss-ruby (4.1.18-aarch64-linux-musl) sha256=3c8426674718a2c98a0649c825ac0b3286ff52acd0b4052d7d19126cd74904f3 + tailwindcss-ruby (4.1.18-arm64-darwin) sha256=f940531d5a030c566d3d616004235bcd4c361abdd328f7d6c7e3a953a32e0155 + tailwindcss-ruby (4.1.18-x86_64-darwin) sha256=6a82115b606a6f748c600c666a19c16ee28f5736217af8d0c20cee5abe1ce2f7 + tailwindcss-ruby (4.1.18-x86_64-linux-gnu) sha256=e0a2220163246fe0126c5c5bafb95bc6206e7d21fce2a2878fd9c9a359137534 + tailwindcss-ruby (4.1.18-x86_64-linux-musl) sha256=d957cf545b09d2db7eb6267450cc1fc589e126524066537a0c4d5b99d701f4b2 + terminal-table (4.0.0) sha256=f504793203f8251b2ea7c7068333053f0beeea26093ec9962e62ea79f94301d2 + thor (1.5.0) sha256=e3a9e55fe857e44859ce104a84675ab6e8cd59c650a49106a05f55f136425e73 + timecop (0.9.10) sha256=12ba45ce57cdcf6b1043cb6cdffa6381fd89ce10d369c28a7f6f04dc1b0cd8eb + timeout (0.6.0) sha256=6d722ad619f96ee383a0c557ec6eb8c4ecb08af3af62098a0be5057bf00de1af + traces (0.18.2) sha256=80f1649cb4daace1d7174b81f3b3b7427af0b93047759ba349960cb8f315e214 + tracking_number (2.4.0) sha256=2a4c6b39cbdfc8326c234626556da1e269c8dfe33478786bea93461331c247ff + tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f + turbo-rails (2.0.20) sha256=cbcbb4dd3ce59f6471c9f911b1655b2c721998cc8303959d982da347f374ea95 + tzinfo (2.0.6) sha256=8daf828cc77bcf7d63b0e3bdb6caa47e2272dcfaf4fbfe46f8c3a9df087a829b + unaccent (0.4.0) sha256=d9278230016edc5317c053f8e84c1e1dea6827dec028e5481e2294a4e11e7333 + unicode-display_width (3.2.0) sha256=0cdd96b5681a5949cdbc2c55e7b420facae74c4aaf9a9815eee1087cb1853c42 + unicode-emoji (4.2.0) sha256=519e69150f75652e40bf736106cfbc8f0f73aa3fb6a65afe62fefa7f80b0f80f + uri (1.1.1) sha256=379fa58d27ffb1387eaada68c749d1426738bd0f654d812fcc07e7568f5c57c6 + useragent (0.16.11) sha256=700e6413ad4bb954bb63547fa098dddf7b0ebe75b40cc6f93b8d54255b173844 + validates_zipcode (0.5.4) sha256=fc2a064bed9303fa2fda36f730367a4d5d95a8f4c74baa1aae2b80119da8cc0f + wannabe_bool (0.7.1) sha256=838ef671066868424de888fb22eeae57c7012709aabd5d17141ea6fea964c61f + warden (1.2.9) sha256=46684f885d35a69dbb883deabf85a222c8e427a957804719e143005df7a1efd0 + webdrivers (5.3.1) sha256=d7d5531fbc3c44b6a2a7710e3e6c5edc54f35c68af20017309e3eccf15ec4aa5 + webmock (3.26.1) sha256=4f696fb57c90a827c20aadb2d4f9058bbff10f7f043bd0d4c3f58791143b1cd7 + websocket (1.2.11) sha256=b7e7a74e2410b5e85c25858b26b3322f29161e300935f70a0e0d3c35e0462737 + websocket-driver (0.8.0) sha256=ed0dba4b943c22f17f9a734817e808bc84cdce6a7e22045f5315aa57676d4962 + websocket-extensions (0.1.5) sha256=1c6ba63092cda343eb53fc657110c71c754c56484aad42578495227d717a8241 + xpath (3.2.0) sha256=6dfda79d91bb3b949b947ecc5919f042ef2f399b904013eb3ef6d20dd3a4082e + zeitwerk (2.7.4) sha256=2bef90f356bdafe9a6c2bd32bcd804f83a4f9b8bc27f3600fff051eb3edcec8b + BUNDLED WITH - 2.6.5 + 4.0.2 diff --git a/app/controllers/spl/spree/storefront/checkout_controller_decorator.rb b/app/controllers/spl/spree/storefront/checkout_controller_decorator.rb index 49bcc4d..3337716 100644 --- a/app/controllers/spl/spree/storefront/checkout_controller_decorator.rb +++ b/app/controllers/spl/spree/storefront/checkout_controller_decorator.rb @@ -4,6 +4,8 @@ module Spl module Spree module Storefront module CheckoutControllerDecorator + include ErrorHandlingHelper + def self.prepended(base) base.before_action :promotion_switcher base.before_action :load_user_coupons, except: %i[activate_coupon deactivate_coupon] @@ -12,6 +14,8 @@ def self.prepended(base) def activate_coupon Spl::Coupons::ActivateCouponService.new(@order.user, @order.store, params[:coupon_code]).call load_user_coupons + rescue => e + handle_spl_error(e) ensure respond_to do |format| format.turbo_stream @@ -24,6 +28,8 @@ def deactivate_coupon .new(@order.user, @order.store, params[:coupon_code]) .call load_user_coupons + rescue => e + handle_spl_error(e) ensure respond_to do |format| format.turbo_stream diff --git a/app/controllers/spl/spree/storefront/profile_controller_decorator.rb b/app/controllers/spl/spree/storefront/profile_controller_decorator.rb index 478fa32..8f3a9f0 100644 --- a/app/controllers/spl/spree/storefront/profile_controller_decorator.rb +++ b/app/controllers/spl/spree/storefront/profile_controller_decorator.rb @@ -5,6 +5,7 @@ module Spree module Storefront module ProfileControllerDecorator include BooleanHelper + include ErrorHandlingHelper def self.prepended(base) base.before_action :validate_spl_no_card, only: :update @@ -105,10 +106,6 @@ def phone_parser @phone_parser ||= PhoneParserService.new(login_code_params[:phone]) end - def clear_errors - try_spree_current_user.errors.clear - end - def render_login_code_error render turbo_stream: turbo_stream.replace( 'loyalty_connect_form', @@ -153,14 +150,6 @@ def update_user_after_otp_request ) end - def handle_spl_error(error) - payload = Spl::ErrorPayloadParser.parse(error.message) || error - msg = Spl::ErrorTranslator.translate(payload) - - clear_errors - try_spree_current_user.errors.add(:base, msg) - end - def assign_card_number(user, store, params) Spl::LoginAccountService.new(user, store, params).call AssignSpartaCardNumberService.new(user, store).call diff --git a/app/helpers/error_handling_helper.rb b/app/helpers/error_handling_helper.rb new file mode 100644 index 0000000..861b69e --- /dev/null +++ b/app/helpers/error_handling_helper.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module ErrorHandlingHelper + # Parses and translate occurred error, then adds it to user errors. + # Dedicated for storefront controllers. + # @param [StandardError] + def handle_spl_error(error) + payload = Spl::ErrorPayloadParser.parse(error.message) || error + msg = Spl::ErrorTranslator.translate(payload) + + clear_errors + try_spree_current_user.errors.add(:base, msg) + end + + def clear_errors + try_spree_current_user.errors.clear + end + + def token_expired?(err_msg) + 'TOKEN_EXPIRED' == err_msg + end +end diff --git a/app/helpers/spl_service_helper.rb b/app/helpers/spl_service_helper.rb index ba677ba..40f7d1e 100644 --- a/app/helpers/spl_service_helper.rb +++ b/app/helpers/spl_service_helper.rb @@ -4,4 +4,17 @@ module SplServiceHelper def send_request(url, body) Spl::SendRequestService.new(url, body).call end + + # Refreshes user private token to keep possible + # using customer oriented endpoints + # @param [user: Spree::User] + def refresh_user_token(user) + return if @user.private_metadata.nil? + return if @user.private_metadata['spl_refresh_token'].nil? + + response = Spl::OauthTokenService.refresh_token(@user.private_metadata['spl_refresh_token']) + + user.update!(private_metadata: { spl_access_token: response.dig('access_token'), + spl_refresh_token: response.dig('refresh_token') }) + end end diff --git a/app/services/spl/coupons/activate_coupon_service.rb b/app/services/spl/coupons/activate_coupon_service.rb index 3994372..a0d1351 100644 --- a/app/services/spl/coupons/activate_coupon_service.rb +++ b/app/services/spl/coupons/activate_coupon_service.rb @@ -7,6 +7,7 @@ module Coupons class ActivateCouponService class ActivateCouponServiceError < StandardError; end include SplServiceHelper + include ErrorHandlingHelper def initialize(user, store, coupon_code) @store = store @@ -16,6 +17,9 @@ def initialize(user, store, coupon_code) end def call + return unless @user.present? && @user.private_metadata.present? + + retry_counter ||= 0 body = prepare_body response = send_request(@activate_coupons_url, body) response_body = JSON.parse(response.body) @@ -23,6 +27,15 @@ def call raise ActivateCouponServiceError, response_body['msg'] if response_body['errorCode'] != '0' response_body['response'] + rescue ActivateCouponServiceError => e + raise e unless token_expired?(response_body['errorCode']) && retry_counter < 1 + + if refresh_user_token(@user) + retry_counter += 1 + retry + else + raise e + end end private diff --git a/app/services/spl/coupons/deactivate_coupon_service.rb b/app/services/spl/coupons/deactivate_coupon_service.rb index f2a961e..b9742ac 100644 --- a/app/services/spl/coupons/deactivate_coupon_service.rb +++ b/app/services/spl/coupons/deactivate_coupon_service.rb @@ -7,6 +7,7 @@ module Coupons class DeactivateCouponService class DeactivateCouponServiceError < StandardError; end include SplServiceHelper + include ErrorHandlingHelper def initialize(user, store, coupon_code) @store = store @@ -16,6 +17,9 @@ def initialize(user, store, coupon_code) end def call + return unless @user.present? && @user.private_metadata.present? + + retry_counter ||= 0 body = prepare_body response = send_request(@deactivate_coupons_url, body) response_body = JSON.parse(response.body) @@ -23,6 +27,15 @@ def call raise DeactivateCouponServiceError, response_body['msg'] if response_body['errorCode'] != '0' response_body['response'] + rescue DeactivateCouponServiceError => e + raise e unless token_expired?(response_body['errorCode']) && retry_counter < 1 + + if refresh_user_token(@user) + retry_counter += 1 + retry + else + raise e + end end private diff --git a/app/services/spl/coupons/get_coupons_service.rb b/app/services/spl/coupons/get_coupons_service.rb index 9f0c9aa..9558a68 100644 --- a/app/services/spl/coupons/get_coupons_service.rb +++ b/app/services/spl/coupons/get_coupons_service.rb @@ -7,6 +7,7 @@ module Coupons class GetCouponsService class SplGetCouponError < StandardError; end include SplServiceHelper + include ErrorHandlingHelper def initialize(user, store) @store = store @@ -15,6 +16,9 @@ def initialize(user, store) end def call + return unless @user.present? && @user.private_metadata.present? + + retry_counter ||= 0 body = prepare_body response = send_request(@find_coupons_url, body) response_body = JSON.parse(response.body) @@ -24,6 +28,15 @@ def call response_body['response']&.filter do |coupon| active?(coupon) end + rescue SplGetCouponError => e + raise e unless token_expired?(response_body['errorCode']) && retry_counter < 1 + + if refresh_user_token(@user) + retry_counter += 1 + retry + else + raise e + end end private diff --git a/app/services/spl/login_account_service.rb b/app/services/spl/login_account_service.rb index bc1895f..cd7040f 100644 --- a/app/services/spl/login_account_service.rb +++ b/app/services/spl/login_account_service.rb @@ -57,10 +57,10 @@ def generate_login def get_access_token(access_token) token_body = Spl::OauthTokenService.new(DateTime.current, @store).authorization_code_token(access_token) - add_loyalty_tokents_to_user(token_body['accessToken'], token_body['refreshToken']) + add_loyalty_tokens_to_user(token_body['accessToken'], token_body['refreshToken']) end - def add_loyalty_tokents_to_user(access_token, refresh_token) + def add_loyalty_tokens_to_user(access_token, refresh_token) @user.update!(private_metadata: { spl_access_token: access_token, spl_refresh_token: refresh_token }) end diff --git a/app/services/spl/me_service.rb b/app/services/spl/me_service.rb index 87e1287..e0d2d42 100644 --- a/app/services/spl/me_service.rb +++ b/app/services/spl/me_service.rb @@ -6,6 +6,7 @@ module Spl class MeService class SplMeError < StandardError; end include SplServiceHelper + include ErrorHandlingHelper def initialize(user, store) @me_url = URI.parse(Spl::UrlCreatorService.new(store.private_metadata['spl_url']).me) @@ -14,6 +15,9 @@ def initialize(user, store) end def call + return unless @user.present? && @user.private_metadata.present? + + retry_counter ||= 0 body = prepare_me_body response = send_request(@me_url, body) response_body = JSON.parse(response.body) @@ -21,6 +25,15 @@ def call raise SplMeError, response_body if response_body['errorCode'] != '0' response_body + rescue SplMeError => e + raise e unless token_expired?(response_body['errorCode']) && retry_counter < 1 + + if refresh_user_token(@user) + retry_counter += 1 + retry + else + raise e + end end private diff --git a/app/services/spl/oauth_token_service.rb b/app/services/spl/oauth_token_service.rb index 71d3194..db67004 100644 --- a/app/services/spl/oauth_token_service.rb +++ b/app/services/spl/oauth_token_service.rb @@ -33,6 +33,16 @@ def authorization_code_token(auth_code) response_body['response'] end + def self.refresh_token(refresh_token) + body = prepare_refresh_token_body(refresh_token) + response = send_request(@token_url, body) + response_body = JSON.parse(response.body) + + raise OauthTokenError, response_body['msg'] if response_body['errorCode'] != '0' + + response_body['response'] + end + private def prepare_oauth_token_body_with_signature @@ -60,6 +70,18 @@ def prepare_oauth_token_body_with_oauth_code(auth_code) } end + def prepare_refresh_token_body(refresh_token) + { + context: { + prgCode: @env['spl_prg_code'] + }, + apiUser: @env['spl_api_user'], + apiToken: @env['spl_api_token'], + refreshToken: refresh_token, + grantType: 'refresh_token' + } + end + def generate_signature Spl::ClientSignatureService.new(@date, @env['spl_api_token'], diff --git a/spec/controllers/checkout_controller_decorator_spec.rb b/spec/controllers/checkout_controller_decorator_spec.rb index e5815a4..fe54e0e 100644 --- a/spec/controllers/checkout_controller_decorator_spec.rb +++ b/spec/controllers/checkout_controller_decorator_spec.rb @@ -108,6 +108,11 @@ def set_order describe '#activate_coupon' do let(:activate) { instance_double(Spl::Coupons::ActivateCouponService, call: true) } + + before do + allow(instance_of(Spl::Coupons::ActivateCouponService)).to receive(:try_spree_current_user).and_return(user) + end + context 'when coupon is valid' do it 'calls ActivateCouponService, reloads coupons, and redirects (HTML)' do expect(Spl::Coupons::ActivateCouponService) @@ -126,8 +131,6 @@ def set_order end context 'when coupon is invalid' do - before { allow(activate).to receive(:call).and_raise(Spl::Coupons::ActivateCouponService::ActivateCouponServiceError) } - it 'still redirects even if ActivateCouponService raises' do expect(Spl::Coupons::ActivateCouponService).to receive(:new).with(user, store, 'YOLO_90').and_return(activate) expect(activate).to receive(:call).and_raise(Spl::Coupons::ActivateCouponService::ActivateCouponServiceError, @@ -144,6 +147,10 @@ def set_order end describe '#deactivate_coupon' do + before do + allow(instance_of(Spl::Coupons::ActivateCouponService)).to receive(:try_spree_current_user).and_return(user) + end + it 'calls DeactivateCouponService, reloads coupons, and redirects (HTML)' do deactivate = instance_double('Spl::Coupons::DeactivateCouponService', call: true) expect(Spl::Coupons::DeactivateCouponService) diff --git a/spec/controllers/spl/spree/account/profile_controller_decorator_spec.rb b/spec/controllers/spl/spree/account/profile_controller_decorator_spec.rb index 1a3b0b8..3fa1c4b 100644 --- a/spec/controllers/spl/spree/account/profile_controller_decorator_spec.rb +++ b/spec/controllers/spl/spree/account/profile_controller_decorator_spec.rb @@ -46,7 +46,7 @@ it 'adds error and renders 422 (does not touch OTP service)' do expect(Spl::SendOtpService).not_to receive(:new) - expect(controller).to receive(:render).with(hash_including(status: :unprocessable_entity)) + expect(controller).to receive(:render).with(hash_including(status: :unprocessable_content)) controller.send(:validate_login_code_request) expect(user.errors.full_messages.join(' ')).to include(I18n.t('spl.user.errors.must_accept_yc_terms')) end @@ -57,7 +57,7 @@ it 'adds phone error and renders 422 (does not touch OTP service)' do expect(Spl::SendOtpService).not_to receive(:new) - expect(controller).to receive(:render).with(hash_including(status: :unprocessable_entity)) + expect(controller).to receive(:render).with(hash_including(status: :unprocessable_content)) controller.send(:validate_login_code_request) expect(user.errors.full_messages.join(' ')).to include(I18n.t('spl.user.errors.invalid_phone')) end @@ -91,7 +91,7 @@ expect(service_instance).to receive(:call).and_raise(Spl::SendOtpService::SplSendOtpError.new(payload.inspect)) - expect(controller).to receive(:render).with(hash_including(status: :unprocessable_entity)) + expect(controller).to receive(:render).with(hash_including(status: :unprocessable_content)) controller.login_code @@ -164,7 +164,7 @@ allow(login_service).to receive(:call) .and_raise(error_class.new(payload.inspect)) - expect(controller).to receive(:render).with(hash_including(status: :unprocessable_entity)) + expect(controller).to receive(:render).with(hash_including(status: :unprocessable_content)) controller.connect_loyalty_account diff --git a/spec/services/phone_parser_service_spec.rb b/spec/services/phone_parser_service_spec.rb index 5c2bcdb..29c8a25 100644 --- a/spec/services/phone_parser_service_spec.rb +++ b/spec/services/phone_parser_service_spec.rb @@ -20,7 +20,7 @@ end it 'delegates to the parsed phone object' do - expect(described_class.new('anything')).to be_valid + expect(described_class.new('+48500600700')).to be_valid end end From b09072e8cee15538a88ca8c2a927fdce3ea4d8dd Mon Sep 17 00:00:00 2001 From: Tomasz Strzeszewski Date: Mon, 19 Jan 2026 18:57:45 +0100 Subject: [PATCH 47/63] rubocop offences --- .../spree/storefront/checkout_controller_decorator.rb | 4 ++-- app/helpers/error_handling_helper.rb | 2 +- app/helpers/spl_service_helper.rb | 4 ++-- app/services/phone_parser_service.rb | 4 ++-- app/services/spl/coupons/activate_coupon_service.rb | 10 ++++------ app/services/spl/coupons/deactivate_coupon_service.rb | 10 ++++------ app/services/spl/coupons/get_coupons_service.rb | 10 ++++------ app/services/spl/login_account_service.rb | 2 +- app/services/spl/me_service.rb | 10 ++++------ app/services/spl/oauth_token_service.rb | 2 +- app/services/spl/send_otp_service.rb | 2 +- 11 files changed, 26 insertions(+), 34 deletions(-) diff --git a/app/controllers/spl/spree/storefront/checkout_controller_decorator.rb b/app/controllers/spl/spree/storefront/checkout_controller_decorator.rb index 3337716..a608e8e 100644 --- a/app/controllers/spl/spree/storefront/checkout_controller_decorator.rb +++ b/app/controllers/spl/spree/storefront/checkout_controller_decorator.rb @@ -14,7 +14,7 @@ def self.prepended(base) def activate_coupon Spl::Coupons::ActivateCouponService.new(@order.user, @order.store, params[:coupon_code]).call load_user_coupons - rescue => e + rescue StandardError => e handle_spl_error(e) ensure respond_to do |format| @@ -28,7 +28,7 @@ def deactivate_coupon .new(@order.user, @order.store, params[:coupon_code]) .call load_user_coupons - rescue => e + rescue StandardError => e handle_spl_error(e) ensure respond_to do |format| diff --git a/app/helpers/error_handling_helper.rb b/app/helpers/error_handling_helper.rb index 861b69e..9ee48c6 100644 --- a/app/helpers/error_handling_helper.rb +++ b/app/helpers/error_handling_helper.rb @@ -17,6 +17,6 @@ def clear_errors end def token_expired?(err_msg) - 'TOKEN_EXPIRED' == err_msg + err_msg == 'TOKEN_EXPIRED' end end diff --git a/app/helpers/spl_service_helper.rb b/app/helpers/spl_service_helper.rb index 40f7d1e..7ac3c04 100644 --- a/app/helpers/spl_service_helper.rb +++ b/app/helpers/spl_service_helper.rb @@ -14,7 +14,7 @@ def refresh_user_token(user) response = Spl::OauthTokenService.refresh_token(@user.private_metadata['spl_refresh_token']) - user.update!(private_metadata: { spl_access_token: response.dig('access_token'), - spl_refresh_token: response.dig('refresh_token') }) + user.update!(private_metadata: { spl_access_token: response['access_token'], + spl_refresh_token: response['refresh_token'] }) end end diff --git a/app/services/phone_parser_service.rb b/app/services/phone_parser_service.rb index 353851c..a7e16af 100644 --- a/app/services/phone_parser_service.rb +++ b/app/services/phone_parser_service.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class PhoneParserService - COUNTRY_CODE_REGEX = /\A\+\d{1,3}\d+\z/.freeze + COUNTRY_CODE_REGEX = /\A\+\d{1,3}\d+\z/ attr_reader :raw, :phone @@ -32,7 +32,7 @@ def national_number def e164 return unless valid? - + phone.e164 end diff --git a/app/services/spl/coupons/activate_coupon_service.rb b/app/services/spl/coupons/activate_coupon_service.rb index a0d1351..0b013dd 100644 --- a/app/services/spl/coupons/activate_coupon_service.rb +++ b/app/services/spl/coupons/activate_coupon_service.rb @@ -30,12 +30,10 @@ def call rescue ActivateCouponServiceError => e raise e unless token_expired?(response_body['errorCode']) && retry_counter < 1 - if refresh_user_token(@user) - retry_counter += 1 - retry - else - raise e - end + raise e unless refresh_user_token(@user) + + retry_counter += 1 + retry end private diff --git a/app/services/spl/coupons/deactivate_coupon_service.rb b/app/services/spl/coupons/deactivate_coupon_service.rb index b9742ac..ba85b33 100644 --- a/app/services/spl/coupons/deactivate_coupon_service.rb +++ b/app/services/spl/coupons/deactivate_coupon_service.rb @@ -30,12 +30,10 @@ def call rescue DeactivateCouponServiceError => e raise e unless token_expired?(response_body['errorCode']) && retry_counter < 1 - if refresh_user_token(@user) - retry_counter += 1 - retry - else - raise e - end + raise e unless refresh_user_token(@user) + + retry_counter += 1 + retry end private diff --git a/app/services/spl/coupons/get_coupons_service.rb b/app/services/spl/coupons/get_coupons_service.rb index 9558a68..55e821d 100644 --- a/app/services/spl/coupons/get_coupons_service.rb +++ b/app/services/spl/coupons/get_coupons_service.rb @@ -31,12 +31,10 @@ def call rescue SplGetCouponError => e raise e unless token_expired?(response_body['errorCode']) && retry_counter < 1 - if refresh_user_token(@user) - retry_counter += 1 - retry - else - raise e - end + raise e unless refresh_user_token(@user) + + retry_counter += 1 + retry end private diff --git a/app/services/spl/login_account_service.rb b/app/services/spl/login_account_service.rb index cd7040f..5308f10 100644 --- a/app/services/spl/login_account_service.rb +++ b/app/services/spl/login_account_service.rb @@ -30,7 +30,7 @@ def call private - def prepare_login_body # rubocop:disable Metrics/MethodLength + def prepare_login_body { context: { prgCode: @env['spl_prg_code'] diff --git a/app/services/spl/me_service.rb b/app/services/spl/me_service.rb index e0d2d42..7d277df 100644 --- a/app/services/spl/me_service.rb +++ b/app/services/spl/me_service.rb @@ -28,12 +28,10 @@ def call rescue SplMeError => e raise e unless token_expired?(response_body['errorCode']) && retry_counter < 1 - if refresh_user_token(@user) - retry_counter += 1 - retry - else - raise e - end + raise e unless refresh_user_token(@user) + + retry_counter += 1 + retry end private diff --git a/app/services/spl/oauth_token_service.rb b/app/services/spl/oauth_token_service.rb index db67004..bec50f3 100644 --- a/app/services/spl/oauth_token_service.rb +++ b/app/services/spl/oauth_token_service.rb @@ -41,7 +41,7 @@ def self.refresh_token(refresh_token) raise OauthTokenError, response_body['msg'] if response_body['errorCode'] != '0' response_body['response'] - end + end private diff --git a/app/services/spl/send_otp_service.rb b/app/services/spl/send_otp_service.rb index 7e19eae..b1e77e7 100644 --- a/app/services/spl/send_otp_service.rb +++ b/app/services/spl/send_otp_service.rb @@ -27,7 +27,7 @@ def call private - def prepare_sms_otp_body # rubocop:disable Metrics/MethodLength + def prepare_sms_otp_body { context: { prgCode: @env['spl_prg_code'] From cd52b342b049ec98bbb9baa74898f4f09398fc9e Mon Sep 17 00:00:00 2001 From: Tomasz Strzeszewski Date: Mon, 19 Jan 2026 19:07:58 +0100 Subject: [PATCH 48/63] rubocop offences --- app/helpers/spl_service_helper.rb | 10 ++++++--- app/services/phone_parser_service.rb | 4 ++-- .../spl/coupons/activate_coupon_service.rb | 11 ++++------ .../spl/coupons/deactivate_coupon_service.rb | 11 ++++------ .../spl/coupons/get_coupons_service.rb | 21 ++++++++++--------- app/services/spl/me_service.rb | 7 ++----- 6 files changed, 30 insertions(+), 34 deletions(-) diff --git a/app/helpers/spl_service_helper.rb b/app/helpers/spl_service_helper.rb index 7ac3c04..9560321 100644 --- a/app/helpers/spl_service_helper.rb +++ b/app/helpers/spl_service_helper.rb @@ -9,12 +9,16 @@ def send_request(url, body) # using customer oriented endpoints # @param [user: Spree::User] def refresh_user_token(user) - return if @user.private_metadata.nil? - return if @user.private_metadata['spl_refresh_token'].nil? + return if user.private_metadata.nil? + return if user.private_metadata['spl_refresh_token'].nil? - response = Spl::OauthTokenService.refresh_token(@user.private_metadata['spl_refresh_token']) + response = Spl::OauthTokenService.refresh_token(user.private_metadata['spl_refresh_token']) user.update!(private_metadata: { spl_access_token: response['access_token'], spl_refresh_token: response['refresh_token'] }) end + + def token_refresh_needed(response_body, retry_counter) + token_expired?(response_body['errorCode']) && retry_counter < 1 && refresh_user_token(user) + end end diff --git a/app/services/phone_parser_service.rb b/app/services/phone_parser_service.rb index a7e16af..e36da21 100644 --- a/app/services/phone_parser_service.rb +++ b/app/services/phone_parser_service.rb @@ -11,10 +11,10 @@ def initialize(raw_phone) end def valid? - has_country_code? && phone.valid? + country_code? && phone.valid? end - def has_country_code? + def country_code? raw.match?(COUNTRY_CODE_REGEX) end diff --git a/app/services/spl/coupons/activate_coupon_service.rb b/app/services/spl/coupons/activate_coupon_service.rb index 0b013dd..5454c0c 100644 --- a/app/services/spl/coupons/activate_coupon_service.rb +++ b/app/services/spl/coupons/activate_coupon_service.rb @@ -14,13 +14,12 @@ def initialize(user, store, coupon_code) @activate_coupons_url = URI.parse(Spl::UrlCreatorService.new(store.private_metadata['spl_url']).coupon_activate) @user = user @coupon_code = coupon_code + @retry_counter = 0 end def call return unless @user.present? && @user.private_metadata.present? - retry_counter ||= 0 - body = prepare_body response = send_request(@activate_coupons_url, body) response_body = JSON.parse(response.body) Rails.logger.debug response_body @@ -28,17 +27,15 @@ def call response_body['response'] rescue ActivateCouponServiceError => e - raise e unless token_expired?(response_body['errorCode']) && retry_counter < 1 + raise e unless token_refresh_needed(response_body, @user) - raise e unless refresh_user_token(@user) - - retry_counter += 1 + @retry_counter += 1 retry end private - def prepare_body + def body { context: { prgCode: @store.private_metadata['spl_prg_code'], diff --git a/app/services/spl/coupons/deactivate_coupon_service.rb b/app/services/spl/coupons/deactivate_coupon_service.rb index ba85b33..e80ae29 100644 --- a/app/services/spl/coupons/deactivate_coupon_service.rb +++ b/app/services/spl/coupons/deactivate_coupon_service.rb @@ -14,13 +14,12 @@ def initialize(user, store, coupon_code) @deactivate_coupons_url = URI.parse(Spl::UrlCreatorService.new(store.private_metadata['spl_url']).coupon_deactivate) @user = user @coupon_code = coupon_code + @retry_counter = 0 end def call return unless @user.present? && @user.private_metadata.present? - retry_counter ||= 0 - body = prepare_body response = send_request(@deactivate_coupons_url, body) response_body = JSON.parse(response.body) Rails.logger.debug response_body @@ -28,17 +27,15 @@ def call response_body['response'] rescue DeactivateCouponServiceError => e - raise e unless token_expired?(response_body['errorCode']) && retry_counter < 1 + raise e unless token_refresh_needed(response_body, @user) - raise e unless refresh_user_token(@user) - - retry_counter += 1 + @retry_counter += 1 retry end private - def prepare_body + def body { context: { prgCode: @store.private_metadata['spl_prg_code'], diff --git a/app/services/spl/coupons/get_coupons_service.rb b/app/services/spl/coupons/get_coupons_service.rb index 55e821d..dd028ae 100644 --- a/app/services/spl/coupons/get_coupons_service.rb +++ b/app/services/spl/coupons/get_coupons_service.rb @@ -13,33 +13,28 @@ def initialize(user, store) @store = store @find_coupons_url = URI.parse(Spl::UrlCreatorService.new(store.private_metadata['spl_url']).coupon_find) @user = user + @retry_counter = 0 end def call return unless @user.present? && @user.private_metadata.present? - retry_counter ||= 0 - body = prepare_body response = send_request(@find_coupons_url, body) response_body = JSON.parse(response.body) Rails.logger.debug response_body raise SplGetCouponError, response_body['msg'] if response_body['errorCode'] != '0' - response_body['response']&.filter do |coupon| - active?(coupon) - end + filtered_coupons(response_body) rescue SplGetCouponError => e - raise e unless token_expired?(response_body['errorCode']) && retry_counter < 1 + raise e unless token_refresh_needed(response_body, @user) - raise e unless refresh_user_token(@user) - - retry_counter += 1 + @retry_counter += 1 retry end private - def prepare_body + def body { context: { prgCode: @store.private_metadata['spl_prg_code'], @@ -49,6 +44,12 @@ def prepare_body } end + def filtered_coupons(response_body) + response_body['response']&.filter do |coupon| + active?(coupon) + end + end + def active?(coupon) coupon['used'] != true && coupon['usageTemporaryBlocked'] != true && correct_time?(coupon['expirationDate']) && coupon['usageDisabled'] != true diff --git a/app/services/spl/me_service.rb b/app/services/spl/me_service.rb index 7d277df..894ab9c 100644 --- a/app/services/spl/me_service.rb +++ b/app/services/spl/me_service.rb @@ -18,7 +18,6 @@ def call return unless @user.present? && @user.private_metadata.present? retry_counter ||= 0 - body = prepare_me_body response = send_request(@me_url, body) response_body = JSON.parse(response.body) Rails.logger.debug response_body @@ -26,9 +25,7 @@ def call response_body rescue SplMeError => e - raise e unless token_expired?(response_body['errorCode']) && retry_counter < 1 - - raise e unless refresh_user_token(@user) + raise e unless token_refresh_needed(response_body, @user) retry_counter += 1 retry @@ -36,7 +33,7 @@ def call private - def prepare_me_body + def body { context: { prgCode: @env['spl_prg_code'], From e0976e0b6464863875708f4831c94c762dd27544 Mon Sep 17 00:00:00 2001 From: vi4me Date: Mon, 19 Jan 2026 21:39:32 +0100 Subject: [PATCH 49/63] EK-683 Add Generators::InstallGenerator for JS --- README.md | 7 ++ app/views/spl/_loyalty_connect_form.html.erb | 113 +++++++++--------- .../spl/install/install_generator.rb | 27 +++++ .../install/templates/login_spl_controller.js | 21 ++++ 4 files changed, 113 insertions(+), 55 deletions(-) create mode 100644 lib/generators/spl/install/install_generator.rb create mode 100644 lib/generators/spl/install/templates/login_spl_controller.js diff --git a/README.md b/README.md index 009c6bc..8d0e19d 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,13 @@ _______ _______ _______ +To add JavaScript to your application, enter the following in the terminal: + +```sh +bin/rails g spl:install +``` + + After installation, add the following line to `Spree::Adjustable::AdjustmentsUpdaterDecorator#persist_totals`: ```sh diff --git a/app/views/spl/_loyalty_connect_form.html.erb b/app/views/spl/_loyalty_connect_form.html.erb index 8e78767..9aee054 100644 --- a/app/views/spl/_loyalty_connect_form.html.erb +++ b/app/views/spl/_loyalty_connect_form.html.erb @@ -1,66 +1,69 @@ <%= turbo_frame_tag "loyalty_connect_form" do %> <%= render partial: 'spree/shared/error_messages', locals: { target: @user } %> -
- <%= check_box_tag :has_account, '1', true, id: 'has_account_checkbox', class: 'mt-1' %> - <%= label_tag :has_account, I18n.t('spl.user.have_account'), class: 'text-sm text-neutral-700' %> -
+
+
+ <%= check_box_tag :has_account, '1', true, id: 'has_account_checkbox', class: 'mt-1', + data: { login_spl_target: "checkbox", action: "login-spl#toggle" }%> + <%= label_tag :has_account, I18n.t('spl.user.have_account'), class: 'text-sm text-neutral-700' %> +
-
- <%= form_with scope: :user, url: login_code_account_profile_path, method: :patch, data: { turbo_frame: "loyalty_connect_form" } do |f| %> -
- <%= f.label :phone, - Spree.t(:phone), - class: 'text-xs text-neutral-600 block mb-2' %> - <%= f.telephone_field :phone, - class: 'text-input w-full', - placeholder: Spree.t('loyalty.phone_label'), - value: try_spree_current_user&.phone %> -
+
+ <%= form_with scope: :user, url: login_code_account_profile_path, method: :patch, data: { turbo_frame: "loyalty_connect_form" } do |f| %> +
+ <%= f.label :phone, + Spree.t(:phone), + class: 'text-xs text-neutral-600 block mb-2' %> + <%= f.telephone_field :phone, + class: 'text-input w-full', + placeholder: Spree.t('loyalty.phone_label'), + value: try_spree_current_user&.phone %> +
-
- <%= f.check_box :accept_yc_terms, - { - class: 'mt-1', - checked: ActiveModel::Type::Boolean.new.cast( - try_spree_current_user&.public_metadata&.fetch('accept_yc_terms', false) - ) - }, - 'true', - 'false' %> - <%= f.label :accept_yc_terms, I18n.t('spl.user.accept_yc_terms_label'), class: 'text-sm text-neutral-700' %> -
+
+ <%= f.check_box :accept_yc_terms, + { + class: 'mt-1', + checked: ActiveModel::Type::Boolean.new.cast( + try_spree_current_user&.public_metadata&.fetch('accept_yc_terms', false) + ) + }, + 'true', + 'false' %> + <%= f.label :accept_yc_terms, I18n.t('spl.user.accept_yc_terms_label'), class: 'text-sm text-neutral-700' %> +
-
- <%= f.submit I18n.t('buttons.send_log_in_code'), class: 'btn-primary w-full' %> -
- <% end %> -
+
+ <%= f.submit I18n.t('buttons.send_log_in_code'), class: 'btn-primary w-full' %> +
+ <% end %> +
- <% end %> diff --git a/lib/generators/spl/install/install_generator.rb b/lib/generators/spl/install/install_generator.rb new file mode 100644 index 0000000..ebe749c --- /dev/null +++ b/lib/generators/spl/install/install_generator.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Spl + module Generators + class InstallGenerator < Rails::Generators::Base + source_root File.expand_path('templates', __dir__) + + desc 'Install SPL Stimulus controller' + + def copy_stimulus_controller + copy_file('login_spl_controller.js', 'app/javascript/controllers/login_spl_controller.js') + end + + def ensure_controllers_autoload + controllers_index = 'app/javascript/controllers/index.js' + + return unless File.exist?(controllers_index) + + content = File.read(controllers_index) + + return if content.include?('login_spl') + + say_status :info, 'Stimulus controllers are auto-loaded, nothing to register', :blue + end + end + end +end diff --git a/lib/generators/spl/install/templates/login_spl_controller.js b/lib/generators/spl/install/templates/login_spl_controller.js new file mode 100644 index 0000000..1a39f2d --- /dev/null +++ b/lib/generators/spl/install/templates/login_spl_controller.js @@ -0,0 +1,21 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static targets = ["checkbox", "loginForm", "registrationForm"] + + connect() { + this.toggle() + } + + toggle() { + if (!this.hasCheckboxTarget) return + + if (this.checkboxTarget.checked) { + this.loginFormTarget.classList.remove("hidden") + this.registrationFormTarget.classList.add("hidden") + } else { + this.loginFormTarget.classList.add("hidden") + this.registrationFormTarget.classList.remove("hidden") + } + } +} From 37bfdea7a2892e3de34400b05bbf1ff16441e7e3 Mon Sep 17 00:00:00 2001 From: vi4me Date: Tue, 20 Jan 2026 12:07:22 +0100 Subject: [PATCH 50/63] EK-683 Add check if file exist to InstallGenerator --- .../spl/install/install_generator.rb | 20 ++++++++++++------- .../install/templates/login_spl_controller.js | 1 + 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/lib/generators/spl/install/install_generator.rb b/lib/generators/spl/install/install_generator.rb index ebe749c..ebaa5e3 100644 --- a/lib/generators/spl/install/install_generator.rb +++ b/lib/generators/spl/install/install_generator.rb @@ -8,19 +8,25 @@ class InstallGenerator < Rails::Generators::Base desc 'Install SPL Stimulus controller' def copy_stimulus_controller - copy_file('login_spl_controller.js', 'app/javascript/controllers/login_spl_controller.js') + destination = 'app/javascript/controllers/login_spl_controller.js' + + if File.exist?(destination) + say_status :skip, "#{destination} already exists", :yellow + else + copy_file 'login_spl_controller.js', destination + say_status :create, destination, :green + end end def ensure_controllers_autoload controllers_index = 'app/javascript/controllers/index.js' - return unless File.exist?(controllers_index) + unless File.exist?(controllers_index) + say_status :warning, 'Stimulus controllers index.js not found', :yellow + return + end - content = File.read(controllers_index) - - return if content.include?('login_spl') - - say_status :info, 'Stimulus controllers are auto-loaded, nothing to register', :blue + say_status :info, 'Stimulus controllers are auto-loaded', :blue end end end diff --git a/lib/generators/spl/install/templates/login_spl_controller.js b/lib/generators/spl/install/templates/login_spl_controller.js index 1a39f2d..8e233ab 100644 --- a/lib/generators/spl/install/templates/login_spl_controller.js +++ b/lib/generators/spl/install/templates/login_spl_controller.js @@ -1,3 +1,4 @@ +// This file is generated using the spree-spl gem. import { Controller } from "@hotwired/stimulus" export default class extends Controller { From 6790893a8c12ed0cea584bc6436811ab6e90c02e Mon Sep 17 00:00:00 2001 From: Tomasz Strzeszewski Date: Tue, 20 Jan 2026 12:15:08 +0100 Subject: [PATCH 51/63] Fix tests --- .../spl/spree/storefront/checkout_controller_decorator.rb | 5 +++++ app/helpers/spl_service_helper.rb | 2 +- app/services/spl/coupons/activate_coupon_service.rb | 2 +- app/services/spl/coupons/deactivate_coupon_service.rb | 2 +- app/services/spl/coupons/get_coupons_service.rb | 2 +- app/services/spl/me_service.rb | 2 +- spec/controllers/checkout_controller_decorator_spec.rb | 4 ++-- spec/services/spl/coupons/get_coupon_service_spec.rb | 1 + 8 files changed, 13 insertions(+), 7 deletions(-) diff --git a/app/controllers/spl/spree/storefront/checkout_controller_decorator.rb b/app/controllers/spl/spree/storefront/checkout_controller_decorator.rb index a608e8e..e00f9be 100644 --- a/app/controllers/spl/spree/storefront/checkout_controller_decorator.rb +++ b/app/controllers/spl/spree/storefront/checkout_controller_decorator.rb @@ -16,6 +16,7 @@ def activate_coupon load_user_coupons rescue StandardError => e handle_spl_error(e) + raise e ensure respond_to do |format| format.turbo_stream @@ -30,6 +31,7 @@ def deactivate_coupon load_user_coupons rescue StandardError => e handle_spl_error(e) + raise e ensure respond_to do |format| format.turbo_stream @@ -45,6 +47,9 @@ def promotion_switcher def load_user_coupons @coupons = Spl::Coupons::GetCouponsService.new(@order.user, @order.store).call + rescue + handle_spl_error(e) + raise e end end end diff --git a/app/helpers/spl_service_helper.rb b/app/helpers/spl_service_helper.rb index 9560321..0aee540 100644 --- a/app/helpers/spl_service_helper.rb +++ b/app/helpers/spl_service_helper.rb @@ -18,7 +18,7 @@ def refresh_user_token(user) spl_refresh_token: response['refresh_token'] }) end - def token_refresh_needed(response_body, retry_counter) + def token_refresh_needed(response_body, retry_counter, user) token_expired?(response_body['errorCode']) && retry_counter < 1 && refresh_user_token(user) end end diff --git a/app/services/spl/coupons/activate_coupon_service.rb b/app/services/spl/coupons/activate_coupon_service.rb index 5454c0c..0f7c9b6 100644 --- a/app/services/spl/coupons/activate_coupon_service.rb +++ b/app/services/spl/coupons/activate_coupon_service.rb @@ -27,7 +27,7 @@ def call response_body['response'] rescue ActivateCouponServiceError => e - raise e unless token_refresh_needed(response_body, @user) + raise e unless token_refresh_needed(response_body, @retry_counter, @user) @retry_counter += 1 retry diff --git a/app/services/spl/coupons/deactivate_coupon_service.rb b/app/services/spl/coupons/deactivate_coupon_service.rb index e80ae29..2e0b970 100644 --- a/app/services/spl/coupons/deactivate_coupon_service.rb +++ b/app/services/spl/coupons/deactivate_coupon_service.rb @@ -27,7 +27,7 @@ def call response_body['response'] rescue DeactivateCouponServiceError => e - raise e unless token_refresh_needed(response_body, @user) + raise e unless token_refresh_needed(response_body, @retry_counter, @user) @retry_counter += 1 retry diff --git a/app/services/spl/coupons/get_coupons_service.rb b/app/services/spl/coupons/get_coupons_service.rb index dd028ae..166c3d8 100644 --- a/app/services/spl/coupons/get_coupons_service.rb +++ b/app/services/spl/coupons/get_coupons_service.rb @@ -26,7 +26,7 @@ def call filtered_coupons(response_body) rescue SplGetCouponError => e - raise e unless token_refresh_needed(response_body, @user) + raise e unless token_refresh_needed(response_body, @retry_counter, @user) @retry_counter += 1 retry diff --git a/app/services/spl/me_service.rb b/app/services/spl/me_service.rb index 894ab9c..4bcfe17 100644 --- a/app/services/spl/me_service.rb +++ b/app/services/spl/me_service.rb @@ -25,7 +25,7 @@ def call response_body rescue SplMeError => e - raise e unless token_refresh_needed(response_body, @user) + raise e unless token_refresh_needed(response_body, @retry_counter, @user) retry_counter += 1 retry diff --git a/spec/controllers/checkout_controller_decorator_spec.rb b/spec/controllers/checkout_controller_decorator_spec.rb index fe54e0e..ad55ddd 100644 --- a/spec/controllers/checkout_controller_decorator_spec.rb +++ b/spec/controllers/checkout_controller_decorator_spec.rb @@ -110,7 +110,7 @@ def set_order let(:activate) { instance_double(Spl::Coupons::ActivateCouponService, call: true) } before do - allow(instance_of(Spl::Coupons::ActivateCouponService)).to receive(:try_spree_current_user).and_return(user) + allow(controller).to receive(:try_spree_current_user).and_return(user) end context 'when coupon is valid' do @@ -148,7 +148,7 @@ def set_order describe '#deactivate_coupon' do before do - allow(instance_of(Spl::Coupons::ActivateCouponService)).to receive(:try_spree_current_user).and_return(user) + allow(controller).to receive(:try_spree_current_user).and_return(user) end it 'calls DeactivateCouponService, reloads coupons, and redirects (HTML)' do diff --git a/spec/services/spl/coupons/get_coupon_service_spec.rb b/spec/services/spl/coupons/get_coupon_service_spec.rb index 433bff1..187ff63 100644 --- a/spec/services/spl/coupons/get_coupon_service_spec.rb +++ b/spec/services/spl/coupons/get_coupon_service_spec.rb @@ -109,6 +109,7 @@ before do allow(response_double).to receive(:body).and_return(error_response_body.to_json) + service.instance_variable_set(:@retry_counter, 0) end it 'raises SplGetCouponError with message' do From 064cb948dd4f5d7e39a6b44cea2c5fdf77fc1782 Mon Sep 17 00:00:00 2001 From: Tomasz Strzeszewski Date: Tue, 20 Jan 2026 12:15:25 +0100 Subject: [PATCH 52/63] rubocop --- .../spl/spree/storefront/checkout_controller_decorator.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/spl/spree/storefront/checkout_controller_decorator.rb b/app/controllers/spl/spree/storefront/checkout_controller_decorator.rb index e00f9be..ca6c22c 100644 --- a/app/controllers/spl/spree/storefront/checkout_controller_decorator.rb +++ b/app/controllers/spl/spree/storefront/checkout_controller_decorator.rb @@ -47,7 +47,7 @@ def promotion_switcher def load_user_coupons @coupons = Spl::Coupons::GetCouponsService.new(@order.user, @order.store).call - rescue + rescue StandardError handle_spl_error(e) raise e end From e4a10693cd8b940731029623b1068a9442e5c83e Mon Sep 17 00:00:00 2001 From: vi4me Date: Wed, 21 Jan 2026 10:42:50 +0100 Subject: [PATCH 53/63] EK-683 Change spree_current_user to try_spree_current_user --- .../spl/spree/storefront/profile_controller_decorator.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/controllers/spl/spree/storefront/profile_controller_decorator.rb b/app/controllers/spl/spree/storefront/profile_controller_decorator.rb index c289e8f..a58e1b1 100644 --- a/app/controllers/spl/spree/storefront/profile_controller_decorator.rb +++ b/app/controllers/spl/spree/storefront/profile_controller_decorator.rb @@ -41,7 +41,7 @@ def registration_code end def register_loyalty_account - Spl::RegisterAccountService.new(spree_current_user, current_store, params['user']['spl_auth_code']).call + Spl::RegisterAccountService.new(try_spree_current_user, current_store, params['user']['spl_auth_code']).call redirect_to spree.edit_account_profile_path, notice: ::Spree.t(:successfully_updated, resource: ::Spree.t(:account)) rescue Spl::RegisterAccountService::SplRegisterAccountError, Spl::OauthTokenService::OauthTokenError => e @@ -78,7 +78,7 @@ def request_otp(phone, store, params) def validate_card(metadata) return unless disactivated_card? - ::Spl::ValidateCardService.new(metadata[:spl_no_card], spree_current_user, current_store).call + ::Spl::ValidateCardService.new(metadata[:spl_no_card], try_spree_current_user, current_store).call end def handle_validation_error(error) @@ -87,7 +87,7 @@ def handle_validation_error(error) end def update_order(spl_card, active) - current_order = spree_current_user.orders.last + current_order = try_spree_current_user.orders.last return unless %w[cart address delivery payment].include?(current_order.state) current_order.update( From 1528881aff524f02a76486f6079ebe26f56797a8 Mon Sep 17 00:00:00 2001 From: Tomasz Strzeszewski Date: Wed, 21 Jan 2026 11:18:46 +0100 Subject: [PATCH 54/63] Bump spree version to ~>5.2, fulfill missing locales --- Gemfile.lock | 34 +++++++++++++++++----------------- config/locales/cs.yml | 7 ++++++- config/locales/en.yml | 6 +++--- config/locales/pl.yml | 2 +- config/locales/sk.yml | 7 ++++++- spree-spl.gemspec | 2 +- 6 files changed, 34 insertions(+), 24 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index e05004c..d863693 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -10,8 +10,8 @@ PATH rspec-rails rspec-retry rubocop-rspec - spree (~> 5.1, >= 5.1.4) - spree_api (~> 5.1, >= 5.1.4) + spree (~> 5.2, >= 5.2.4) + spree_api (~> 5.2, >= 5.2.4) spree_auth_devise spree_dev_tools @@ -222,7 +222,7 @@ GEM faraday-net_http (>= 2.0, < 3.5) json logger - faraday-http-cache (2.5.1) + faraday-http-cache (2.6.1) faraday (>= 0.8) faraday-net_http (3.4.2) net-http (~> 0.5) @@ -239,10 +239,10 @@ GEM fiber-local (1.1.0) fiber-storage fiber-storage (1.0.1) - friendly_id (5.4.2) + friendly_id (5.5.1) activerecord (>= 4.0.0) - friendly_id-mobility (1.0.4) - friendly_id (>= 5.0.0, < 5.5) + friendly_id-mobility (1.0.5) + friendly_id (>= 5.0.0, < 5.6) mobility (>= 1.0.1, < 2.0) gem-release (2.2.4) geocoder (1.8.6) @@ -407,8 +407,8 @@ GEM prettyprint (0.2.0) prism (1.8.0) protocol-hpack (1.5.1) - protocol-http (0.58.0) - protocol-http1 (0.36.0) + protocol-http (0.58.1) + protocol-http1 (0.37.0) protocol-http (~> 0.58) protocol-http2 (0.24.0) protocol-hpack (~> 1.4) @@ -422,7 +422,7 @@ GEM date stringio public_suffix (7.0.2) - puma (7.1.0) + puma (7.2.0) nio4r (~> 2.0) racc (1.8.1) rack (3.2.4) @@ -716,7 +716,7 @@ GEM activesupport (>= 4.2.5) json (>= 1.8.3) tsort (0.2.0) - turbo-rails (2.0.20) + turbo-rails (2.0.21) actionpack (>= 7.1.0) railties (>= 7.1.0) tzinfo (2.0.6) @@ -844,7 +844,7 @@ CHECKSUMS factory_bot (6.5.6) sha256=12beb373214dccc086a7a63763d6718c49769d5606f0501e0a4442676917e077 factory_bot_rails (6.5.1) sha256=d3cc4851eae4dea8a665ec4a4516895045e710554d2b5ac9e68b94d351bc6d68 faraday (2.14.0) sha256=8699cfe5d97e55268f2596f9a9d5a43736808a943714e3d9a53e6110593941cd - faraday-http-cache (2.5.1) sha256=cbfc129a85e1ad8580130189832cf59e7e117d94fe277547d61fc531f7689483 + faraday-http-cache (2.6.1) sha256=7772ae61c5fe1c0243452dec1f069b15bcc3304e9436e4f8545ccdb5963c4285 faraday-net_http (3.4.2) sha256=f147758260d3526939bf57ecf911682f94926a3666502e24c69992765875906c ffaker (2.25.0) sha256=e485c5adf8195aac55662875b7f515469bca46d77b60d0e7d08db6861bcbec40 ffi (1.17.3-aarch64-linux-gnu) sha256=28ad573df26560f0aedd8a90c3371279a0b2bd0b4e834b16a2baa10bd7a97068 @@ -858,8 +858,8 @@ CHECKSUMS fiber-annotation (0.2.0) sha256=7abfadf1d119f508867d4103bf231c0354d019cc39a5738945dec2edadaf6c03 fiber-local (1.1.0) sha256=c885f94f210fb9b05737de65d511136ea602e00c5105953748aa0f8793489f06 fiber-storage (1.0.1) sha256=f48e5b6d8b0be96dac486332b55cee82240057065dc761c1ea692b2e719240e1 - friendly_id (5.4.2) sha256=02bc2e9a0da1c4c70c428db49f21f95d1a21ef8d16027d051ff6367b35fe7d4d - friendly_id-mobility (1.0.4) sha256=ef4d61536e8d5d1ca9d90659a712045174221a6a8bba36a0318eb324de22eb10 + friendly_id (5.5.1) sha256=e018f2b89bfc143276fee6d378a64792385cd4fddd3d4fce501f59ec19c06207 + friendly_id-mobility (1.0.5) sha256=8c0cf4d0a1358311db1b2dcc0e4c33077ae929c7e34749485268d066adbaf2e4 gem-release (2.2.4) sha256=2f11124c1580c811507c3b47e875e420cf3ed792a98105b49df11971e6e94db3 geocoder (1.8.6) sha256=e0ca1554b499f466de9b003f7dff70f89a5888761c2ca68ed9f86b6e5e24e74c github_changelog_generator (1.16.4) sha256=21d2dfe41d50cf3a7ffe896774779d9d61da6aaf08d390f8ddff0dacbdf8a611 @@ -934,14 +934,14 @@ CHECKSUMS prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193 prism (1.8.0) sha256=84453a16ef5530ea62c5f03ec16b52a459575ad4e7b9c2b360fd8ce2c39c1254 protocol-hpack (1.5.1) sha256=6feca238b8078da1cd295677d6f306c6001af92d75fe0643d33e6956cbc3ad91 - protocol-http (0.58.0) sha256=b77c5799e7a7acd31afaa9fed5c3714fe91ab7746925d66e9e4d92a5c74555fa - protocol-http1 (0.36.0) sha256=057344d2509a59338d0b4bc8f092a52126bb9ed51b76adbb4365f132bbd4ef09 + protocol-http (0.58.1) sha256=6d0694aa07cd51ff589c6219f9613c6827aebd5ffdf8fb8a0169254feb0f13fd + protocol-http1 (0.37.0) sha256=5bdd739e28792b341134596f6f5ab21a9d4b395f67bae69e153743eb0e69d123 protocol-http2 (0.24.0) sha256=65327a019b7e36d2774e94050bf57a43bb60212775d2fcf02ae1d2ed4f01ef28 protocol-url (0.4.0) sha256=64d4c03b6b51ad815ac6fdaf77a1d91e5baf9220d26becb846c5459dacdea9e1 pry (0.16.0) sha256=d76c69065698ed1f85e717bd33d7942c38a50868f6b0673c636192b3d1b6054e psych (5.3.1) sha256=eb7a57cef10c9d70173ff74e739d843ac3b2c019a003de48447b2963d81b1974 public_suffix (7.0.2) sha256=9114090c8e4e7135c1fd0e7acfea33afaab38101884320c65aaa0ffb8e26a857 - puma (7.1.0) sha256=e45c10cb124f224d448c98db653a75499794edbecadc440ad616cf50f2fd49dd + puma (7.2.0) sha256=bf8ef4ab514a4e6d4554cb4326b2004eba5036ae05cf765cfe51aba9706a72a8 racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f rack (3.2.4) sha256=5d74b6f75082a643f43c1e76b419c40f0e5527fcfee1e669ac1e6b73c0ccb6f6 rack-session (2.1.1) sha256=0b6dc07dea7e4b583f58a48e8b806d4c9f1c6c9214ebc202ec94562cbea2e4e9 @@ -1029,7 +1029,7 @@ CHECKSUMS traces (0.18.2) sha256=80f1649cb4daace1d7174b81f3b3b7427af0b93047759ba349960cb8f315e214 tracking_number (2.4.0) sha256=2a4c6b39cbdfc8326c234626556da1e269c8dfe33478786bea93461331c247ff tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f - turbo-rails (2.0.20) sha256=cbcbb4dd3ce59f6471c9f911b1655b2c721998cc8303959d982da347f374ea95 + turbo-rails (2.0.21) sha256=02070ea29fd11d8c1a07d9d7be980729a20e94e39b8c6c819f690f7959216bc7 tzinfo (2.0.6) sha256=8daf828cc77bcf7d63b0e3bdb6caa47e2272dcfaf4fbfe46f8c3a9df087a829b unaccent (0.4.0) sha256=d9278230016edc5317c053f8e84c1e1dea6827dec028e5481e2294a4e11e7333 unicode-display_width (3.2.0) sha256=0cdd96b5681a5949cdbc2c55e7b420facae74c4aaf9a9815eee1087cb1853c42 diff --git a/config/locales/cs.yml b/config/locales/cs.yml index 9156601..f403505 100644 --- a/config/locales/cs.yml +++ b/config/locales/cs.yml @@ -8,7 +8,7 @@ cs: code_label: One-time code errors: must_accept_yc_terms: You must accept the loyalty club terms and conditions to continue - invalid_phone: Podaj poprawny numer telefonu + invalid_phone: Invalid phone number card_validation: errors: card_not_active: Karta není aktivní @@ -17,6 +17,8 @@ cs: coupons: find: checkout_list: Your SPL coupons + activate: Activate + deactivate: Deactivate profile: connect_button: Connect to your loyalty account errors: @@ -35,3 +37,6 @@ cs: connect_account: Connect loyalty account send_code: Send one-time code verify_button: Verify phone number + user: + card_number: The club card number has been assigned to the account + card_active: SPL card activation diff --git a/config/locales/en.yml b/config/locales/en.yml index dd6b7bb..d74f76e 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -8,7 +8,7 @@ en: code_label: One-time code errors: must_accept_yc_terms: You must accept the loyalty club terms and conditions to continue - invalid_phone: Podaj poprawny numer telefonu + invalid_phone: Invalid phone number card_validation: errors: card_not_active: Card is not active @@ -17,8 +17,8 @@ en: coupons: find: checkout_list: Your SPL coupons - activate: Aktywuj - deactivate: Dezaktywuj + activate: Activate + deactivate: Deactivate profile: connect_button: Connect to your loyalty account errors: diff --git a/config/locales/pl.yml b/config/locales/pl.yml index 375b2ca..4c3ca2c 100644 --- a/config/locales/pl.yml +++ b/config/locales/pl.yml @@ -16,7 +16,7 @@ pl: wrong_owner: Karta przypisana innemu użytkownikowi coupons: find: - checkout_list: Your SPL coupons + checkout_list: Twoje kupony lojalnościowe activate: Aktywuj deactivate: Dezaktywuj errors: diff --git a/config/locales/sk.yml b/config/locales/sk.yml index a2849d7..b8046b9 100644 --- a/config/locales/sk.yml +++ b/config/locales/sk.yml @@ -8,7 +8,7 @@ sk: code_label: One-time code errors: must_accept_yc_terms: You must accept the loyalty club terms and conditions to continue - invalid_phone: Podaj poprawny numer telefonu + invalid_phone: Invalid phone number card_validation: errors: card_not_active: Karta nie je aktívna @@ -17,6 +17,8 @@ sk: coupons: find: checkout_list: Your SPL coupons + activate: Activate + deactivate: Deactivate profile: connect_button: Connect to your loyalty account errors: @@ -35,3 +37,6 @@ sk: connect_account: Connect loyalty account send_code: Send one-time code verify_button: Verify phone number + user: + card_number: The club card number has been assigned to the account + card_active: SPL card activation diff --git a/spree-spl.gemspec b/spree-spl.gemspec index b352310..6887eb8 100644 --- a/spree-spl.gemspec +++ b/spree-spl.gemspec @@ -29,7 +29,7 @@ Gem::Specification.new do |spec| spec.add_dependency('faraday') spec.add_dependency('openssl') - spree_version = ['~> 5.1', '>= 5.1.4'] + spree_version = ['~> 5.2', '>= 5.2.4'] spec.add_dependency 'bigdecimal' spec.add_dependency 'rails', '~> 7.0' spec.add_dependency 'spree', spree_version From 2a34be9262a2b271d1add2552e4e490f7087e182 Mon Sep 17 00:00:00 2001 From: Tomasz Strzeszewski Date: Wed, 21 Jan 2026 11:55:22 +0100 Subject: [PATCH 55/63] rubocop offences --- app/services/apply_sparta_discount_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/apply_sparta_discount_service.rb b/app/services/apply_sparta_discount_service.rb index 8847b52..83bf018 100644 --- a/app/services/apply_sparta_discount_service.rb +++ b/app/services/apply_sparta_discount_service.rb @@ -10,7 +10,7 @@ def initialize(response, order) @response = response end - def call # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity + def call # rubocop:disable Metrics/AbcSize return unless response_valid? line_items.each do |line_item| From a7b70663070388378e8343da6b4e97440452f720 Mon Sep 17 00:00:00 2001 From: Tomasz Strzeszewski Date: Wed, 21 Jan 2026 12:41:34 +0100 Subject: [PATCH 56/63] Refactor, fix bug with wrong invoke of refresh token action --- .../spl/spree/storefront/checkout_controller_decorator.rb | 2 +- app/helpers/spl_service_helper.rb | 8 ++++---- app/services/spl/coupons/activate_coupon_service.rb | 2 +- app/services/spl/coupons/deactivate_coupon_service.rb | 2 +- app/services/spl/coupons/get_coupons_service.rb | 2 +- app/services/spl/me_service.rb | 2 +- app/services/spl/oauth_token_service.rb | 8 ++++---- 7 files changed, 13 insertions(+), 13 deletions(-) diff --git a/app/controllers/spl/spree/storefront/checkout_controller_decorator.rb b/app/controllers/spl/spree/storefront/checkout_controller_decorator.rb index ca6c22c..62b448b 100644 --- a/app/controllers/spl/spree/storefront/checkout_controller_decorator.rb +++ b/app/controllers/spl/spree/storefront/checkout_controller_decorator.rb @@ -47,7 +47,7 @@ def promotion_switcher def load_user_coupons @coupons = Spl::Coupons::GetCouponsService.new(@order.user, @order.store).call - rescue StandardError + rescue StandardError => e handle_spl_error(e) raise e end diff --git a/app/helpers/spl_service_helper.rb b/app/helpers/spl_service_helper.rb index 0aee540..30f7e56 100644 --- a/app/helpers/spl_service_helper.rb +++ b/app/helpers/spl_service_helper.rb @@ -8,17 +8,17 @@ def send_request(url, body) # Refreshes user private token to keep possible # using customer oriented endpoints # @param [user: Spree::User] - def refresh_user_token(user) + def refresh_user_token(user, store) return if user.private_metadata.nil? return if user.private_metadata['spl_refresh_token'].nil? - response = Spl::OauthTokenService.refresh_token(user.private_metadata['spl_refresh_token']) + response = Spl::OauthTokenService.new(DateTime.current, store).refresh_token(user.private_metadata['spl_refresh_token']) user.update!(private_metadata: { spl_access_token: response['access_token'], spl_refresh_token: response['refresh_token'] }) end - def token_refresh_needed(response_body, retry_counter, user) - token_expired?(response_body['errorCode']) && retry_counter < 1 && refresh_user_token(user) + def token_refresh_needed(response_body, retry_counter, user, store) + token_expired?(response_body['errorCode']) && retry_counter < 1 && refresh_user_token(user, store) end end diff --git a/app/services/spl/coupons/activate_coupon_service.rb b/app/services/spl/coupons/activate_coupon_service.rb index a399c92..5d2f2c7 100644 --- a/app/services/spl/coupons/activate_coupon_service.rb +++ b/app/services/spl/coupons/activate_coupon_service.rb @@ -29,7 +29,7 @@ def call response_body['response'] rescue ActivateCouponServiceError => e - raise e unless token_refresh_needed(response_body, @retry_counter, @user) + raise e unless token_refresh_needed(response_body, @retry_counter, @user, @store) @retry_counter += 1 retry diff --git a/app/services/spl/coupons/deactivate_coupon_service.rb b/app/services/spl/coupons/deactivate_coupon_service.rb index df4af34..d5ee195 100644 --- a/app/services/spl/coupons/deactivate_coupon_service.rb +++ b/app/services/spl/coupons/deactivate_coupon_service.rb @@ -29,7 +29,7 @@ def call response_body['response'] rescue DeactivateCouponServiceError => e - raise e unless token_refresh_needed(response_body, @retry_counter, @user) + raise e unless token_refresh_needed(response_body, @retry_counter, @user, @store) @retry_counter += 1 retry diff --git a/app/services/spl/coupons/get_coupons_service.rb b/app/services/spl/coupons/get_coupons_service.rb index df81605..1d7193a 100644 --- a/app/services/spl/coupons/get_coupons_service.rb +++ b/app/services/spl/coupons/get_coupons_service.rb @@ -28,7 +28,7 @@ def call filtered_coupons(response_body) rescue SplGetCouponError => e - raise e unless token_refresh_needed(response_body, @retry_counter, @user) + raise e unless token_refresh_needed(response_body, @retry_counter, @user, @store) @retry_counter += 1 retry diff --git a/app/services/spl/me_service.rb b/app/services/spl/me_service.rb index a4f97f3..7807320 100644 --- a/app/services/spl/me_service.rb +++ b/app/services/spl/me_service.rb @@ -27,7 +27,7 @@ def call response_body rescue SplMeError => e - raise e unless token_refresh_needed(response_body, @retry_counter, @user) + raise e unless token_refresh_needed(response_body, @retry_counter, @user, @store) retry_counter += 1 retry diff --git a/app/services/spl/oauth_token_service.rb b/app/services/spl/oauth_token_service.rb index bec50f3..3bcea97 100644 --- a/app/services/spl/oauth_token_service.rb +++ b/app/services/spl/oauth_token_service.rb @@ -33,8 +33,8 @@ def authorization_code_token(auth_code) response_body['response'] end - def self.refresh_token(refresh_token) - body = prepare_refresh_token_body(refresh_token) + def refresh_token(token) + body = prepare_refresh_token_body(token) response = send_request(@token_url, body) response_body = JSON.parse(response.body) @@ -70,14 +70,14 @@ def prepare_oauth_token_body_with_oauth_code(auth_code) } end - def prepare_refresh_token_body(refresh_token) + def prepare_refresh_token_body(token) { context: { prgCode: @env['spl_prg_code'] }, apiUser: @env['spl_api_user'], apiToken: @env['spl_api_token'], - refreshToken: refresh_token, + refreshToken: token, grantType: 'refresh_token' } end From c767750c0cb7409548158d78d51af41f099a7436 Mon Sep 17 00:00:00 2001 From: Tomasz Strzeszewski Date: Mon, 26 Jan 2026 15:56:44 +0100 Subject: [PATCH 57/63] Add refactor comment, remove unused helper usage --- app/services/spl/validate_card_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/spl/validate_card_service.rb b/app/services/spl/validate_card_service.rb index d55f04d..56feb3b 100644 --- a/app/services/spl/validate_card_service.rb +++ b/app/services/spl/validate_card_service.rb @@ -8,7 +8,6 @@ module Spl # Validates SPL card number class ValidateCardService class SplCardValidationError < StandardError; end - include SplServiceHelper def initialize(card_number, user, store) @card_number = card_number @@ -44,6 +43,7 @@ def check_for_errors(response_body) 'status') != 'A' end + # @todo: refactor this and build_post_request method usages to use Spl::SendRequestService instead def verify_card_request url = URI.parse(Spl::UrlCreatorService.new(@store.private_metadata['spl_url']).check_card) http = Net::HTTP.new(url.host, url.port) From a48769d3ae072445bc878f90817e53edb79abbb5 Mon Sep 17 00:00:00 2001 From: Tomasz Strzeszewski Date: Tue, 27 Jan 2026 13:21:37 +0100 Subject: [PATCH 58/63] update gemfile.lock --- Gemfile.lock | 67 ++++++++++++++++++++++++++++------------------------ 1 file changed, 36 insertions(+), 31 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index f754fe8..236cfd7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -802,44 +802,45 @@ DEPENDENCIES CHECKSUMS abbrev (0.1.2) sha256=ad1b4eaaaed4cb722d5684d63949e4bde1d34f2a95e20db93aecfe7cbac74242 - actioncable (7.2.3) sha256=e15d17b245f1dfe7cafdda4a0c6f7ba8ebaab1af33884415e09cfef4e93ad4f9 - actionmailbox (7.2.3) sha256=16bbf0a7c330f2d08d52d5e3c1b03813a8ef60bfb0a48e89c0bf92b069cb4d5e - actionmailer (7.2.3) sha256=68d646b852a6d2b25d8834fc796c3dc10f76a4c7fd77b3251c3f4dd832ec8ab8 - actionpack (7.2.3) sha256=2a14e4c64695777041ea7aaf498462284cadd561f009654393daf9b2de7207cf - actiontext (7.2.3) sha256=a6ffd9efb7b7b4e26029e5c88e8a2ea9aae8d6cefdfed960be139772f1a94037 - actionview (7.2.3) sha256=1f427d7a41b43804d7250911535740451b9c32b6416239d87e6dab9d5948ecb2 + action_text-trix (2.1.16) sha256=f645a2c21821b8449fd1d6770708f4031c91a2eedf9ef476e9be93c64e703a8a + actioncable (8.1.2) sha256=dc31efc34cca9cdefc5c691ddb8b4b214c0ea5cd1372108cbc1377767fb91969 + actionmailbox (8.1.2) sha256=058b2fb1980e5d5a894f675475fcfa45c62631103d5a2596d9610ec81581889b + actionmailer (8.1.2) sha256=f4c1d2060f653bfe908aa7fdc5a61c0e5279670de992146582f2e36f8b9175e9 + actionpack (8.1.2) sha256=ced74147a1f0daafaa4bab7f677513fd4d3add574c7839958f7b4f1de44f8423 + actiontext (8.1.2) sha256=0bf57da22a9c19d970779c3ce24a56be31b51c7640f2763ec64aa72e358d2d2d + actionview (8.1.2) sha256=80455b2588911c9b72cec22d240edacb7c150e800ef2234821269b2b2c3e2e5b active_link_to (1.0.5) sha256=4830847b3d14589df1e9fc62038ceec015257fce975ec1c2a77836c461b139ba active_storage_validations (1.3.0) sha256=788e4189d51c7b55049bf2825ede964baa2fe531e22a31c59f67a962ebf8d71e - activejob (7.2.3) sha256=e44964472de267b69e93752f088193c8ad2e56d2ef451d059dd7a53761e5ffb0 + activejob (8.1.2) sha256=908dab3713b101859536375819f4156b07bdf4c232cc645e7538adb9e302f825 activemerchant (1.137.0) sha256=fdf5e33f5c94d1981bd632d72e282c0a5afaf06bac6df3f933c08a95e98bd5ce - activemodel (7.2.3) sha256=bbaf66aeb93212e98ebf6ab900f8290f9a831645f0b235427f5acf0e074739db - activerecord (7.2.3) sha256=6facb7478ceb5f6baa9f0647daa50b4a3a43934997900f0011e6c667ff41a0d7 - activestorage (7.2.3) sha256=4c1422bbfaa60c89e7b43cc38ade7bd3b8dc81024c48a21c1ac56814cf34ca2f - activesupport (7.2.3) sha256=5675c9770dac93e371412684249f9dc3c8cec104efd0624362a520ae685c7b10 + activemodel (8.1.2) sha256=e21358c11ce68aed3f9838b7e464977bc007b4446c6e4059781e1d5c03bcf33e + activerecord (8.1.2) sha256=acfbe0cadfcc50fa208011fe6f4eb01cae682ebae0ef57145ba45380c74bcc44 + activestorage (8.1.2) sha256=8a63a48c3999caeee26a59441f813f94681fc35cc41aba7ce1f836add04fba76 + activesupport (8.1.2) sha256=88842578ccd0d40f658289b0e8c842acfe9af751afee2e0744a7873f50b6fdae acts-as-taggable-on (13.0.0) sha256=dca776c6ddebc458d175b57554ad692488e283f27047117e01ed871f1cba6135 acts_as_list (1.2.6) sha256=8345380900b7bee620c07ad00991ccee59af3d8c9e8574f426e321da2865fdc8 addressable (2.8.8) sha256=7c13b8f9536cf6364c03b9d417c19986019e28f7c00ac8132da4eb0fe393b057 any_ascii (0.3.3) sha256=9bba37e4aadc7c57bc29e4bae520418416d1b69ea167d04f2aeaf9c07166f724 + ar_lazy_preload (2.1.0) sha256=03effc2aac5aef2a1be8ff3ed4bb60f4e92e57f192dde61750bd8ea2131ad517 ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383 - async (2.35.3) sha256=6bfc75ac10182d0485b1394a50803b827f65e73f9e3ebe0aeaa4ecd770238271 - async-http (0.94.0) sha256=479dc4a11c078aff7e054a729fc79fef7d0c28ca26bfe81a4d125672b28759ea + async (2.36.0) sha256=090623f4c65706664355c9efa6c7bfb86771a513e65cd681c51cb27747530550 + async-http (0.94.2) sha256=c5ca94b337976578904a373833abe5b8dfb466a2946af75c4ae38c409c5c78b2 async-http-faraday (0.22.1) sha256=414c3197f32157bc0a7a187b1fb4c669097a8ef71832613eff2371c9808e9923 async-pool (0.11.1) sha256=98e1583e199a75f7dc70f8e65fc8d0d3b28636c3f256595d43e206642ad8fbda - auto_strip_attributes (2.6.0) sha256=a7e2e0cf744de2bcd947fd68014220702bcc88c81274c1cd9ce6f7316aae39b0 awesome_nested_set (3.9.0) sha256=3ce99e816550f97f4de118e621630070aacf24928b920fe4a68846578a8daaed awesome_print (1.9.2) sha256=e99b32b704acff16d768b3468680793ced40bfdc4537eb07e06a4be11133786e base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b bcrypt (3.1.21) sha256=5964613d750a42c7ee5dc61f7b9336fb6caca429ba4ac9f2011609946e4a2dcf - benchmark (0.5.0) sha256=465df122341aedcb81a2a24b4d3bd19b6c67c1530713fd533f3ff034e419236c bigdecimal (4.0.1) sha256=8b07d3d065a9f921c80ceaea7c9d4ae596697295b584c296fe599dd0ad01c4a7 brakeman (7.1.2) sha256=6b04927710a2e7d13a72248b5d404c633188e02417f28f3d853e4b6370d26dce + breadcrumbs_on_rails (4.1.0) sha256=6146ecf669f6251507231028719d51d92b8f42f1f982b83a2481fa1465cb6cee builder (3.3.0) sha256=497918d2f9dca528fdca4b88d84e4ef4387256d984b8154e9d5d3fe5a9c8835f byebug (13.0.0) sha256=d2263efe751941ca520fa29744b71972d39cbc41839496706f5d9b22e92ae05d cancancan (3.6.1) sha256=975c1d5cbf58d5df48a9452a7f61ae3d254608cd87570402f5925a8864c56b62 capybara (3.40.0) sha256=42dba720578ea1ca65fd7a41d163dd368502c191804558f6e0f71b391054aeef capybara-screenshot (1.0.26) sha256=816b9370a07752097c82a05f568aaf5d3b7f45c3db5d3aab2014071e1b3c0c77 carmen (1.1.3) sha256=970dbcb6a1743f0c7d765ebf4f0e5b59e99c4f96460baaa22e7306d5a2952e68 - cgi (0.5.1) sha256=e93fcafc69b8a934fe1e6146121fa35430efa8b4a4047c4893764067036f18e9 + chartkick (5.2.1) sha256=2848d7de87189f30f28d077eb0bbdebc8a1f0f6f81de1ded95008fe564369949 childprocess (5.1.0) sha256=9a8d484be2fd4096a0e90a0cd3e449a05bc3aa33f8ac9e4d6dcef6ac1455b6ec coderay (1.1.3) sha256=dc530018a4684512f8f38143cd2a096c9f02a1fc2459edcfe534787a7fc77d4b concurrent-ruby (1.3.6) sha256=6b56837e1e7e5292f9864f34b69c5a2cbc75c0cf5338f1ce9903d10fa762d5ab @@ -886,26 +887,24 @@ CHECKSUMS geocoder (1.8.6) sha256=e0ca1554b499f466de9b003f7dff70f89a5888761c2ca68ed9f86b6e5e24e74c github_changelog_generator (1.16.4) sha256=21d2dfe41d50cf3a7ffe896774779d9d61da6aaf08d390f8ddff0dacbdf8a611 globalid (1.3.0) sha256=05c639ad6eb4594522a0b07983022f04aa7254626ab69445a0e493aa3786ff11 + groupdate (6.7.0) sha256=beaa8d5bf3856814681914a1d4a20e77436a2214b85d0017dc2ea5c355fb6777 hashdiff (1.2.1) sha256=9c079dbc513dfc8833ab59c0c2d8f230fa28499cc5efb4b8dd276cf931457cd1 heroicon (1.0.0) sha256=e63a535a630d6fd3d99c0179d722753c8cd3478df1277eca7a4215c135b6e9cb highline (3.1.2) sha256=67cbd34d19f6ef11a7ee1d82ffab5d36dfd5b3be861f450fc1716c7125f4bb4a + hightop (0.6.0) sha256=d3799d2232e4d0bc69bfd940e49832d66bcf18b7098be252a4fd65f1eb29b778 i18n (1.14.8) sha256=285778639134865c5e0f6269e0b818256017e8cde89993fdfcbfb64d088824a5 i18n-tasks (1.1.2) sha256=4dcfba49e52a623f30661cb316cb80d84fbba5cb8c6d88ef5e02545fffa3637a image_processing (1.14.0) sha256=754cc169c9c262980889bec6bfd325ed1dafad34f85242b5a07b60af004742fb importmap-rails (2.2.3) sha256=7101be2a4dc97cf1558fb8f573a718404c5f6bcfe94f304bf1f39e444feeb16a inline_svg (1.10.0) sha256=5b652934236fd9f8adc61f3fd6e208b7ca3282698b19f28659971da84bf9a10f io-console (0.8.2) sha256=d6e3ae7a7cc7574f4b8893b4fca2162e57a825b223a177b7afa236c5ef9814cc - io-endpoint (0.16.0) sha256=b5517d9d2cde28e32ef0b0e13d4422d172a093760f50aad244d50282e990a114 + io-endpoint (0.17.1) sha256=e7bf943d89fdf83472b3fe3c06d7d290735b7e58bb968dcfa8c687a06ad11ee7 io-event (1.14.2) sha256=b0a069190eafe86005c22f7464f744971b5bd82f153740d34e6ab49548d4f613 io-stream (0.11.1) sha256=fa5f551fcff99581c1757b9d1cee2c37b124f07d2ca4f40b756a05ab9bd21b87 irb (1.16.0) sha256=2abe56c9ac947cdcb2f150572904ba798c1e93c890c256f8429981a7675b0806 json (2.18.0) sha256=b10506aee4183f5cf49e0efc48073d7b75843ce3782c68dbeb763351c08fd505 jsonapi-rspec (0.0.11) sha256=995f1adb59b386cc36e3f6823c828df575167586937e3e42601914c4ee650082 jsonapi-serializer (2.2.0) sha256=f8141ac6f0c1e17e8513df68f8341afe2d7bffc285841d7090bc07f07efb0029 - kaminari (1.2.2) sha256=c4076ff9adccc6109408333f87b5c4abbda5e39dc464bd4c66d06d9f73442a3e - kaminari-actionview (1.2.2) sha256=1330f6fc8b59a4a4ef6a549ff8a224797289ebf7a3a503e8c1652535287cc909 - kaminari-activerecord (1.2.2) sha256=0dd3a67bab356a356f36b3b7236bcb81cef313095365befe8e98057dd2472430 - kaminari-core (1.2.2) sha256=3bd26fec7370645af40ca73b9426a448d09b8a8ba7afa9ba3c3e0d39cdbb83ff language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc launchy (3.1.1) sha256=72b847b5cc961589dde2c395af0108c86ff0119f42d4648d25b5440ebb10059e lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87 @@ -914,6 +913,7 @@ CHECKSUMS loofah (2.25.0) sha256=df5ed7ac3bac6a4ec802df3877ee5cc86d027299f8952e6243b3dac446b060e6 mail (2.9.0) sha256=6fa6673ecd71c60c2d996260f9ee3dd387d4673b8169b502134659ece6d34941 mail_form (1.11.0) sha256=54aaaf11b69e7e5bd4be220dfc80f2469bb809d6ed1f893b8d940f0b67232c5a + mapkick-rb (0.2.0) sha256=c70da2e1edc56da290ccab51fd932ee5b1c562fe9f7877de0333b5562937e28e marcel (1.1.0) sha256=fdcfcfa33cc52e93c4308d40e4090a5d4ea279e160a7f6af988260fa970e0bee matrix (0.4.3) sha256=a0d5ab7ddcc1973ff690ab361b67f359acbb16958d1dc072b8b956a286564c5b method_source (1.1.0) sha256=181301c9c45b731b4769bc81e8860e72f9161ad7d66dd99103c9ab84f560f5c5 @@ -947,6 +947,7 @@ CHECKSUMS openssl (4.0.0) sha256=185711ed93d4e9c9a9db6efea7edb202dfe04f7d3692fbab988e3d84e498ee91 orm_adapter (0.5.0) sha256=aa5d0be5d540cbb46d3a93e88061f4ece6a25f6e97d6a47122beb84fe595e9b9 ostruct (0.6.3) sha256=95a2ed4a4bd1d190784e666b47b2d3f078e4a9efda2fccf18f84ddc6538ed912 + pagy (43.2.8) sha256=3d38376df4a9a7893361bb68825643896371707c4c49ef73acf1bd3b50af7562 parallel (1.27.0) sha256=4ac151e1806b755fb4e2dc2332cbf0e54f2e24ba821ff2d3dcf86bf6dc4ae130 paranoia (3.1.0) sha256=36574a46e8df663d0da7d9008e987b72a6a9eccee13c058b7094acc810e39f52 parser (3.3.10.1) sha256=06f6a725d2cd91e5e7f2b7c32ba143631e1f7c8ae2fb918fc4cebec187e6a688 @@ -969,12 +970,12 @@ CHECKSUMS rack-session (2.1.1) sha256=0b6dc07dea7e4b583f58a48e8b806d4c9f1c6c9214ebc202ec94562cbea2e4e9 rack-test (2.2.0) sha256=005a36692c306ac0b4a9350355ee080fd09ddef1148a5f8b2ac636c720f5c463 rackup (2.3.1) sha256=6c79c26753778e90983761d677a48937ee3192b3ffef6bc963c0950f94688868 - rails (7.2.3) sha256=9a9812eb131189676e64665f6883fc9c4051f412cc87ef9e3fa242a09c609bff + rails (8.1.2) sha256=5069061b23dfa8706b9f0159ae8b9d35727359103178a26962b868a680ba7d95 rails-controller-testing (1.0.5) sha256=741448db59366073e86fc965ba403f881c636b79a2c39a48d0486f2607182e94 rails-dom-testing (2.3.0) sha256=8acc7953a7b911ca44588bf08737bc16719f431a1cc3091a292bca7317925c1d rails-html-sanitizer (1.6.2) sha256=35fce2ca8242da8775c83b6ba9c1bcaad6751d9eb73c1abaa8403475ab89a560 - rails-i18n (7.0.10) sha256=efae16e0ac28c0f42e98555c8db1327d69ab02058c8b535e0933cb106dd931ca - railties (7.2.3) sha256=6eb010a6bfe6f223e783f739ddfcbdb5b88b1f3a87f7739f0a0685e466250422 + rails-i18n (8.1.0) sha256=52d5fd6c0abef28d84223cc05647f6ae0fd552637a1ede92deee9545755b6cf3 + railties (8.1.2) sha256=1289ece76b4f7668fc46d07e55cc992b5b8751f2ad85548b7da351b8c59f8055 rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a rake (13.3.1) sha256=8c9e89d09f66a26a01264e7e3480ec0607f0c497a861ef16063604b1b08eb19c ransack (4.4.1) sha256=6aeaac36fc19088570e10da1044e6cfd88c740e20f871b84566fd30e32b7a63d @@ -993,13 +994,13 @@ CHECKSUMS rspec-retry (0.6.2) sha256=6101ba23a38809811ae3484acde4ab481c54d846ac66d5037ccb40131a60d858 rspec-support (3.13.6) sha256=2e8de3702427eab064c9352fe74488cc12a1bfae887ad8b91cba480ec9f8afb2 rspec_junit_formatter (0.6.0) sha256=40dde674e6ae4e6cc0ff560da25497677e34fefd2338cc467a8972f602b62b15 - rubocop (1.82.1) sha256=09f1a6a654a960eda767aebea33e47603080f8e9c9a3f019bf9b94c9cab5e273 + rubocop (1.84.0) sha256=88dec310153bb685a879f5a7cdb601f6287b8f0ee675d9dc63a17c7204c4190a rubocop-ast (1.49.0) sha256=49c3676d3123a0923d333e20c6c2dbaaae2d2287b475273fddee0c61da9f71fd rubocop-performance (1.26.1) sha256=cd19b936ff196df85829d264b522fd4f98b6c89ad271fa52744a8c11b8f71834 rubocop-rails (2.34.3) sha256=10d37989024865ecda8199f311f3faca990143fbac967de943f88aca11eb9ad2 rubocop-rails-omakase (1.1.0) sha256=2af73ac8ee5852de2919abbd2618af9c15c19b512c4cfc1f9a5d3b6ef009109d rubocop-rspec (3.9.0) sha256=8fa70a3619408237d789aeecfb9beef40576acc855173e60939d63332fdb55e2 - ruby-lsp (0.26.4) sha256=1cb3046a066c8f1983dfe5f0cd3baa76034d6ba156e8ab460b15ab129d37a9f7 + ruby-lsp (0.26.5) sha256=19272659139b292a81a700d78e1b4d8988c4812e96b54fd6c30a21ce5e82b189 ruby-oembed (0.18.1) sha256=877b87b3d98e8137599044277fdb82a5536973bd9df68a93129163b781ae3a5f ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33 ruby-vips (2.3.0) sha256=e685ec02c13969912debbd98019e50492e12989282da5f37d05f5471442f5374 @@ -1011,15 +1012,17 @@ CHECKSUMS simplecov (0.22.0) sha256=fe2622c7834ff23b98066bb0a854284b2729a569ac659f82621fc22ef36213a5 simplecov-html (0.13.2) sha256=bd0b8e54e7c2d7685927e8d6286466359b6f16b18cb0df47b508e8d73c777246 simplecov_json_formatter (0.1.4) sha256=529418fbe8de1713ac2b2d612aa3daa56d316975d307244399fa4838c601b428 - spree (5.2.6) sha256=04ac44abd2e6adea5d13b2aed0289d7025ee08fa0e8e06287b298f25c33dcd18 + spree (5.3.0) sha256=5455d28c79e96b76c597a5787d301132b6ee7b830c95de77c9b15a36af2c9fb1 spree-spl (2.0.0) - spree_api (5.2.6) sha256=a0bb2892bd32e9c4d53ece74e3906be8d1653b59fd7739de3b2c2277c7cec70b + spree_admin (5.3.0) sha256=86d2492c3c98efcc9d6ec099594eff23251f4aff1154eb266a3d2510524d4559 + spree_api (5.3.0) sha256=b3ef192c1f2453770d20ef5e1dcb38568b7b0a128ddde38bdca7c2a138dc5756 spree_auth_devise (4.6.3) sha256=5eeada77570f5ab335a9c6c150d7cdaee39203ce43b70ac24d2ffb195952ed6e - spree_cli (5.2.6) sha256=8c86d35c2eab2bc19a4dc6220a73c52c877dd90a0fa47f2fe97bdc206bda7aab - spree_core (5.2.6) sha256=3f4d99e1ea5256d0f0aa5767aeedfff121d32498791a1b94c9a5738c99214358 + spree_cli (5.3.0) sha256=a5cfd2d5bde7f120f6631bb59c8d337d1a29a93d3523c02ab0febf93511d1dac + spree_core (5.3.0) sha256=e96ad7492b758fe15261d4fe9a15e1149034bf1b34844acad0c0383e06d42889 spree_dev_tools (0.5.2) sha256=faa8bda07eb785e066032e5527663292bbe0dfab653fc06c61d56798dd4deea7 spree_extension (0.1.0) sha256=b648327f4787295e051be4ea0c0a47baec47c2e245bca9c93ecf73605fc3a051 - spree_storefront (5.2.6) sha256=da704b62e870ae3bac382635d57c03905bf51551ea600ee3e8716d2f6ce4fbfa + spree_page_builder (5.3.0) sha256=1501377b720c29ffd2adfd8b195221580d8023b22552cc34d99aa899112bae8e + spree_storefront (5.3.0) sha256=f7db814f8ac43390660bc6d987bf67e97d48ca27c0502fd93b9115bd7d2b63cd sprockets (4.2.2) sha256=761e5a49f1c288704763f73139763564c845a8f856d52fba013458f8af1b59b1 sprockets-rails (3.5.2) sha256=a9e88e6ce9f8c912d349aa5401509165ec42326baf9e942a85de4b76dbc4119e sqlite3 (2.9.0-aarch64-linux-gnu) sha256=cfe1e0216f46d7483839719bf827129151e6c680317b99d7b8fc1597a3e13473 @@ -1048,6 +1051,7 @@ CHECKSUMS thor (1.5.0) sha256=e3a9e55fe857e44859ce104a84675ab6e8cd59c650a49106a05f55f136425e73 timecop (0.9.10) sha256=12ba45ce57cdcf6b1043cb6cdffa6381fd89ce10d369c28a7f6f04dc1b0cd8eb timeout (0.6.0) sha256=6d722ad619f96ee383a0c557ec6eb8c4ecb08af3af62098a0be5057bf00de1af + tinymce-rails (6.8.6.1) sha256=b249fccfb300d8cdb361a443e859d6b45ec6d7bbd1466817008ee0af2a3d516c traces (0.18.2) sha256=80f1649cb4daace1d7174b81f3b3b7427af0b93047759ba349960cb8f315e214 tracking_number (2.4.0) sha256=2a4c6b39cbdfc8326c234626556da1e269c8dfe33478786bea93461331c247ff tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f @@ -1067,6 +1071,7 @@ CHECKSUMS websocket-driver (0.8.0) sha256=ed0dba4b943c22f17f9a734817e808bc84cdce6a7e22045f5315aa57676d4962 websocket-extensions (0.1.5) sha256=1c6ba63092cda343eb53fc657110c71c754c56484aad42578495227d717a8241 xpath (3.2.0) sha256=6dfda79d91bb3b949b947ecc5919f042ef2f399b904013eb3ef6d20dd3a4082e + yaml (0.4.0) sha256=240e69d1e6ce3584d6085978719a0faa6218ae426e034d8f9b02fb54d3471942 zeitwerk (2.7.4) sha256=2bef90f356bdafe9a6c2bd32bcd804f83a4f9b8bc27f3600fff051eb3edcec8b BUNDLED WITH From 69e39384bfa87749f8b600059869a352b1705a58 Mon Sep 17 00:00:00 2001 From: Tomasz Strzeszewski Date: Tue, 27 Jan 2026 13:23:53 +0100 Subject: [PATCH 59/63] regenerate rubocop --- .rubocop_todo.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 2ffd4ad..6e462d5 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2026-01-27 11:43:39 UTC using RuboCop version 1.84.0. +# on 2026-01-27 12:23:34 UTC using RuboCop version 1.84.0. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new @@ -18,7 +18,7 @@ Lint/RedundantCopDisableDirective: Metrics/AbcSize: Max: 18 -# Offense count: 1 +# Offense count: 2 # Configuration parameters: AllowedMethods, AllowedPatterns. Metrics/CyclomaticComplexity: Max: 8 @@ -31,7 +31,7 @@ Metrics/MethodLength: # Offense count: 1 # Configuration parameters: CountComments, CountAsOne. Metrics/ModuleLength: - Max: 126 + Max: 156 # Offense count: 2 # Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames. @@ -92,7 +92,7 @@ Style/IfUnlessModifier: - 'app/services/assign_sparta_card_number_service.rb' - 'app/services/spl/validate_card_service.rb' -# Offense count: 4 +# Offense count: 5 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, AllowRBSInlineAnnotation, AllowCopDirectives, AllowedPatterns, SplitStrings. # URISchemes: http, https From 945edd5f7f70d850fa8e14a57fc5eca0920504e7 Mon Sep 17 00:00:00 2001 From: nika-piotrowska Date: Tue, 27 Jan 2026 16:44:30 +0100 Subject: [PATCH 60/63] Remove redundant methods from profile controller decorator --- .../profile_controller_decorator.rb | 41 ------------------- 1 file changed, 41 deletions(-) diff --git a/app/controllers/spl/spree/storefront/profile_controller_decorator.rb b/app/controllers/spl/spree/storefront/profile_controller_decorator.rb index c12a1e8..35513b5 100644 --- a/app/controllers/spl/spree/storefront/profile_controller_decorator.rb +++ b/app/controllers/spl/spree/storefront/profile_controller_decorator.rb @@ -129,39 +129,6 @@ def clear_errors try_spree_current_user.errors.clear end - def render_login_code_error - render turbo_stream: turbo_stream.replace( - 'loyalty_connect_form', - partial: 'spl/loyalty_connect_form', - locals: { user: try_spree_current_user } - ), - status: :unprocessable_content - end - - def render_login_code_success(phone) - render turbo_stream: turbo_stream.replace( - 'loyalty_connect_form', - partial: 'spl/otp_code_form', - locals: { - user: try_spree_current_user, - phone_e164: phone.respond_to?(:e164) ? phone.e164 : nil - } - ), - status: :ok - end - - def render_connect_loyalty_account_error(phone) - render turbo_stream: turbo_stream.replace( - 'otp_code_form', - partial: 'spl/otp_code_form', - locals: { - user: try_spree_current_user, - phone_e164: phone.respond_to?(:e164) ? phone.e164 : nil - } - ), - status: :unprocessable_content - end - def send_otp(phone, store) Spl::SendOtpService.new(DateTime.current, phone.country_code, phone.national_number, store).call end @@ -173,14 +140,6 @@ def update_user_after_otp_request ) end - def handle_spl_error(error) - payload = Spl::ErrorPayloadParser.parse(error.message) || error - msg = Spl::ErrorTranslator.translate(payload || { errorCode: I18n.t('spl.errors.unknow_error') }) - - clear_errors - try_spree_current_user.errors.add(:base, msg) - end - def assign_card_number(user, store, params) Spl::LoginAccountService.new(user, store, params).call AssignSpartaCardNumberService.new(user, store).call From 047b829f433e2dcd467614a991ece0debc48c37b Mon Sep 17 00:00:00 2001 From: vi4me Date: Tue, 27 Jan 2026 19:58:03 +0100 Subject: [PATCH 61/63] EK-685 Fix RegisterAccountService update user --- .../spl/spree/storefront/profile_controller_decorator.rb | 1 + app/services/spl/register_account_service.rb | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/controllers/spl/spree/storefront/profile_controller_decorator.rb b/app/controllers/spl/spree/storefront/profile_controller_decorator.rb index 35513b5..6271a7c 100644 --- a/app/controllers/spl/spree/storefront/profile_controller_decorator.rb +++ b/app/controllers/spl/spree/storefront/profile_controller_decorator.rb @@ -88,6 +88,7 @@ def handle_validation_error(error) def update_order(spl_card, active) current_order = try_spree_current_user.orders.last + return if current_order.nil? return unless %w[cart address delivery payment].include?(current_order.state) current_order.update( diff --git a/app/services/spl/register_account_service.rb b/app/services/spl/register_account_service.rb index 51d5743..e71c4fc 100644 --- a/app/services/spl/register_account_service.rb +++ b/app/services/spl/register_account_service.rb @@ -59,8 +59,8 @@ def update_account(card_number, oauth_response_body) @user.private_metadata ||= {} if @user.private_metadata.blank? @user.update(public_metadata: @user.public_metadata.merge(spl_no_card: card_number, spl_card_active: true), private_metadata: @user.private_metadata.merge( - accessToken: oauth_response_body.dig('response', 'accessToken'), - refreshToken: oauth_response_body.dig('response', 'refreshToken') + spl_access_token: oauth_response_body.dig('response', 'accessToken'), + spl_refresh_token: oauth_response_body.dig('response', 'refreshToken') )) end end From 38730fa9f7e2b6ac169bc0dd806f29adb466e7b0 Mon Sep 17 00:00:00 2001 From: nika-piotrowska Date: Tue, 27 Jan 2026 20:31:17 +0100 Subject: [PATCH 62/63] Improve error handling helper --- .../spl/spree/api/account_controller_decorator.rb | 14 ++++++++++---- .../storefront/checkout_controller_decorator.rb | 6 +++--- .../storefront/profile_controller_decorator.rb | 14 +++++--------- app/helpers/error_handling_helper.rb | 10 +++++----- config/locales/pl.yml | 2 +- 5 files changed, 24 insertions(+), 22 deletions(-) diff --git a/app/controllers/spl/spree/api/account_controller_decorator.rb b/app/controllers/spl/spree/api/account_controller_decorator.rb index 5b1f1fb..cb9f0d6 100644 --- a/app/controllers/spl/spree/api/account_controller_decorator.rb +++ b/app/controllers/spl/spree/api/account_controller_decorator.rb @@ -5,6 +5,8 @@ module Spree module Api # Account decorator to validate spl card no module AccountControllerDecorator + include ErrorHandlingHelper + def self.prepended(base) base.before_action :validate_spl_no_card, only: :update end @@ -16,7 +18,8 @@ def connect_loyalty_account render_serialized_payload { serialize_resource(spree_current_user) } rescue Spl::LoginAccountService::SplLoginAccountError, AssignSpartaCardNumberService::AssignSpartaCardNumberError, Spl::MeService::SplMeError => e - render json: { error: e }, status: :bad_request + handle_spl_error(e, spree_current_user) + render json: { error: spree_current_user.errors }, status: :bad_request end def register_loyalty_account @@ -25,14 +28,16 @@ def register_loyalty_account spree_current_user.reload render_serialized_payload { serialize_resource(spree_current_user) } rescue Spl::RegisterAccountService::SplRegisterAccountError, Spl::OauthTokenService::OauthTokenError => e - render json: { error: e }, status: :bad_request + handle_spl_error(e, spree_current_user) + render json: { error: spree_current_user.errors }, status: :bad_request end def registration_code Spl::RequestOtpService.new(DateTime.current, current_store, params).call head :no_content rescue Spl::RequestOtpService::SplRequestOtpError, Spl::OauthTokenService::OauthTokenError => e - render json: { error: e }, status: :bad_request + handle_spl_error(e, spree_current_user) + render json: { error: spree_current_user.errors }, status: :bad_request end def login_code @@ -40,7 +45,8 @@ def login_code Spl::SendOtpService.new(DateTime.current, params[:mobile_country], params[:phone_number], current_store).call head :no_content rescue Spl::SendOtpService::SplSendOtpError => e - render json: { error: e }, status: :bad_request + handle_spl_error(e, spree_current_user) + render json: { error: spree_current_user.errors }, status: :bad_request end private diff --git a/app/controllers/spl/spree/storefront/checkout_controller_decorator.rb b/app/controllers/spl/spree/storefront/checkout_controller_decorator.rb index 275d778..624a9d8 100644 --- a/app/controllers/spl/spree/storefront/checkout_controller_decorator.rb +++ b/app/controllers/spl/spree/storefront/checkout_controller_decorator.rb @@ -17,7 +17,7 @@ def activate_coupon Spl::Coupons::ActivateCouponService.new(@order.user, @order.store, params[:coupon_code]).call load_user_coupons rescue StandardError => e - handle_spl_error(e) + handle_spl_error(e, @order.user) raise e ensure respond_to do |format| @@ -32,7 +32,7 @@ def deactivate_coupon .call load_user_coupons rescue StandardError => e - handle_spl_error(e) + handle_spl_error(e, @order.user) raise e ensure respond_to do |format| @@ -63,7 +63,7 @@ def perform_update_sparta_state_job def load_user_coupons @coupons = Spl::Coupons::GetCouponsService.new(@order.user, @order.store).call rescue StandardError => e - handle_spl_error(e) + handle_spl_error(e, @order.user) raise e end end diff --git a/app/controllers/spl/spree/storefront/profile_controller_decorator.rb b/app/controllers/spl/spree/storefront/profile_controller_decorator.rb index 6271a7c..e060517 100644 --- a/app/controllers/spl/spree/storefront/profile_controller_decorator.rb +++ b/app/controllers/spl/spree/storefront/profile_controller_decorator.rb @@ -18,7 +18,7 @@ def login_code update_user_after_otp_request render_login_code_success(try_spree_current_user, phone_parser, 'otp_code_form') rescue Spl::SendOtpService::SplSendOtpError => e - handle_spl_error(e) + handle_spl_error(e, try_spree_current_user) render_login_code_error(try_spree_current_user) end @@ -28,7 +28,7 @@ def connect_loyalty_account notice: ::Spree.t(:successfully_updated, resource: ::Spree.t(:account)) rescue Spl::LoginAccountService::SplLoginAccountError, AssignSpartaCardNumberService::AssignSpartaCardNumberError, Spl::MeService::SplMeError => e - handle_spl_error(e) + handle_spl_error(e, try_spree_current_user) render_connect_loyalty_account_error(try_spree_current_user, try_spree_current_user.phone, 'otp_code_form') end @@ -37,7 +37,7 @@ def registration_code update_user_after_otp_request render_login_code_success(try_spree_current_user, phone_parser, 'otp_registration_form') rescue Spl::RequestOtpService::SplRequestOtpError, Spl::OauthTokenService::OauthTokenError => e - handle_spl_error(e) + handle_spl_error(e, try_spree_current_user) render_login_code_error(try_spree_current_user) end @@ -46,7 +46,7 @@ def register_loyalty_account redirect_to spree.edit_account_profile_path, notice: ::Spree.t(:successfully_updated, resource: ::Spree.t(:account)) rescue Spl::RegisterAccountService::SplRegisterAccountError, Spl::OauthTokenService::OauthTokenError => e - handle_spl_error(e) + handle_spl_error(e, try_spree_current_user) user = try_spree_current_user render_connect_loyalty_account_error(user, user.phone, 'otp_registration_form') end @@ -64,7 +64,7 @@ def validate_spl_no_card end def validate_login_code_request - clear_errors + clear_errors(try_spree_current_user) validate_yc_terms validate_phone @@ -126,10 +126,6 @@ def phone_parser @phone_parser ||= PhoneParserService.new(login_code_params[:phone]) end - def clear_errors - try_spree_current_user.errors.clear - end - def send_otp(phone, store) Spl::SendOtpService.new(DateTime.current, phone.country_code, phone.national_number, store).call end diff --git a/app/helpers/error_handling_helper.rb b/app/helpers/error_handling_helper.rb index 9ee48c6..683dda6 100644 --- a/app/helpers/error_handling_helper.rb +++ b/app/helpers/error_handling_helper.rb @@ -4,16 +4,16 @@ module ErrorHandlingHelper # Parses and translate occurred error, then adds it to user errors. # Dedicated for storefront controllers. # @param [StandardError] - def handle_spl_error(error) + def handle_spl_error(error, model) payload = Spl::ErrorPayloadParser.parse(error.message) || error msg = Spl::ErrorTranslator.translate(payload) - clear_errors - try_spree_current_user.errors.add(:base, msg) + clear_errors(model) + model.errors.add(:base, msg) end - def clear_errors - try_spree_current_user.errors.clear + def clear_errors(model) + model.errors.clear end def token_expired?(err_msg) diff --git a/config/locales/pl.yml b/config/locales/pl.yml index a706b06..4cd854e 100644 --- a/config/locales/pl.yml +++ b/config/locales/pl.yml @@ -26,7 +26,7 @@ pl: activate: Aktywuj deactivate: Dezaktywuj errors: - generic: Nie udało się połączyć konta lojalnościowego. Spróbuj ponownie. + generic: Nie udało się wykonać akcji. Spróbuj ponownie. auth_temporary_blocked: Zbyt wiele nieudanych prób. Spróbuj ponownie później. auth_failed: Uwierzytelnienie nie powiodło się. token_expired: Twoja sesja wygasła. Zaloguj się ponownie. From d4627f32e5e63f87164e7db1390ca49241bc7112 Mon Sep 17 00:00:00 2001 From: nika-piotrowska Date: Tue, 27 Jan 2026 20:31:48 +0100 Subject: [PATCH 63/63] Improve message when response error code != 0 --- app/services/spl/oauth_token_service.rb | 6 +++--- app/services/spl/request_otp_service.rb | 2 +- app/services/spl/sparta_loyalty_service.rb | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/services/spl/oauth_token_service.rb b/app/services/spl/oauth_token_service.rb index 3bcea97..45cfaa3 100644 --- a/app/services/spl/oauth_token_service.rb +++ b/app/services/spl/oauth_token_service.rb @@ -18,7 +18,7 @@ def annonymus_token response = send_request(@token_url, body) response_body = JSON.parse(response.body) - raise OauthTokenError, response_body['msg'] if response_body['errorCode'] != '0' + raise OauthTokenError, response_body if response_body['errorCode'] != '0' response_body end @@ -28,7 +28,7 @@ def authorization_code_token(auth_code) response = send_request(@token_url, body) response_body = JSON.parse(response.body) - raise OauthTokenError, response_body['msg'] if response_body['errorCode'] != '0' + raise OauthTokenError, response_body if response_body['errorCode'] != '0' response_body['response'] end @@ -38,7 +38,7 @@ def refresh_token(token) response = send_request(@token_url, body) response_body = JSON.parse(response.body) - raise OauthTokenError, response_body['msg'] if response_body['errorCode'] != '0' + raise OauthTokenError, response_body if response_body['errorCode'] != '0' response_body['response'] end diff --git a/app/services/spl/request_otp_service.rb b/app/services/spl/request_otp_service.rb index d8aed32..f0a9ca1 100644 --- a/app/services/spl/request_otp_service.rb +++ b/app/services/spl/request_otp_service.rb @@ -24,7 +24,7 @@ def call request_otp_response = send_request(@request_otp_url, request_otp_body) request_otp_response_body = JSON.parse(request_otp_response.body) Rails.logger.debug request_otp_response_body - raise SplRequestOtpError, request_otp_response_body['msg'] if request_otp_response_body['errorCode'] != '0' + raise SplRequestOtpError, request_otp_response_body if request_otp_response_body['errorCode'] != '0' request_otp_response_body end diff --git a/app/services/spl/sparta_loyalty_service.rb b/app/services/spl/sparta_loyalty_service.rb index df86506..5989cd5 100644 --- a/app/services/spl/sparta_loyalty_service.rb +++ b/app/services/spl/sparta_loyalty_service.rb @@ -43,7 +43,7 @@ def prepare_basket_body # rubocop:disable Metrics/MethodLength apiToken: @store.private_metadata['spl_api_token'], partnerCode: @store.private_metadata['spl_partner_code'], placeCode: @store.private_metadata['spl_place_code'], - mode: @store.private_metadata['spl_mode'], + # mode: @store.private_metadata['spl_mode'], date: @date, no: @order_token, orderNo: @order_token,