diff --git a/config/feature_flags/development/hamilton_seat_management.yml b/config/feature_flags/development/hamilton_seat_management.yml
new file mode 100644
index 0000000000000000000000000000000000000000..15ac8e07f682aebeb8338d9b5e3575448a5fbcb3
--- /dev/null
+++ b/config/feature_flags/development/hamilton_seat_management.yml
@@ -0,0 +1,8 @@
+---
+name: hamilton_seat_management
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/126964
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/419175
+milestone: '16.3'
+type: development
+group: group::purchase
+default_enabled: false
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index c7966e3b138c90dbafd17157e481a9a1f6725beb..f4e9460ace85e9a0d97baeef7b9ee1aaf6ab2e74 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -6865,6 +6865,29 @@ Input type: `UserAchievementsDeleteInput`
 | <a id="mutationuserachievementsdeleteerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
 | <a id="mutationuserachievementsdeleteuserachievement"></a>`userAchievement` | [`UserAchievement`](#userachievement) | Deleted user achievement. |
 
+### `Mutation.userAddOnAssignmentCreate`
+
+WARNING:
+**Introduced** in 16.3.
+This feature is an Experiment. It can be changed or removed at any time.
+
+Input type: `UserAddOnAssignmentCreateInput`
+
+#### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationuseraddonassignmentcreateaddonpurchaseid"></a>`addOnPurchaseId` | [`GitlabSubscriptionsAddOnPurchaseID!`](#gitlabsubscriptionsaddonpurchaseid) | Global ID of AddOnPurchase to be assinged to. |
+| <a id="mutationuseraddonassignmentcreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationuseraddonassignmentcreateuserid"></a>`userId` | [`UserID!`](#userid) | Global ID of user to be assigned. |
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationuseraddonassignmentcreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationuseraddonassignmentcreateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
+
 ### `Mutation.userCalloutCreate`
 
 Input type: `UserCalloutCreateInput`
diff --git a/ee/app/graphql/ee/types/mutation_type.rb b/ee/app/graphql/ee/types/mutation_type.rb
index 63e1ab66249dd712fcfcf66cf1d2aa2121f2a584..6716222103ddee196f2dfe79a472a67a67c3e02d 100644
--- a/ee/app/graphql/ee/types/mutation_type.rb
+++ b/ee/app/graphql/ee/types/mutation_type.rb
@@ -22,6 +22,7 @@ module MutationType
         mount_mutation ::Mutations::Epics::AddIssue
         mount_mutation ::Mutations::Geo::Registries::Update, alpha: { milestone: '16.1' }
         mount_mutation ::Mutations::GitlabSubscriptions::Activate
+        mount_mutation ::Mutations::GitlabSubscriptions::UserAddOnAssignments::Create, alpha: { milestone: '16.3' }
         mount_mutation ::Mutations::Projects::SetLocked
         mount_mutation ::Mutations::Iterations::Create
         mount_mutation ::Mutations::Iterations::Update
diff --git a/ee/app/graphql/mutations/gitlab_subscriptions/user_add_on_assignments/create.rb b/ee/app/graphql/mutations/gitlab_subscriptions/user_add_on_assignments/create.rb
new file mode 100644
index 0000000000000000000000000000000000000000..6c1a573dec962d30bfd79a35edad2c0870e9d3e7
--- /dev/null
+++ b/ee/app/graphql/mutations/gitlab_subscriptions/user_add_on_assignments/create.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+module Mutations
+  module GitlabSubscriptions
+    module UserAddOnAssignments
+      class Create < BaseMutation
+        graphql_name 'UserAddOnAssignmentCreate'
+
+        argument :add_on_purchase_id, ::Types::GlobalIDType[::GitlabSubscriptions::AddOnPurchase],
+          required: true, description: 'Global ID of AddOnPurchase to be assinged to.'
+
+        argument :user_id, ::Types::GlobalIDType[::User],
+          required: true, description: 'Global ID of user to be assigned.'
+
+        def resolve(**)
+          authorize!
+
+          create_service = ::GitlabSubscriptions::UserAddOnAssignments::CreateService.new(
+            add_on_purchase: add_on_purchase,
+            user: user_to_be_assigned
+          ).execute
+
+          {
+            errors: create_service.errors
+          }
+        end
+
+        def ready?(add_on_purchase_id:, user_id:)
+          @add_on_purchase = ::Gitlab::Graphql::Lazy.force(GitlabSchema.find_by_gid(add_on_purchase_id))
+          @user_to_be_assigned = ::Gitlab::Graphql::Lazy.force(GitlabSchema.find_by_gid(user_id))
+
+          raise_resource_not_available_error! unless feature_enabled? && add_on_purchase&.active? && user_to_be_assigned
+
+          super
+        end
+
+        private
+
+        attr_reader :add_on_purchase, :user_to_be_assigned
+
+        def feature_enabled?
+          Feature.enabled?(:hamilton_seat_management)
+        end
+
+        def authorize!
+          return if self_assignment? ||
+            Ability.allowed?(current_user, :admin_add_on_purchase, add_on_purchase.namespace)
+
+          raise_resource_not_available_error!
+        end
+
+        def self_assignment?
+          current_user == user_to_be_assigned
+        end
+      end
+    end
+  end
+end
diff --git a/ee/app/models/ee/group.rb b/ee/app/models/ee/group.rb
index 60ac5d0af61afd866c702360798d0432caba0f23..fc246721a9f7e21615a71edc0f96a2e6c371af7a 100644
--- a/ee/app/models/ee/group.rb
+++ b/ee/app/models/ee/group.rb
@@ -789,6 +789,26 @@ def billed_invited_group_to_project_users(exclude_guests: false)
       users_without_bots(members)
     end
 
+    # Checks if user belongs to billed_group_users
+    def billed_group_user?(user, exclude_guests: false)
+      billed_group_users(exclude_guests: exclude_guests).exists?(id: user.id)
+    end
+
+    # Checks if user belongs to billed_project_users
+    def billed_project_user?(user, exclude_guests: false)
+      billed_project_users(exclude_guests: exclude_guests).exists?(id: user.id)
+    end
+
+    # Checks if user belongs to billed_shared_group_users
+    def billed_shared_group_user?(user, exclude_guests: false)
+      billed_shared_group_users(exclude_guests: exclude_guests).exists?(id: user.id)
+    end
+
+    # Checks if user belongs to billed_invited_group_to_project_users
+    def billed_shared_project_user?(user, exclude_guests: false)
+      billed_invited_group_to_project_users(exclude_guests: exclude_guests).exists?(id: user.id)
+    end
+
     def parent_epic_ids_in_ancestor_groups
       ids = Set.new
       epics.has_parent.each_batch(of: EPIC_BATCH_SIZE, column: :iid) do |batch|
diff --git a/ee/app/models/gitlab_subscriptions/add_on_purchase.rb b/ee/app/models/gitlab_subscriptions/add_on_purchase.rb
index bdbfd3f8b99d6ccefa397179b800f0825b5d86e2..af84c4b98b4bd9aad24ec78db7313b6350e1968f 100644
--- a/ee/app/models/gitlab_subscriptions/add_on_purchase.rb
+++ b/ee/app/models/gitlab_subscriptions/add_on_purchase.rb
@@ -20,5 +20,13 @@ class AddOnPurchase < ApplicationRecord
     scope :by_add_on_name, ->(name) { joins(:add_on).where(add_on: { name: name }) }
     scope :for_code_suggestions, -> { where(subscription_add_on_id: AddOn.code_suggestions.pick(:id)) }
     scope :for_project, ->(project_id) { where(namespace: Project.find_by_id(project_id)&.root_namespace) }
+
+    def already_assigned?(user)
+      assigned_users.where(user: user).exists?
+    end
+
+    def active?
+      expires_on >= Date.current
+    end
   end
 end
diff --git a/ee/app/services/gitlab_subscriptions/user_add_on_assignments/create_service.rb b/ee/app/services/gitlab_subscriptions/user_add_on_assignments/create_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e927100ab30d8ac1960ee46698825bb261b48e8f
--- /dev/null
+++ b/ee/app/services/gitlab_subscriptions/user_add_on_assignments/create_service.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+module GitlabSubscriptions
+  module UserAddOnAssignments
+    class CreateService < BaseService
+      ERROR_NO_SEATS_AVAILABLE = 'NO_SEATS_AVAILABLE'
+      ERROR_INVALID_USER_MEMBERSHIP = 'INVALID_USER_MEMBERSHIP'
+
+      def initialize(add_on_purchase:, user:)
+        @add_on_purchase = add_on_purchase
+        @user = user
+      end
+
+      def execute
+        return ServiceResponse.success if user_already_assigned?
+
+        errors = validate
+
+        if errors.blank?
+          # TODO: implement resource locking to avoid race condition
+          # https://gitlab.com/gitlab-org/gitlab/-/issues/415584#race-condition
+          add_on_purchase.assigned_users.create!(user: user)
+
+          ServiceResponse.success
+        else
+          ServiceResponse.error(message: errors)
+        end
+      end
+
+      private
+
+      attr_reader :add_on_purchase, :user
+
+      def validate
+        return ERROR_NO_SEATS_AVAILABLE unless seats_available?
+        return ERROR_INVALID_USER_MEMBERSHIP unless billed_member_of_namespace?
+      end
+
+      def seats_available?
+        add_on_purchase.quantity > assigned_seats
+      end
+
+      def assigned_seats
+        @assigned_seats ||= add_on_purchase.assigned_users.count
+      end
+
+      def user_already_assigned?
+        add_on_purchase.already_assigned?(user)
+      end
+
+      def billed_member_of_namespace?
+        namespace.billed_group_user?(user, exclude_guests: true) ||
+          namespace.billed_project_user?(user, exclude_guests: true) ||
+          namespace.billed_shared_group_user?(user, exclude_guests: true) ||
+          namespace.billed_shared_project_user?(user, exclude_guests: true)
+      end
+
+      def namespace
+        @namespace ||= add_on_purchase.namespace
+      end
+    end
+  end
+end
diff --git a/ee/spec/models/ee/group_spec.rb b/ee/spec/models/ee/group_spec.rb
index dd1ca64b930e6435edae633d680d7ac12d136a00..0e64f7e80106a1f58654c5301998d4a4cf4c1879 100644
--- a/ee/spec/models/ee/group_spec.rb
+++ b/ee/spec/models/ee/group_spec.rb
@@ -1533,6 +1533,110 @@
     end
   end
 
+  describe '#billed_group_user?' do
+    let_it_be(:group) { create(:group) }
+    let_it_be(:sub_group) { create(:group, parent: group) }
+    let_it_be(:developer) { group.add_developer(create(:user)).user }
+    let_it_be(:sub_developer) { sub_group.add_developer(create(:user)).user }
+    let_it_be(:guest) { group.add_guest(create(:user)).user }
+
+    where(:user, :exclude_guests, :result) do
+      ref(:developer)     | false | true
+      ref(:sub_developer) | false | true
+      ref(:guest)         | false | true
+      ref(:developer)     | true  | true
+      ref(:sub_developer) | true  | true
+      ref(:guest)         | true  | false
+    end
+
+    subject { group.billed_group_user?(user, exclude_guests: exclude_guests) }
+
+    with_them do
+      it { is_expected.to eq(result) }
+    end
+  end
+
+  describe '#billed_project_user?' do
+    let_it_be(:group) { create(:group) }
+    let_it_be(:project) { create(:project, namespace: group) }
+    let_it_be(:sub_group_project) { create(:project, namespace: create(:group, parent: group)) }
+    let_it_be(:developer) { project.add_developer(create(:user)).user }
+    let_it_be(:sub_developer) { sub_group_project.add_developer(create(:user)).user }
+    let_it_be(:guest) { project.add_guest(create(:user)).user }
+
+    where(:user, :exclude_guests, :result) do
+      ref(:developer)     | false | true
+      ref(:sub_developer) | false | true
+      ref(:guest)         | false | true
+      ref(:developer)     | true  | true
+      ref(:sub_developer) | true  | true
+      ref(:guest)         | true  | false
+    end
+
+    subject { group.billed_project_user?(user, exclude_guests: exclude_guests) }
+
+    with_them do
+      it { is_expected.to eq(result) }
+    end
+  end
+
+  describe '#billed_shared_group_user?' do
+    let_it_be(:group) { create(:group) }
+    let_it_be(:ancestor_invited_group) { create(:group) }
+    let_it_be(:invited_group) { create(:group, parent: ancestor_invited_group) }
+    let_it_be(:ancestor_invited_developer) { ancestor_invited_group.add_developer(create(:user)).user }
+    let_it_be(:invited_developer) { invited_group.add_developer(create(:user)).user }
+    let_it_be(:invited_guest) { invited_group.add_guest(create(:user)).user }
+
+    before_all do
+      create(:group_group_link, { shared_with_group: invited_group, shared_group: group })
+    end
+
+    where(:user, :exclude_guests, :result) do
+      ref(:ancestor_invited_developer) | false | true
+      ref(:invited_developer)          | false | true
+      ref(:invited_guest)              | false | true
+      ref(:ancestor_invited_developer) | true  | true
+      ref(:invited_developer)          | true  | true
+      ref(:invited_guest)              | true  | false
+    end
+
+    subject { group.billed_shared_group_user?(user, exclude_guests: exclude_guests) }
+
+    with_them do
+      it { is_expected.to eq(result) }
+    end
+  end
+
+  describe '#billed_shared_project_user?' do
+    let_it_be(:group) { create(:group) }
+    let_it_be(:project) { create(:project, namespace: group) }
+    let_it_be(:ancestor_invited_group) { create(:group) }
+    let_it_be(:invited_group) { create(:group, parent: ancestor_invited_group) }
+    let_it_be(:ancestor_invited_developer) { ancestor_invited_group.add_developer(create(:user)).user }
+    let_it_be(:invited_developer) { invited_group.add_developer(create(:user)).user }
+    let_it_be(:invited_guest) { invited_group.add_guest(create(:user)).user }
+
+    before_all do
+      create(:project_group_link, project: project, group: invited_group)
+    end
+
+    where(:user, :exclude_guests, :result) do
+      ref(:ancestor_invited_developer) | false | true
+      ref(:invited_developer)          | false | true
+      ref(:invited_guest)              | false | true
+      ref(:ancestor_invited_developer) | true  | true
+      ref(:invited_developer)          | true  | true
+      ref(:invited_guest)              | true  | false
+    end
+
+    subject { group.billed_shared_project_user?(user, exclude_guests: exclude_guests) }
+
+    with_them do
+      it { is_expected.to eq(result) }
+    end
+  end
+
   describe '#capacity_left_for_user?' do
     let_it_be(:group) { create(:group) }
     let_it_be(:user) { create(:user) }
diff --git a/ee/spec/models/gitlab_subscriptions/add_on_purchase_spec.rb b/ee/spec/models/gitlab_subscriptions/add_on_purchase_spec.rb
index 8e8de76baf9d1d447ce1601e48f853a91563244d..ac4b36723a3d618f113f8c5c2bac1e55b90a4eba 100644
--- a/ee/spec/models/gitlab_subscriptions/add_on_purchase_spec.rb
+++ b/ee/spec/models/gitlab_subscriptions/add_on_purchase_spec.rb
@@ -99,4 +99,36 @@
       end
     end
   end
+
+  describe '#already_assigned?' do
+    let_it_be(:add_on_purchase) { create(:gitlab_subscription_add_on_purchase) }
+
+    let(:user) { create(:user) }
+
+    subject { add_on_purchase.already_assigned?(user) }
+
+    context 'when the user has been already assigned' do
+      before do
+        create(:gitlab_subscription_user_add_on_assignment, add_on_purchase: add_on_purchase, user: user)
+      end
+
+      it { is_expected.to eq(true) }
+    end
+
+    context 'when user is not already assigned' do
+      it { is_expected.to eq(false) }
+    end
+  end
+
+  describe '#active?' do
+    let_it_be(:add_on_purchase) { create(:gitlab_subscription_add_on_purchase) }
+
+    subject { add_on_purchase.active? }
+
+    it { is_expected.to eq(true) }
+
+    context 'when subscription has expired' do
+      it { travel_to(add_on_purchase.expires_on + 1.day) { is_expected.to eq(false) } }
+    end
+  end
 end
diff --git a/ee/spec/requests/api/graphql/gitlab_subscriptions/user_add_on_assignments/create_spec.rb b/ee/spec/requests/api/graphql/gitlab_subscriptions/user_add_on_assignments/create_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a033a12110d59236f8bdf6fb02c99c82893f3643
--- /dev/null
+++ b/ee/spec/requests/api/graphql/gitlab_subscriptions/user_add_on_assignments/create_spec.rb
@@ -0,0 +1,169 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'UserAddOnAssignmentCreate', feature_category: :seat_cost_management do
+  include GraphqlHelpers
+
+  let_it_be(:current_user) { create(:user) }
+  let_it_be(:namespace) { create(:group) }
+  let_it_be(:add_on) { create(:gitlab_subscription_add_on) }
+  let_it_be(:add_on_purchase) { create(:gitlab_subscription_add_on_purchase, namespace: namespace, add_on: add_on) }
+  let_it_be(:assignee_user) { create(:user) }
+
+  let(:user_id) { global_id_of(assignee_user) }
+  let(:add_on_purchase_id) { global_id_of(add_on_purchase) }
+
+  let(:input) do
+    {
+      user_id: user_id,
+      add_on_purchase_id: add_on_purchase_id
+    }
+  end
+
+  let(:mutation) { graphql_mutation(:user_add_on_assignment_create, input) }
+  let(:mutation_response) { graphql_mutation_response(:user_add_on_assignment_create) }
+
+  before_all do
+    namespace.add_owner(current_user)
+    namespace.add_developer(assignee_user)
+  end
+
+  shared_examples 'empty response' do
+    it 'returns nil' do
+      post_graphql_mutation(mutation, current_user: current_user)
+
+      expect(mutation_response).to be_nil
+    end
+  end
+
+  shared_examples 'error response' do |error_message|
+    it 'returns expected errors' do
+      post_graphql_mutation(mutation, current_user: current_user)
+
+      expect(mutation_response["errors"]).to include(error_message)
+    end
+  end
+
+  shared_examples 'success response' do |_expected_response|
+    it 'returns expected response' do
+      post_graphql_mutation(mutation, current_user: current_user)
+
+      expect(mutation_response["errors"]).to eq([])
+    end
+  end
+
+  it_behaves_like 'success response'
+
+  context 'when feature flag hamilton_seat_management is disabled' do
+    before do
+      stub_feature_flags(hamilton_seat_management: false)
+    end
+
+    it_behaves_like 'empty response'
+  end
+
+  context 'when current_user is admin' do
+    let(:current_user) { create(:admin) }
+
+    it_behaves_like 'success response'
+  end
+
+  context 'when current_user is assigning itself' do
+    let(:current_user) { namespace.add_developer(create(:user)).user }
+    let(:user_id) { global_id_of(current_user) }
+
+    it_behaves_like 'success response'
+  end
+
+  context 'when current_user is not owner or admin' do
+    let(:current_user) { create(:user) }
+
+    it_behaves_like 'empty response'
+  end
+
+  context 'when the user is already assigned' do
+    before do
+      create(:gitlab_subscription_user_add_on_assignment, add_on_purchase: add_on_purchase, user: assignee_user)
+    end
+
+    it_behaves_like 'success response'
+  end
+
+  context 'when add_on_purchase_id does not exists' do
+    let(:add_on_purchase_id) { global_id_of(id: 666, model_name: '::GitlabSubscriptions::AddOnPurchase') }
+
+    it_behaves_like 'empty response'
+  end
+
+  context 'when ad_on_purchase has expired' do
+    before do
+      add_on_purchase.update!(expires_on: 1.day.ago)
+    end
+
+    it_behaves_like 'empty response'
+  end
+
+  context 'when user_id does not exists' do
+    let(:user_id) { global_id_of(id: 666, model_name: '::User') }
+
+    it_behaves_like 'empty response'
+  end
+
+  context 'when there are no free seats available' do
+    before do
+      add_on_purchase.assigned_users.create!(user: create(:user))
+    end
+
+    it_behaves_like 'error response', 'NO_SEATS_AVAILABLE'
+  end
+
+  context 'when user is guest' do
+    let(:user_id) { global_id_of(namespace.add_guest(create(:user)).user) }
+
+    it_behaves_like 'error response', 'INVALID_USER_MEMBERSHIP'
+  end
+
+  context 'when user does not belong to namespace' do
+    let(:user_id) { global_id_of(create(:user)) }
+
+    it_behaves_like 'error response', 'INVALID_USER_MEMBERSHIP'
+  end
+
+  context 'when user belongs to subgroup' do
+    let(:subgroup) { create(:group, parent: namespace) }
+    let(:user_id) { global_id_of(subgroup.add_developer(create(:user)).user) }
+
+    it_behaves_like 'success response'
+  end
+
+  context 'when user belongs to project' do
+    let_it_be(:project) { create(:project, namespace: namespace) }
+    let(:user_id) { global_id_of(project.add_developer(create(:user)).user) }
+
+    it_behaves_like 'success response'
+  end
+
+  context 'when user is member of shared group' do
+    let(:invited_group) { create(:group) }
+    let(:user) { global_id_of(invited_group.add_developer(create(:user)).user) }
+
+    before do
+      create(:group_group_link, { shared_with_group: invited_group, shared_group: namespace })
+    end
+
+    it_behaves_like 'success response'
+  end
+
+  context 'when user is member of shared project' do
+    let(:invited_group) { create(:group) }
+    let_it_be(:project) { create(:project, namespace: namespace) }
+    let(:user_id) { global_id_of(invited_group.add_developer(create(:user)).user) }
+
+    before do
+      create(:project_group_link, project: project, group: invited_group)
+    end
+
+    it_behaves_like 'success response'
+  end
+end
diff --git a/ee/spec/services/gitlab_subscriptions/user_add_on_assignments/create_service_spec.rb b/ee/spec/services/gitlab_subscriptions/user_add_on_assignments/create_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7a70af094d29a5baefb747685a98cc6ec3359599
--- /dev/null
+++ b/ee/spec/services/gitlab_subscriptions/user_add_on_assignments/create_service_spec.rb
@@ -0,0 +1,104 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSubscriptions::UserAddOnAssignments::CreateService, feature_category: :seat_cost_management do
+  let_it_be(:namespace) { create(:group) }
+  let_it_be(:add_on) { create(:gitlab_subscription_add_on) }
+  let_it_be(:add_on_purchase) { create(:gitlab_subscription_add_on_purchase, namespace: namespace, add_on: add_on) }
+  let_it_be(:user) { create(:user) }
+
+  subject(:response) do
+    described_class.new(add_on_purchase: add_on_purchase, user: user).execute
+  end
+
+  before_all do
+    namespace.add_developer(user)
+  end
+
+  describe '#execute' do
+    shared_examples 'success response' do
+      it 'creates new records' do
+        expect { subject }.to change { add_on_purchase.assigned_users.where(user: user).count }.by(1)
+        expect(response).to be_success
+      end
+    end
+
+    shared_examples 'error response' do |error_message|
+      it 'does not create new records' do
+        expect { subject }.not_to change { add_on_purchase.assigned_users.count }
+        expect(response.errors).to include(error_message)
+      end
+    end
+
+    it_behaves_like 'success response'
+
+    context 'when user is already assigned' do
+      before do
+        create(:gitlab_subscription_user_add_on_assignment, add_on_purchase: add_on_purchase, user: user)
+      end
+
+      it 'does not create new record' do
+        expect { subject }.not_to change { add_on_purchase.assigned_users.count }
+        expect(response).to be_success
+      end
+    end
+
+    context 'when seats are not available' do
+      before do
+        create(:gitlab_subscription_user_add_on_assignment, add_on_purchase: add_on_purchase, user: create(:user))
+      end
+
+      it_behaves_like 'error response', 'NO_SEATS_AVAILABLE'
+    end
+
+    context 'when user is not member of namespace' do
+      let(:user) { create(:user) }
+
+      it_behaves_like 'error response', 'INVALID_USER_MEMBERSHIP'
+    end
+
+    context 'when user has guest role' do
+      let(:user) { namespace.add_guest(create(:user)).user }
+
+      it_behaves_like 'error response', 'INVALID_USER_MEMBERSHIP'
+    end
+
+    context 'when user is member of subgroup' do
+      let(:subgroup) { create(:group, parent: namespace) }
+      let(:user) { subgroup.add_developer(create(:user)).user }
+
+      it_behaves_like 'success response'
+    end
+
+    context 'when user is member of project' do
+      let_it_be(:project) { create(:project, namespace: namespace) }
+      let(:user) { project.add_developer(create(:user)).user }
+
+      it_behaves_like 'success response'
+    end
+
+    context 'when user is member of shared group' do
+      let(:invited_group) { create(:group) }
+      let(:user) { invited_group.add_developer(create(:user)).user }
+
+      before do
+        create(:group_group_link, { shared_with_group: invited_group, shared_group: namespace })
+      end
+
+      it_behaves_like 'success response'
+    end
+
+    context 'when user is member of shared project' do
+      let(:invited_group) { create(:group) }
+      let_it_be(:project) { create(:project, namespace: namespace) }
+      let(:user) { invited_group.add_developer(create(:user)).user }
+
+      before do
+        create(:project_group_link, project: project, group: invited_group)
+      end
+
+      it_behaves_like 'success response'
+    end
+  end
+end