From d5b79c4f1ce28ae06cfebeb4fe1fdc912b762e02 Mon Sep 17 00:00:00 2001
From: Bishwa Hang Rai <bhrai@gitlab.com>
Date: Mon, 26 Jun 2023 13:49:45 +0000
Subject: [PATCH] Create table subscription_users_add_on_assignments

- create model 'GitlabSubscriptions::UserAddOnAssignment'
- add valdiation and specs

Changelog: added
---
 .../subscription_user_add_on_assignments.yml  | 10 ++++++
 ...te_subscription_user_add_on_assignments.rb | 17 ++++++++++
 ...on_subscription_user_add_on_assignments.rb | 16 ++++++++++
 ...on_subscription_user_add_on_assignments.rb | 15 +++++++++
 db/schema_migrations/20230616164309           |  1 +
 db/schema_migrations/20230616164705           |  1 +
 db/schema_migrations/20230616164731           |  1 +
 db/structure.sql                              | 32 +++++++++++++++++++
 ee/app/models/ee/user.rb                      |  2 ++
 ee/app/models/gitlab_subscriptions.rb         |  7 ++++
 ee/app/models/gitlab_subscriptions/add_on.rb  |  2 --
 .../gitlab_subscriptions/add_on_purchase.rb   |  4 +--
 .../upcoming_reconciliation.rb                |  2 ++
 .../user_add_on_assignment.rb                 | 11 +++++++
 .../user_add_on_assignment.rb                 |  8 +++++
 ee/spec/models/ee/user_spec.rb                |  1 +
 .../add_on_purchase_spec.rb                   |  5 +++
 .../user_add_on_assignment_spec.rb            | 21 ++++++++++++
 18 files changed, 152 insertions(+), 4 deletions(-)
 create mode 100644 db/docs/subscription_user_add_on_assignments.yml
 create mode 100644 db/migrate/20230616164309_create_subscription_user_add_on_assignments.rb
 create mode 100644 db/migrate/20230616164705_add_foreign_key_add_on_purchase_id_on_subscription_user_add_on_assignments.rb
 create mode 100644 db/migrate/20230616164731_add_foreign_key_user_id_on_subscription_user_add_on_assignments.rb
 create mode 100644 db/schema_migrations/20230616164309
 create mode 100644 db/schema_migrations/20230616164705
 create mode 100644 db/schema_migrations/20230616164731
 create mode 100644 ee/app/models/gitlab_subscriptions.rb
 create mode 100644 ee/app/models/gitlab_subscriptions/user_add_on_assignment.rb
 create mode 100644 ee/spec/factories/gitlab_subscriptions/user_add_on_assignment.rb
 create mode 100644 ee/spec/models/gitlab_subscriptions/user_add_on_assignment_spec.rb

