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>
This commit is contained in:
527
reactrebuild0825/tests/checkout-connect.spec.ts
Normal file
527
reactrebuild0825/tests/checkout-connect.spec.ts
Normal file
@@ -0,0 +1,527 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
// Mock Firebase Firestore data for testing
|
||||
const mockOrgData = {
|
||||
id: 'test-org-1',
|
||||
name: 'Test Organization',
|
||||
payment: {
|
||||
stripe: {
|
||||
accountId: 'acct_test_123',
|
||||
chargesEnabled: true,
|
||||
detailsSubmitted: true,
|
||||
businessName: 'Test Business'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const mockEventData = {
|
||||
id: 'test-event-1',
|
||||
orgId: 'test-org-1',
|
||||
name: 'Test Concert 2024',
|
||||
startAt: new Date('2024-08-01T19:00:00Z'),
|
||||
endAt: new Date('2024-08-01T23:00:00Z'),
|
||||
location: 'Test Venue, Denver CO',
|
||||
status: 'published'
|
||||
};
|
||||
|
||||
const mockTicketTypeData = {
|
||||
id: 'test-ticket-type-1',
|
||||
orgId: 'test-org-1',
|
||||
eventId: 'test-event-1',
|
||||
name: 'General Admission',
|
||||
priceCents: 5000, // $50.00
|
||||
currency: 'USD',
|
||||
inventory: 100,
|
||||
sold: 3
|
||||
};
|
||||
|
||||
const mockSessionId = 'cs_test_session_123';
|
||||
const mockQrCode = '550e8400-e29b-41d4-a716-446655440000';
|
||||
|
||||
test.describe('Stripe Checkout Connected Accounts Flow', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Mock network requests to Firebase Functions
|
||||
await page.route('**/createCheckout', async (route) => {
|
||||
const request = await route.request().postDataJSON();
|
||||
|
||||
if (request.qty > mockTicketTypeData.inventory - mockTicketTypeData.sold) {
|
||||
await route.fulfill({
|
||||
status: 400,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
error: `Not enough tickets available. Requested: ${request.qty}, Available: ${mockTicketTypeData.inventory - mockTicketTypeData.sold}`
|
||||
})
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
url: `https://checkout.stripe.com/c/pay/${mockSessionId}`,
|
||||
sessionId: mockSessionId
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
await page.route('**/getOrder', async (route) => {
|
||||
const request = await route.request().postDataJSON();
|
||||
|
||||
if (request.sessionId === mockSessionId) {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
id: mockSessionId,
|
||||
orgId: mockOrgData.id,
|
||||
eventId: mockEventData.id,
|
||||
ticketTypeId: mockTicketTypeData.id,
|
||||
qty: 2,
|
||||
status: 'paid',
|
||||
totalCents: 10000,
|
||||
purchaserEmail: 'test@example.com',
|
||||
eventName: mockEventData.name,
|
||||
ticketTypeName: mockTicketTypeData.name,
|
||||
eventDate: mockEventData.startAt.toISOString(),
|
||||
eventLocation: mockEventData.location,
|
||||
createdAt: new Date().toISOString()
|
||||
})
|
||||
});
|
||||
} else {
|
||||
await route.fulfill({
|
||||
status: 404,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ error: 'Order not found' })
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
await page.route('**/verifyTicket', async (route) => {
|
||||
const request = await route.request().postDataJSON();
|
||||
|
||||
if (request.qr === mockQrCode) {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
valid: true,
|
||||
ticket: {
|
||||
id: 'ticket-123',
|
||||
eventId: mockEventData.id,
|
||||
ticketTypeId: mockTicketTypeData.id,
|
||||
eventName: mockEventData.name,
|
||||
ticketTypeName: mockTicketTypeData.name,
|
||||
status: 'scanned',
|
||||
purchaserEmail: 'test@example.com'
|
||||
}
|
||||
})
|
||||
});
|
||||
} else if (request.qr === 'already-scanned-qr') {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
valid: false,
|
||||
reason: 'already_scanned',
|
||||
scannedAt: new Date().toISOString(),
|
||||
ticket: {
|
||||
id: 'ticket-456',
|
||||
eventId: mockEventData.id,
|
||||
ticketTypeId: mockTicketTypeData.id,
|
||||
status: 'scanned'
|
||||
}
|
||||
})
|
||||
});
|
||||
} else {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
valid: false,
|
||||
reason: 'Ticket not found'
|
||||
})
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Mock Firebase authentication
|
||||
await page.addInitScript(() => {
|
||||
// @ts-ignore
|
||||
window.mockUser = {
|
||||
email: 'test@example.com',
|
||||
organization: {
|
||||
id: 'test-org-1',
|
||||
name: 'Test Organization'
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
test('should successfully create checkout session and redirect to Stripe', async ({ page }) => {
|
||||
// Mock the checkout page with ticket purchase component
|
||||
await page.setContent(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Test Checkout</title>
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
<body>
|
||||
<div id="test-checkout">
|
||||
<h1>Test Concert 2024</h1>
|
||||
<div class="ticket-purchase">
|
||||
<h3>General Admission - $50.00</h3>
|
||||
<input type="number" id="quantity" value="2" min="1" max="10" />
|
||||
<input type="email" id="email" value="test@example.com" />
|
||||
<button id="purchase-btn">Purchase Tickets - $100.00</button>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
document.getElementById('purchase-btn').addEventListener('click', async () => {
|
||||
const qty = parseInt(document.getElementById('quantity').value);
|
||||
const email = document.getElementById('email').value;
|
||||
|
||||
const response = await fetch('/createCheckout', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
orgId: 'test-org-1',
|
||||
eventId: 'test-event-1',
|
||||
ticketTypeId: 'test-ticket-type-1',
|
||||
qty,
|
||||
purchaserEmail: email,
|
||||
successUrl: window.location.origin + '/checkout/success',
|
||||
cancelUrl: window.location.origin + '/checkout/cancel'
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
window.location.href = data.url;
|
||||
} else {
|
||||
const error = await response.json();
|
||||
alert('Error: ' + error.error);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
|
||||
// Verify initial page content
|
||||
await expect(page.locator('h1')).toContainText('Test Concert 2024');
|
||||
await expect(page.locator('#quantity')).toHaveValue('2');
|
||||
await expect(page.locator('#email')).toHaveValue('test@example.com');
|
||||
|
||||
// Test successful checkout creation
|
||||
let redirectUrl = '';
|
||||
page.on('beforeunload', () => {
|
||||
redirectUrl = page.url();
|
||||
});
|
||||
|
||||
// Intercept the redirect to Stripe Checkout
|
||||
await page.route('https://checkout.stripe.com/**', async (route) => {
|
||||
redirectUrl = route.request().url();
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'text/html',
|
||||
body: '<html><body><h1>Stripe Checkout (Mock)</h1></body></html>'
|
||||
});
|
||||
});
|
||||
|
||||
await page.click('#purchase-btn');
|
||||
|
||||
// Should redirect to Stripe Checkout
|
||||
await page.waitForURL(/checkout\.stripe\.com/);
|
||||
expect(redirectUrl).toContain('checkout.stripe.com');
|
||||
expect(redirectUrl).toContain(mockSessionId);
|
||||
});
|
||||
|
||||
test('should handle insufficient inventory gracefully', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<button id="purchase-btn">Purchase 100 Tickets</button>
|
||||
<div id="error-display"></div>
|
||||
<script>
|
||||
document.getElementById('purchase-btn').addEventListener('click', async () => {
|
||||
try {
|
||||
const response = await fetch('/createCheckout', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
orgId: 'test-org-1',
|
||||
eventId: 'test-event-1',
|
||||
ticketTypeId: 'test-ticket-type-1',
|
||||
qty: 100, // More than available (97 available)
|
||||
purchaserEmail: 'test@example.com',
|
||||
successUrl: window.location.origin + '/success',
|
||||
cancelUrl: window.location.origin + '/cancel'
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
document.getElementById('error-display').textContent = error.error;
|
||||
}
|
||||
} catch (err) {
|
||||
document.getElementById('error-display').textContent = 'Network error';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
|
||||
await page.click('#purchase-btn');
|
||||
|
||||
// Should show inventory error
|
||||
await expect(page.locator('#error-display')).toContainText('Not enough tickets available');
|
||||
await expect(page.locator('#error-display')).toContainText('Requested: 100, Available: 97');
|
||||
});
|
||||
|
||||
test('should display order details on success page', async ({ page }) => {
|
||||
await page.goto(`/checkout/success?session_id=${mockSessionId}`);
|
||||
|
||||
// Mock the success page
|
||||
await page.setContent(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<div id="loading">Loading...</div>
|
||||
<div id="success-content" style="display: none;">
|
||||
<h1>Purchase Successful!</h1>
|
||||
<div id="event-name"></div>
|
||||
<div id="ticket-type"></div>
|
||||
<div id="quantity"></div>
|
||||
<div id="email"></div>
|
||||
<div id="total"></div>
|
||||
</div>
|
||||
<div id="error-content" style="display: none;">
|
||||
<h1>Error</h1>
|
||||
<div id="error-message"></div>
|
||||
</div>
|
||||
<script>
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const sessionId = urlParams.get('session_id');
|
||||
|
||||
if (sessionId) {
|
||||
fetch('/getOrder', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ sessionId })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.status === 'paid') {
|
||||
document.getElementById('loading').style.display = 'none';
|
||||
document.getElementById('success-content').style.display = 'block';
|
||||
document.getElementById('event-name').textContent = data.eventName;
|
||||
document.getElementById('ticket-type').textContent = data.ticketTypeName;
|
||||
document.getElementById('quantity').textContent = data.qty + ' tickets';
|
||||
document.getElementById('email').textContent = data.purchaserEmail;
|
||||
document.getElementById('total').textContent = '$' + (data.totalCents / 100).toFixed(2);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
document.getElementById('loading').style.display = 'none';
|
||||
document.getElementById('error-content').style.display = 'block';
|
||||
document.getElementById('error-message').textContent = err.message;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
|
||||
// Should show loading initially
|
||||
await expect(page.locator('#loading')).toBeVisible();
|
||||
|
||||
// Should load order details
|
||||
await expect(page.locator('#success-content')).toBeVisible({ timeout: 3000 });
|
||||
await expect(page.locator('#event-name')).toContainText('Test Concert 2024');
|
||||
await expect(page.locator('#ticket-type')).toContainText('General Admission');
|
||||
await expect(page.locator('#quantity')).toContainText('2 tickets');
|
||||
await expect(page.locator('#email')).toContainText('test@example.com');
|
||||
await expect(page.locator('#total')).toContainText('$100.00');
|
||||
});
|
||||
|
||||
test('should verify valid tickets correctly', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<h1>Ticket Verification</h1>
|
||||
<input type="text" id="qr-input" placeholder="Enter QR code" />
|
||||
<button id="verify-btn">Verify Ticket</button>
|
||||
<div id="result"></div>
|
||||
<script>
|
||||
document.getElementById('verify-btn').addEventListener('click', async () => {
|
||||
const qr = document.getElementById('qr-input').value;
|
||||
|
||||
try {
|
||||
const response = await fetch('/verifyTicket', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ qr })
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
const resultDiv = document.getElementById('result');
|
||||
|
||||
if (result.valid) {
|
||||
resultDiv.innerHTML = \`
|
||||
<div class="success">
|
||||
<h3>✅ Valid Ticket</h3>
|
||||
<p>Event: \${result.ticket.eventName}</p>
|
||||
<p>Type: \${result.ticket.ticketTypeName}</p>
|
||||
<p>Status: \${result.ticket.status}</p>
|
||||
</div>
|
||||
\`;
|
||||
} else {
|
||||
resultDiv.innerHTML = \`
|
||||
<div class="error">
|
||||
<h3>❌ Invalid Ticket</h3>
|
||||
<p>Reason: \${result.reason}</p>
|
||||
</div>
|
||||
\`;
|
||||
}
|
||||
} catch (err) {
|
||||
document.getElementById('result').innerHTML = \`
|
||||
<div class="error">Error: \${err.message}</div>
|
||||
\`;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
|
||||
// Test valid ticket verification
|
||||
await page.fill('#qr-input', mockQrCode);
|
||||
await page.click('#verify-btn');
|
||||
|
||||
await expect(page.locator('#result .success')).toBeVisible();
|
||||
await expect(page.locator('#result')).toContainText('✅ Valid Ticket');
|
||||
await expect(page.locator('#result')).toContainText('Test Concert 2024');
|
||||
await expect(page.locator('#result')).toContainText('General Admission');
|
||||
await expect(page.locator('#result')).toContainText('scanned');
|
||||
});
|
||||
|
||||
test('should handle already scanned tickets', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<input type="text" id="qr-input" />
|
||||
<button id="verify-btn">Verify</button>
|
||||
<div id="result"></div>
|
||||
<script>
|
||||
document.getElementById('verify-btn').addEventListener('click', async () => {
|
||||
const qr = document.getElementById('qr-input').value;
|
||||
const response = await fetch('/verifyTicket', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ qr })
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
const resultDiv = document.getElementById('result');
|
||||
|
||||
if (result.valid) {
|
||||
resultDiv.textContent = 'Valid ticket';
|
||||
} else {
|
||||
resultDiv.textContent = 'Invalid: ' + result.reason;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
|
||||
await page.fill('#qr-input', 'already-scanned-qr');
|
||||
await page.click('#verify-btn');
|
||||
|
||||
await expect(page.locator('#result')).toContainText('Invalid: already_scanned');
|
||||
});
|
||||
|
||||
test('should handle ticket not found', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<input type="text" id="qr-input" />
|
||||
<button id="verify-btn">Verify</button>
|
||||
<div id="result"></div>
|
||||
<script>
|
||||
document.getElementById('verify-btn').addEventListener('click', async () => {
|
||||
const qr = document.getElementById('qr-input').value;
|
||||
const response = await fetch('/verifyTicket', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ qr })
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
document.getElementById('result').textContent = result.valid ? 'Valid' : 'Invalid: ' + result.reason;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
|
||||
await page.fill('#qr-input', 'nonexistent-qr-code');
|
||||
await page.click('#verify-btn');
|
||||
|
||||
await expect(page.locator('#result')).toContainText('Invalid: Ticket not found');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Idempotency and Concurrency Tests', () => {
|
||||
test('should handle duplicate webhook processing gracefully', async ({ page }) => {
|
||||
// This test would be more relevant for backend testing
|
||||
// Here we simulate the frontend behavior when webhook processing is complete
|
||||
|
||||
await page.setContent(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<div id="status">Checking order status...</div>
|
||||
<script>
|
||||
let attempts = 0;
|
||||
const maxAttempts = 3;
|
||||
|
||||
const checkOrder = async () => {
|
||||
attempts++;
|
||||
const response = await fetch('/getOrder', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ sessionId: '${mockSessionId}' })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.status === 'paid') {
|
||||
document.getElementById('status').textContent = 'Order completed successfully';
|
||||
return;
|
||||
}
|
||||
|
||||
if (attempts < maxAttempts) {
|
||||
setTimeout(checkOrder, 1000);
|
||||
} else {
|
||||
document.getElementById('status').textContent = 'Order still processing';
|
||||
}
|
||||
};
|
||||
|
||||
checkOrder();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
|
||||
// Should eventually show completed order
|
||||
await expect(page.locator('#status')).toContainText('Order completed successfully', { timeout: 5000 });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user