Merge remote-tracking branch 'origin/feature/stripe_v2' into feature/stripe_v2_fe
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
module Enterprise::Api::V1::Accounts::Concerns::BillingV2
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
before_action :validate_topup_amount, only: [:v2_topup]
|
||||
end
|
||||
|
||||
def credits_balance
|
||||
service = Enterprise::Billing::V2::CreditManagementService.new(account: @account)
|
||||
balance = service.credit_balance
|
||||
|
||||
render json: {
|
||||
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]
|
||||
}
|
||||
end
|
||||
|
||||
def credit_grants
|
||||
service = Enterprise::Billing::V2::CreditManagementService.new(account: @account)
|
||||
grants = service.fetch_credit_grants
|
||||
|
||||
render json: { credit_grants: grants }
|
||||
end
|
||||
|
||||
def v2_pricing_plans
|
||||
plans = Enterprise::Billing::V2::PlanCatalog.plans
|
||||
render json: { pricing_plans: plans }
|
||||
end
|
||||
|
||||
def v2_topup_options
|
||||
options = Enterprise::Billing::V2::TopupCatalog.options
|
||||
render json: { topup_options: options }
|
||||
end
|
||||
|
||||
def v2_topup
|
||||
service = Enterprise::Billing::V2::TopupService.new(account: @account)
|
||||
result = service.create_topup(credits: params[:credits].to_i)
|
||||
|
||||
if result[:success]
|
||||
render json: { success: true, message: result[:message] }
|
||||
else
|
||||
render json: { error: result[:message] }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def v2_subscribe
|
||||
service = Enterprise::Billing::V2::CheckoutSessionService.new(account: @account)
|
||||
result = service.create_subscription_checkout(
|
||||
pricing_plan_id: params[:pricing_plan_id],
|
||||
quantity: subscription_quantity
|
||||
)
|
||||
|
||||
if result[:success]
|
||||
render json: { success: true, redirect_url: result[:redirect_url], checkout_session_id: result[:checkout_session_id] }
|
||||
else
|
||||
render json: { error: result[:message] }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def cancel_subscription
|
||||
service = Enterprise::Billing::V2::CancelSubscriptionService.new(account: @account)
|
||||
result = service.cancel_subscription
|
||||
|
||||
if result[:success]
|
||||
# Include account ID and updated attributes for frontend store update
|
||||
@account.reload
|
||||
render json: result.merge(
|
||||
id: @account.id,
|
||||
custom_attributes: @account.custom_attributes
|
||||
)
|
||||
else
|
||||
render json: { error: result[:message] }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def update_subscription_quantity
|
||||
service = Enterprise::Billing::V2::UpdateSubscriptionService.new(account: @account)
|
||||
result = service.update_quantity(quantity: params[:quantity].to_i)
|
||||
|
||||
if result[:success]
|
||||
# Include account ID and updated attributes for frontend store update
|
||||
@account.reload
|
||||
render json: result.merge(
|
||||
id: @account.id,
|
||||
custom_attributes: @account.custom_attributes
|
||||
)
|
||||
else
|
||||
render json: { error: result[:message] }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def change_pricing_plan
|
||||
service = Enterprise::Billing::V2::ChangePlanService.new(account: @account)
|
||||
result = service.change_plan(
|
||||
new_pricing_plan_id: params[:pricing_plan_id],
|
||||
quantity: params[:quantity].to_i
|
||||
)
|
||||
|
||||
if result[:success]
|
||||
# Include account ID and updated attributes for frontend store update
|
||||
@account.reload
|
||||
render json: result.merge(
|
||||
id: @account.id,
|
||||
custom_attributes: @account.custom_attributes
|
||||
)
|
||||
else
|
||||
render json: { error: result[:message] }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def subscription_quantity
|
||||
[params[:quantity].to_i, 1].max
|
||||
end
|
||||
|
||||
def validate_topup_amount
|
||||
return if params[:credits].to_i.positive?
|
||||
|
||||
render json: { error: 'Topup amount must be greater than 0' }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
@@ -1,10 +1,10 @@
|
||||
# rubocop:disable Metrics/ClassLength
|
||||
class Enterprise::Api::V1::AccountsController < Api::BaseController
|
||||
include BillingHelper
|
||||
include Enterprise::Api::V1::Accounts::Concerns::BillingV2
|
||||
|
||||
before_action :fetch_account
|
||||
before_action :check_authorization
|
||||
before_action :check_cloud_env, only: [:limits, :toggle_deletion]
|
||||
before_action :validate_topup_amount, only: [:v2_topup]
|
||||
|
||||
def subscription
|
||||
if stripe_customer_id.blank? && @account.custom_attributes['is_creating_customer'].blank?
|
||||
@@ -57,114 +57,6 @@ class Enterprise::Api::V1::AccountsController < Api::BaseController
|
||||
end
|
||||
end
|
||||
|
||||
# V2 Billing Endpoints
|
||||
def credits_balance
|
||||
service = Enterprise::Billing::V2::CreditManagementService.new(account: @account)
|
||||
balance = service.credit_balance
|
||||
|
||||
render json: {
|
||||
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]
|
||||
}
|
||||
end
|
||||
|
||||
def credit_grants
|
||||
service = Enterprise::Billing::V2::CreditManagementService.new(account: @account)
|
||||
grants = service.fetch_credit_grants
|
||||
|
||||
render json: { credit_grants: grants }
|
||||
end
|
||||
|
||||
def v2_pricing_plans
|
||||
plans = Enterprise::Billing::V2::PlanCatalog.plans
|
||||
render json: { pricing_plans: plans }
|
||||
end
|
||||
|
||||
def v2_topup_options
|
||||
options = Enterprise::Billing::V2::TopupCatalog.options
|
||||
render json: { topup_options: options }
|
||||
end
|
||||
|
||||
def v2_topup
|
||||
service = Enterprise::Billing::V2::TopupService.new(account: @account)
|
||||
result = service.create_topup(credits: params[:credits].to_i)
|
||||
|
||||
if result[:success]
|
||||
render json: { success: true, message: result[:message] }
|
||||
else
|
||||
render json: { error: result[:message] }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def v2_subscribe
|
||||
service = Enterprise::Billing::V2::CheckoutSessionService.new(account: @account)
|
||||
result = service.create_subscription_checkout(
|
||||
pricing_plan_id: params[:pricing_plan_id],
|
||||
quantity: subscription_quantity
|
||||
)
|
||||
|
||||
if result[:success]
|
||||
render json: { success: true, redirect_url: result[:redirect_url], checkout_session_id: result[:checkout_session_id] }
|
||||
else
|
||||
render json: { error: result[:message] }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def cancel_subscription
|
||||
service = Enterprise::Billing::V2::CancelSubscriptionService.new(account: @account)
|
||||
result = service.cancel_subscription
|
||||
|
||||
if result[:success]
|
||||
# Include account ID and updated attributes for frontend store update
|
||||
@account.reload
|
||||
render json: result.merge(
|
||||
id: @account.id,
|
||||
custom_attributes: @account.custom_attributes
|
||||
)
|
||||
else
|
||||
render json: { error: result[:message] }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def update_subscription_quantity
|
||||
service = Enterprise::Billing::V2::UpdateSubscriptionService.new(account: @account)
|
||||
result = service.update_quantity(quantity: params[:quantity].to_i)
|
||||
|
||||
if result[:success]
|
||||
# Include account ID and updated attributes for frontend store update
|
||||
@account.reload
|
||||
render json: result.merge(
|
||||
id: @account.id,
|
||||
custom_attributes: @account.custom_attributes
|
||||
)
|
||||
else
|
||||
render json: { error: result[:message] }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def change_pricing_plan
|
||||
service = Enterprise::Billing::V2::ChangePlanService.new(account: @account)
|
||||
result = service.change_plan(
|
||||
new_pricing_plan_id: params[:pricing_plan_id],
|
||||
quantity: params[:quantity].to_i
|
||||
)
|
||||
|
||||
if result[:success]
|
||||
# Include account ID and updated attributes for frontend store update
|
||||
@account.reload
|
||||
render json: result.merge(
|
||||
id: @account.id,
|
||||
custom_attributes: @account.custom_attributes
|
||||
)
|
||||
else
|
||||
render json: { error: result[:message] }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_cloud_env
|
||||
@@ -223,18 +115,6 @@ class Enterprise::Api::V1::AccountsController < Api::BaseController
|
||||
render json: { redirect_url: redirect_url }
|
||||
end
|
||||
|
||||
def subscription_quantity
|
||||
quantity = params[:quantity].to_i
|
||||
quantity.positive? ? quantity : 1
|
||||
end
|
||||
|
||||
def validate_topup_amount
|
||||
amount = params[:credits].to_i
|
||||
return if amount.positive?
|
||||
|
||||
render json: { error: 'Topup amount must be greater than 0' }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def pundit_user
|
||||
{
|
||||
user: current_user,
|
||||
@@ -243,4 +123,3 @@ class Enterprise::Api::V1::AccountsController < Api::BaseController
|
||||
}
|
||||
end
|
||||
end
|
||||
# rubocop:enable Metrics/ClassLength
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Enterprise::Billing::Concerns::PlanFeatureManager
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
|
||||
@@ -13,8 +13,7 @@ class Enterprise::Billing::V2::ChangePlanService < Enterprise::Billing::V2::Base
|
||||
|
||||
with_locked_account do
|
||||
billing_intent = create_change_plan_intent(new_pricing_plan_id, quantity)
|
||||
reserved_intent = reserve_billing_intent(billing_intent)
|
||||
handle_payment_if_needed(reserved_intent)
|
||||
reserve_billing_intent(billing_intent)
|
||||
commit_billing_intent(billing_intent)
|
||||
update_account_plan(new_pricing_plan_id, quantity)
|
||||
success_response(new_pricing_plan_id, quantity)
|
||||
@@ -94,35 +93,6 @@ class Enterprise::Billing::V2::ChangePlanService < Enterprise::Billing::V2::Base
|
||||
)
|
||||
end
|
||||
|
||||
def handle_payment_if_needed(reserved_intent)
|
||||
# Check if there's a proration charge that needs to be paid
|
||||
total_amount = extract_attribute(reserved_intent['amount_details'], 'total')
|
||||
return if total_amount.nil? || total_amount.to_i <= 0
|
||||
|
||||
# Get customer's default payment method
|
||||
stripe_customer_id = custom_attribute('stripe_customer_id')
|
||||
raise StandardError, 'No Stripe customer ID found' if stripe_customer_id.blank?
|
||||
|
||||
customer = Stripe::Customer.retrieve(stripe_customer_id)
|
||||
payment_method_id = customer.invoice_settings&.default_payment_method
|
||||
|
||||
raise StandardError, 'No default payment method found. Please add a payment method first.' if payment_method_id.blank?
|
||||
|
||||
# Create and confirm payment intent for proration charge
|
||||
Stripe::PaymentIntent.create({
|
||||
amount: total_amount.to_i,
|
||||
currency: 'usd',
|
||||
customer: stripe_customer_id,
|
||||
payment_method: payment_method_id,
|
||||
confirm: true,
|
||||
automatic_payment_methods: { enabled: true, allow_redirects: 'never' },
|
||||
metadata: {
|
||||
account_id: account.id,
|
||||
type: 'plan_change_proration'
|
||||
}
|
||||
})
|
||||
end
|
||||
|
||||
def commit_billing_intent(billing_intent)
|
||||
StripeV2Client.request(
|
||||
:post,
|
||||
|
||||
@@ -115,7 +115,6 @@ class Enterprise::Billing::V2::SubscriptionProvisioningService < Enterprise::Bil
|
||||
"pricing_plan_id=#{pricing_plan_id}, quantity=#{quantity}"
|
||||
|
||||
attributes = {
|
||||
'stripe_billing_version' => 2,
|
||||
'stripe_subscription_id' => subscription_id,
|
||||
'subscribed_quantity' => quantity,
|
||||
'subscription_status' => 'active',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
class Enterprise::Billing::V2::WebhookHandlerService
|
||||
def perform(event:)
|
||||
@event = event
|
||||
return { success: false, message: 'Event is required' } if @event.blank?
|
||||
|
||||
return { success: false, message: 'Account not found' } if account.blank?
|
||||
|
||||
|
||||
@@ -15,10 +15,6 @@ class Messages::AudioTranscriptionService < Llm::BaseOpenAiService
|
||||
transcriptions = transcribe_audio
|
||||
Rails.logger.info "Audio transcription successful: #{transcriptions}"
|
||||
{ success: true, transcriptions: transcriptions }
|
||||
rescue StandardError => e
|
||||
# Refund credit if transcription failed
|
||||
refund_credit
|
||||
raise e
|
||||
end
|
||||
|
||||
private
|
||||
@@ -30,20 +26,6 @@ class Messages::AudioTranscriptionService < Llm::BaseOpenAiService
|
||||
account.usage_limits[:captain][:responses][:current_available].positive?
|
||||
end
|
||||
|
||||
def refund_credit
|
||||
credit_service = Enterprise::Ai::CaptainCreditService.new(account: account)
|
||||
credit_service.check_and_use_credits(
|
||||
feature: 'ai_audio_transcription',
|
||||
amount: -1, # Negative to refund
|
||||
metadata: {
|
||||
'message_id' => message.id,
|
||||
'attachment_id' => attachment.id,
|
||||
'conversation_id' => message.conversation_id,
|
||||
'refund' => true
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
def fetch_audio_file
|
||||
temp_dir = Rails.root.join('tmp/uploads')
|
||||
FileUtils.mkdir_p(temp_dir)
|
||||
|
||||
@@ -2,8 +2,14 @@ require 'rails_helper'
|
||||
require 'ostruct'
|
||||
|
||||
describe Enterprise::Billing::V2::TopupService do
|
||||
let(:account) { create(:account, custom_attributes: { 'stripe_customer_id' => 'cus_123' }) }
|
||||
let(:account) { create(:account, custom_attributes: { 'stripe_customer_id' => 'cus_123', 'plan_name' => 'Startups' }) }
|
||||
let(:service) { described_class.new(account: account) }
|
||||
let(:invoice_settings) { instance_double(Stripe::InvoiceSettings, default_payment_method: 'pm_123') }
|
||||
let(:stripe_customer) do
|
||||
instance_double(Stripe::Customer,
|
||||
invoice_settings: invoice_settings,
|
||||
default_source: nil)
|
||||
end
|
||||
let(:invoice) { Stripe::Invoice.construct_from(id: 'in_123', customer: 'cus_123', currency: 'usd') }
|
||||
let(:invoice_item) do
|
||||
Stripe::InvoiceItem.construct_from(
|
||||
@@ -30,6 +36,9 @@ describe Enterprise::Billing::V2::TopupService do
|
||||
currency: 'usd'
|
||||
)
|
||||
|
||||
# Mock Stripe Customer retrieval for payment method check
|
||||
allow(Stripe::Customer).to receive(:retrieve).and_return(stripe_customer)
|
||||
|
||||
# Mock Stripe Invoice creation
|
||||
allow(Stripe::Invoice).to receive(:create).and_return(invoice)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user