Commit f88de08a authored by Andy Soiron's avatar Andy Soiron

Merge branch 'require_verification_for_namespace_creation' into 'master'

Add verification before namespace creation

See merge request gitlab-org/gitlab!77315
parents 57837051 62c6ea0f
# frozen_string_literal: true
class RequireVerificationForNamespaceCreationExperiment < ApplicationExperiment # rubocop:disable Gitlab/NamespacedClass
def control_behavior
false
end
def candidate_behavior
true
end
def candidate?
run
end
def record_conversion(namespace)
return unless should_track?
Experiment.by_name(name).record_conversion_event_for_subject(subject, namespace_id: namespace.id)
end
private
def subject
context.value[:user]
end
end
......@@ -7,7 +7,7 @@ class Experiment < ApplicationRecord
validates :name, presence: true, uniqueness: true, length: { maximum: 255 }
def self.add_user(name, group_type, user, context = {})
find_or_create_by!(name: name).record_user_and_group(user, group_type, context)
by_name(name).record_user_and_group(user, group_type, context)
end
def self.add_group(name, variant:, group:)
......@@ -15,11 +15,15 @@ class Experiment < ApplicationRecord
end
def self.add_subject(name, variant:, subject:)
find_or_create_by!(name: name).record_subject_and_variant!(subject, variant)
by_name(name).record_subject_and_variant!(subject, variant)
end
def self.record_conversion_event(name, user, context = {})
find_or_create_by!(name: name).record_conversion_event_for_user(user, context)
by_name(name).record_conversion_event_for_user(user, context)
end
def self.by_name(name)
find_or_create_by!(name: name)
end
# Create or update the recorded experiment_user row for the user in this experiment.
......@@ -41,6 +45,16 @@ class Experiment < ApplicationRecord
experiment_user.update!(converted_at: Time.current, context: merged_context(experiment_user, context))
end
def record_conversion_event_for_subject(subject, context = {})
raise 'Incompatible subject provided!' unless ExperimentSubject.valid_subject?(subject)
attr_name = subject.class.table_name.singularize.to_sym
experiment_subject = experiment_subjects.find_by(attr_name => subject)
return unless experiment_subject
experiment_subject.update!(converted_at: Time.current, context: merged_context(experiment_subject, context))
end
def record_subject_and_variant!(subject, variant)
raise 'Incompatible subject provided!' unless ExperimentSubject.valid_subject?(subject)
......@@ -57,7 +71,7 @@ class Experiment < ApplicationRecord
private
def merged_context(experiment_user, new_context)
experiment_user.context.deep_merge(new_context.deep_stringify_keys)
def merged_context(experiment_subject, new_context)
experiment_subject.context.deep_merge(new_context.deep_stringify_keys)
end
end
......@@ -331,6 +331,7 @@ class User < ApplicationRecord
delegate :pronouns, :pronouns=, to: :user_detail, allow_nil: true
delegate :pronunciation, :pronunciation=, to: :user_detail, allow_nil: true
delegate :registration_objective, :registration_objective=, to: :user_detail, allow_nil: true
delegate :requires_credit_card_verification, :requires_credit_card_verification=, to: :user_detail, allow_nil: true
accepts_nested_attributes_for :user_preference, update_only: true
accepts_nested_attributes_for :user_detail, update_only: true
......
......@@ -2,8 +2,9 @@
module Users
class UpsertCreditCardValidationService < BaseService
def initialize(params)
def initialize(params, user)
@params = params.to_h.with_indifferent_access
@current_user = user
end
def execute
......@@ -18,6 +19,8 @@ module Users
::Users::CreditCardValidation.upsert(@params)
::Users::UpdateService.new(current_user, user: current_user, requires_credit_card_verification: false).execute!
ServiceResponse.success(message: 'CreditCardValidation was set')
rescue ActiveRecord::InvalidForeignKey, ActiveRecord::NotNullViolation => e
ServiceResponse.error(message: "Could not set CreditCardValidation: #{e.message}")
......
---
name: require_verification_for_namespace_creation
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/77315
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/350251
milestone: '14.8'
type: experiment
group: group::activation
default_enabled: false
# frozen_string_literal: true
class AddRequiresVerificationToUserDetails < Gitlab::Database::Migration[1.0]
enable_lock_retries!
def change
add_column :user_details, :requires_credit_card_verification, :boolean, null: false, default: false
end
end
1199adba4c13e9234eabadefeb55ed3cfb19e9d5a87c07b90d438e4f48a973f7
\ No newline at end of file
......@@ -20327,6 +20327,7 @@ CREATE TABLE user_details (
pronunciation text,
registration_objective smallint,
phone text,
requires_credit_card_verification boolean DEFAULT false NOT NULL,
CONSTRAINT check_245664af82 CHECK ((char_length(webauthn_xid) <= 100)),
CONSTRAINT check_a73b398c60 CHECK ((char_length(phone) <= 50)),
CONSTRAINT check_b132136b01 CHECK ((char_length(other_role) <= 100)),
<script>
import { GlButton } from '@gitlab/ui';
import Zuora from 'ee/billings/components/zuora.vue';
import { I18N, IFRAME_MINIMUM_HEIGHT } from '../constants';
import StaticToggle from './static_toggle.vue';
export default {
components: {
GlButton,
StaticToggle,
Zuora,
},
inject: ['completed', 'iframeUrl', 'allowedOrigin'],
data() {
return {
verificationCompleted: this.completed,
};
},
watch: {
verificationCompleted() {
this.toggleProjectCreation();
},
},
mounted() {
this.toggleProjectCreation();
},
methods: {
submit() {
this.$refs.zuora.submit();
},
verified() {
this.verificationCompleted = true;
},
toggleProjectCreation() {
// Workaround until we refactor group and project creation into Vue
// https://gitlab.com/gitlab-org/gitlab/-/issues/339998
const el = document.querySelector('.js-toggle-container');
el.classList.toggle('gl-display-none', !this.verificationCompleted);
},
},
i18n: I18N,
iframeHeight: IFRAME_MINIMUM_HEIGHT,
};
</script>
<template>
<div class="gl-display-flex gl-flex-direction-column gl-align-items-center gl-w-full">
<static-toggle
ref="verifyToggle"
:enabled="!verificationCompleted"
:completed="verificationCompleted"
:title="$options.i18n.verifyToggle"
/>
<div
v-if="!verificationCompleted"
class="gl-border-gray-100 gl-border-solid gl-border-1 gl-rounded-base gl-px-2 gl-py-5 gl-text-left"
>
<div class="gl-px-4 gl-text-secondary gl-font-sm">
{{ $options.i18n.explanation }}
</div>
<zuora
ref="zuora"
:initial-height="$options.iframeHeight"
:iframe-url="iframeUrl"
:allowed-origin="allowedOrigin"
@success="verified"
/>
<div class="gl-px-4">
<gl-button
ref="submitButton"
variant="confirm"
type="submit"
class="gl-w-full!"
@click="submit"
>
{{ $options.i18n.submitVerify }}
</gl-button>
</div>
</div>
<static-toggle
ref="createToggle"
:enabled="verificationCompleted"
:title="$options.i18n.createToggle"
/>
</div>
</template>
<script>
import { GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
export default {
components: {
GlIcon,
},
props: {
enabled: {
type: Boolean,
required: true,
},
completed: {
type: Boolean,
required: false,
default: false,
},
title: {
type: String,
required: true,
},
},
computed: {
icon() {
return this.enabled ? 'chevron-down' : 'chevron-right';
},
},
i18n: {
label: __('Completed'),
},
};
</script>
<template>
<div class="gl-border-gray-100 gl-border-l-solid gl-border-1 gl-w-full gl-my-3 gl-pl-3">
<div class="gl-display-flex gl-align-items-center">
<span
class="gl-display-flex gl-align-items-center gl-flex-grow-1 gl-font-weight-bold gl-text-blue-500"
:class="{ 'gl-text-gray-400!': !enabled }"
>
<gl-icon :name="icon" :size="24" class="gl-text-gray-500 gl-mr-3" />
{{ title }}
</span>
<gl-icon
v-if="completed"
name="check-circle-filled"
:size="16"
class="gl-text-green-600"
:aria-label="$options.i18n.label"
/>
</div>
</div>
</template>
import { s__ } from '~/locale';
export const IFRAME_MINIMUM_HEIGHT = 312;
export const I18N = {
verifyToggle: s__('IdentityVerification|Verify your identity'),
createToggle: s__('IdentityVerification|Create a project'),
explanation: s__(
'IdentityVerification|Before you create your first project, we need you to verify your identity with a valid payment method. You will not be charged during this step. If we ever need to charge you, we will let you know.',
),
submitVerify: s__('IdentityVerification|Verify your identity'),
};
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import $ from 'jquery';
import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import { bindHowToImport } from '~/projects/project_new';
import { displayGroupPath, displayProjectPath } from './path_display';
import showTooltip from './show_tooltip';
import CreditCardVerification from './components/credit_card_verification.vue';
const importButtonsSubmit = () => {
const buttons = document.querySelectorAll('.js-import-project-buttons a');
......@@ -33,6 +36,28 @@ const setAutofocus = () => {
const mobileTooltipOpts = () => (bp.getBreakpointSize() === 'xs' ? { placement: 'bottom' } : {});
const mountVerification = () => {
const el = document.querySelector('.js-credit-card-verification');
if (!el) {
return null;
}
const { completed, iframeUrl, allowedOrigin } = el.dataset;
return new Vue({
el,
provide: {
completed: parseBoolean(completed),
iframeUrl,
allowedOrigin,
},
render(createElement) {
return createElement(CreditCardVerification);
},
});
};
export default () => {
displayGroupPath('.js-group-path-source', '.js-group-path-display');
displayGroupPath('.js-import-group-path-source', '.js-import-group-path-display');
......@@ -41,4 +66,5 @@ export default () => {
importButtonsSubmit();
bindHowToImport();
setAutofocus();
mountVerification();
};
# frozen_string_literal: true
module Registrations::Verification
extend ActiveSupport::Concern
included do
before_action :require_verification, if: :verification_required?
private
def verification_required?
html_request? &&
request.get? &&
current_user&.requires_credit_card_verification
end
def require_verification
redirect_to new_users_sign_up_groups_project_path
end
def set_requires_verification
::Users::UpdateService.new(current_user, user: current_user, requires_credit_card_verification: true).execute!
end
end
end
......@@ -6,6 +6,8 @@ module EE
extend ::Gitlab::Utils::Override
prepended do
include ::Registrations::Verification
around_action :set_current_ip_address
end
......
......@@ -6,11 +6,17 @@ module Registrations
include Registrations::CreateGroup
include OneTrustCSP
skip_before_action :require_verification, only: :new
before_action :set_requires_verification, only: :new, if: -> { helpers.require_verification_experiment.candidate? }
before_action :require_verification, only: [:create, :import], if: -> { current_user.requires_credit_card_verification }
layout 'minimal'
feature_category :onboarding
def new
helpers.require_verification_experiment.publish_to_database
@group = Group.new(visibility_level: helpers.default_group_visibility)
@project = Project.new(namespace: @group)
......@@ -51,6 +57,7 @@ module Registrations
success_url = new_trial_path
end
helpers.require_verification_experiment.record_conversion(@group)
redirect_to success_url
end
else
......@@ -66,6 +73,7 @@ module Registrations
@group = Groups::CreateService.new(current_user, modified_group_params).execute
if @group.persisted?
combined_registration_experiment.track(:create_group, namespace: @group)
helpers.require_verification_experiment.record_conversion(@group)
import_url = URI.join(root_url, params[:import_url], "?namespace_id=#{@group.id}").to_s
redirect_to import_url
......
......@@ -38,6 +38,20 @@ module EE
{ next_step_url: url }
end
def require_verification_experiment
strong_memoize(:require_verification_experiment) do
experiment(:require_verification_for_namespace_creation, user: current_user)
end
end
def credit_card_verification_data
{
completed: current_user.credit_card_validation.present?.to_s,
iframe_url: ::Gitlab::SubscriptionPortal::REGISTRATION_VALIDATION_FORM_URL,
allowed_origin: ::Gitlab::SubscriptionPortal::SUBSCRIPTIONS_URL
}
end
private
def redirect_path
......
......@@ -18,7 +18,9 @@
%p.gl-text-center= _('Projects help you organize your work. They contain your file repository, issues, merge requests, and so much more.')
.js-toggle-container.gl-w-full
- if (verify = require_verification_experiment.candidate?)
.js-credit-card-verification{ data: credit_card_verification_data }
.js-toggle-container.gl-w-full{ class: ('gl-display-none' if verify) }
%ul.nav.nav-tabs.nav-links.gitlab-tabs.js-group-project-tabs{ role: 'tablist' }
%li.nav-item{ role: 'presentation' }
%a#blank-project-tab.nav-link.active{ href: '#blank-project-pane', data: { toggle: 'tab', track_label: 'blank_project', track_action: 'click_tab', track_value: '' }, role: 'tab' }
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Registrations::Verification do
controller(ActionController::Base) do
include Registrations::Verification
before_action :set_requires_verification, only: :new
def index
head :ok
end
def create
head :ok
end
def new
head :ok
end
def html_request?
request.format.html?
end
end
let_it_be(:user) { create(:user) }
before do
sign_in(user)
end
describe '#require_verification' do
describe 'verification is not required' do
it 'does not redirect' do
get :index
expect(response).to have_gitlab_http_status(:ok)
end
end
describe 'verification is required' do
let_it_be(:user) { create(:user, requires_credit_card_verification: true) }
it 'redirects to the new users sign_up groups_project path' do
get :index
expect(response).to redirect_to(new_users_sign_up_groups_project_path)
end
it 'does not redirect on JS requests' do
get :index, format: :js
expect(response).to have_gitlab_http_status(:ok)
end
it 'does not redirect on POST requests' do
post :create
expect(response).to have_gitlab_http_status(:ok)
end
end
end
describe '#set_requires_verification' do
it 'sets the requires_credit_card_verification attribute' do
expect { get :new }.to change { user.reload.requires_credit_card_verification }.to(true)
end
end
end
......@@ -28,6 +28,28 @@ RSpec.describe Registrations::GroupsProjectsController, :experiment do
subject
end
it 'publishes the required verification experiment to the database' do
expect_next_instance_of(RequireVerificationForNamespaceCreationExperiment) do |experiment|
expect(experiment).to receive(:publish_to_database)
end
subject
end
end
end
shared_context 'records a conversion event' do
let_it_be(:experiment) { create(:experiment, name: :require_verification_for_namespace_creation) }
let_it_be(:experiment_subject) { create(:experiment_subject, experiment: experiment, user: user) }
before do
stub_experiments(require_verification_for_namespace_creation: true)
end
it 'records a conversion event for the required verification experiment' do
expect { subject }.to change { experiment_subject.reload.converted_at }.from(nil)
.and change { experiment_subject.context }.to include('namespace_id')
end
end
......@@ -55,6 +77,8 @@ RSpec.describe Registrations::GroupsProjectsController, :experiment do
it_behaves_like 'hides email confirmation warning'
it_behaves_like 'records a conversion event'
context 'when group and project can be created' do
it 'creates a group' do
expect { post_create }.to change { Group.count }.by(1)
......@@ -234,6 +258,8 @@ RSpec.describe Registrations::GroupsProjectsController, :experiment do
it_behaves_like 'hides email confirmation warning'
it_behaves_like 'records a conversion event'
context "when a group can't be created" do
before do
allow_next_instance_of(::Groups::CreateService) do |service|
......
import { shallowMount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
import CreditCardVerification from 'ee/registrations/groups_projects/new/components/credit_card_verification.vue';
import { IFRAME_MINIMUM_HEIGHT } from 'ee/registrations/groups_projects/new/constants';
import { setHTMLFixture } from 'helpers/fixtures';
describe('CreditCardVerification', () => {
let wrapper;
let zuoraSubmitSpy;
const IFRAME_URL = 'https://customers.gitlab.com/payment_forms/cc_registration_validation';
const ALLOWED_ORIGIN = 'https://customers.gitlab.com';
const createComponent = (completed = false) => {
wrapper = shallowMount(CreditCardVerification, {
provide: {
completed,
iframeUrl: IFRAME_URL,
allowedOrigin: ALLOWED_ORIGIN,
},
stubs: {
GlButton,
},
});
};
const verifyToggleEnabled = () =>
wrapper.find({ ref: 'verifyToggle' }).attributes('enabled') === 'true';
const createToggleEnabled = () =>
wrapper.find({ ref: 'createToggle' }).attributes('enabled') === 'true';
const findZuora = () => wrapper.find({ ref: 'zuora' });
const findSubmitButton = () => wrapper.find({ ref: 'submitButton' });
const toggleContainerHidden = () =>
document.querySelector('.js-toggle-container').classList.contains('gl-display-none');
beforeEach(() => {
setHTMLFixture('<div class="js-toggle-container gl-display-none" />');
createComponent();
});
afterEach(() => {
wrapper.destroy();
});
describe('when the component is mounted', () => {
it('enables the right toggles', () => {
expect(verifyToggleEnabled()).toBe(true);
expect(createToggleEnabled()).toBe(false);
});
it('hides the toggleContainer', () => {
expect(toggleContainerHidden()).toBe(true);
});
it('renders the Zuora component with the right attributes', () => {
expect(findZuora().exists()).toBe(true);
expect(findZuora().attributes()).toMatchObject({
iframeurl: IFRAME_URL,
allowedorigin: ALLOWED_ORIGIN,
initialheight: IFRAME_MINIMUM_HEIGHT.toString(),
});
});
describe('when verification is completed', () => {
beforeEach(() => {
createComponent(true);
});
it('enables the right toggles', () => {
expect(verifyToggleEnabled()).toBe(false);
expect(createToggleEnabled()).toBe(true);
});
it('shows the toggleContainer', () => {
expect(toggleContainerHidden()).toBe(false);
});
it('hides the Zuora component', () => {
expect(findZuora().exists()).toBe(false);
});
});
});
describe('when the submit button is clicked', () => {
beforeEach(() => {
zuoraSubmitSpy = jest.fn();
wrapper.vm.$refs.zuora = { submit: zuoraSubmitSpy };
findSubmitButton().trigger('click');
});
it('calls the submit method of the Zuora component', () => {
expect(zuoraSubmitSpy).toHaveBeenCalled();
});
});
describe('when the Zuora component emits a success event', () => {
beforeEach(() => {
findZuora().vm.$emit('success');
});
it('enables the right toggles', () => {
expect(verifyToggleEnabled()).toBe(false);
expect(createToggleEnabled()).toBe(true);
});
it('shows the toggleContainer', () => {
expect(toggleContainerHidden()).toBe(false);
});
it('hides the Zuora component', () => {
expect(findZuora().exists()).toBe(false);
});
});
});
......@@ -120,4 +120,20 @@ RSpec.describe EE::RegistrationsHelper do
end
end
end
describe '#credit_card_verification_data' do
before do
allow(helper).to receive(:current_user).and_return(build(:user))
end
it 'returns the expected data' do
expect(helper.credit_card_verification_data).to eq(
{
completed: 'false',
iframe_url: ::Gitlab::SubscriptionPortal::REGISTRATION_VALIDATION_FORM_URL,
allowed_origin: ::Gitlab::SubscriptionPortal::SUBSCRIPTIONS_URL
}
)
end
end
end
......@@ -1076,7 +1076,7 @@ module API
attrs = declared_params(include_missing: false)
service = ::Users::UpsertCreditCardValidationService.new(attrs).execute
service = ::Users::UpsertCreditCardValidationService.new(attrs, user).execute
if service.success?
present user.credit_card_validation, with: Entities::UserCreditCardValidations
......
......@@ -17847,9 +17847,15 @@ msgstr ""
msgid "Identities"
msgstr ""
msgid "IdentityVerification|Before you create your first project, we need you to verify your identity with a valid payment method. You will not be charged during this step. If we ever need to charge you, we will let you know."
msgstr ""
msgid "IdentityVerification|Before you create your group, we need you to verify your identity with a valid payment method."
msgstr ""
msgid "IdentityVerification|Create a project"
msgstr ""
msgid "IdentityVerification|Verify your identity"
msgstr ""
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe RequireVerificationForNamespaceCreationExperiment, :experiment do
subject(:experiment) { described_class.new(user: user) }
let_it_be(:user) { create(:user) }
describe '#candidate?' do
context 'when experiment subject is candidate' do
before do
stub_experiments(require_verification_for_namespace_creation: :candidate)
end
it 'returns true' do
expect(experiment.candidate?).to eq(true)
end
end
context 'when experiment subject is control' do
before do
stub_experiments(require_verification_for_namespace_creation: :control)
end
it 'returns false' do
expect(experiment.candidate?).to eq(false)
end
end
end
describe '#record_conversion' do
let_it_be(:namespace) { create(:namespace) }
context 'when should_track? is false' do
before do
allow(experiment).to receive(:should_track?).and_return(false)
end
it 'does not record a conversion event' do
expect(experiment.publish_to_database).to be_nil
expect(experiment.record_conversion(namespace)).to be_nil
end
end
context 'when should_track? is true' do
before do
allow(experiment).to receive(:should_track?).and_return(true)
end
it 'records a conversion event' do
experiment_subject = experiment.publish_to_database
expect { experiment.record_conversion(namespace) }.to change { experiment_subject.reload.converted_at }.from(nil)
.and change { experiment_subject.context }.to include('namespace_id' => namespace.id)
end
end
end
end
......@@ -235,6 +235,54 @@ RSpec.describe Experiment do
end
end
describe '#record_conversion_event_for_subject' do
let_it_be(:user) { create(:user) }
let_it_be(:experiment) { create(:experiment) }
let_it_be(:context) { { a: 42 } }
subject(:record_conversion) { experiment.record_conversion_event_for_subject(user, context) }
context 'when no existing experiment_subject record exists for the given user' do
it 'does not update or create an experiment_subject record' do
expect { record_conversion }.not_to change { ExperimentSubject.all.to_a }
end
end
context 'when an existing experiment_subject exists for the given user' do
context 'but it has already been converted' do
let(:experiment_subject) { create(:experiment_subject, experiment: experiment, user: user, converted_at: 2.days.ago) }
it 'does not update the converted_at value' do
expect { record_conversion }.not_to change { experiment_subject.converted_at }
end
end
context 'and it has not yet been converted' do
let(:experiment_subject) { create(:experiment_subject, experiment: experiment, user: user) }
it 'updates the converted_at value' do
expect { record_conversion }.to change { experiment_subject.reload.converted_at }
end
end
context 'with no existing context' do
let(:experiment_subject) { create(:experiment_subject, experiment: experiment, user: user) }
it 'updates the context' do
expect { record_conversion }.to change { experiment_subject.reload.context }.to('a' => 42)
end
end
context 'with an existing context' do
let(:experiment_subject) { create(:experiment_subject, experiment: experiment, user: user, converted_at: 2.days.ago, context: { b: 1 } ) }
it 'merges the context' do
expect { record_conversion }.to change { experiment_subject.reload.context }.to('a' => 42, 'b' => 1)
end
end
end
end
describe '#record_subject_and_variant!' do
let_it_be(:subject_to_record) { create(:group) }
let_it_be(:variant) { :control }
......
......@@ -83,6 +83,9 @@ RSpec.describe User do
it { is_expected.to delegate_method(:registration_objective).to(:user_detail).allow_nil }
it { is_expected.to delegate_method(:registration_objective=).to(:user_detail).with_arguments(:args).allow_nil }
it { is_expected.to delegate_method(:requires_credit_card_verification).to(:user_detail).allow_nil }
it { is_expected.to delegate_method(:requires_credit_card_verification=).to(:user_detail).with_arguments(:args).allow_nil }
end
describe 'associations' do
......
......@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Users::UpsertCreditCardValidationService do
let_it_be(:user) { create(:user) }
let_it_be(:user) { create(:user, requires_credit_card_verification: true) }
let(:user_id) { user.id }
let(:credit_card_validated_time) { Time.utc(2020, 1, 1) }
......@@ -21,7 +21,7 @@ RSpec.describe Users::UpsertCreditCardValidationService do
end
describe '#execute' do
subject(:service) { described_class.new(params) }
subject(:service) { described_class.new(params, user) }
context 'successfully set credit card validation record for the user' do
context 'when user does not have credit card validation record' do
......@@ -42,6 +42,10 @@ RSpec.describe Users::UpsertCreditCardValidationService do
expiration_date: Date.new(expiration_year, 1, 31)
)
end
it 'sets the requires_credit_card_verification attribute on the user to false' do
expect { service.execute }.to change { user.reload.requires_credit_card_verification }.to(false)
end
end
context 'when user has credit card validation record' do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment