feat: add support for topup credits

This commit is contained in:
Tanmay Deep Sharma
2025-11-10 17:40:17 +05:30
parent 8d702ad299
commit 1e11f271f0
2 changed files with 153 additions and 1 deletions
@@ -23,7 +23,14 @@ module Enterprise::Api::V1::Accounts::Concerns::BillingV2
end
def v2_topup
render json: { success: true, message: 'Topup successful.' }
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
@@ -0,0 +1,145 @@
class Enterprise::Billing::V2::TopupService < Enterprise::Billing::V2::BaseService
def create_topup(credits:)
validation_result = validate_topup_request(credits)
return validation_result unless validation_result[:valid]
topup_definition = validation_result[:topup_definition]
amount_cents = (topup_definition[:amount] * 100).to_i
currency = topup_definition[:currency] || 'usd'
with_locked_account do
process_topup_transaction(credits, amount_cents, currency, topup_definition[:amount])
end
rescue Stripe::StripeError => e
{ success: false, message: "Stripe error: #{e.message}" }
end
private
def validate_topup_request(credits)
return { valid: false, success: false, message: 'Invalid topup amount' } unless credits.to_i.positive?
plan_validation = validate_subscription_plan
return plan_validation unless plan_validation[:valid]
topup_definition = Enterprise::Billing::V2::TopupCatalog.find_option(credits)
return { valid: false, success: false, message: 'Unsupported topup amount' } unless topup_definition
return { valid: false, success: false, message: 'Stripe customer not configured' } if stripe_customer_id.blank?
payment_service = Enterprise::Billing::V2::InvoicePaymentService.new(account: account)
payment_method_validation = payment_service.validate_payment_method
return payment_method_validation.merge(valid: false) if payment_method_validation
{ valid: true, topup_definition: topup_definition }
end
def validate_subscription_plan
plan_name = custom_attribute('plan_name')
# Block topup if no plan or on Hacker plan
if plan_name.blank? || plan_name.downcase == 'hacker'
return {
valid: false,
success: false,
message: 'Top-ups are only available for Startup, Business, and Enterprise plans. Please upgrade your plan to purchase credits.'
}
end
{ valid: true }
end
def process_topup_transaction(credits, amount_cents, currency, amount)
line_items = build_topup_line_items(credits, amount_cents)
invoice_result = charge_topup_invoice(line_items, currency)
return invoice_result unless invoice_result[:success]
credit_grant = create_stripe_credit_grant(amount_cents, currency, credits)
return { success: false, message: 'Failed to create credit grant in Stripe' } unless credit_grant
build_success_response(credits, amount, currency, invoice_result[:invoice_id], credit_grant['id'])
end
def build_topup_line_items(credits, amount_cents)
[{
amount: amount_cents,
description: "Credit Topup: #{credits} credits",
metadata: {
account_id: account.id.to_s,
credits: credits.to_s,
topup: 'true'
}
}]
end
def charge_topup_invoice(line_items, currency)
payment_service = Enterprise::Billing::V2::InvoicePaymentService.new(account: account)
payment_service.create_and_pay_invoice(
line_items: line_items,
description: 'Credit top-up purchase',
currency: currency,
metadata: {
account_id: account.id.to_s,
topup: 'true'
}
)
end
def build_success_response(credits, amount, currency, invoice_id, credit_grant_id)
{
success: true,
message: 'Top-up purchased successfully',
credits: credits,
amount: amount,
currency: currency,
invoice_id: invoice_id,
credit_grant_id: credit_grant_id
}
end
# Create Credit Grant in Stripe using monetary amount (not custom_pricing_unit)
# Following Stripe UBB Integration Guide section 8
def create_stripe_credit_grant(amount_cents, currency, credits)
Stripe::Billing::CreditGrant.create(
credit_grant_params(amount_cents, currency, credits)
)
end
def credit_grant_params(amount_cents, currency, credits)
{
customer: stripe_customer_id,
name: "Topup: #{credits} credits",
amount: credit_grant_amount(amount_cents, currency),
applicability_config: credit_grant_applicability,
category: 'paid',
metadata: credit_grant_metadata(credits)
}
end
def credit_grant_amount(amount_cents, currency)
{
type: 'monetary',
monetary: {
currency: currency,
value: amount_cents
}
}
end
def credit_grant_applicability
# Apply credit grant to all metered usage for this customer
# This ensures topup credits offset meter-based billing
{
scope: {
price_type: 'metered'
}
}
end
def credit_grant_metadata(credits)
{
account_id: account.id.to_s,
source: 'topup',
credits: credits.to_s
}
end
end