From 6bdb1dcb7c7a2851037bfbcc05833eff1779c59d Mon Sep 17 00:00:00 2001
From: Corinna Gogolok <cgogolok@gitlab.com>
Date: Wed, 7 Jun 2023 13:57:38 +0000
Subject: [PATCH] Add database structure for add-ons

This change adds the models and their database tables for add-ons.

Changelog: added
---
 db/docs/subscription_add_on_purchases.yml     | 10 +++
 db/docs/subscription_add_ons.yml              | 10 +++
 ...30531134916_create_subscription_add_ons.rb | 12 ++++
 ...01_create_subscription_add_on_purchases.rb | 18 ++++++
 ..._on_id_on_subscription_add_on_purchases.rb | 18 ++++++
 ...ace_id_on_subscription_add_on_purchases.rb | 15 +++++
 db/schema_migrations/20230531134916           |  1 +
 db/schema_migrations/20230531135001           |  1 +
 db/schema_migrations/20230531142032           |  1 +
 db/schema_migrations/20230531142053           |  1 +
 db/structure.sql                              | 61 +++++++++++++++++++
 ee/app/models/ee/namespace.rb                 |  1 +
 ee/app/models/gitlab_subscriptions/add_on.rb  | 20 ++++++
 .../gitlab_subscriptions/add_on_purchase.rb   | 18 ++++++
 ee/spec/factories/add_on.rb                   |  8 +++
 ee/spec/factories/add_on_purchase.rb          | 11 ++++
 ee/spec/models/ee/namespace_spec.rb           |  1 +
 .../add_on_purchase_spec.rb                   | 24 ++++++++
 .../gitlab_subscriptions/add_on_spec.rb       | 19 ++++++
 19 files changed, 250 insertions(+)
 create mode 100644 db/docs/subscription_add_on_purchases.yml
 create mode 100644 db/docs/subscription_add_ons.yml
 create mode 100644 db/migrate/20230531134916_create_subscription_add_ons.rb
 create mode 100644 db/migrate/20230531135001_create_subscription_add_on_purchases.rb
 create mode 100644 db/migrate/20230531142032_add_foreign_key_subscription_add_on_id_on_subscription_add_on_purchases.rb
 create mode 100644 db/migrate/20230531142053_add_foreign_key_namespace_id_on_subscription_add_on_purchases.rb
 create mode 100644 db/schema_migrations/20230531134916
 create mode 100644 db/schema_migrations/20230531135001
 create mode 100644 db/schema_migrations/20230531142032
 create mode 100644 db/schema_migrations/20230531142053
 create mode 100644 ee/app/models/gitlab_subscriptions/add_on.rb
 create mode 100644 ee/app/models/gitlab_subscriptions/add_on_purchase.rb
 create mode 100644 ee/spec/factories/add_on.rb
 create mode 100644 ee/spec/factories/add_on_purchase.rb
 create mode 100644 ee/spec/models/gitlab_subscriptions/add_on_purchase_spec.rb
 create mode 100644 ee/spec/models/gitlab_subscriptions/add_on_spec.rb

