diff --git a/bundle install b/bundle install new file mode 100644 index 0000000..776baee --- /dev/null +++ b/bundle install @@ -0,0 +1,360 @@ +require "dotenv" +require "stripe" +require "sinatra" +require "sinatra/json" +require "json" +Dotenv.load + +Stripe.api_key = ENV["STRIPE_SECRET_KEY"] +stripe_client = Stripe::StripeClient.new(ENV["STRIPE_SECRET_KEY"]) + + +set :public_folder, File.dirname(__FILE__) + "/public" +enable :sessions + +# Create a sample product and return a price for it +post "/api/create-product" do + data = parse_request_body + product_name = data['productName'] + product_description = data['productDescription'] + product_price = data['productPrice'] + account_id = data['accountId'] + + begin + # Create the product on the connected account + product = Stripe::Product.create({ + name: product_name, + description: product_description, + }, { stripe_account: account_id }) + + # Create a price for the product on the connected account + price = Stripe::Price.create({ + product: product.id, + unit_amount: product_price, + currency: 'usd', + }, { stripe_account: account_id }) + + content_type :json + { + productName: product_name, + productDescription: product_description, + productPrice: product_price, + priceId: price.id + }.to_json + rescue Stripe::StripeError => e + status 500 + { error: e.message }.to_json + end +end + +# Create a Connected Account +post "/api/create-connect-account" do + data = parse_request_body + + begin + + account = stripe_client.v2.core.accounts.create({ + display_name: data['email'], + contact_email: data['email'], + dashboard: 'full', + defaults: { + responsibilities: { + fees_collector: 'stripe', + losses_collector: 'stripe', + }, + }, + identity: { + country: 'US', + entity_type: 'company', + }, + configuration: { + customer: {}, + merchant: { + capabilities: { + card_payments: { requested: true }, + }, + }, + }, + }) + + + content_type :json + { accountId: account.id }.to_json + rescue Stripe::StripeError => e + status 500 + { error: e.message }.to_json + end +end + +# Create Account Link for onboarding +post "/api/create-account-link" do + data = parse_request_body + account_id = data['accountId'] + + begin + + account_link = stripe_client.v2.core.account_links.create({ + account: account_id, + use_case: { + type: 'account_onboarding', + account_onboarding: { + configurations: ['merchant', 'customer'], + + refresh_url: 'https://example.com', + return_url: "https://example.com?accountId=#{account_id}", + }, + }, + }) + + content_type :json + { url: account_link.url }.to_json + rescue Stripe::StripeError => e + status 500 + { error: e.message }.to_json + end +end + +# Get Connected Account Status +get "/api/account-status/:account_id" do + account_id = params[:account_id] + + begin + + account = stripe_client.v2.core.accounts.retrieve(account_id, { + include: ['requirements', 'configuration.merchant'], + + }) + + payouts_enabled = account.configuration&.merchant&.capabilities&.stripe_balance&.payouts&.status == 'active' + charges_enabled = account.configuration&.merchant&.capabilities&.card_payments&.status == 'active' + summary_status = account.requirements&.summary&.minimum_deadline&.status + details_submitted = summary_status.nil? || summary_status == 'eventually_due' + + content_type :json + { + id: account.id, + payoutsEnabled: payouts_enabled, + chargesEnabled: charges_enabled, + detailsSubmitted: details_submitted, + requirements: account.requirements&.entries, + }.to_json + rescue Stripe::StripeError => e + status 500 + { error: e.message }.to_json + end +end + +# Fetch products for a specific account +get "/api/products/:account_id" do + account_id = params[:account_id] + + begin + options = {} + if account_id != 'platform' + options[:stripe_account] = account_id + end + + prices = Stripe::Price.list({ + expand: ['data.product'], + active: true, + limit: 100, + }, options) + + products = prices.data.map do |price| + { + id: price.product.id, + name: price.product.name, + description: price.product.description, + price: price.unit_amount, + priceId: price.id, + image: 'https://i.imgur.com/6Mvijcm.png' + } + end + + content_type :json + products.to_json + rescue Stripe::StripeError => e + status 500 + { error: e.message }.to_json + end +end +# Create a subscription from the connected account to the platform +post "/api/subscribe-to-platform" do + data = parse_request_body + account_id = data['accountId'] + price_id = ENV['PLATFORM_PRICE_ID'] # Price ID created on the platform account + + session = Stripe::Checkout::Session.create({ + mode: 'subscription', + line_items: [{ + price: price_id, + quantity: 1, + }], + # Pass the V2 Account ID + customer_account: account_id, + # Defines where Stripe will redirect a customer after successful payment + success_url: "#{ENV['DOMAIN']}?session_id={CHECKOUT_SESSION_ID}&success=true", + # Defines where Stripe will redirect if a customer cancels payment + cancel_url: "#{ENV['DOMAIN']}?canceled=true", + }) + + content_type :json + { url: session.url }.to_json +end + +# Create checkout session +post "/api/create-checkout-session" do + data = parse_request_body + price_id = data['priceId'] + account_id = data['accountId'] + + # Get the price's type from Stripe + price = Stripe::Price.retrieve(price_id, { stripe_account: account_id }) + price_type = price.type + mode = price_type == 'recurring' ? 'subscription' : 'payment' + + session_params = { + line_items: [ + { + price: price_id, + quantity: 1, + }, + ], + mode: mode, + # Defines where Stripe will redirect a customer after successful payment + success_url: "#{ENV['DOMAIN']}/done?session_id={CHECKOUT_SESSION_ID}", + # Defines where Stripe will redirect if a customer cancels payment + cancel_url: "#{ENV['DOMAIN']}", + } + + # Add Connect-specific parameters based on payment mode + if mode == 'subscription' + session_params[:subscription_data] ||= {} + session_params[:subscription_data][:application_fee_amount] = 123 + else + session_params[:payment_intent_data] = { + application_fee_amount: 123, + } + end + + session = Stripe::Checkout::Session.create(session_params, { + stripe_account: account_id + }) + + # Redirect to the Stripe hosted checkout URL + redirect session.url, 303 +end + + + +# Create a billing portal session +post "/api/create-portal-session" do + # Get the Stripe customer we previously created + # Normally you'd fetch this from your database based on the authenticated user + data = parse_request_body + session_id = data['session_id'] + checkout_session = Stripe::Checkout::Session.retrieve(session_id) + portal_session = Stripe::BillingPortal::Session.create({ + # Set the customer_account to the V2 Account's ID + customer_account: checkout_session.customer_account, + return_url: "#{ENV['DOMAIN']}/?session_id=#{session_id}", + }) + + # Redirect to the billing portal + redirect portal_session.url, 303 +end + +post "/api/webhook" do + request.body.rewind + payload = request.body.read + + # Replace this endpoint secret with your endpoint's unique secret + # If you are testing with the CLI, find the secret by running 'stripe listen' + # If you are using an endpoint defined with the API or dashboard, look in your webhook settings + # at https://dashboard.stripe.com/webhooks + endpoint_secret = "" + + # Only verify the event if you have an endpoint secret defined. + # Otherwise use the basic event deserialized directly. + if endpoint_secret != "" + signature = request.env["HTTP_STRIPE_SIGNATURE"] + begin + event = Stripe::Webhook.construct_event(payload, signature, endpoint_secret) + rescue => e + puts "⚠️ Webhook signature verification failed. #{e.message}" + halt 400 + end + else + event = JSON.parse(payload, symbolize_names: true) + end + + case event[:type] + when "customer.subscription.trial_will_end" + stripe_object = event[:data][:object] + status = stripe_object[:status] + puts "Subscription status is #{status}." + # handle_subscription_trial_ending(stripe_object) + when "customer.subscription.deleted" + stripe_object = event[:data][:object] + status = stripe_object[:status] + puts "Subscription status is #{status}." + # handle_subscription_deleted(stripe_object) + when "checkout.session.completed" + stripe_object = event[:data][:object] + status = stripe_object[:status] + puts "Checkout Session status is #{status}." + # handle_checkout_session_completed(stripe_object) + when "checkout.session.async_payment_failed" + stripe_object = event[:data][:object] + status = stripe_object[:status] + puts "Checkout Session status is #{status}." + # handle_checkout_session_failed(stripe_object) + else + # Unexpected event type + puts "Unhandled event type #{event[:type]}." + end + + status 200 +end + +post "/api/thin-webhook" do + request.body.rewind + payload = request.body.read + + # Replace this endpoint secret with your endpoint's unique secret + # If you are testing with the CLI, find the secret by running 'stripe listen' + # If you are using an endpoint defined with the API or dashboard, look in your webhook settings + # at https://dashboard.stripe.com/webhooks + thin_endpoint_secret = nil + signature = request.env["HTTP_STRIPE_SIGNATURE"] + begin + event_notif = stripe_client.parse_event_notification(payload, signature, thin_endpoint_secret) + rescue => e + puts "⚠️ Webhook signature verification failed. #{e.message}" + halt 400 + end + + if event_notif.type == "v2.account.created" + stripe_object = event_notif.fetch_related_object + event = event_notif.fetch_event + # handle_v2_account_created(stripe_object) + else + puts "Unhandled event type #{event_notif.type}." + end + status 200 +end + + +# Helper method to parse request body (supports both JSON and form data) +def parse_request_body + request.body.rewind + + if request.content_type&.include?('application/json') + JSON.parse(request.body.read) + else request.content_type&.include?('application/x-www-form-urlencoded') + params.to_h + end +end + +set :port, 4242 +puts "Server running on port 4242"