diff --git a/app/graphql/types/coupons/create_input.rb b/app/graphql/types/coupons/create_input.rb index 2aebb09a038..e9a096d5db7 100644 --- a/app/graphql/types/coupons/create_input.rb +++ b/app/graphql/types/coupons/create_input.rb @@ -7,7 +7,7 @@ class CreateInput < Types::BaseInputObject argument :amount_cents, GraphQL::Types::BigInt, required: false argument :amount_currency, Types::CurrencyEnum, required: false - argument :code, String, required: false + argument :code, String, required: true argument :coupon_type, Types::Coupons::CouponTypeEnum, required: true argument :description, String, required: false argument :frequency, Types::Coupons::FrequencyEnum, required: true diff --git a/app/graphql/types/coupons/object.rb b/app/graphql/types/coupons/object.rb index 43e5fe5c9ac..d3339ea3195 100644 --- a/app/graphql/types/coupons/object.rb +++ b/app/graphql/types/coupons/object.rb @@ -10,7 +10,7 @@ class Object < Types::BaseObject field :amount_cents, GraphQL::Types::BigInt, null: true field :amount_currency, Types::CurrencyEnum, null: true - field :code, String, null: true + field :code, String, null: false field :coupon_type, Types::Coupons::CouponTypeEnum, null: false field :description, String, null: true field :frequency, Types::Coupons::FrequencyEnum, null: false diff --git a/app/graphql/types/coupons/update_input.rb b/app/graphql/types/coupons/update_input.rb index 13a156c106c..305987c4f25 100644 --- a/app/graphql/types/coupons/update_input.rb +++ b/app/graphql/types/coupons/update_input.rb @@ -5,6 +5,7 @@ module Coupons class UpdateInput < Types::Coupons::CreateInput graphql_name "UpdateCouponInput" + argument :code, String, required: false argument :id, String, required: true end end diff --git a/app/models/coupon.rb b/app/models/coupon.rb index 30879336ea2..dd3b5b7027f 100644 --- a/app/models/coupon.rb +++ b/app/models/coupon.rb @@ -49,7 +49,7 @@ class Coupon < ApplicationRecord monetize :amount_cents, disable_validation: true, allow_nil: true validates :name, presence: true - validates :code, uniqueness: {conditions: -> { where(deleted_at: nil) }, scope: :organization_id} + validates :code, presence: true, uniqueness: {conditions: -> { where(deleted_at: nil) }, scope: :organization_id} validates :amount_cents, presence: true, if: :fixed_amount? validates :amount_cents, numericality: {greater_than: 0}, allow_nil: true @@ -101,7 +101,7 @@ def parent_and_overriden_plans # id :uuid not null, primary key # amount_cents :bigint # amount_currency :string -# code :string +# code :string not null # coupon_type :integer default("fixed_amount"), not null # deleted_at :datetime # description :text diff --git a/db/migrate/20260512155310_backfill_coupon_codes.rb b/db/migrate/20260512155310_backfill_coupon_codes.rb new file mode 100644 index 00000000000..1593d5abf19 --- /dev/null +++ b/db/migrate/20260512155310_backfill_coupon_codes.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class BackfillCouponCodes < ActiveRecord::Migration[8.0] + disable_ddl_transaction! + + def up + Coupon.unscoped.where(code: nil).find_in_batches(batch_size: 1000) do |batch| + Coupon.unscoped.where(id: batch.pluck(:id)) + .update_all("code = 'coupon-' || id::text") # rubocop:disable Rails/SkipsModelValidations + end + end + + def down + # irreversible + end +end diff --git a/db/migrate/20260513105209_add_code_not_null_check_constraint_to_coupons.rb b/db/migrate/20260513105209_add_code_not_null_check_constraint_to_coupons.rb new file mode 100644 index 00000000000..b7f4516ed89 --- /dev/null +++ b/db/migrate/20260513105209_add_code_not_null_check_constraint_to_coupons.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +class AddCodeNotNullCheckConstraintToCoupons < ActiveRecord::Migration[8.0] + def change + add_check_constraint :coupons, "code IS NOT NULL", + name: "coupons_code_not_null", validate: false, if_not_exists: true + end +end diff --git a/db/migrate/20260513105210_validate_coupons_code_not_null.rb b/db/migrate/20260513105210_validate_coupons_code_not_null.rb new file mode 100644 index 00000000000..dc4fc733d57 --- /dev/null +++ b/db/migrate/20260513105210_validate_coupons_code_not_null.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class ValidateCouponsCodeNotNull < ActiveRecord::Migration[8.0] + disable_ddl_transaction! + + def up + validate_check_constraint :coupons, name: "coupons_code_not_null" + change_column_null :coupons, :code, false + remove_check_constraint :coupons, name: "coupons_code_not_null" + end + + def down + add_check_constraint :coupons, "code IS NOT NULL", name: "coupons_code_not_null", validate: false + change_column_null :coupons, :code, true + end +end diff --git a/db/structure.sql b/db/structure.sql index 07b1535c37b..6e1d5b99933 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -2109,7 +2109,7 @@ CREATE TABLE public.coupons ( id uuid DEFAULT gen_random_uuid() NOT NULL, organization_id uuid NOT NULL, name character varying NOT NULL, - code character varying, + code character varying NOT NULL, status integer DEFAULT 0 NOT NULL, terminated_at timestamp(6) without time zone, amount_cents bigint, @@ -12216,6 +12216,9 @@ SET search_path TO "$user", public; INSERT INTO "schema_migrations" (version) VALUES ('20260517101105'), +('20260513105210'), +('20260513105209'), +('20260512155310'), ('20260512142543'), ('20260504134804'), ('20260430102814'), @@ -13214,4 +13217,3 @@ INSERT INTO "schema_migrations" (version) VALUES ('20220530091046'), ('20220526101535'), ('20220525122759'); - diff --git a/schema.graphql b/schema.graphql index 7af030852f4..c74139eb317 100644 --- a/schema.graphql +++ b/schema.graphql @@ -2547,7 +2547,7 @@ type Coupon { amountCurrency: CurrencyEnum appliedCouponsCount: Int! billableMetrics: [BillableMetric!] - code: String + code: String! couponType: CouponTypeEnum! createdAt: ISO8601DateTime! @@ -2790,7 +2790,7 @@ input CreateCouponInput { A unique identifier for the client performing the mutation. """ clientMutationId: String - code: String + code: String! couponType: CouponTypeEnum! description: String expiration: CouponExpiration! diff --git a/schema.json b/schema.json index 52806bbaa51..8c109b69d68 100644 --- a/schema.json +++ b/schema.json @@ -9761,9 +9761,13 @@ "description": null, "args": [], "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } }, "isDeprecated": false, "deprecationReason": null @@ -11432,9 +11436,13 @@ "name": "code", "description": null, "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } }, "defaultValue": null, "isDeprecated": false, diff --git a/spec/graphql/types/coupons/object_spec.rb b/spec/graphql/types/coupons/object_spec.rb index 1cd982547a1..f476e5ef721 100644 --- a/spec/graphql/types/coupons/object_spec.rb +++ b/spec/graphql/types/coupons/object_spec.rb @@ -10,7 +10,7 @@ expect(subject).to have_field(:organization).of_type("Organization") expect(subject).to have_field(:amount_cents).of_type("BigInt") expect(subject).to have_field(:amount_currency).of_type("CurrencyEnum") - expect(subject).to have_field(:code).of_type("String") + expect(subject).to have_field(:code).of_type("String!") expect(subject).to have_field(:coupon_type).of_type("CouponTypeEnum!") expect(subject).to have_field(:description).of_type("String") expect(subject).to have_field(:frequency).of_type("CouponFrequency!") diff --git a/spec/models/coupon_spec.rb b/spec/models/coupon_spec.rb index 6fb56a2bff8..2ef94e7903e 100644 --- a/spec/models/coupon_spec.rb +++ b/spec/models/coupon_spec.rb @@ -21,6 +21,7 @@ describe "validations" do it { is_expected.to validate_presence_of(:name) } + it { is_expected.to validate_presence_of(:code) } it { is_expected.to validate_exclusion_of(:reusable).in_array([nil]) } describe "of amount cents" do