feat: Complete platform enhancement with multi-tenant architecture
Major additions: - Territory manager system with application workflow - Custom pricing and page builder with Craft.js - Enhanced Stripe Connect onboarding - CodeReadr QR scanning integration - Kiosk mode for venue sales - Super admin dashboard and analytics - MCP integration for AI-powered operations Infrastructure improvements: - Centralized API client and routing system - Enhanced authentication with organization context - Comprehensive theme management system - Advanced event management with custom tabs - Performance monitoring and accessibility features Database schema updates: - Territory management tables - Custom pages and pricing structures - Kiosk PIN system - Enhanced organization profiles - CodeReadr integration tables 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
281
src/pages/api/stripe/onboarding-url.ts
Normal file
281
src/pages/api/stripe/onboarding-url.ts
Normal file
@@ -0,0 +1,281 @@
|
||||
export const prerender = false;
|
||||
|
||||
import type { APIRoute } from 'astro';
|
||||
import { stripe } from '../../../lib/stripe';
|
||||
import { supabase, supabaseAdmin } from '../../../lib/supabase';
|
||||
import { verifyAuth } from '../../../lib/auth';
|
||||
import { logUserActivity } from '../../../lib/logger';
|
||||
|
||||
export const POST: APIRoute = async ({ request, url }) => {
|
||||
try {
|
||||
console.log('Starting hosted onboarding URL generation...');
|
||||
|
||||
// Verify authentication
|
||||
const authContext = await verifyAuth(request);
|
||||
if (!authContext) {
|
||||
console.error('Hosted onboarding failed: Unauthorized request');
|
||||
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
const { user } = authContext;
|
||||
|
||||
// Get user's organization ID first (using admin client to bypass RLS)
|
||||
const { data: userData, error: userError } = await (supabaseAdmin || supabase)
|
||||
.from('users')
|
||||
.select('organization_id')
|
||||
.eq('id', user.id)
|
||||
.single();
|
||||
|
||||
if (userError || !userData?.organization_id) {
|
||||
console.error('Hosted onboarding failed: Organization not found', {
|
||||
userId: user.id,
|
||||
userEmail: user.email,
|
||||
error: userError
|
||||
});
|
||||
return new Response(JSON.stringify({ error: 'Organization not found' }), {
|
||||
status: 404,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
// Now get the organization details
|
||||
const { data: organization, error: orgError } = await (supabaseAdmin || supabase)
|
||||
.from('organizations')
|
||||
.select('*')
|
||||
.eq('id', userData.organization_id)
|
||||
.single();
|
||||
|
||||
if (orgError || !organization) {
|
||||
console.error('Hosted onboarding failed: Organization details not found', {
|
||||
userId: user.id,
|
||||
organizationId: userData.organization_id,
|
||||
error: orgError
|
||||
});
|
||||
return new Response(JSON.stringify({ error: 'Organization details not found' }), {
|
||||
status: 404,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
// Check if Stripe is properly initialized
|
||||
if (!stripe) {
|
||||
console.error('Hosted onboarding failed: Stripe not configured', {
|
||||
userId: user.id,
|
||||
organizationId: organization.id,
|
||||
envVars: {
|
||||
hasSecretKey: !!process.env.STRIPE_SECRET_KEY,
|
||||
hasPublishableKey: !!process.env.PUBLIC_STRIPE_PUBLISHABLE_KEY
|
||||
}
|
||||
});
|
||||
return new Response(JSON.stringify({ error: 'Stripe not configured' }), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
// Check if organization is approved for Stripe onboarding
|
||||
if (organization.account_status !== 'approved') {
|
||||
console.warn('Hosted onboarding blocked: Organization not approved', {
|
||||
userId: user.id,
|
||||
organizationId: organization.id,
|
||||
organizationName: organization.name,
|
||||
currentStatus: organization.account_status
|
||||
});
|
||||
return new Response(JSON.stringify({
|
||||
error: 'Organization must be approved before starting Stripe onboarding',
|
||||
account_status: organization.account_status
|
||||
}), {
|
||||
status: 403,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
let stripeAccountId = organization.stripe_account_id;
|
||||
|
||||
// Create Stripe account if it doesn't exist
|
||||
if (!stripeAccountId) {
|
||||
console.log('Creating new Stripe Express account for hosted onboarding', {
|
||||
userId: user.id,
|
||||
organizationId: organization.id,
|
||||
organizationName: organization.name,
|
||||
businessType: organization.business_type,
|
||||
country: organization.country || 'US'
|
||||
});
|
||||
|
||||
const account = await stripe.accounts.create({
|
||||
type: 'express',
|
||||
country: organization.country || 'US',
|
||||
email: user.email,
|
||||
business_type: organization.business_type === 'company' ? 'company' : 'individual',
|
||||
capabilities: {
|
||||
card_payments: { requested: true },
|
||||
transfers: { requested: true }
|
||||
},
|
||||
business_profile: {
|
||||
name: organization.name,
|
||||
url: organization.website_url || undefined,
|
||||
support_email: user.email,
|
||||
support_phone: organization.phone_number || undefined,
|
||||
mcc: '7922' // Event ticket agencies
|
||||
},
|
||||
...(organization.business_type === 'company' && {
|
||||
company: {
|
||||
name: organization.name,
|
||||
phone: organization.phone_number || undefined,
|
||||
...(organization.address_line1 && {
|
||||
address: {
|
||||
line1: organization.address_line1,
|
||||
line2: organization.address_line2 || undefined,
|
||||
city: organization.city || undefined,
|
||||
state: organization.state || undefined,
|
||||
postal_code: organization.postal_code || undefined,
|
||||
country: organization.country || 'US'
|
||||
}
|
||||
})
|
||||
}
|
||||
}),
|
||||
...(organization.business_type === 'individual' && {
|
||||
individual: {
|
||||
email: user.email,
|
||||
phone: organization.phone_number || undefined,
|
||||
...(organization.address_line1 && {
|
||||
address: {
|
||||
line1: organization.address_line1,
|
||||
line2: organization.address_line2 || undefined,
|
||||
city: organization.city || undefined,
|
||||
state: organization.state || undefined,
|
||||
postal_code: organization.postal_code || undefined,
|
||||
country: organization.country || 'US'
|
||||
}
|
||||
})
|
||||
}
|
||||
}),
|
||||
settings: {
|
||||
payouts: {
|
||||
schedule: {
|
||||
interval: 'daily'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
stripeAccountId = account.id;
|
||||
|
||||
// Update organization with Stripe account ID
|
||||
const { error: updateError } = await (supabaseAdmin || supabase)
|
||||
.from('organizations')
|
||||
.update({
|
||||
stripe_account_id: account.id,
|
||||
stripe_onboarding_status: 'in_progress',
|
||||
stripe_details_submitted: false,
|
||||
stripe_charges_enabled: false,
|
||||
stripe_payouts_enabled: false
|
||||
})
|
||||
.eq('id', organization.id);
|
||||
|
||||
if (updateError) {
|
||||
console.error('Failed to update organization with Stripe account ID', {
|
||||
organizationId: organization.id,
|
||||
stripeAccountId: account.id,
|
||||
error: updateError
|
||||
});
|
||||
// Don't fail the request, just log the error
|
||||
}
|
||||
|
||||
// Log the successful account creation
|
||||
await logUserActivity({
|
||||
userId: user.id,
|
||||
action: 'stripe_account_created',
|
||||
resourceType: 'organization',
|
||||
resourceId: organization.id,
|
||||
details: {
|
||||
stripe_account_id: account.id,
|
||||
business_type: organization.business_type,
|
||||
country: organization.country || 'US',
|
||||
onboarding_type: 'hosted'
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Stripe account created successfully for hosted onboarding', {
|
||||
userId: user.id,
|
||||
organizationId: organization.id,
|
||||
stripeAccountId: account.id
|
||||
});
|
||||
}
|
||||
|
||||
// Generate the hosted onboarding URL
|
||||
const baseUrl = url.origin;
|
||||
const returnUrl = `${baseUrl}/dashboard?stripe_onboarding=completed`;
|
||||
const refreshUrl = `${baseUrl}/onboarding/stripe`;
|
||||
|
||||
console.log('Creating hosted onboarding account link', {
|
||||
stripeAccountId,
|
||||
returnUrl,
|
||||
refreshUrl
|
||||
});
|
||||
|
||||
const accountLink = await stripe.accountLinks.create({
|
||||
account: stripeAccountId,
|
||||
refresh_url: refreshUrl,
|
||||
return_url: returnUrl,
|
||||
type: 'account_onboarding',
|
||||
collect: 'eventually_due'
|
||||
});
|
||||
|
||||
// Log the successful link generation
|
||||
await logUserActivity({
|
||||
userId: user.id,
|
||||
action: 'stripe_hosted_onboarding_started',
|
||||
resourceType: 'organization',
|
||||
resourceId: organization.id,
|
||||
details: {
|
||||
stripe_account_id: stripeAccountId,
|
||||
return_url: returnUrl,
|
||||
refresh_url: refreshUrl
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Hosted onboarding URL generated successfully', {
|
||||
userId: user.id,
|
||||
organizationId: organization.id,
|
||||
stripeAccountId,
|
||||
url: accountLink.url
|
||||
});
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
onboarding_url: accountLink.url,
|
||||
stripe_account_id: stripeAccountId,
|
||||
return_url: returnUrl,
|
||||
expires_at: accountLink.expires_at
|
||||
}), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Hosted onboarding URL generation error:', error);
|
||||
console.error('Error details:', {
|
||||
message: error instanceof Error ? error.message : 'Unknown error',
|
||||
type: error?.constructor?.name,
|
||||
raw: error
|
||||
});
|
||||
|
||||
// Check if it's a Stripe error with specific details
|
||||
if (error && typeof error === 'object' && 'type' in error) {
|
||||
console.error('Stripe error type:', error.type);
|
||||
console.error('Stripe error code:', error.code);
|
||||
console.error('Stripe error param:', error.param);
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
error: 'Failed to generate onboarding URL',
|
||||
details: error instanceof Error ? error.message : 'Unknown error'
|
||||
}), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user