remove credit transactions table and use custome attributes instead

This commit is contained in:
Tanmay Deep Sharma
2025-11-05 12:37:23 +05:30
parent e511527f32
commit eb04882e5a
13 changed files with 10 additions and 321 deletions
-1
View File
@@ -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
-28
View File
@@ -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
@@ -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
-13
View File
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
@@ -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
@@ -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
@@ -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
@@ -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