Files
blackcanyontickets/reactrebuild0825/functions/lib/stripeConnect.js
dzinesco aa81eb5adb feat: add advanced analytics and territory management system
- Add comprehensive analytics components with export functionality
- Implement territory management with manager performance tracking
- Add seatmap components for venue layout management
- Create customer management features with modal interface
- Add advanced hooks for dashboard flags and territory data
- Implement seat selection and venue management utilities
- Add type definitions for ticketing and seatmap systems

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-26 09:25:10 -06:00

827 lines
32 KiB
JavaScript

"use strict";
const __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.stripeConnectWebhook = exports.createStripeCheckout = exports.stripeWebhook = exports.stripeConnectStatus = exports.stripeConnectStart = exports.stripeRefund = exports.stripe = void 0;
const https_1 = require("firebase-functions/v2/https");
const firestore_1 = require("firebase-admin/firestore");
const stripe_1 = __importDefault(require("stripe"));
const firestore_2 = require("firebase-admin/firestore");
// Initialize Stripe with secret key
exports.stripe = new stripe_1.default(process.env.STRIPE_SECRET_KEY, {
apiVersion: "2024-06-20",
});
const db = (0, firestore_1.getFirestore)();
// Platform fee configuration
const PLATFORM_FEE_BPS = parseInt(process.env.PLATFORM_FEE_BPS || "300"); // Default 3%
const PLATFORM_FEE_FIXED = parseInt(process.env.PLATFORM_FEE_FIXED || "30"); // Default $0.30
function logWithContext(level, message, context) {
const logData = {
timestamp: new Date().toISOString(),
level,
message,
...context
};
console.log(JSON.stringify(logData));
}
// Helper function to validate request
function validateApiRequest(req, allowedMethods) {
if (!allowedMethods.includes(req.method)) {
return false;
}
return true;
}
// Helper function to get app URL from environment
function getAppUrl() {
return process.env.APP_URL || "http://localhost:5173";
}
/**
* POST /api/stripe/connect/start
* Starts the Stripe Connect onboarding flow for an organization
*/
/**
* POST /api/stripe/refund
* Process refunds for tickets with proper organization validation
*/
exports.stripeRefund = (0, https_1.onRequest)({
cors: {
origin: [getAppUrl(), "http://localhost:5173", "https://localhost:5173"],
methods: ["POST"],
allowedHeaders: ["Content-Type", "Authorization"],
},
}, async (req, res) => {
try {
if (!validateApiRequest(req, ["POST"])) {
res.status(405).json({ error: "Method not allowed" });
return;
}
const { orgId, sessionId, paymentIntentId, amount, reason = "requested_by_customer" } = req.body;
if (!orgId || (!sessionId && !paymentIntentId)) {
res.status(400).json({
error: "Missing required fields: orgId and (sessionId or paymentIntentId)"
});
return;
}
logWithContext('info', 'Processing refund request', {
action: 'refund_start',
orgId,
sessionId,
paymentIntentId,
amount,
reason
});
// Get organization to verify connected account
const orgRef = db.collection("orgs").doc(orgId);
const orgDoc = await orgRef.get();
if (!orgDoc.exists) {
res.status(404).json({ error: "Organization not found" });
return;
}
const orgData = orgDoc.data();
const accountId = orgData?.payment?.stripe?.accountId;
if (!accountId) {
res.status(400).json({
error: "Organization does not have a connected Stripe account"
});
return;
}
// Find the order to validate ownership and get payment details
let orderQuery = db.collection("orders").where("orgId", "==", orgId);
if (sessionId) {
orderQuery = orderQuery.where("stripeSessionId", "==", sessionId);
}
else {
orderQuery = orderQuery.where("metadata.paymentIntentId", "==", paymentIntentId);
}
const orderDocs = await orderQuery.get();
if (orderDocs.empty) {
res.status(404).json({ error: "Order not found for this organization" });
return;
}
const orderDoc = orderDocs.docs[0];
const orderData = orderDoc.data();
// Determine payment intent ID and refund amount
const finalPaymentIntentId = paymentIntentId || orderData.metadata?.paymentIntentId;
const refundAmount = amount || orderData.totalAmount;
if (!finalPaymentIntentId) {
res.status(400).json({ error: "Could not determine payment intent ID" });
return;
}
// Create refund with connected account context
const refund = await exports.stripe.refunds.create({
payment_intent: finalPaymentIntentId,
amount: refundAmount,
reason,
metadata: {
orderId: orderData.id,
orgId,
eventId: orderData.eventId,
refundedBy: "api" // Could be enhanced with user info
}
}, {
stripeAccount: accountId
});
// Update order status
await orderDoc.ref.update({
status: refundAmount >= orderData.totalAmount ? "refunded" : "partially_refunded",
refunds: firestore_2.FieldValue.arrayUnion({
refundId: refund.id,
amount: refundAmount,
reason,
createdAt: new Date().toISOString()
})
});
// Update ticket statuses if full refund
if (refundAmount >= orderData.totalAmount && orderData.ticketIds) {
const batch = db.batch();
orderData.ticketIds.forEach((ticketId) => {
const ticketRef = db.collection("tickets").doc(ticketId);
batch.update(ticketRef, { status: "refunded" });
});
await batch.commit();
}
logWithContext('info', 'Refund processed successfully', {
action: 'refund_success',
refundId: refund.id,
orgId,
orderId: orderData.id,
amount: refundAmount,
accountId
});
const response = {
refundId: refund.id,
amount: refundAmount,
status: refund.status
};
res.status(200).json(response);
}
catch (error) {
logWithContext('error', 'Refund processing failed', {
action: 'refund_error',
error: error instanceof Error ? error.message : 'Unknown error',
orgId: req.body.orgId
});
res.status(500).json({
error: "Internal server error",
details: error instanceof Error ? error.message : "Unknown error",
});
}
});
exports.stripeConnectStart = (0, https_1.onRequest)({
cors: {
origin: [getAppUrl(), "http://localhost:5173", "https://localhost:5173"],
methods: ["POST"],
allowedHeaders: ["Content-Type", "Authorization"],
},
}, async (req, res) => {
try {
// Validate request method
if (!validateApiRequest(req, ["POST"])) {
res.status(405).json({ error: "Method not allowed" });
return;
}
const { orgId, returnTo } = req.body;
if (!orgId || typeof orgId !== "string") {
res.status(400).json({ error: "orgId is required" });
return;
}
// Get organization document
const orgRef = db.collection("orgs").doc(orgId);
const orgDoc = await orgRef.get();
if (!orgDoc.exists) {
res.status(404).json({ error: "Organization not found" });
return;
}
const orgData = orgDoc.data();
let accountId = orgData?.payment?.stripe?.accountId;
// Create Stripe account if it doesn't exist
if (!accountId) {
const account = await exports.stripe.accounts.create({
type: "express",
country: "US", // Default to US, can be made configurable
email: orgData?.email || undefined,
business_profile: {
name: orgData?.name || `Organization ${orgId}`,
},
});
accountId = account.id;
// Save account ID to Firestore
await orgRef.update({
"payment.provider": "stripe",
"payment.stripe.accountId": accountId,
"payment.connected": false,
});
}
// Create account link for onboarding
const baseUrl = getAppUrl();
const returnUrl = returnTo
? `${baseUrl}${returnTo}?status=connected`
: `${baseUrl}/org/${orgId}/payments?status=connected`;
const refreshUrl = `${baseUrl}/org/${orgId}/payments?status=refresh`;
const accountLink = await exports.stripe.accountLinks.create({
account: accountId,
refresh_url: refreshUrl,
return_url: returnUrl,
type: "account_onboarding",
});
const response = {
url: accountLink.url,
};
res.status(200).json(response);
}
catch (error) {
console.error("Error starting Stripe Connect:", error);
res.status(500).json({
error: "Internal server error",
details: error instanceof Error ? error.message : "Unknown error",
});
}
});
/**
* GET /api/stripe/connect/status?orgId=...
* Gets the current Stripe Connect status for an organization
*/
exports.stripeConnectStatus = (0, https_1.onRequest)({
cors: {
origin: [getAppUrl(), "http://localhost:5173", "https://localhost:5173"],
methods: ["GET"],
allowedHeaders: ["Content-Type", "Authorization"],
},
}, async (req, res) => {
try {
// Validate request method
if (!validateApiRequest(req, ["GET"])) {
res.status(405).json({ error: "Method not allowed" });
return;
}
const {orgId} = req.query;
if (!orgId || typeof orgId !== "string") {
res.status(400).json({ error: "orgId is required" });
return;
}
// Get organization document
const orgRef = db.collection("orgs").doc(orgId);
const orgDoc = await orgRef.get();
if (!orgDoc.exists) {
res.status(404).json({ error: "Organization not found" });
return;
}
const orgData = orgDoc.data();
const accountId = orgData?.payment?.stripe?.accountId;
if (!accountId) {
res.status(404).json({ error: "Stripe account not found for organization" });
return;
}
// Fetch current account status from Stripe
const account = await exports.stripe.accounts.retrieve(accountId);
// Update our Firestore document with latest status
const paymentData = {
provider: "stripe",
connected: account.charges_enabled && account.details_submitted,
stripe: {
accountId: account.id,
detailsSubmitted: account.details_submitted,
chargesEnabled: account.charges_enabled,
businessName: account.business_profile?.name ||
account.settings?.dashboard?.display_name ||
"",
},
};
await orgRef.update({
payment: paymentData,
});
const response = {
payment: paymentData,
};
res.status(200).json(response);
}
catch (error) {
console.error("Error getting Stripe Connect status:", error);
res.status(500).json({
error: "Internal server error",
details: error instanceof Error ? error.message : "Unknown error",
});
}
});
/**
* POST /api/stripe/webhook
* Handles Stripe platform-level webhooks
*/
exports.stripeWebhook = (0, https_1.onRequest)({
cors: false, // Webhooks don't need CORS
}, async (req, res) => {
try {
// Validate request method
if (!validateApiRequest(req, ["POST"])) {
res.status(405).json({ error: "Method not allowed" });
return;
}
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
if (!webhookSecret) {
console.error("Missing STRIPE_WEBHOOK_SECRET environment variable");
res.status(500).json({ error: "Webhook secret not configured" });
return;
}
const sig = req.headers["stripe-signature"];
if (!sig) {
res.status(400).json({ error: "Missing stripe-signature header" });
return;
}
let event;
try {
// Verify webhook signature
event = exports.stripe.webhooks.constructEvent(req.rawBody, sig, webhookSecret);
}
catch (err) {
console.error("Webhook signature verification failed:", err);
res.status(400).json({ error: "Invalid signature" });
return;
}
// Handle the event
switch (event.type) {
case "account.updated": {
const account = event.data.object;
// Find the organization with this account ID
const orgsQuery = await db.collection("orgs")
.where("payment.stripe.accountId", "==", account.id)
.get();
if (orgsQuery.empty) {
console.warn(`No organization found for account ${account.id}`);
break;
}
// Update each organization (should typically be just one)
const batch = db.batch();
orgsQuery.docs.forEach((doc) => {
const updateData = {
connected: account.charges_enabled && account.details_submitted,
stripe: {
accountId: account.id,
detailsSubmitted: account.details_submitted,
chargesEnabled: account.charges_enabled,
businessName: account.business_profile?.name ||
account.settings?.dashboard?.display_name ||
"",
},
};
batch.update(doc.ref, {
"payment.connected": updateData.connected,
"payment.stripe": updateData.stripe,
});
});
await batch.commit();
console.log(`Updated ${orgsQuery.docs.length} organizations for account ${account.id}`);
break;
}
default:
console.log(`Unhandled event type: ${event.type}`);
}
res.status(200).json({ received: true });
}
catch (error) {
console.error("Error handling webhook:", error);
res.status(500).json({
error: "Internal server error",
details: error instanceof Error ? error.message : "Unknown error",
});
}
});
/**
* POST /api/stripe/checkout/create
* Creates a Stripe Checkout session using the organization's connected account
*/
exports.createStripeCheckout = (0, https_1.onRequest)({
cors: {
origin: [getAppUrl(), "http://localhost:5173", "https://localhost:5173"],
methods: ["POST"],
allowedHeaders: ["Content-Type", "Authorization"],
},
}, async (req, res) => {
try {
// Validate request method
if (!validateApiRequest(req, ["POST"])) {
res.status(405).json({ error: "Method not allowed" });
return;
}
const { orgId, eventId, ticketTypeId, quantity, customerEmail, successUrl, cancelUrl, } = req.body;
// Validate required fields
if (!orgId || !eventId || !ticketTypeId || !quantity || quantity < 1) {
res.status(400).json({
error: "Missing required fields: orgId, eventId, ticketTypeId, quantity"
});
return;
}
// Get organization and verify connected account
const orgRef = db.collection("orgs").doc(orgId);
const orgDoc = await orgRef.get();
if (!orgDoc.exists) {
res.status(404).json({ error: "Organization not found" });
return;
}
const orgData = orgDoc.data();
const accountId = orgData?.payment?.stripe?.accountId;
const isConnected = orgData?.payment?.connected;
if (!accountId || !isConnected) {
res.status(400).json({
error: "Organization does not have a connected Stripe account"
});
return;
}
// Get event details for pricing and validation
const eventRef = db.collection("events").doc(eventId);
const eventDoc = await eventRef.get();
if (!eventDoc.exists) {
res.status(404).json({ error: "Event not found" });
return;
}
const eventData = eventDoc.data();
if (eventData?.orgId !== orgId) {
res.status(403).json({ error: "Event does not belong to organization" });
return;
}
// Get ticket type details
const ticketTypeRef = db.collection("ticketTypes").doc(ticketTypeId);
const ticketTypeDoc = await ticketTypeRef.get();
if (!ticketTypeDoc.exists) {
res.status(404).json({ error: "Ticket type not found" });
return;
}
const ticketTypeData = ticketTypeDoc.data();
if (ticketTypeData?.eventId !== eventId) {
res.status(403).json({ error: "Ticket type does not belong to event" });
return;
}
// Calculate pricing (price is stored in cents)
const unitPrice = ticketTypeData.price; // Already in cents
const totalAmount = unitPrice * quantity;
// Calculate platform fee using configurable rates
const platformFee = Math.round(totalAmount * (PLATFORM_FEE_BPS / 10000)) + PLATFORM_FEE_FIXED;
logWithContext('info', 'Creating checkout session', {
action: 'checkout_create_start',
sessionId: 'pending',
accountId,
orgId,
eventId,
ticketTypeId,
quantity,
unitPrice,
totalAmount,
platformFee
});
const baseUrl = getAppUrl();
const defaultSuccessUrl = `${baseUrl}/checkout/success?session_id={CHECKOUT_SESSION_ID}`;
const defaultCancelUrl = `${baseUrl}/checkout/cancel`;
// Create Stripe Checkout Session with connected account
const session = await exports.stripe.checkout.sessions.create({
mode: "payment",
payment_method_types: ["card"],
line_items: [
{
price_data: {
currency: "usd",
product_data: {
name: `${eventData.title} - ${ticketTypeData.name}`,
description: `${quantity} x ${ticketTypeData.name} ticket${quantity > 1 ? "s" : ""} for ${eventData.title}`,
metadata: {
eventId,
ticketTypeId,
},
},
unit_amount: unitPrice,
},
quantity,
},
],
success_url: successUrl || defaultSuccessUrl,
cancel_url: cancelUrl || defaultCancelUrl,
customer_email: customerEmail,
payment_intent_data: {
application_fee_amount: platformFee,
metadata: {
orgId,
eventId,
ticketTypeId,
quantity: quantity.toString(),
unitPrice: unitPrice.toString(),
platformFee: platformFee.toString(),
},
},
metadata: {
orgId,
eventId,
ticketTypeId,
quantity: quantity.toString(),
type: "ticket_purchase",
},
}, {
stripeAccount: accountId, // Use the connected account
});
logWithContext('info', 'Checkout session created successfully', {
action: 'checkout_create_success',
sessionId: session.id,
accountId,
orgId,
eventId,
ticketTypeId,
quantity
});
const response = {
url: session.url,
sessionId: session.id,
};
res.status(200).json(response);
}
catch (error) {
logWithContext('error', 'Failed to create checkout session', {
action: 'checkout_create_error',
error: error instanceof Error ? error.message : 'Unknown error',
orgId: req.body.orgId,
eventId: req.body.eventId,
ticketTypeId: req.body.ticketTypeId
});
res.status(500).json({
error: "Internal server error",
details: error instanceof Error ? error.message : "Unknown error",
});
}
});
/**
* POST /api/stripe/webhook/connect
* Handles Stripe Connect webhooks from connected accounts
* This endpoint receives events from connected accounts, not the platform
*/
exports.stripeConnectWebhook = (0, https_1.onRequest)({
cors: false, // Webhooks don't need CORS
}, async (req, res) => {
try {
// Validate request method
if (!validateApiRequest(req, ["POST"])) {
res.status(405).json({ error: "Method not allowed" });
return;
}
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
if (!webhookSecret) {
console.error("Missing STRIPE_WEBHOOK_SECRET environment variable");
res.status(500).json({ error: "Webhook secret not configured" });
return;
}
const sig = req.headers["stripe-signature"];
if (!sig) {
res.status(400).json({ error: "Missing stripe-signature header" });
return;
}
// Get the connected account ID - check both header and event.account
let stripeAccount = req.headers["stripe-account"];
// Parse event first to potentially get account from event data
let tempEvent;
try {
tempEvent = exports.stripe.webhooks.constructEvent(req.rawBody, sig, webhookSecret);
// Use event.account if available, fallback to header
stripeAccount = tempEvent.account || stripeAccount;
}
catch (err) {
console.error("Initial webhook signature verification failed:", err);
res.status(400).json({ error: "Invalid signature" });
return;
}
if (!stripeAccount) {
res.status(400).json({ error: "Missing stripe-account identifier" });
return;
}
// Use the pre-verified event
const event = tempEvent;
logWithContext('info', 'Received connect webhook', {
action: 'webhook_received',
eventType: event.type,
accountId: stripeAccount,
eventId: event.id
});
// Handle the event
switch (event.type) {
case "checkout.session.completed": {
const session = event.data.object;
if (session.metadata?.type === "ticket_purchase") {
await handleTicketPurchaseCompleted(session, stripeAccount);
}
break;
}
case "payment_intent.succeeded": {
const paymentIntent = event.data.object;
logWithContext('info', 'Payment intent succeeded', {
action: 'payment_succeeded',
paymentIntentId: paymentIntent.id,
accountId: stripeAccount,
amount: paymentIntent.amount
});
break;
}
default:
logWithContext('info', 'Unhandled webhook event type', {
action: 'webhook_unhandled',
eventType: event.type,
accountId: stripeAccount
});
}
res.status(200).json({ received: true });
}
catch (error) {
logWithContext('error', 'Connect webhook processing failed', {
action: 'webhook_error',
error: error instanceof Error ? error.message : 'Unknown error'
});
// Return 200 to Stripe to prevent retries for application errors
res.status(200).json({
received: true,
error: error instanceof Error ? error.message : "Unknown error",
});
}
});
/**
* Handle completed ticket purchase with idempotency and transactional inventory
*/
async function handleTicketPurchaseCompleted(session, stripeAccount) {
const { orgId, eventId, ticketTypeId, quantity, } = session.metadata;
const sessionId = session.id;
const quantityNum = parseInt(quantity);
logWithContext('info', 'Starting ticket purchase processing', {
action: 'ticket_purchase_start',
sessionId,
accountId: stripeAccount,
orgId,
eventId,
ticketTypeId,
quantity: quantityNum
});
// Step 1: Idempotency check using processedSessions collection
const processedSessionRef = db.collection('processedSessions').doc(sessionId);
try {
await db.runTransaction(async (transaction) => {
// Check if session already processed
const processedDoc = await transaction.get(processedSessionRef);
if (processedDoc.exists) {
logWithContext('warn', 'Session already processed - skipping', {
action: 'idempotency_skip',
sessionId,
accountId: stripeAccount,
orgId,
eventId,
ticketTypeId
});
return; // Exit early - session already processed
}
// Mark session as processing (prevents concurrent processing)
transaction.set(processedSessionRef, {
sessionId,
orgId,
eventId,
ticketTypeId,
quantity: quantityNum,
stripeAccount,
processedAt: new Date().toISOString(),
status: 'processing'
});
// Step 2: Transactional inventory check and update
const ticketTypeRef = db.collection('ticketTypes').doc(ticketTypeId);
const ticketTypeDoc = await transaction.get(ticketTypeRef);
if (!ticketTypeDoc.exists) {
throw new Error(`Ticket type ${ticketTypeId} not found`);
}
const ticketTypeData = ticketTypeDoc.data();
const currentInventory = ticketTypeData.inventory || 0;
const currentSold = ticketTypeData.sold || 0;
// Check for overselling
if (currentInventory < quantityNum) {
logWithContext('error', 'Insufficient inventory - sold out', {
action: 'inventory_sold_out',
sessionId,
accountId: stripeAccount,
orgId,
eventId,
ticketTypeId,
requestedQuantity: quantityNum,
availableInventory: currentInventory
});
throw new Error('SOLD_OUT');
}
// Update inventory atomically
transaction.update(ticketTypeRef, {
inventory: currentInventory - quantityNum,
sold: currentSold + quantityNum,
lastSaleDate: new Date().toISOString()
});
// Step 3: Generate and save tickets
const customerEmail = session.customer_details?.email || session.customer_email;
if (!customerEmail) {
throw new Error('No customer email found in session');
}
const tickets = [];
const ticketIds = [];
for (let i = 0; i < quantityNum; i++) {
// Use crypto-strong ticket ID generation
const ticketId = `ticket_${Date.now()}_${Math.random().toString(36).substr(2, 12)}_${i}`;
ticketIds.push(ticketId);
const ticket = {
id: ticketId,
eventId,
ticketTypeId,
orgId,
customerEmail,
customerName: session.customer_details?.name || '',
purchaseDate: new Date().toISOString(),
status: 'active',
qrCode: ticketId, // Use ticket ID as QR code
stripeSessionId: sessionId,
stripeAccount,
metadata: {
paymentIntentId: session.payment_intent,
amountPaid: session.amount_total,
currency: session.currency
}
};
tickets.push(ticket);
// Add ticket to transaction
const ticketRef = db.collection('tickets').doc(ticketId);
transaction.set(ticketRef, ticket);
}
// Step 4: Create order record
const orderId = `order_${Date.now()}_${Math.random().toString(36).substr(2, 12)}`;
const orderRef = db.collection('orders').doc(orderId);
transaction.set(orderRef, {
id: orderId,
orgId,
eventId,
ticketTypeId,
customerEmail,
customerName: session.customer_details?.name || '',
quantity: quantityNum,
totalAmount: session.amount_total,
currency: session.currency,
status: 'completed',
createdAt: new Date().toISOString(),
stripeSessionId: sessionId,
stripeAccount,
ticketIds
});
// Step 5: Mark session as completed
transaction.update(processedSessionRef, {
status: 'completed',
orderId,
ticketIds,
completedAt: new Date().toISOString()
});
logWithContext('info', 'Ticket purchase completed successfully', {
action: 'ticket_purchase_success',
sessionId,
accountId: stripeAccount,
orgId,
eventId,
ticketTypeId,
quantity: quantityNum,
orderId,
ticketCount: tickets.length
});
// TODO: Send confirmation email with tickets
// This would typically use a service like Resend or SendGrid
console.log(`Would send confirmation email to ${customerEmail} with ${tickets.length} tickets`);
});
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
logWithContext('error', 'Ticket purchase processing failed', {
action: 'ticket_purchase_error',
sessionId,
accountId: stripeAccount,
orgId,
eventId,
ticketTypeId,
error: errorMessage
});
// For sold out scenario, mark session as failed but don't throw
if (errorMessage === 'SOLD_OUT') {
try {
await processedSessionRef.set({
sessionId,
orgId,
eventId,
ticketTypeId,
quantity: quantityNum,
stripeAccount,
processedAt: new Date().toISOString(),
status: 'failed',
error: 'SOLD_OUT',
failedAt: new Date().toISOString()
});
}
catch (markError) {
logWithContext('error', 'Failed to mark session as failed', {
action: 'mark_session_failed_error',
sessionId,
error: markError instanceof Error ? markError.message : 'Unknown error'
});
}
return; // Don't throw - webhook should return 200
}
throw error; // Re-throw for other errors
}
}
// # sourceMappingURL=stripeConnect.js.map