From eb04882e5aa30fd715f3030cb24aa327839e86dc Mon Sep 17 00:00:00 2001 From: Tanmay Deep Sharma Date: Wed, 5 Nov 2025 12:37:23 +0530 Subject: [PATCH] remove credit transactions table and use custome attributes instead --- app/models/account.rb | 1 - app/models/credit_transaction.rb | 28 ----- ...251007111938_create_credit_transactions.rb | 15 --- db/schema.rb | 13 -- .../api/v1/accounts/concerns/billing_v2.rb | 4 +- .../conversation/response_builder_job.rb | 15 --- .../services/captain/copilot/chat_service.rb | 21 ---- .../enterprise/ai/captain_credit_service.rb | 25 ---- .../concerns/stripe_v2_client_helper.rb | 4 +- .../enterprise/billing/v2/base_service.rb | 10 -- .../billing/v2/credit_management_service.rb | 116 +----------------- .../integrations/openai_processor_service.rb | 28 ----- .../v2/credit_management_service_spec.rb | 51 +------- 13 files changed, 10 insertions(+), 321 deletions(-) delete mode 100644 app/models/credit_transaction.rb delete mode 100644 db/migrate/20251007111938_create_credit_transactions.rb delete mode 100644 enterprise/app/services/enterprise/ai/captain_credit_service.rb diff --git a/app/models/account.rb b/app/models/account.rb index a8eaa04ff..7efb13bb5 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -69,7 +69,6 @@ class Account < ApplicationRecord has_many :categories, dependent: :destroy_async, class_name: '::Category' has_many :contacts, dependent: :destroy_async has_many :conversations, dependent: :destroy_async - has_many :credit_transactions, dependent: :destroy_async has_many :csat_survey_responses, dependent: :destroy_async has_many :custom_attribute_definitions, dependent: :destroy_async has_many :custom_filters, dependent: :destroy_async diff --git a/app/models/credit_transaction.rb b/app/models/credit_transaction.rb deleted file mode 100644 index 096f2cb67..000000000 --- a/app/models/credit_transaction.rb +++ /dev/null @@ -1,28 +0,0 @@ -# == Schema Information -# -# Table name: credit_transactions -# -# id :bigint not null, primary key -# amount :integer not null -# credit_type :string not null -# description :string -# metadata :json -# transaction_type :string not null -# created_at :datetime not null -# updated_at :datetime not null -# account_id :bigint not null -# -# Indexes -# -# index_credit_transactions_on_account_id (account_id) -# index_credit_transactions_on_account_id_and_created_at (account_id,created_at) -# -class CreditTransaction < ApplicationRecord - belongs_to :account - - validates :amount, presence: true, numericality: true - validates :transaction_type, presence: true, inclusion: { in: %w[grant expire use topup refund] } - validates :credit_type, presence: true, inclusion: { in: %w[monthly topup mixed] } - - scope :recent, -> { order(created_at: :desc) } -end diff --git a/db/migrate/20251007111938_create_credit_transactions.rb b/db/migrate/20251007111938_create_credit_transactions.rb deleted file mode 100644 index 044f72900..000000000 --- a/db/migrate/20251007111938_create_credit_transactions.rb +++ /dev/null @@ -1,15 +0,0 @@ -class CreateCreditTransactions < ActiveRecord::Migration[7.0] - def change - create_table :credit_transactions do |t| - t.references :account, null: false - t.string :transaction_type, null: false - t.integer :amount, null: false - t.string :credit_type, null: false - t.string :description - t.jsonb :metadata, default: {} - t.timestamps - end - - add_index :credit_transactions, [:account_id, :created_at] - end -end diff --git a/db/schema.rb b/db/schema.rb index 73dc1f2a7..45165d5c5 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -709,19 +709,6 @@ ActiveRecord::Schema[7.1].define(version: 2025_10_22_152158) do t.index ["user_id"], name: "index_copilot_threads_on_user_id" end - create_table "credit_transactions", force: :cascade do |t| - t.bigint "account_id", null: false - t.string "transaction_type", null: false - t.integer "amount", null: false - t.string "credit_type", null: false - t.string "description" - t.json "metadata" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.index ["account_id", "created_at"], name: "index_credit_transactions_on_account_id_and_created_at" - t.index ["account_id"], name: "index_credit_transactions_on_account_id" - end - create_table "csat_survey_responses", force: :cascade do |t| t.bigint "account_id", null: false t.bigint "conversation_id", null: false diff --git a/enterprise/app/controllers/enterprise/api/v1/accounts/concerns/billing_v2.rb b/enterprise/app/controllers/enterprise/api/v1/accounts/concerns/billing_v2.rb index 97c6c22cc..1d38466d2 100644 --- a/enterprise/app/controllers/enterprise/api/v1/accounts/concerns/billing_v2.rb +++ b/enterprise/app/controllers/enterprise/api/v1/accounts/concerns/billing_v2.rb @@ -13,9 +13,7 @@ module Enterprise::Api::V1::Accounts::Concerns::BillingV2 id: @account.id, monthly_credits: balance[:monthly], topup_credits: balance[:topup], - total_credits: balance[:total], - usage_this_month: balance[:usage_this_month], - usage_total: balance[:usage_total] + total_credits: balance[:total] } end diff --git a/enterprise/app/jobs/captain/conversation/response_builder_job.rb b/enterprise/app/jobs/captain/conversation/response_builder_job.rb index cef317cb3..828123321 100644 --- a/enterprise/app/jobs/captain/conversation/response_builder_job.rb +++ b/enterprise/app/jobs/captain/conversation/response_builder_job.rb @@ -122,7 +122,6 @@ class Captain::Conversation::ResponseBuilderJob < ApplicationJob def handle_error(error) log_error(error) - refund_credit process_action('handoff') true end @@ -134,18 +133,4 @@ class Captain::Conversation::ResponseBuilderJob < ApplicationJob def captain_v2_enabled? account.feature_enabled?('captain_integration_v2') end - - def refund_credit - credit_service = Enterprise::Ai::CaptainCreditService.new(conversation: @conversation) - credit_service.check_and_use_credits( - feature: 'ai_captain_conversation', - amount: -1, # Negative to refund - metadata: { - 'assistant_id' => @assistant.id, - 'conversation_id' => @conversation.id, - 'inbox_id' => @inbox.id, - 'refund' => true - } - ) - end end diff --git a/enterprise/app/services/captain/copilot/chat_service.rb b/enterprise/app/services/captain/copilot/chat_service.rb index d439a6e49..ca7057c86 100644 --- a/enterprise/app/services/captain/copilot/chat_service.rb +++ b/enterprise/app/services/captain/copilot/chat_service.rb @@ -20,14 +20,6 @@ class Captain::Copilot::ChatService < Llm::BaseOpenAiService end def generate_response(input) - # Check and use credits before making API call - credit_result = check_and_use_credits - - unless credit_result[:success] - error_message = credit_result[:message] || 'Insufficient credits' - return { error: error_message, error_code: 402 } - end - @messages << { role: 'user', content: input } if input.present? response = request_chat_completion @@ -121,17 +113,4 @@ class Captain::Copilot::ChatService < Llm::BaseOpenAiService message_type: message_type ) end - - def check_and_use_credits - credit_service = Enterprise::Ai::CaptainCreditService.new(account: @account) - credit_service.check_and_use_credits( - feature: 'ai_copilot', - amount: 1, - metadata: { - 'assistant_id' => @assistant.id, - 'user_id' => @user&.id, - 'copilot_thread_id' => @copilot_thread&.id - } - ) - end end diff --git a/enterprise/app/services/enterprise/ai/captain_credit_service.rb b/enterprise/app/services/enterprise/ai/captain_credit_service.rb deleted file mode 100644 index 41937065c..000000000 --- a/enterprise/app/services/enterprise/ai/captain_credit_service.rb +++ /dev/null @@ -1,25 +0,0 @@ -class Enterprise::Ai::CaptainCreditService - attr_reader :account, :conversation - - def initialize(account: nil, conversation: nil) - @account = account || conversation&.account - @conversation = conversation - end - - def check_and_use_credits(feature: 'ai_captain', amount: 1, metadata: {}) - # V1 accounts don't use credit system - return { success: true } unless v2_enabled? - - # V2 accounts use credit-based billing - service = Enterprise::Billing::V2::CreditManagementService.new(account: account) - result = service.use_credit(feature: feature, amount: amount, metadata: metadata) - Rails.logger.info "Credit result: #{result.inspect}" - result - end - - private - - def v2_enabled? - ENV.fetch('STRIPE_BILLING_V2_ENABLED', 'false') == 'true' - end -end diff --git a/enterprise/app/services/enterprise/billing/concerns/stripe_v2_client_helper.rb b/enterprise/app/services/enterprise/billing/concerns/stripe_v2_client_helper.rb index e7f2e39d9..bd04b0312 100644 --- a/enterprise/app/services/enterprise/billing/concerns/stripe_v2_client_helper.rb +++ b/enterprise/app/services/enterprise/billing/concerns/stripe_v2_client_helper.rb @@ -107,11 +107,11 @@ module Enterprise::Billing::Concerns::StripeV2ClientHelper end def default_stripe_version - '2025-08-27.preview' + '2025-10-29.preview' end def checkout_stripe_version - '2025-08-27.preview;checkout_product_catalog_preview=v1' + '2025-10-29.preview;checkout_product_catalog_preview=v1' end def extract_attribute(object, key) diff --git a/enterprise/app/services/enterprise/billing/v2/base_service.rb b/enterprise/app/services/enterprise/billing/v2/base_service.rb index 914b7255a..92da4f306 100644 --- a/enterprise/app/services/enterprise/billing/v2/base_service.rb +++ b/enterprise/app/services/enterprise/billing/v2/base_service.rb @@ -51,16 +51,6 @@ class Enterprise::Billing::V2::BaseService account.with_lock(&) end - def log_credit_transaction(type:, amount:, credit_type:, description: nil, metadata: nil) - account.credit_transactions.create!( - transaction_type: type, - amount: amount, - credit_type: credit_type, - description: description, - metadata: (metadata || {}).stringify_keys - ) - end - def with_stripe_error_handling yield rescue Stripe::StripeError => e diff --git a/enterprise/app/services/enterprise/billing/v2/credit_management_service.rb b/enterprise/app/services/enterprise/billing/v2/credit_management_service.rb index 2ca8270e0..77f7a93cf 100644 --- a/enterprise/app/services/enterprise/billing/v2/credit_management_service.rb +++ b/enterprise/app/services/enterprise/billing/v2/credit_management_service.rb @@ -1,72 +1,13 @@ class Enterprise::Billing::V2::CreditManagementService < Enterprise::Billing::V2::BaseService - # rubocop:disable Metrics/MethodLength - def use_credit(feature: 'ai_captain', amount: 1, metadata: {}) - with_locked_account do - # Handle refunds (negative amounts) - if amount.negative? - refund_credits(amount.abs) - log_credit_transaction( - type: 'use', - amount: amount, - credit_type: 'topup', - description: "Refund for #{feature}", - metadata: metadata - ) - { success: true, credits_refunded: amount.abs, remaining: total_credits } - elsif total_credits < amount - # Handle usage (positive amounts) - { success: false, message: 'Insufficient credits' } - else - # Report usage to Stripe - stripe_result = report_usage_to_stripe(amount, feature) - - if stripe_result[:success] - # Deduct credits locally (monthly first, then topup) - deduct_credits(amount) - - # Log transaction - log_credit_transaction( - type: 'use', - amount: amount, - credit_type: determine_credit_type(amount), - description: "Used for #{feature}", - metadata: metadata - ) - - { success: true, credits_used: amount, remaining: total_credits } - else - { success: false, message: stripe_result[:message] } - end - end - end - end - # rubocop:enable Metrics/MethodLength - - def sync_monthly_credits(amount, metadata: {}) + def sync_monthly_credits(amount) with_locked_account do update_credits(monthly: amount) - if amount.positive? - log_credit_transaction( - type: 'grant', - amount: amount, - credit_type: 'monthly', - description: 'Monthly credits from Stripe', - metadata: metadata - ) - end end end - def add_topup_credits(amount, metadata: {}) + def add_topup_credits(amount) with_locked_account do update_credits(topup: topup_credits + amount) - log_credit_transaction( - type: 'topup', - amount: amount, - credit_type: 'topup', - description: 'Topup credits added', - metadata: metadata - ) end end @@ -79,14 +20,10 @@ class Enterprise::Billing::V2::CreditManagementService < Enterprise::Billing::V2 end def credit_balance - usage_stats = calculate_usage_stats - { monthly: monthly_credits, topup: topup_credits, - total: total_credits, - usage_this_month: usage_stats[:this_month], - usage_total: usage_stats[:total] + total: total_credits } end @@ -108,25 +45,6 @@ class Enterprise::Billing::V2::CreditManagementService < Enterprise::Billing::V2 [] end - def calculate_usage_stats - month_start = Time.current.beginning_of_month - - this_month_usage = account.credit_transactions - .where(transaction_type: 'use', created_at: month_start..Time.current) - .sum(:amount) - .abs - - total_usage = account.credit_transactions - .where(transaction_type: 'use') - .sum(:amount) - .abs - - { - this_month: this_month_usage, - total: total_usage - } - end - def total_credits monthly_credits + topup_credits end @@ -173,32 +91,4 @@ class Enterprise::Billing::V2::CreditManagementService < Enterprise::Billing::V2 def stripe_api_options { api_key: ENV.fetch('STRIPE_SECRET_KEY', nil), stripe_version: '2025-08-27.preview' } end - - def report_usage_to_stripe(amount, feature) - Enterprise::Billing::V2::UsageReporterService.new(account: account).report(amount, feature) - end - - def deduct_credits(amount) - if monthly_credits >= amount - update_credits(monthly: monthly_credits - amount) - else - remaining = amount - monthly_credits - update_credits(monthly: 0, topup: topup_credits - remaining) - end - end - - def refund_credits(amount) - # Refunds are added to topup credits - update_credits(topup: topup_credits + amount) - end - - def determine_credit_type(amount) - if monthly_credits >= amount - 'monthly' - elsif monthly_credits.positive? - 'mixed' - else - 'topup' - end - end end diff --git a/enterprise/lib/enterprise/integrations/openai_processor_service.rb b/enterprise/lib/enterprise/integrations/openai_processor_service.rb index f9acdd06e..5a98ad4c4 100644 --- a/enterprise/lib/enterprise/integrations/openai_processor_service.rb +++ b/enterprise/lib/enterprise/integrations/openai_processor_service.rb @@ -3,30 +3,6 @@ module Enterprise::Integrations::OpenaiProcessorService make_friendly make_formal simplify].freeze CACHEABLE_EVENTS = %w[label_suggestion].freeze - def perform - # Check and use credits if account is on V2 billing - if v2_enabled? - credit_service = Enterprise::Ai::CaptainCreditService.new( - account: hook.account, - conversation: conversation - ) - - result = credit_service.check_and_use_credits( - feature: "ai_#{event_name}", - amount: 1, - metadata: { - 'event_name' => event_name, - 'conversation_id' => conversation&.id - } - ) - - return { error: 'Insufficient credits', error_code: 402 } unless result[:success] - end - - # Call the parent perform method - super - end - def label_suggestion_message payload = label_suggestion_body return nil if payload.blank? @@ -103,8 +79,4 @@ module Enterprise::Integrations::OpenaiProcessorService def label_suggestions_enabled? hook.settings['label_suggestion'].present? end - - def v2_enabled? - ENV.fetch('STRIPE_BILLING_V2_ENABLED', 'false') == 'true' - end end diff --git a/spec/enterprise/services/enterprise/billing/v2/credit_management_service_spec.rb b/spec/enterprise/services/enterprise/billing/v2/credit_management_service_spec.rb index 5f200c414..1e1b74330 100644 --- a/spec/enterprise/services/enterprise/billing/v2/credit_management_service_spec.rb +++ b/spec/enterprise/services/enterprise/billing/v2/credit_management_service_spec.rb @@ -8,17 +8,12 @@ describe Enterprise::Billing::V2::CreditManagementService do account.update!( custom_attributes: { 'stripe_billing_version' => 2, - 'monthly_credits' => 100, - 'topup_credits' => 50, 'stripe_customer_id' => 'cus_test_123', - 'stripe_meter_event_name' => 'ai_prompts' + 'stripe_meter_event_name' => 'ai_prompts', + 'monthly_credits' => 100, + 'topup_credits' => 50 } ) - - # Stub Stripe meter event creation - allow(Stripe::Billing::MeterEvent).to receive(:create).and_return( - OpenStruct.new(identifier: 'test_event_123') - ) end describe '#total_credits' do @@ -27,49 +22,12 @@ describe Enterprise::Billing::V2::CreditManagementService do end end - describe '#use_credit' do - context 'when sufficient credits' do - it 'uses credits and reports to Stripe' do - result = service.use_credit(feature: 'ai_test', amount: 10) - - expect(result[:success]).to be(true) - expect(result[:credits_used]).to eq(10) - expect(result[:remaining]).to eq(140) - - account.reload - expect(account.custom_attributes['monthly_credits']).to eq(90) - expect(account.custom_attributes['topup_credits']).to eq(50) - end - end - - context 'when insufficient credits' do - it 'returns error' do - result = service.use_credit(feature: 'ai_test', amount: 200) - - expect(result[:success]).to be(false) - expect(result[:message]).to eq('Insufficient credits') - end - end - - context 'when using mixed credits' do - it 'uses monthly first then topup' do - result = service.use_credit(feature: 'ai_test', amount: 120) - - expect(result[:success]).to be(true) - account.reload - expect(account.custom_attributes['monthly_credits']).to eq(0) - expect(account.custom_attributes['topup_credits']).to eq(30) - end - end - end - describe '#sync_monthly_credits' do - it 'updates monthly credits from Stripe' do + it 'updates monthly credits' do service.sync_monthly_credits(500) account.reload expect(account.custom_attributes['monthly_credits']).to eq(500) - expect(account.credit_transactions.last.description).to eq('Monthly credits from Stripe') end end @@ -89,7 +47,6 @@ describe Enterprise::Billing::V2::CreditManagementService do account.reload expect(account.custom_attributes['topup_credits']).to eq(150) - expect(account.credit_transactions.last.description).to eq('Topup credits added') end end end