diff --git a/db/docs/subscription_add_on_purchases.yml b/db/docs/subscription_add_on_purchases.yml
new file mode 100644
index 0000000000000..21915cff54529
--- /dev/null
+++ b/db/docs/subscription_add_on_purchases.yml
@@ -0,0 +1,10 @@
+---
+table_name: subscription_add_on_purchases
+description: Stores add-on purchase information
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/122662
+milestone: '16.1'
+feature_categories:
+- subscription_management
+classes:
+- GitlabSubscriptions::AddOnPurchase
+gitlab_schema: gitlab_main
diff --git a/db/docs/subscription_add_ons.yml b/db/docs/subscription_add_ons.yml
new file mode 100644
index 0000000000000..93730f80a999f
--- /dev/null
+++ b/db/docs/subscription_add_ons.yml
@@ -0,0 +1,10 @@
+---
+table_name: subscription_add_ons
+description: Stores available add-ons for which purchases are stored in `subscription_add_on_purchases`.
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/122662
+milestone: '16.1'
+feature_categories:
+- subscription_management
+classes:
+- GitlabSubscriptions::AddOn
+gitlab_schema: gitlab_main
diff --git a/db/migrate/20230531134916_create_subscription_add_ons.rb b/db/migrate/20230531134916_create_subscription_add_ons.rb
new file mode 100644
index 0000000000000..5faee04953403
--- /dev/null
+++ b/db/migrate/20230531134916_create_subscription_add_ons.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+class CreateSubscriptionAddOns < Gitlab::Database::Migration[2.1]
+  def change
+    create_table :subscription_add_ons, if_not_exists: true do |t|
+      t.timestamps_with_timezone null: false
+
+      t.integer :name, limit: 2, null: false, index: { unique: true }
+      t.text    :description, null: false, limit: 512
+    end
+  end
+end
diff --git a/db/migrate/20230531135001_create_subscription_add_on_purchases.rb b/db/migrate/20230531135001_create_subscription_add_on_purchases.rb
new file mode 100644
index 0000000000000..6fdf1fdd49545
--- /dev/null
+++ b/db/migrate/20230531135001_create_subscription_add_on_purchases.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class CreateSubscriptionAddOnPurchases < Gitlab::Database::Migration[2.1]
+  def change
+    create_table :subscription_add_on_purchases, if_not_exists: true do |t|
+      t.timestamps_with_timezone null: false
+
+      t.bigint  :subscription_add_on_id, null: false
+      t.bigint  :namespace_id, null: false
+      t.integer :quantity, null: false
+      t.date    :expires_on, null: false
+      t.text    :purchase_xid, null: false, limit: 255
+
+      t.index :subscription_add_on_id
+      t.index :namespace_id
+    end
+  end
+end
diff --git a/db/migrate/20230531142032_add_foreign_key_subscription_add_on_id_on_subscription_add_on_purchases.rb b/db/migrate/20230531142032_add_foreign_key_subscription_add_on_id_on_subscription_add_on_purchases.rb
new file mode 100644
index 0000000000000..234cd2fa3af3e
--- /dev/null
+++ b/db/migrate/20230531142032_add_foreign_key_subscription_add_on_id_on_subscription_add_on_purchases.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class AddForeignKeySubscriptionAddOnIdOnSubscriptionAddOnPurchases < Gitlab::Database::Migration[2.1]
+  disable_ddl_transaction!
+
+  def up
+    add_concurrent_foreign_key :subscription_add_on_purchases,
+      :subscription_add_ons,
+      column: :subscription_add_on_id,
+      on_delete: :cascade
+  end
+
+  def down
+    with_lock_retries do
+      remove_foreign_key :subscription_add_on_purchases, column: :subscription_add_on_id
+    end
+  end
+end
diff --git a/db/migrate/20230531142053_add_foreign_key_namespace_id_on_subscription_add_on_purchases.rb b/db/migrate/20230531142053_add_foreign_key_namespace_id_on_subscription_add_on_purchases.rb
new file mode 100644
index 0000000000000..7f7083a3a9c38
--- /dev/null
+++ b/db/migrate/20230531142053_add_foreign_key_namespace_id_on_subscription_add_on_purchases.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class AddForeignKeyNamespaceIdOnSubscriptionAddOnPurchases < Gitlab::Database::Migration[2.1]
+  disable_ddl_transaction!
+
+  def up
+    add_concurrent_foreign_key :subscription_add_on_purchases, :namespaces, column: :namespace_id, on_delete: :cascade
+  end
+
+  def down
+    with_lock_retries do
+      remove_foreign_key :subscription_add_on_purchases, column: :namespace_id
+    end
+  end
+end
diff --git a/db/schema_migrations/20230531134916 b/db/schema_migrations/20230531134916
new file mode 100644
index 0000000000000..5cf00727101d3
--- /dev/null
+++ b/db/schema_migrations/20230531134916
@@ -0,0 +1 @@
+fc2e3d8e6aca7b00569340b0468488a4b0545b39e67857a5b40824f6d0a62a97
\ No newline at end of file
diff --git a/db/schema_migrations/20230531135001 b/db/schema_migrations/20230531135001
new file mode 100644
index 0000000000000..32850b297da14
--- /dev/null
+++ b/db/schema_migrations/20230531135001
@@ -0,0 +1 @@
+1a672c9412b8ceeec35fd375bf86dde325781c9cb94340995d2cab4bb804e4bf
\ No newline at end of file
diff --git a/db/schema_migrations/20230531142032 b/db/schema_migrations/20230531142032
new file mode 100644
index 0000000000000..bae2817773a59
--- /dev/null
+++ b/db/schema_migrations/20230531142032
@@ -0,0 +1 @@
+3e77f991a4daa9756b541255e3b8da9d8accb52a5a4b625613771982e3dff3b5
\ No newline at end of file
diff --git a/db/schema_migrations/20230531142053 b/db/schema_migrations/20230531142053
new file mode 100644
index 0000000000000..55da4601012df
--- /dev/null
+++ b/db/schema_migrations/20230531142053
@@ -0,0 +1 @@
+0a4b3b8848f486e34e1f0426bae4e15f67e851447fc3fe397cf2039e03b185b5
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index cc22a017044e4..23e2df53c913f 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -23031,6 +23031,45 @@ CREATE SEQUENCE status_page_settings_project_id_seq
 
 ALTER SEQUENCE status_page_settings_project_id_seq OWNED BY status_page_settings.project_id;
 