diff --git a/db/docs/subscription_user_add_on_assignments.yml b/db/docs/subscription_user_add_on_assignments.yml
new file mode 100644
index 0000000000000..acd9b821115f6
--- /dev/null
+++ b/db/docs/subscription_user_add_on_assignments.yml
@@ -0,0 +1,10 @@
+---
+table_name: subscription_user_add_on_assignments
+description: Tracks the assignment of an add-on to a user within a namespace
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/123967
+milestone: '16.2'
+feature_categories:
+- seat_cost_management
+classes:
+- GitlabSubscriptions::UserAddOnAssignment
+gitlab_schema: gitlab_main
diff --git a/db/migrate/20230616164309_create_subscription_user_add_on_assignments.rb b/db/migrate/20230616164309_create_subscription_user_add_on_assignments.rb
new file mode 100644
index 0000000000000..cb184cd1987d2
--- /dev/null
+++ b/db/migrate/20230616164309_create_subscription_user_add_on_assignments.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class CreateSubscriptionUserAddOnAssignments < Gitlab::Database::Migration[2.1]
+  UNIQUE_INDEX_NAME = 'uniq_idx_user_add_on_assignments_on_add_on_purchase_and_user'
+
+  def change
+    create_table :subscription_user_add_on_assignments do |t|
+      t.bigint :add_on_purchase_id, null: false
+      t.bigint :user_id, null: false
+
+      t.timestamps_with_timezone null: false
+
+      t.index [:add_on_purchase_id, :user_id], unique: true, name: UNIQUE_INDEX_NAME
+      t.index :user_id
+    end
+  end
+end
diff --git a/db/migrate/20230616164705_add_foreign_key_add_on_purchase_id_on_subscription_user_add_on_assignments.rb b/db/migrate/20230616164705_add_foreign_key_add_on_purchase_id_on_subscription_user_add_on_assignments.rb
new file mode 100644
index 0000000000000..d0d89bd5027ca
--- /dev/null
+++ b/db/migrate/20230616164705_add_foreign_key_add_on_purchase_id_on_subscription_user_add_on_assignments.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class AddForeignKeyAddOnPurchaseIdOnSubscriptionUserAddOnAssignments < Gitlab::Database::Migration[2.1]
+  disable_ddl_transaction!
+
+  def up
+    add_concurrent_foreign_key :subscription_user_add_on_assignments, :subscription_add_on_purchases,
+      column: :add_on_purchase_id, on_delete: :cascade
+  end
+
+  def down
+    with_lock_retries do
+      remove_foreign_key :subscription_user_add_on_assignments, column: :add_on_purchase_id
+    end
+  end
+end
diff --git a/db/migrate/20230616164731_add_foreign_key_user_id_on_subscription_user_add_on_assignments.rb b/db/migrate/20230616164731_add_foreign_key_user_id_on_subscription_user_add_on_assignments.rb
new file mode 100644
index 0000000000000..a28c798deec4e
--- /dev/null
+++ b/db/migrate/20230616164731_add_foreign_key_user_id_on_subscription_user_add_on_assignments.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class AddForeignKeyUserIdOnSubscriptionUserAddOnAssignments < Gitlab::Database::Migration[2.1]
+  disable_ddl_transaction!
+
+  def up
+    add_concurrent_foreign_key :subscription_user_add_on_assignments, :users, column: :user_id, on_delete: :cascade
+  end
+
+  def down
+    with_lock_retries do
+      remove_foreign_key :subscription_user_add_on_assignments, column: :user_id
+    end
+  end
+end
diff --git a/db/schema_migrations/20230616164309 b/db/schema_migrations/20230616164309
new file mode 100644
index 0000000000000..b9fbdc7d33eef
--- /dev/null
+++ b/db/schema_migrations/20230616164309
@@ -0,0 +1 @@
+f3b14748f1702972e7f5069edd9ed25d9896dfb11f4fc4a4386ca9c94533e10a
\ No newline at end of file
diff --git a/db/schema_migrations/20230616164705 b/db/schema_migrations/20230616164705
new file mode 100644
index 0000000000000..1bcb723524bd1
--- /dev/null
+++ b/db/schema_migrations/20230616164705
@@ -0,0 +1 @@
+75310614bb98a598b8425aa87a0a4a6561fa1b166461d55329c21aff849d71fc
\ No newline at end of file
diff --git a/db/schema_migrations/20230616164731 b/db/schema_migrations/20230616164731
new file mode 100644
index 0000000000000..2588271d9acd7
--- /dev/null
+++ b/db/schema_migrations/20230616164731
@@ -0,0 +1 @@
+e6308ee437b6e57da16e1b8aff1d6a571ef849c4c7cccafe940710c799fa6eea
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 35c119628ce21..5cff7ab4337bc 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -23057,6 +23057,23 @@ CREATE SEQUENCE subscription_add_ons_id_seq
 
 ALTER SEQUENCE subscription_add_ons_id_seq OWNED BY subscription_add_ons.id;
 
