From 06d2847c5edc5a396a69355e758f76062ca188a3 Mon Sep 17 00:00:00 2001 From: Ivan Novosad Date: Fri, 9 Aug 2024 16:17:07 +0200 Subject: [PATCH] feat(tresholds): Add GraphQL for usage tresholds --- app/graphql/mutations/plans/create.rb | 1 + app/graphql/mutations/plans/update.rb | 1 + app/graphql/types/plans/object.rb | 1 + .../subscriptions/plan_overrides_input.rb | 1 + .../usage_threshold_overrides_input.rb | 13 + app/graphql/types/usage_thresholds/input.rb | 15 + app/graphql/types/usage_thresholds/object.rb | 18 + schema.graphql | 27 ++ schema.json | 321 ++++++++++++++++++ spec/graphql/mutations/plans/create_spec.rb | 41 +++ spec/graphql/mutations/plans/update_spec.rb | 41 +++ .../mutations/subscriptions/create_spec.rb | 16 + spec/graphql/types/plans/object_spec.rb | 1 + .../plan_overrides_input_spec.rb | 1 + .../usage_threshold_overrides_input_spec.rb | 12 + .../types/usage_thresholds/input_spec.rb | 12 + .../types/usage_thresholds/object_spec.rb | 14 + 17 files changed, 536 insertions(+) create mode 100644 app/graphql/types/subscriptions/usage_threshold_overrides_input.rb create mode 100644 app/graphql/types/usage_thresholds/input.rb create mode 100644 app/graphql/types/usage_thresholds/object.rb create mode 100644 spec/graphql/types/subscriptions/usage_threshold_overrides_input_spec.rb create mode 100644 spec/graphql/types/usage_thresholds/input_spec.rb create mode 100644 spec/graphql/types/usage_thresholds/object_spec.rb diff --git a/app/graphql/mutations/plans/create.rb b/app/graphql/mutations/plans/create.rb index 2ad6d3ad36ed..2cd486782ae1 100644 --- a/app/graphql/mutations/plans/create.rb +++ b/app/graphql/mutations/plans/create.rb @@ -25,6 +25,7 @@ class Create < BaseMutation argument :charges, [Types::Charges::Input] argument :minimum_commitment, Types::Commitments::Input, required: false + argument :usage_thresholds, [Types::UsageThresholds::Input], required: false type Types::Plans::Object diff --git a/app/graphql/mutations/plans/update.rb b/app/graphql/mutations/plans/update.rb index 319f9df473ca..f712e0854109 100644 --- a/app/graphql/mutations/plans/update.rb +++ b/app/graphql/mutations/plans/update.rb @@ -25,6 +25,7 @@ class Update < BaseMutation argument :charges, [Types::Charges::Input] argument :minimum_commitment, Types::Commitments::Input, required: false + argument :usage_thresholds, [Types::UsageThresholds::Input], required: false type Types::Plans::Object diff --git a/app/graphql/types/plans/object.rb b/app/graphql/types/plans/object.rb index e9474af0ed9e..0e644533f68f 100644 --- a/app/graphql/types/plans/object.rb +++ b/app/graphql/types/plans/object.rb @@ -20,6 +20,7 @@ class Object < Types::BaseObject field :parent, Types::Plans::Object, null: true field :pay_in_advance, Boolean, null: false field :trial_period, Float + field :usage_thresholds, [Types::UsageThresholds::Object] field :charges, [Types::Charges::Object] field :taxes, [Types::Taxes::Object] diff --git a/app/graphql/types/subscriptions/plan_overrides_input.rb b/app/graphql/types/subscriptions/plan_overrides_input.rb index 5847b6bc75e0..28ad3b5b2620 100644 --- a/app/graphql/types/subscriptions/plan_overrides_input.rb +++ b/app/graphql/types/subscriptions/plan_overrides_input.rb @@ -12,6 +12,7 @@ class PlanOverridesInput < Types::BaseInputObject argument :name, String, required: false argument :tax_codes, [String], required: false argument :trial_period, Float, required: false + argument :usage_thresholds, [Types::Subscriptions::UsageThresholdOverridesInput], required: false end end end diff --git a/app/graphql/types/subscriptions/usage_threshold_overrides_input.rb b/app/graphql/types/subscriptions/usage_threshold_overrides_input.rb new file mode 100644 index 000000000000..593f80c96e28 --- /dev/null +++ b/app/graphql/types/subscriptions/usage_threshold_overrides_input.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Types + module Subscriptions + class UsageThresholdOverridesInput < Types::BaseInputObject + argument :id, ID, required: true + + argument :amount_cents, GraphQL::Types::BigInt, required: false + argument :recurring, Boolean, required: false + argument :threshold_display_name, String, required: false + end + end +end diff --git a/app/graphql/types/usage_thresholds/input.rb b/app/graphql/types/usage_thresholds/input.rb new file mode 100644 index 000000000000..8b807ed7a691 --- /dev/null +++ b/app/graphql/types/usage_thresholds/input.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Types + module UsageThresholds + class Input < BaseInputObject + graphql_name 'UsageThresholdInput' + + argument :id, ID, required: false + + argument :amount_cents, GraphQL::Types::BigInt, required: false + argument :recurring, Boolean, required: false + argument :threshold_display_name, String, required: false + end + end +end diff --git a/app/graphql/types/usage_thresholds/object.rb b/app/graphql/types/usage_thresholds/object.rb new file mode 100644 index 000000000000..41e05ddb50b8 --- /dev/null +++ b/app/graphql/types/usage_thresholds/object.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Types + module UsageThresholds + class Object < Types::BaseObject + graphql_name 'UsageThreshold' + + field :id, ID, null: false + + field :amount_cents, GraphQL::Types::BigInt, null: false + field :recurring, Boolean, null: false + field :threshold_display_name, String, null: true + + field :created_at, GraphQL::Types::ISO8601DateTime, null: false + field :updated_at, GraphQL::Types::ISO8601DateTime, null: false + end + end +end diff --git a/schema.graphql b/schema.graphql index a949cc6622fc..45b615f3e1e3 100644 --- a/schema.graphql +++ b/schema.graphql @@ -2044,6 +2044,7 @@ input CreatePlanInput { payInAdvance: Boolean! taxCodes: [String!] trialPeriod: Float + usageThresholds: [UsageThresholdInput!] } input CreateRecurringTransactionRuleInput { @@ -5333,6 +5334,7 @@ type Plan { taxes: [Tax!] trialPeriod: Float updatedAt: ISO8601DateTime! + usageThresholds: [UsageThreshold!] } type PlanCollection { @@ -5357,6 +5359,7 @@ input PlanOverridesInput { name: String taxCodes: [String!] trialPeriod: Float + usageThresholds: [UsageThresholdOverridesInput!] } type Properties { @@ -7232,6 +7235,7 @@ input UpdatePlanInput { payInAdvance: Boolean! taxCodes: [String!] trialPeriod: Float + usageThresholds: [UsageThresholdInput!] } input UpdateRecurringTransactionRuleInput { @@ -7293,6 +7297,29 @@ input UpdateXeroIntegrationInput { syncPayments: Boolean } +type UsageThreshold { + amountCents: BigInt! + createdAt: ISO8601DateTime! + id: ID! + recurring: Boolean! + thresholdDisplayName: String + updatedAt: ISO8601DateTime! +} + +input UsageThresholdInput { + amountCents: BigInt + id: ID + recurring: Boolean + thresholdDisplayName: String +} + +input UsageThresholdOverridesInput { + amountCents: BigInt + id: ID! + recurring: Boolean + thresholdDisplayName: String +} + type User { createdAt: ISO8601DateTime! email: String diff --git a/schema.json b/schema.json index 1ecdd4dc20a9..74df0c3a7ea6 100644 --- a/schema.json +++ b/schema.json @@ -8612,6 +8612,26 @@ "defaultValue": null, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "usageThresholds", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "UsageThresholdInput", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null } ], "enumValues": null @@ -26764,6 +26784,28 @@ "deprecationReason": null, "args": [ + ] + }, + { + "name": "usageThresholds", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "UsageThreshold", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null, + "args": [ + ] } ], @@ -26993,6 +27035,26 @@ "defaultValue": null, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "usageThresholds", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "UsageThresholdOverridesInput", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null } ], "enumValues": null @@ -35988,6 +36050,26 @@ "defaultValue": null, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "usageThresholds", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "UsageThresholdInput", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null } ], "enumValues": null @@ -36392,6 +36474,245 @@ ], "enumValues": null }, + { + "kind": "OBJECT", + "name": "UsageThreshold", + "description": null, + "interfaces": [ + + ], + "possibleTypes": null, + "fields": [ + { + "name": "amountCents", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "BigInt", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null, + "args": [ + + ] + }, + { + "name": "createdAt", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ISO8601DateTime", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null, + "args": [ + + ] + }, + { + "name": "id", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null, + "args": [ + + ] + }, + { + "name": "recurring", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null, + "args": [ + + ] + }, + { + "name": "thresholdDisplayName", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null, + "args": [ + + ] + }, + { + "name": "updatedAt", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ISO8601DateTime", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null, + "args": [ + + ] + } + ], + "inputFields": null, + "enumValues": null + }, + { + "kind": "INPUT_OBJECT", + "name": "UsageThresholdInput", + "description": null, + "interfaces": null, + "possibleTypes": null, + "fields": null, + "inputFields": [ + { + "name": "id", + "description": null, + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "amountCents", + "description": null, + "type": { + "kind": "SCALAR", + "name": "BigInt", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "recurring", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "thresholdDisplayName", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "enumValues": null + }, + { + "kind": "INPUT_OBJECT", + "name": "UsageThresholdOverridesInput", + "description": null, + "interfaces": null, + "possibleTypes": null, + "fields": null, + "inputFields": [ + { + "name": "id", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "amountCents", + "description": null, + "type": { + "kind": "SCALAR", + "name": "BigInt", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "recurring", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "thresholdDisplayName", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "enumValues": null + }, { "kind": "OBJECT", "name": "User", diff --git a/spec/graphql/mutations/plans/create_spec.rb b/spec/graphql/mutations/plans/create_spec.rb index b745e5483d97..6875499575e8 100644 --- a/spec/graphql/mutations/plans/create_spec.rb +++ b/spec/graphql/mutations/plans/create_spec.rb @@ -56,6 +56,12 @@ properties { amount } } } + usageThresholds { + id, + amountCents, + thresholdDisplayName, + recurring + } } } GQL @@ -78,6 +84,8 @@ around { |test| lago_premium!(&test) } + before { organization.update!(premium_integrations: ['progressive_billing']) } + it_behaves_like 'requires current user' it_behaves_like 'requires current organization' it_behaves_like 'requires permission', 'plans:create' @@ -198,6 +206,21 @@ ] } } + ], + usageThresholds: [ + { + amountCents: 100, + thresholdDisplayName: 'Threshold 1' + }, + { + amountCents: 200, + thresholdDisplayName: 'Threshold 2' + }, + { + amountCents: 1, + thresholdDisplayName: 'Threshold 3 Recurring', + recurring: true + } ] } } @@ -215,6 +238,7 @@ expect(result_data['amountCents']).to eq('200') expect(result_data['taxes'][0]['code']).to eq(plan_tax.code) expect(result_data['charges'].count).to eq(6) + expect(result_data['usageThresholds'].count).to eq(3) standard_charge = result_data['charges'][0] expect(standard_charge['properties']['amount']).to eq('100.00') @@ -259,6 +283,23 @@ 'amountCents' => minimum_commitment_amount_cents.to_s ) expect(result_data['minimumCommitment']['taxes'].count).to eq(1) + + thresholds = result_data['usageThresholds'].sort_by { |threshold| threshold['thresholdDisplayName'] } + expect(thresholds).to include hash_including( + 'thresholdDisplayName' => 'Threshold 1', + 'amountCents' => '100', + 'recurring' => false + ) + expect(thresholds).to include hash_including( + 'thresholdDisplayName' => 'Threshold 2', + 'amountCents' => '200', + 'recurring' => false + ) + expect(thresholds).to include hash_including( + 'thresholdDisplayName' => 'Threshold 3 Recurring', + 'amountCents' => '1', + 'recurring' => true + ) end end end diff --git a/spec/graphql/mutations/plans/update_spec.rb b/spec/graphql/mutations/plans/update_spec.rb index ad3d993ef096..bc6257e8d0cd 100644 --- a/spec/graphql/mutations/plans/update_spec.rb +++ b/spec/graphql/mutations/plans/update_spec.rb @@ -48,6 +48,12 @@ values properties { amount } } + }, + usageThresholds { + id, + amountCents, + thresholdDisplayName, + recurring } } } @@ -161,6 +167,21 @@ ] } } + ], + usageThresholds: [ + { + amountCents: 100, + thresholdDisplayName: 'Threshold 1' + }, + { + amountCents: 200, + thresholdDisplayName: 'Threshold 2' + }, + { + amountCents: 1, + thresholdDisplayName: 'Threshold 3 Recurring', + recurring: true + } ] } } @@ -175,6 +196,8 @@ context 'with premium license' do around { |test| lago_premium!(&test) } + before { organization.update!(premium_integrations: ['progressive_billing']) } + it 'updates a plan' do result = execute_graphql(**graphql) @@ -190,6 +213,7 @@ expect(result_data['amountCents']).to eq('200') expect(result_data['amountCurrency']).to eq('EUR') expect(result_data['charges'].count).to eq(5) + expect(result_data['usageThresholds'].count).to eq(3) standard_charge = result_data['charges'][0] expect(standard_charge['properties']['amount']).to eq('100.00') @@ -228,6 +252,23 @@ 'amountCents' => minimum_commitment_amount_cents.to_s ) expect(result_data['minimumCommitment']['taxes'].count).to eq(1) + + thresholds = result_data['usageThresholds'].sort_by { |threshold| threshold['thresholdDisplayName'] } + expect(thresholds).to include hash_including( + 'thresholdDisplayName' => 'Threshold 1', + 'amountCents' => '100', + 'recurring' => false + ) + expect(thresholds).to include hash_including( + 'thresholdDisplayName' => 'Threshold 2', + 'amountCents' => '200', + 'recurring' => false + ) + expect(thresholds).to include hash_including( + 'thresholdDisplayName' => 'Threshold 3 Recurring', + 'amountCents' => '1', + 'recurring' => true + ) end end diff --git a/spec/graphql/mutations/subscriptions/create_spec.rb b/spec/graphql/mutations/subscriptions/create_spec.rb index 03bb44f9f05d..b811a385af11 100644 --- a/spec/graphql/mutations/subscriptions/create_spec.rb +++ b/spec/graphql/mutations/subscriptions/create_spec.rb @@ -8,6 +8,7 @@ let(:organization) { membership.organization } let(:plan) { create(:plan, organization:) } let(:charge) { create(:standard_charge, plan:) } + let(:threshold) { create(:usage_threshold, plan:) } let(:ending_at) { Time.current.beginning_of_day + 1.year } let(:customer) { create(:customer, organization:) } let(:mutation) do @@ -28,6 +29,11 @@ plan { id amountCents + usageThresholds { + id + amountCents + thresholdDisplayName + } } } } @@ -36,6 +42,8 @@ around { |test| lago_premium!(&test) } + before { organization.update!(premium_integrations: ['progressive_billing']) } + it_behaves_like 'requires current user' it_behaves_like 'requires current organization' it_behaves_like 'requires permission', 'subscriptions:create' @@ -60,6 +68,10 @@ id: charge.id, billableMetricId: charge.billable_metric_id, invoiceDisplayName: 'invoice display name' + ], + usageThresholds: [ + id: threshold.id, + thresholdDisplayName: 'threshold display name' ] } } @@ -84,5 +96,9 @@ 'id' => String, 'amountCents' => '100' ) + expect(result_data['plan']['usageThresholds'].first).to include( + 'thresholdDisplayName' => 'threshold display name', + 'amountCents' => '100' + ) end end diff --git a/spec/graphql/types/plans/object_spec.rb b/spec/graphql/types/plans/object_spec.rb index 93acbb86459c..e2d9f4ec5e3b 100644 --- a/spec/graphql/types/plans/object_spec.rb +++ b/spec/graphql/types/plans/object_spec.rb @@ -28,4 +28,5 @@ it { is_expected.to have_field(:customers_count).of_type('Int!') } it { is_expected.to have_field(:draft_invoices_count).of_type('Int!') } it { is_expected.to have_field(:subscriptions_count).of_type('Int!') } + it { is_expected.to have_field(:usage_thresholds).of_type('[UsageThreshold!]') } end diff --git a/spec/graphql/types/subscriptions/plan_overrides_input_spec.rb b/spec/graphql/types/subscriptions/plan_overrides_input_spec.rb index 8281764e7734..2325eaa780d8 100644 --- a/spec/graphql/types/subscriptions/plan_overrides_input_spec.rb +++ b/spec/graphql/types/subscriptions/plan_overrides_input_spec.rb @@ -14,4 +14,5 @@ it { is_expected.to accept_argument(:name).of_type('String') } it { is_expected.to accept_argument(:tax_codes).of_type('[String!]') } it { is_expected.to accept_argument(:trial_period).of_type('Float') } + it { is_expected.to accept_argument(:usage_thresholds).of_type('[UsageThresholdOverridesInput!]') } end diff --git a/spec/graphql/types/subscriptions/usage_threshold_overrides_input_spec.rb b/spec/graphql/types/subscriptions/usage_threshold_overrides_input_spec.rb new file mode 100644 index 000000000000..35519eb4c3b0 --- /dev/null +++ b/spec/graphql/types/subscriptions/usage_threshold_overrides_input_spec.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Types::Subscriptions::UsageThresholdOverridesInput do + subject { described_class } + + it { is_expected.to accept_argument(:id).of_type('ID!') } + it { is_expected.to accept_argument(:amount_cents).of_type('BigInt') } + it { is_expected.to accept_argument(:recurring).of_type('Boolean') } + it { is_expected.to accept_argument(:threshold_display_name).of_type('String') } +end diff --git a/spec/graphql/types/usage_thresholds/input_spec.rb b/spec/graphql/types/usage_thresholds/input_spec.rb new file mode 100644 index 000000000000..5f39b3d83309 --- /dev/null +++ b/spec/graphql/types/usage_thresholds/input_spec.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Types::UsageThresholds::Input do + subject { described_class } + + it { is_expected.to accept_argument(:id).of_type('ID') } + it { is_expected.to accept_argument(:amount_cents).of_type('BigInt') } + it { is_expected.to accept_argument(:recurring).of_type('Boolean') } + it { is_expected.to accept_argument(:threshold_display_name).of_type('String') } +end diff --git a/spec/graphql/types/usage_thresholds/object_spec.rb b/spec/graphql/types/usage_thresholds/object_spec.rb new file mode 100644 index 000000000000..fd0bb8019757 --- /dev/null +++ b/spec/graphql/types/usage_thresholds/object_spec.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Types::UsageThresholds::Object do + subject { described_class } + + it { is_expected.to have_field(:id).of_type('ID!') } + it { is_expected.to have_field(:amount_cents).of_type('BigInt!') } + it { is_expected.to have_field(:threshold_display_name).of_type('String') } + it { is_expected.to have_field(:recurring).of_type('Boolean!') } + it { is_expected.to have_field(:created_at).of_type('ISO8601DateTime!') } + it { is_expected.to have_field(:updated_at).of_type('ISO8601DateTime!') } +end