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>
281 lines
9.2 KiB
TypeScript
281 lines
9.2 KiB
TypeScript
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' }
|
|
});
|
|
}
|
|
}; |