+CREATE TABLE subscription_user_add_on_assignments (
+    id bigint NOT NULL,
+    add_on_purchase_id bigint NOT NULL,
+    user_id bigint NOT NULL,
+    created_at timestamp with time zone NOT NULL,
+    updated_at timestamp with time zone NOT NULL
+);
+
+CREATE SEQUENCE subscription_user_add_on_assignments_id_seq
+    START WITH 1
+    INCREMENT BY 1
+    NO MINVALUE
+    NO MAXVALUE
+    CACHE 1;
+
+ALTER SEQUENCE subscription_user_add_on_assignments_id_seq OWNED BY subscription_user_add_on_assignments.id;
+
 CREATE TABLE subscriptions (
     id integer NOT NULL,
     user_id integer,
@@ -25862,6 +25879,8 @@ ALTER TABLE ONLY subscription_add_on_purchases ALTER COLUMN id SET DEFAULT nextv
 
 ALTER TABLE ONLY subscription_add_ons ALTER COLUMN id SET DEFAULT nextval('subscription_add_ons_id_seq'::regclass);
 
+ALTER TABLE ONLY subscription_user_add_on_assignments ALTER COLUMN id SET DEFAULT nextval('subscription_user_add_on_assignments_id_seq'::regclass);
+
 ALTER TABLE ONLY subscriptions ALTER COLUMN id SET DEFAULT nextval('subscriptions_id_seq'::regclass);
 
 ALTER TABLE ONLY suggestions ALTER COLUMN id SET DEFAULT nextval('suggestions_id_seq'::regclass);
@@ -28351,6 +28370,9 @@ ALTER TABLE ONLY subscription_add_on_purchases
 ALTER TABLE ONLY subscription_add_ons
     ADD CONSTRAINT subscription_add_ons_pkey PRIMARY KEY (id);
 
+ALTER TABLE ONLY subscription_user_add_on_assignments
+    ADD CONSTRAINT subscription_user_add_on_assignments_pkey PRIMARY KEY (id);
+
 ALTER TABLE ONLY subscriptions
     ADD CONSTRAINT subscriptions_pkey PRIMARY KEY (id);
 
@@ -32910,6 +32932,8 @@ CREATE INDEX index_subscription_add_on_purchases_on_subscription_add_on_id ON su
 
 CREATE UNIQUE INDEX index_subscription_add_ons_on_name ON subscription_add_ons USING btree (name);
 
+CREATE INDEX index_subscription_user_add_on_assignments_on_user_id ON subscription_user_add_on_assignments USING btree (user_id);
+
 CREATE INDEX index_subscriptions_on_project_id ON subscriptions USING btree (project_id);
 
 CREATE UNIQUE INDEX index_subscriptions_on_subscribable_and_user_id_and_project_id ON subscriptions USING btree (subscribable_id, subscribable_type, user_id, project_id);
@@ -33556,6 +33580,8 @@ CREATE UNIQUE INDEX u_project_compliance_standards_adherence_for_reporting ON pr
 
 CREATE UNIQUE INDEX uniq_idx_packages_packages_on_project_id_name_version_ml_model ON packages_packages USING btree (project_id, name, version) WHERE (package_type = 14);
 
+CREATE UNIQUE INDEX uniq_idx_user_add_on_assignments_on_add_on_purchase_and_user ON subscription_user_add_on_assignments USING btree (add_on_purchase_id, user_id);
+
 CREATE UNIQUE INDEX uniq_pkgs_deb_grp_architectures_on_distribution_id_and_name ON packages_debian_group_architectures USING btree (distribution_id, name);
 
 CREATE UNIQUE INDEX uniq_pkgs_deb_grp_components_on_distribution_id_and_name ON packages_debian_group_components USING btree (distribution_id, name);
@@ -35227,6 +35253,9 @@ ALTER TABLE ONLY notification_settings
 ALTER TABLE ONLY lists
     ADD CONSTRAINT fk_0d3f677137 FOREIGN KEY (board_id) REFERENCES boards(id) ON DELETE CASCADE;
 
+ALTER TABLE ONLY subscription_user_add_on_assignments
+    ADD CONSTRAINT fk_0d89020c49 FOREIGN KEY (add_on_purchase_id) REFERENCES subscription_add_on_purchases(id) ON DELETE CASCADE;
+
 ALTER TABLE ONLY deployment_approvals
     ADD CONSTRAINT fk_0f58311058 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
 
@@ -35596,6 +35625,9 @@ ALTER TABLE ONLY integrations
 ALTER TABLE ONLY user_interacted_projects
     ADD CONSTRAINT fk_722ceba4f7 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
 
+ALTER TABLE ONLY subscription_user_add_on_assignments
+    ADD CONSTRAINT fk_724c2df9a8 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
+
 ALTER TABLE ONLY vulnerabilities
     ADD CONSTRAINT fk_725465b774 FOREIGN KEY (dismissed_by_id) REFERENCES users(id) ON DELETE SET NULL;
 
diff --git a/ee/app/models/ee/user.rb b/ee/app/models/ee/user.rb
index eb12df91d3bbc..3be56ce669ebd 100644
--- a/ee/app/models/ee/user.rb
+++ b/ee/app/models/ee/user.rb
@@ -103,6 +103,8 @@ module User
 
       has_many :dependency_list_exports, class_name: 'Dependencies::DependencyListExport', inverse_of: :author
 
+      has_many :assigned_add_ons, class_name: 'GitlabSubscriptions::UserAddOnAssignment', inverse_of: :user
+
       scope :not_managed, ->(group: nil) {
         scope = where(managing_group_id: nil)
         scope = scope.or(where.not(managing_group_id: group.id)) if group
diff --git a/ee/app/models/gitlab_subscriptions.rb b/ee/app/models/gitlab_subscriptions.rb
new file mode 100644
index 0000000000000..2ee838aeceb65
--- /dev/null
+++ b/ee/app/models/gitlab_subscriptions.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module GitlabSubscriptions
+  def self.table_name_prefix
+    'subscription_'
+  end
+end
diff --git a/ee/app/models/gitlab_subscriptions/add_on.rb b/ee/app/models/gitlab_subscriptions/add_on.rb
index ba8d85c5af3d4..9416eac2c61ab 100644
--- a/ee/app/models/gitlab_subscriptions/add_on.rb
+++ b/ee/app/models/gitlab_subscriptions/add_on.rb
@@ -2,8 +2,6 @@
 
 module GitlabSubscriptions
   class AddOn < ApplicationRecord
-    self.table_name = 'subscription_add_ons'
-
     has_many :add_on_purchases, foreign_key: :subscription_add_on_id, inverse_of: :add_on
 
     validates :name,
diff --git a/ee/app/models/gitlab_subscriptions/add_on_purchase.rb b/ee/app/models/gitlab_subscriptions/add_on_purchase.rb
index 246052cdce7b5..9496b935845ec 100644
--- a/ee/app/models/gitlab_subscriptions/add_on_purchase.rb
+++ b/ee/app/models/gitlab_subscriptions/add_on_purchase.rb
@@ -2,11 +2,11 @@
 
 module GitlabSubscriptions
   class AddOnPurchase < ApplicationRecord
-    self.table_name = 'subscription_add_on_purchases'
-
     belongs_to :add_on, foreign_key: :subscription_add_on_id, inverse_of: :add_on_purchases
     belongs_to :namespace
 
+    has_many :assigned_users, class_name: 'GitlabSubscriptions::UserAddOnAssignment', inverse_of: :add_on_purchase
+
     validates :add_on, :namespace, :expires_on, presence: true
     validates :subscription_add_on_id, uniqueness: { scope: :namespace_id }
     validates :quantity,
diff --git a/ee/app/models/gitlab_subscriptions/upcoming_reconciliation.rb b/ee/app/models/gitlab_subscriptions/upcoming_reconciliation.rb
index 599b22924835e..2f6983660a6bf 100644
--- a/ee/app/models/gitlab_subscriptions/upcoming_reconciliation.rb
+++ b/ee/app/models/gitlab_subscriptions/upcoming_reconciliation.rb
@@ -4,6 +4,8 @@ module GitlabSubscriptions
   class UpcomingReconciliation < ApplicationRecord
     include BulkInsertSafe
 
+    self.table_name = 'upcoming_reconciliations'
+
     belongs_to :namespace, inverse_of: :upcoming_reconciliation, optional: true
 
     # Validate presence of namespace_id if this is running on a GitLab instance
diff --git a/ee/app/models/gitlab_subscriptions/user_add_on_assignment.rb b/ee/app/models/gitlab_subscriptions/user_add_on_assignment.rb
new file mode 100644
index 0000000000000..c3b11329c1594
--- /dev/null
+++ b/ee/app/models/gitlab_subscriptions/user_add_on_assignment.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module GitlabSubscriptions
+  class UserAddOnAssignment < ApplicationRecord
+    belongs_to :user, inverse_of: :assigned_add_ons
+    belongs_to :add_on_purchase, class_name: 'GitlabSubscriptions::AddOnPurchase', inverse_of: :assigned_users
+
+    validates :user, :add_on_purchase, presence: true
+    validates :add_on_purchase_id, uniqueness: { scope: :user_id }
+  end
+end
diff --git a/ee/spec/factories/gitlab_subscriptions/user_add_on_assignment.rb b/ee/spec/factories/gitlab_subscriptions/user_add_on_assignment.rb
new file mode 100644
index 0000000000000..1b4eb4d640352
--- /dev/null
+++ b/ee/spec/factories/gitlab_subscriptions/user_add_on_assignment.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+  factory :gitlab_subscription_user_add_on_assignment, class: 'GitlabSubscriptions::UserAddOnAssignment' do
+    user
+    add_on_purchase { association(:gitlab_subscription_add_on_purchase) }
+  end
+end
diff --git a/ee/spec/models/ee/user_spec.rb b/ee/spec/models/ee/user_spec.rb
index 4ecce8d03e947..b9e513870d111 100644
--- a/ee/spec/models/ee/user_spec.rb
+++ b/ee/spec/models/ee/user_spec.rb
@@ -43,6 +43,7 @@
     it { is_expected.to have_many(:namespace_bans).class_name('Namespaces::NamespaceBan') }
     it { is_expected.to have_many(:dependency_list_exports).class_name('Dependencies::DependencyListExport') }
     it { is_expected.to have_many(:elevated_members).class_name('Member') }
+    it { is_expected.to have_many(:assigned_add_ons).class_name('GitlabSubscriptions::UserAddOnAssignment').inverse_of(:user) }
   end
 
   describe 'nested attributes' do
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 4f92584cc3a0a..f7969eb17e786 100644
--- a/ee/spec/models/gitlab_subscriptions/add_on_purchase_spec.rb
+++ b/ee/spec/models/gitlab_subscriptions/add_on_purchase_spec.rb
@@ -8,6 +8,11 @@
   describe 'associations' do
     it { is_expected.to belong_to(:add_on).with_foreign_key(:subscription_add_on_id).inverse_of(:add_on_purchases) }
     it { is_expected.to belong_to(:namespace) }
+
+    it do
+      is_expected.to have_many(:assigned_users)
+        .class_name('GitlabSubscriptions::UserAddOnAssignment').inverse_of(:add_on_purchase)
+    end
   end
 
   describe 'validations' do
diff --git a/ee/spec/models/gitlab_subscriptions/user_add_on_assignment_spec.rb b/ee/spec/models/gitlab_subscriptions/user_add_on_assignment_spec.rb
new file mode 100644
index 0000000000000..f9760cf36cf6e
--- /dev/null
+++ b/ee/spec/models/gitlab_subscriptions/user_add_on_assignment_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSubscriptions::UserAddOnAssignment, feature_category: :seat_cost_management do
+  describe 'associations' do
+    it { is_expected.to belong_to(:user).inverse_of(:assigned_add_ons) }
+    it { is_expected.to belong_to(:add_on_purchase).inverse_of(:assigned_users) }
+  end
+
+  describe 'validations' do
+    it { is_expected.to validate_presence_of(:user) }
+    it { is_expected.to validate_presence_of(:add_on_purchase) }
+
+    context 'for uniqueness' do
+      subject { build(:gitlab_subscription_user_add_on_assignment) }
+
+      it { is_expected.to validate_uniqueness_of(:add_on_purchase_id).scoped_to(:user_id) }
+    end
+  end
+end
-- 
GitLab