Files
blackcanyontickets/reactrebuild0825/tests/refunds-disputes.spec.ts
dzinesco d5c3953888 fix(typescript): resolve build errors and improve type safety
- Fix billing components ConnectError type compatibility with exactOptionalPropertyTypes
- Update Select component usage to match proper API (options vs children)
- Remove unused imports and fix optional property assignments in system components
- Resolve duplicate Order/Ticket type definitions and add null safety checks
- Handle optional branding properties correctly in organization features
- Add window property type declarations for test environment
- Fix Playwright API usage (page.setOffline → page.context().setOffline)
- Clean up unused imports, variables, and parameters across codebase
- Add comprehensive global type declarations for test window extensions

Resolves major TypeScript compilation issues and improves type safety throughout the application.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-22 13:31:19 -06:00

1102 lines
41 KiB
TypeScript

import { test, expect } from '@playwright/test';
// Mock data removed - was unused
const mockLedgerEntries = [
{
id: 'ledger-1',
orgId: 'test-org-1',
eventId: 'test-event-1',
orderId: 'test-order-1',
type: 'sale',
amountCents: 15000,
currency: 'USD',
createdAt: new Date().toISOString(),
stripe: {
balanceTxnId: 'txn_mock_1',
chargeId: 'ch_mock_1',
accountId: 'acct_mock_1',
},
},
{
id: 'ledger-2',
orgId: 'test-org-1',
eventId: 'test-event-1',
orderId: 'test-order-1',
type: 'platform_fee',
amountCents: 450,
currency: 'USD',
createdAt: new Date().toISOString(),
stripe: {
balanceTxnId: 'txn_mock_1',
chargeId: 'ch_mock_1',
accountId: 'acct_mock_1',
},
},
{
id: 'ledger-3',
orgId: 'test-org-1',
eventId: 'test-event-1',
orderId: 'test-order-1',
type: 'fee',
amountCents: -465,
currency: 'USD',
createdAt: new Date().toISOString(),
stripe: {
balanceTxnId: 'txn_mock_1',
chargeId: 'ch_mock_1',
accountId: 'acct_mock_1',
},
},
];
test.describe('Refunds and Disputes System', () => {
test.beforeEach(async ({ page }) => {
// Mock network requests for refunds API
await page.route('**/createRefund', async (route) => {
const request = await route.request().postDataJSON();
// Simulate validation errors
if (!request.orderId) {
await route.fulfill({
status: 400,
contentType: 'application/json',
body: JSON.stringify({ error: 'orderId is required' })
});
return;
}
if (request.amountCents && request.amountCents > 15000) {
await route.fulfill({
status: 400,
contentType: 'application/json',
body: JSON.stringify({
error: 'Invalid refund amount: exceeds order total',
details: `Refund amount ${request.amountCents} exceeds order total 15000`
})
});
return;
}
// Simulate successful refund
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
refundId: 'refund_mock_123',
stripeRefundId: 're_mock_123',
amountCents: request.amountCents || 15000,
status: 'succeeded'
})
});
});
// Mock get order refunds API
await page.route('**/getOrderRefunds', async (route) => {
const request = await route.request().postDataJSON();
if (request.orderId === 'test-order-1') {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
refunds: [
{
id: 'refund_mock_123',
amountCents: 7500,
status: 'succeeded',
createdAt: new Date().toISOString(),
reason: 'Customer request'
}
]
})
});
} else {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ refunds: [] })
});
}
});
// Mock reconciliation API
await page.route('**/getReconciliationData', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
summary: {
grossSales: 15000,
refunds: 0,
stripeFees: 465,
platformFees: 450,
disputeFees: 0,
netToOrganizer: 14085,
totalTransactions: 1,
period: {
start: '2024-01-01',
end: '2024-12-31'
}
},
entries: mockLedgerEntries,
total: mockLedgerEntries.length
})
});
});
// Mock events API for reconciliation
await page.route('**/getReconciliationEvents', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
events: [
{
id: 'test-event-1',
name: 'Test Concert 2024',
startAt: '2024-08-01T19:00:00Z'
}
]
})
});
});
// Mock dispute API
await page.route('**/getOrderDisputes', async (route) => {
const request = await route.request().postDataJSON();
if (request.orderId === 'disputed-order') {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
orderId: 'disputed-order',
dispute: {
disputeId: 'dp_mock_789',
status: 'warning_needs_response',
reason: 'fraudulent',
createdAt: new Date().toISOString()
}
})
});
} else {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
orderId: request.orderId,
dispute: null
})
});
}
});
// Mock Firebase authentication
await page.addInitScript(() => {
// @ts-ignore
window.mockUser = {
email: 'admin@example.com',
organization: {
id: 'test-org-1',
name: 'Test Organization'
},
role: 'admin'
};
});
});
test('should display orders table with refund actions', async ({ page }) => {
// Mock the orders management page
await page.setContent(`
<!DOCTYPE html>
<html>
<head>
<title>Orders Management</title>
<meta charset="utf-8">
</head>
<body>
<div id="orders-container">
<h1>Event Orders</h1>
<div class="order-card" data-order-id="test-order-1">
<div class="order-header">
<h3>Order #test-order-1</h3>
<span class="status-badge paid">Paid</span>
<span class="amount">$150.00</span>
</div>
<div class="order-details">
<p>Customer: test@example.com</p>
<p>Tickets: 2</p>
</div>
<div class="order-actions">
<button id="refund-btn" class="refund-button">Create Refund</button>
<button class="view-details-btn">View Details</button>
</div>
</div>
</div>
<!-- Refund Modal -->
<div id="refund-modal" class="modal" style="display: none;">
<div class="modal-content">
<h2>Create Refund</h2>
<div class="refund-type-selection">
<label>
<input type="radio" name="refundType" value="full" checked>
Full Order Refund ($150.00)
</label>
<label>
<input type="radio" name="refundType" value="partial">
Custom Amount
</label>
<label>
<input type="radio" name="refundType" value="tickets">
Specific Tickets
</label>
</div>
<div id="custom-amount" style="display: none;">
<label>Refund Amount:</label>
<input type="number" id="refund-amount" step="0.01" max="150.00" placeholder="0.00">
</div>
<div id="ticket-selection" style="display: none;">
<h4>Select Tickets:</h4>
<label>
<input type="checkbox" data-ticket-id="ticket-1" data-amount="75.00">
General Admission - $75.00 (issued)
</label>
<label>
<input type="checkbox" data-ticket-id="ticket-2" data-amount="75.00">
General Admission - $75.00 (scanned)
</label>
</div>
<div class="refund-summary">
<div id="refund-total">Refund Amount: $150.00</div>
</div>
<div class="reason-input">
<label>Reason (Optional):</label>
<input type="text" id="refund-reason" placeholder="Reason for refund...">
</div>
<div class="modal-actions">
<button id="cancel-refund">Cancel</button>
<button id="confirm-refund">Create Refund</button>
</div>
<div id="refund-result" style="display: none;"></div>
</div>
</div>
<script>
const refundBtn = document.getElementById('refund-btn');
const modal = document.getElementById('refund-modal');
const cancelBtn = document.getElementById('cancel-refund');
const confirmBtn = document.getElementById('confirm-refund');
const customAmountDiv = document.getElementById('custom-amount');
const ticketSelectionDiv = document.getElementById('ticket-selection');
const refundTotalDiv = document.getElementById('refund-total');
const resultDiv = document.getElementById('refund-result');
let selectedRefundType = 'full';
let refundAmount = 15000; // $150.00 in cents
refundBtn.addEventListener('click', () => {
modal.style.display = 'block';
});
cancelBtn.addEventListener('click', () => {
modal.style.display = 'none';
resultDiv.style.display = 'none';
resultDiv.className = '';
});
// Handle refund type changes
document.querySelectorAll('input[name="refundType"]').forEach(radio => {
radio.addEventListener('change', (e) => {
selectedRefundType = e.target.value;
customAmountDiv.style.display = selectedRefundType === 'partial' ? 'block' : 'none';
ticketSelectionDiv.style.display = selectedRefundType === 'tickets' ? 'block' : 'none';
updateRefundAmount();
});
});
// Handle custom amount input
document.getElementById('refund-amount').addEventListener('input', updateRefundAmount);
// Handle ticket selection
document.querySelectorAll('input[type="checkbox"][data-ticket-id]').forEach(checkbox => {
checkbox.addEventListener('change', updateRefundAmount);
});
function updateRefundAmount() {
if (selectedRefundType === 'full') {
refundAmount = 15000;
} else if (selectedRefundType === 'partial') {
const customAmount = parseFloat(document.getElementById('refund-amount').value) || 0;
refundAmount = Math.round(customAmount * 100);
} else if (selectedRefundType === 'tickets') {
refundAmount = 0;
document.querySelectorAll('input[type="checkbox"][data-ticket-id]:checked').forEach(checkbox => {
refundAmount += Math.round(parseFloat(checkbox.dataset.amount) * 100);
});
}
refundTotalDiv.textContent = 'Refund Amount: $' + (refundAmount / 100).toFixed(2);
}
confirmBtn.addEventListener('click', async () => {
const reason = document.getElementById('refund-reason').value;
const refundRequest = {
orderId: 'test-order-1',
reason: reason || undefined
};
if (selectedRefundType === 'partial' || (selectedRefundType === 'tickets' &&
document.querySelectorAll('input[type="checkbox"][data-ticket-id]:checked').length > 1)) {
refundRequest.amountCents = refundAmount;
} else if (selectedRefundType === 'tickets') {
const selectedTicket = document.querySelector('input[type="checkbox"][data-ticket-id]:checked');
if (selectedTicket) {
refundRequest.ticketId = selectedTicket.dataset.ticketId;
}
}
try {
const response = await fetch('/createRefund', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(refundRequest)
});
const result = await response.json();
if (response.ok) {
resultDiv.innerHTML = '<p class="success">Refund created successfully: ' + result.refundId + '</p>';
resultDiv.className = 'success';
confirmBtn.disabled = true;
} else {
resultDiv.innerHTML = '<p class="error">Error: ' + result.error + '</p>';
resultDiv.className = 'error';
}
resultDiv.style.display = 'block';
} catch (error) {
resultDiv.innerHTML = '<p class="error">Network error: ' + error.message + '</p>';
resultDiv.className = 'error';
resultDiv.style.display = 'block';
}
});
</script>
<style>
.modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 1000; }
.modal-content { position: relative; margin: 50px auto; width: 500px; background: white; padding: 20px; border-radius: 8px; }
.refund-type-selection label { display: block; margin: 10px 0; }
.modal-actions { margin-top: 20px; text-align: right; }
.modal-actions button { margin-left: 10px; padding: 8px 16px; }
#refund-result.success { color: green; }
#refund-result.error { color: red; }
.order-card { border: 1px solid #ddd; padding: 16px; margin: 16px 0; border-radius: 8px; }
.status-badge.paid { background: green; color: white; padding: 4px 8px; border-radius: 4px; }
.refund-button { background: #f39c12; color: white; padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; }
</style>
</body>
</html>
`);
// Verify initial page content
await expect(page.locator('h1')).toContainText('Event Orders');
await expect(page.locator('.order-card')).toBeVisible();
await expect(page.locator('.status-badge.paid')).toContainText('Paid');
await expect(page.locator('.amount')).toContainText('$150.00');
// Click refund button to open modal
await page.click('#refund-btn');
await expect(page.locator('#refund-modal')).toBeVisible();
await expect(page.locator('#refund-modal h2')).toContainText('Create Refund');
// Test full refund (default)
await expect(page.locator('input[value="full"]')).toBeChecked();
await expect(page.locator('#refund-total')).toContainText('$150.00');
// Create full refund
await page.click('#confirm-refund');
// Verify success message
await expect(page.locator('#refund-result')).toBeVisible();
await expect(page.locator('#refund-result')).toContainText('Refund created successfully');
await expect(page.locator('#refund-result')).toContainText('refund_mock_123');
});
test('should handle partial refund with custom amount', async ({ page }) => {
await page.setContent(`
<!DOCTYPE html>
<html>
<body>
<button id="open-refund-modal">Open Refund Modal</button>
<div id="refund-modal">
<div class="refund-type-selection">
<label>
<input type="radio" name="refundType" value="full">
Full Order Refund ($150.00)
</label>
<label>
<input type="radio" name="refundType" value="partial">
Custom Amount
</label>
</div>
<div id="custom-amount" style="display: none;">
<label>Refund Amount:</label>
<input type="number" id="refund-amount" step="0.01" max="150.00" placeholder="0.00">
</div>
<div id="refund-total">Refund Amount: $150.00</div>
<button id="confirm-refund">Create Refund</button>
<div id="refund-result" style="display: none;"></div>
</div>
<script>
let selectedRefundType = 'full';
let refundAmount = 15000;
document.querySelectorAll('input[name="refundType"]').forEach(radio => {
radio.addEventListener('change', (e) => {
selectedRefundType = e.target.value;
document.getElementById('custom-amount').style.display =
selectedRefundType === 'partial' ? 'block' : 'none';
updateRefundAmount();
});
});
document.getElementById('refund-amount').addEventListener('input', updateRefundAmount);
function updateRefundAmount() {
if (selectedRefundType === 'full') {
refundAmount = 15000;
} else if (selectedRefundType === 'partial') {
const customAmount = parseFloat(document.getElementById('refund-amount').value) || 0;
refundAmount = Math.round(customAmount * 100);
}
document.getElementById('refund-total').textContent = 'Refund Amount: $' + (refundAmount / 100).toFixed(2);
}
document.getElementById('confirm-refund').addEventListener('click', async () => {
const refundRequest = {
orderId: 'test-order-1',
amountCents: refundAmount
};
try {
const response = await fetch('/createRefund', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(refundRequest)
});
const result = await response.json();
const resultDiv = document.getElementById('refund-result');
if (response.ok) {
resultDiv.innerHTML = 'Success: $' + (result.amountCents / 100).toFixed(2) + ' refunded';
} else {
resultDiv.innerHTML = 'Error: ' + result.error;
}
resultDiv.style.display = 'block';
} catch (error) {
document.getElementById('refund-result').innerHTML = 'Network error: ' + error.message;
document.getElementById('refund-result').style.display = 'block';
}
});
</script>
</body>
</html>
`);
// Select partial refund type
await page.click('input[value="partial"]');
await expect(page.locator('#custom-amount')).toBeVisible();
// Enter custom amount
await page.fill('#refund-amount', '75.50');
await expect(page.locator('#refund-total')).toContainText('$75.50');
// Create partial refund
await page.click('#confirm-refund');
// Verify success
await expect(page.locator('#refund-result')).toContainText('Success: $75.50 refunded');
});
test('should handle ticket-specific refunds', async ({ page }) => {
await page.setContent(`
<!DOCTYPE html>
<html>
<body>
<div class="refund-type-selection">
<label>
<input type="radio" name="refundType" value="full">
Full Order Refund
</label>
<label>
<input type="radio" name="refundType" value="tickets">
Specific Tickets
</label>
</div>
<div id="ticket-selection" style="display: none;">
<h4>Select Tickets:</h4>
<label>
<input type="checkbox" data-ticket-id="ticket-1" data-amount="75.00">
General Admission - $75.00 (issued)
</label>
<label>
<input type="checkbox" data-ticket-id="ticket-2" data-amount="75.00">
General Admission - $75.00 (scanned)
</label>
</div>
<div id="refund-total">Refund Amount: $150.00</div>
<button id="confirm-refund">Create Refund</button>
<div id="refund-result" style="display: none;"></div>
<script>
let selectedRefundType = 'full';
document.querySelectorAll('input[name="refundType"]').forEach(radio => {
radio.addEventListener('change', (e) => {
selectedRefundType = e.target.value;
document.getElementById('ticket-selection').style.display =
selectedRefundType === 'tickets' ? 'block' : 'none';
updateRefundAmount();
});
});
document.querySelectorAll('input[type="checkbox"][data-ticket-id]').forEach(checkbox => {
checkbox.addEventListener('change', updateRefundAmount);
});
function updateRefundAmount() {
let refundAmount = 0;
if (selectedRefundType === 'full') {
refundAmount = 15000;
} else if (selectedRefundType === 'tickets') {
document.querySelectorAll('input[type="checkbox"][data-ticket-id]:checked').forEach(checkbox => {
refundAmount += Math.round(parseFloat(checkbox.dataset.amount) * 100);
});
}
document.getElementById('refund-total').textContent = 'Refund Amount: $' + (refundAmount / 100).toFixed(2);
}
document.getElementById('confirm-refund').addEventListener('click', async () => {
let refundRequest = { orderId: 'test-order-1' };
if (selectedRefundType === 'tickets') {
const selectedTickets = document.querySelectorAll('input[type="checkbox"][data-ticket-id]:checked');
if (selectedTickets.length === 1) {
refundRequest.ticketId = selectedTickets[0].dataset.ticketId;
} else if (selectedTickets.length > 1) {
let totalAmount = 0;
selectedTickets.forEach(ticket => {
totalAmount += Math.round(parseFloat(ticket.dataset.amount) * 100);
});
refundRequest.amountCents = totalAmount;
}
}
try {
const response = await fetch('/createRefund', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(refundRequest)
});
const result = await response.json();
const resultDiv = document.getElementById('refund-result');
if (response.ok) {
resultDiv.innerHTML = 'Ticket refund successful';
} else {
resultDiv.innerHTML = 'Error: ' + result.error;
}
resultDiv.style.display = 'block';
} catch (error) {
document.getElementById('refund-result').innerHTML = 'Error: ' + error.message;
document.getElementById('refund-result').style.display = 'block';
}
});
</script>
</body>
</html>
`);
// Select tickets refund type
await page.click('input[value="tickets"]');
await expect(page.locator('#ticket-selection')).toBeVisible();
// Select first ticket only
await page.click('input[data-ticket-id="ticket-1"]');
await expect(page.locator('#refund-total')).toContainText('$75.00');
// Create single ticket refund
await page.click('#confirm-refund');
await expect(page.locator('#refund-result')).toContainText('Ticket refund successful');
});
test('should validate refund amounts and show errors', async ({ page }) => {
await page.setContent(`
<!DOCTYPE html>
<html>
<body>
<div class="refund-type-selection">
<label>
<input type="radio" name="refundType" value="partial" checked>
Custom Amount
</label>
</div>
<div id="custom-amount">
<label>Refund Amount:</label>
<input type="number" id="refund-amount" step="0.01" max="150.00" placeholder="0.00">
</div>
<div id="refund-total">Refund Amount: $0.00</div>
<button id="confirm-refund">Create Refund</button>
<div id="refund-result" style="display: none;"></div>
<script>
function updateRefundAmount() {
const customAmount = parseFloat(document.getElementById('refund-amount').value) || 0;
const refundAmount = Math.round(customAmount * 100);
document.getElementById('refund-total').textContent = 'Refund Amount: $' + (refundAmount / 100).toFixed(2);
}
document.getElementById('refund-amount').addEventListener('input', updateRefundAmount);
document.getElementById('confirm-refund').addEventListener('click', async () => {
const customAmount = parseFloat(document.getElementById('refund-amount').value) || 0;
const refundAmountCents = Math.round(customAmount * 100);
const refundRequest = {
orderId: 'test-order-1',
amountCents: refundAmountCents
};
try {
const response = await fetch('/createRefund', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(refundRequest)
});
const result = await response.json();
const resultDiv = document.getElementById('refund-result');
if (response.ok) {
resultDiv.innerHTML = 'Success: ' + result.refundId;
} else {
resultDiv.innerHTML = 'Error: ' + result.error;
}
resultDiv.style.display = 'block';
} catch (error) {
document.getElementById('refund-result').innerHTML = 'Error: ' + error.message;
document.getElementById('refund-result').style.display = 'block';
}
});
</script>
</body>
</html>
`);
// Test excessive refund amount
await page.fill('#refund-amount', '200.00');
await page.click('#confirm-refund');
await expect(page.locator('#refund-result')).toContainText('Error: Invalid refund amount: exceeds order total');
});
test('should display reconciliation report with correct calculations', async ({ page }) => {
await page.setContent(`
<!DOCTYPE html>
<html>
<body>
<div id="reconciliation-container">
<h1>Reconciliation Report</h1>
<div class="filters">
<input type="date" id="start-date" value="2024-01-01">
<input type="date" id="end-date" value="2024-12-31">
<select id="event-select">
<option value="all">All Events</option>
</select>
<button id="load-data">Load Data</button>
</div>
<div id="summary-cards" style="display: none;">
<div class="summary-card">
<h3>Gross Sales</h3>
<div id="gross-sales">$0.00</div>
</div>
<div class="summary-card">
<h3>Refunds</h3>
<div id="refunds">$0.00</div>
</div>
<div class="summary-card">
<h3>Stripe Fees</h3>
<div id="stripe-fees">$0.00</div>
</div>
<div class="summary-card">
<h3>Platform Fees</h3>
<div id="platform-fees">$0.00</div>
</div>
<div class="summary-card">
<h3>Net to Organizer</h3>
<div id="net-amount">$0.00</div>
</div>
</div>
<div id="transactions-table" style="display: none;">
<h2>Detailed Breakdown</h2>
<table>
<thead>
<tr>
<th>Date</th>
<th>Type</th>
<th>Amount</th>
<th>Order</th>
</tr>
</thead>
<tbody id="transactions-body">
</tbody>
</table>
</div>
<button id="export-csv" style="display: none;">Export CSV</button>
<div id="loading" style="display: none;">Loading...</div>
</div>
<script>
const formatCurrency = (cents) => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(cents / 100);
};
const formatDate = (dateString) => {
return new Date(dateString).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
};
document.getElementById('load-data').addEventListener('click', async () => {
const loadingDiv = document.getElementById('loading');
const summaryDiv = document.getElementById('summary-cards');
const tableDiv = document.getElementById('transactions-table');
const exportBtn = document.getElementById('export-csv');
loadingDiv.style.display = 'block';
summaryDiv.style.display = 'none';
tableDiv.style.display = 'none';
exportBtn.style.display = 'none';
try {
const response = await fetch('/getReconciliationData', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
orgId: 'test-org-1',
startDate: document.getElementById('start-date').value,
endDate: document.getElementById('end-date').value,
eventId: document.getElementById('event-select').value
})
});
if (response.ok) {
const data = await response.json();
const summary = data.summary;
// Update summary cards
document.getElementById('gross-sales').textContent = formatCurrency(summary.grossSales);
document.getElementById('refunds').textContent = formatCurrency(summary.refunds);
document.getElementById('stripe-fees').textContent = formatCurrency(summary.stripeFees);
document.getElementById('platform-fees').textContent = formatCurrency(summary.platformFees);
document.getElementById('net-amount').textContent = formatCurrency(summary.netToOrganizer);
// Update transactions table
const tbody = document.getElementById('transactions-body');
tbody.innerHTML = '';
data.entries.forEach(entry => {
const row = tbody.insertRow();
row.insertCell().textContent = formatDate(entry.createdAt);
row.insertCell().textContent = entry.type.replace('_', ' ');
row.insertCell().textContent = formatCurrency(entry.amountCents);
row.insertCell().textContent = entry.orderId.slice(-8);
});
summaryDiv.style.display = 'block';
tableDiv.style.display = 'block';
exportBtn.style.display = 'block';
} else {
alert('Failed to load reconciliation data');
}
} catch (error) {
alert('Error: ' + error.message);
} finally {
loadingDiv.style.display = 'none';
}
});
document.getElementById('export-csv').addEventListener('click', () => {
alert('CSV export functionality would trigger download here');
});
</script>
<style>
.summary-card { display: inline-block; margin: 10px; padding: 16px; border: 1px solid #ddd; border-radius: 8px; }
table { width: 100%; border-collapse: collapse; margin-top: 16px; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
.filters { margin: 16px 0; }
.filters > * { margin-right: 8px; }
</style>
</body>
</html>
`);
// Verify initial page content
await expect(page.locator('h1')).toContainText('Reconciliation Report');
// Load reconciliation data
await page.click('#load-data');
// Wait for loading to complete
await expect(page.locator('#loading')).toBeVisible();
await expect(page.locator('#summary-cards')).toBeVisible();
// Verify summary calculations
await expect(page.locator('#gross-sales')).toContainText('$150.00');
await expect(page.locator('#stripe-fees')).toContainText('$4.65');
await expect(page.locator('#platform-fees')).toContainText('$4.50');
await expect(page.locator('#net-amount')).toContainText('$140.85');
// Verify transactions table
await expect(page.locator('#transactions-table')).toBeVisible();
await expect(page.locator('#transactions-body tr')).toHaveCount(3); // 3 ledger entries
// Verify export button appears
await expect(page.locator('#export-csv')).toBeVisible();
});
test('should handle dispute status display', async ({ page }) => {
await page.setContent(`
<!DOCTYPE html>
<html>
<body>
<div class="order-card" data-order-id="disputed-order">
<div class="order-header">
<h3>Order #disputed-order</h3>
<span class="status-badge paid">Paid</span>
</div>
<div id="dispute-alert" style="display: none;" class="dispute-warning">
<h4>Payment Dispute</h4>
<p id="dispute-details"></p>
</div>
<div class="tickets">
<div class="ticket" data-status="locked_dispute">
<span class="ticket-status locked-dispute">Dispute</span>
<span>VIP Access - $100.00</span>
</div>
</div>
<button id="load-dispute-info">Load Dispute Info</button>
</div>
<script>
document.getElementById('load-dispute-info').addEventListener('click', async () => {
try {
const response = await fetch('/getOrderDisputes', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ orderId: 'disputed-order' })
});
if (response.ok) {
const data = await response.json();
if (data.dispute) {
const alertDiv = document.getElementById('dispute-alert');
const detailsP = document.getElementById('dispute-details');
detailsP.textContent = 'Status: ' + data.dispute.status + ' • Reason: ' + data.dispute.reason;
alertDiv.style.display = 'block';
}
}
} catch (error) {
console.error('Failed to load dispute info:', error);
}
});
</script>
<style>
.dispute-warning { background: #fff3cd; border: 1px solid #ffeaa7; padding: 12px; margin: 8px 0; border-radius: 4px; }
.ticket-status.locked-dispute { background: #ffc107; color: #212529; padding: 2px 6px; border-radius: 3px; }
</style>
</body>
</html>
`);
// Load dispute information
await page.click('#load-dispute-info');
// Verify dispute alert appears
await expect(page.locator('#dispute-alert')).toBeVisible();
await expect(page.locator('#dispute-details')).toContainText('Status: warning_needs_response');
await expect(page.locator('#dispute-details')).toContainText('Reason: fraudulent');
// Verify ticket shows dispute status
await expect(page.locator('.ticket-status.locked-dispute')).toContainText('Dispute');
});
test('should validate permission requirements for refunds', async ({ page }) => {
// Mock unauthorized request
await page.route('**/createRefund', async (route) => {
await route.fulfill({
status: 403,
contentType: 'application/json',
body: JSON.stringify({ error: 'Insufficient permissions' })
});
});
await page.setContent(`
<!DOCTYPE html>
<html>
<body>
<button id="create-refund">Create Refund</button>
<div id="result"></div>
<script>
document.getElementById('create-refund').addEventListener('click', async () => {
try {
const response = await fetch('/createRefund', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
orderId: 'test-order-1',
amountCents: 5000
})
});
const result = await response.json();
if (!response.ok) {
document.getElementById('result').textContent = 'Permission denied: ' + result.error;
}
} catch (error) {
document.getElementById('result').textContent = 'Error: ' + error.message;
}
});
</script>
</body>
</html>
`);
await page.click('#create-refund');
await expect(page.locator('#result')).toContainText('Permission denied: Insufficient permissions');
});
});
test.describe('Idempotency and Concurrency', () => {
test('should handle duplicate refund requests gracefully', async ({ page }) => {
let requestCount = 0;
await page.route('**/createRefund', async (route) => {
requestCount++;
if (requestCount === 1) {
// First request - create refund
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
refundId: 'refund_mock_123',
status: 'succeeded',
amountCents: 5000
})
});
} else {
// Subsequent requests - return existing refund
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
refundId: 'refund_mock_123',
status: 'succeeded',
message: 'Refund already exists'
})
});
}
});
await page.setContent(`
<!DOCTYPE html>
<html>
<body>
<button id="create-refund">Create Refund</button>
<div id="results"></div>
<script>
let clickCount = 0;
document.getElementById('create-refund').addEventListener('click', async () => {
clickCount++;
try {
const response = await fetch('/createRefund', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
orderId: 'test-order-1',
amountCents: 5000
})
});
const result = await response.json();
const resultDiv = document.createElement('div');
resultDiv.textContent = 'Click ' + clickCount + ': ' + (result.message || 'Created') + ' - ' + result.refundId;
document.getElementById('results').appendChild(resultDiv);
} catch (error) {
console.error('Error:', error);
}
});
</script>
</body>
</html>
`);
// Click multiple times rapidly
await page.click('#create-refund');
await page.click('#create-refund');
await page.click('#create-refund');
// Verify idempotency handling
const results = page.locator('#results div');
await expect(results).toHaveCount(3);
await expect(results.nth(0)).toContainText('Click 1: Created - refund_mock_123');
await expect(results.nth(1)).toContainText('Click 2: Refund already exists - refund_mock_123');
await expect(results.nth(2)).toContainText('Click 3: Refund already exists - refund_mock_123');
});
});