+CREATE TABLE subscription_add_on_purchases (
+    id bigint NOT NULL,
+    created_at timestamp with time zone NOT NULL,
+    updated_at timestamp with time zone NOT NULL,
+    subscription_add_on_id bigint NOT NULL,
+    namespace_id bigint NOT NULL,
+    quantity integer NOT NULL,
+    expires_on date NOT NULL,
+    purchase_xid text NOT NULL,
+    CONSTRAINT check_3313c4d200 CHECK ((char_length(purchase_xid) <= 255))
+);
+
+CREATE SEQUENCE subscription_add_on_purchases_id_seq
+    START WITH 1
+    INCREMENT BY 1
+    NO MINVALUE
+    NO MAXVALUE
+    CACHE 1;
+
+ALTER SEQUENCE subscription_add_on_purchases_id_seq OWNED BY subscription_add_on_purchases.id;
+
+CREATE TABLE subscription_add_ons (
+    id bigint NOT NULL,
+    created_at timestamp with time zone NOT NULL,
+    updated_at timestamp with time zone NOT NULL,
+    name smallint NOT NULL,
+    description text NOT NULL,
+    CONSTRAINT check_4c39d15ada CHECK ((char_length(description) <= 512))
+);
+
+CREATE SEQUENCE subscription_add_ons_id_seq
+    START WITH 1
+    INCREMENT BY 1
+    NO MINVALUE
+    NO MAXVALUE
+    CACHE 1;
+
+ALTER SEQUENCE subscription_add_ons_id_seq OWNED BY subscription_add_ons.id;
+
 CREATE TABLE subscriptions (
     id integer NOT NULL,
     user_id integer,
@@ -25847,6 +25886,10 @@ ALTER TABLE ONLY status_page_published_incidents ALTER COLUMN id SET DEFAULT nex
 
 ALTER TABLE ONLY status_page_settings ALTER COLUMN project_id SET DEFAULT nextval('status_page_settings_project_id_seq'::regclass);
 
+ALTER TABLE ONLY subscription_add_on_purchases ALTER COLUMN id SET DEFAULT nextval('subscription_add_on_purchases_id_seq'::regclass);
+
+ALTER TABLE ONLY subscription_add_ons ALTER COLUMN id SET DEFAULT nextval('subscription_add_ons_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);
@@ -28317,6 +28360,12 @@ ALTER TABLE ONLY status_page_published_incidents
 ALTER TABLE ONLY status_page_settings
     ADD CONSTRAINT status_page_settings_pkey PRIMARY KEY (project_id);
 
+ALTER TABLE ONLY subscription_add_on_purchases
+    ADD CONSTRAINT subscription_add_on_purchases_pkey PRIMARY KEY (id);
+
+ALTER TABLE ONLY subscription_add_ons
+    ADD CONSTRAINT subscription_add_ons_pkey PRIMARY KEY (id);
+
 ALTER TABLE ONLY subscriptions
     ADD CONSTRAINT subscriptions_pkey PRIMARY KEY (id);
 
@@ -32871,6 +32920,12 @@ CREATE UNIQUE INDEX index_status_page_published_incidents_on_issue_id ON status_
 
 CREATE INDEX index_status_page_settings_on_project_id ON status_page_settings USING btree (project_id);
 
+CREATE INDEX index_subscription_add_on_purchases_on_namespace_id ON subscription_add_on_purchases USING btree (namespace_id);
+
+CREATE INDEX index_subscription_add_on_purchases_on_subscription_add_on_id ON subscription_add_on_purchases USING btree (subscription_add_on_id);
+
+CREATE UNIQUE INDEX index_subscription_add_ons_on_name ON subscription_add_ons USING btree (name);
+
 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);
@@ -35420,6 +35475,9 @@ ALTER TABLE ONLY abuse_reports
 ALTER TABLE ONLY protected_environment_approval_rules
     ADD CONSTRAINT fk_405568b491 FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
 
+ALTER TABLE ONLY subscription_add_on_purchases
+    ADD CONSTRAINT fk_410004d68b FOREIGN KEY (subscription_add_on_id) REFERENCES subscription_add_ons(id) ON DELETE CASCADE;
+
 ALTER TABLE ONLY ci_pipeline_schedule_variables
     ADD CONSTRAINT fk_41c35fda51 FOREIGN KEY (pipeline_schedule_id) REFERENCES ci_pipeline_schedules(id) ON DELETE CASCADE;
 
@@ -35768,6 +35826,9 @@ ALTER TABLE ONLY issues
 ALTER TABLE ONLY ml_candidates
     ADD CONSTRAINT fk_a1d5f1bc45 FOREIGN KEY (package_id) REFERENCES packages_packages(id) ON DELETE SET NULL;
 
+ALTER TABLE ONLY subscription_add_on_purchases
+    ADD CONSTRAINT fk_a1db288990 FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE;
+
 ALTER TABLE p_ci_builds
     ADD CONSTRAINT fk_a2141b1522 FOREIGN KEY (auto_canceled_by_id) REFERENCES ci_pipelines(id) ON DELETE SET NULL;
 
diff --git a/ee/app/models/ee/namespace.rb b/ee/app/models/ee/namespace.rb
index 758f1f09e7ba3..a5f193fb6f40d 100644
--- a/ee/app/models/ee/namespace.rb
+++ b/ee/app/models/ee/namespace.rb
@@ -38,6 +38,7 @@ module Namespace
       has_many :ci_minutes_additional_packs, class_name: "Ci::Minutes::AdditionalPack"
       has_many :compliance_management_frameworks, class_name: "ComplianceManagement::Framework"
       has_many :member_roles
+      has_many :subscription_add_on_purchases, class_name: 'GitlabSubscriptions::AddOnPurchase'
 
       accepts_nested_attributes_for :gitlab_subscription, update_only: true
       accepts_nested_attributes_for :namespace_limit
diff --git a/ee/app/models/gitlab_subscriptions/add_on.rb b/ee/app/models/gitlab_subscriptions/add_on.rb
new file mode 100644
index 0000000000000..fa1a714382312
--- /dev/null
+++ b/ee/app/models/gitlab_subscriptions/add_on.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+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,
+      presence: true,
+      uniqueness: true
+    validates :description,
+      presence: true,
+      length: { maximum: 512 }
+
+    enum name: {
+      code_suggestions: 1
+    }
+  end
+end
diff --git a/ee/app/models/gitlab_subscriptions/add_on_purchase.rb b/ee/app/models/gitlab_subscriptions/add_on_purchase.rb
new file mode 100644
index 0000000000000..567b0be4a65b6
--- /dev/null
+++ b/ee/app/models/gitlab_subscriptions/add_on_purchase.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+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
+
+    validates :add_on, :namespace, :expires_on, presence: true
+    validates :quantity,
+      presence: true,
+      numericality: { only_integer: true, greater_than_or_equal_to: 1 }
+    validates :purchase_xid,
+      presence: true,
+      length: { maximum: 255 }
+  end
+end
diff --git a/ee/spec/factories/add_on.rb b/ee/spec/factories/add_on.rb
new file mode 100644
index 0000000000000..b612e2b03e623
--- /dev/null
+++ b/ee/spec/factories/add_on.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+  factory :add_on, class: 'GitlabSubscriptions::AddOn' do
+    name { GitlabSubscriptions::AddOn.names[:code_suggestions] }
+    description { 'AddOn for code suggestion features' }
+  end
+end
diff --git a/ee/spec/factories/add_on_purchase.rb b/ee/spec/factories/add_on_purchase.rb
new file mode 100644
index 0000000000000..e0846db85c92e
--- /dev/null
+++ b/ee/spec/factories/add_on_purchase.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+  factory :add_on_purchase, class: 'GitlabSubscriptions::AddOnPurchase' do
+    add_on
+    namespace { association(:group) }
+    quantity { 1 }
+    expires_on { 1.year.from_now.to_date }
+    purchase_xid { 'S-A00000001' }
+  end
+end
diff --git a/ee/spec/models/ee/namespace_spec.rb b/ee/spec/models/ee/namespace_spec.rb
index 9cdbf4495d950..faf4c51113d9f 100644
--- a/ee/spec/models/ee/namespace_spec.rb
+++ b/ee/spec/models/ee/namespace_spec.rb
@@ -18,6 +18,7 @@
   it { is_expected.to have_one(:storage_limit_exclusion) }
   it { is_expected.to have_many(:ci_minutes_additional_packs) }
   it { is_expected.to have_many(:member_roles) }
+  it { is_expected.to have_many(:subscription_add_on_purchases).class_name('GitlabSubscriptions::AddOnPurchase') }
 
   it { is_expected.to delegate_method(:trial?).to(:gitlab_subscription) }
   it { is_expected.to delegate_method(:trial_ends_on).to(:gitlab_subscription) }
diff --git a/ee/spec/models/gitlab_subscriptions/add_on_purchase_spec.rb b/ee/spec/models/gitlab_subscriptions/add_on_purchase_spec.rb
new file mode 100644
index 0000000000000..77c0a762d9c37
--- /dev/null
+++ b/ee/spec/models/gitlab_subscriptions/add_on_purchase_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSubscriptions::AddOnPurchase, feature_category: :subscription_management do
+  subject { build(:add_on_purchase) }
+
+  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) }
+  end
+
+  describe 'validations' do
+    it { is_expected.to validate_presence_of(:add_on) }
+    it { is_expected.to validate_presence_of(:namespace) }
+    it { is_expected.to validate_presence_of(:expires_on) }
+
+    it { is_expected.to validate_presence_of(:quantity) }
+    it { is_expected.to validate_numericality_of(:quantity).only_integer.is_greater_than_or_equal_to(1) }
+
+    it { is_expected.to validate_presence_of(:purchase_xid) }
+    it { is_expected.to validate_length_of(:purchase_xid).is_at_most(255) }
+  end
+end
diff --git a/ee/spec/models/gitlab_subscriptions/add_on_spec.rb b/ee/spec/models/gitlab_subscriptions/add_on_spec.rb
new file mode 100644
index 0000000000000..f5e1d78751b09
--- /dev/null
+++ b/ee/spec/models/gitlab_subscriptions/add_on_spec.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSubscriptions::AddOn, feature_category: :subscription_management do
+  subject { build(:add_on) }
+
+  describe 'associations' do
+    it { is_expected.to have_many(:add_on_purchases).with_foreign_key(:subscription_add_on_id).inverse_of(:add_on) }
+  end
+
+  describe 'validations' do
+    it { is_expected.to validate_presence_of(:name) }
+    it { is_expected.to validate_uniqueness_of(:name).ignoring_case_sensitivity }
+
+    it { is_expected.to validate_presence_of(:description) }
+    it { is_expected.to validate_length_of(:description).is_at_most(512) }
+  end
+end
-- 
GitLab