Files
blackcanyontickets/reactrebuild0825/functions/lib/stripeConnect.test.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

362 lines
15 KiB
JavaScript

"use strict";
const __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) {k2 = k;}
let desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) {k2 = k;}
o[k2] = m[k];
}));
const __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
const __importStar = (this && this.__importStar) || (function () {
let ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
const ar = [];
for (const k in o) {if (Object.prototype.hasOwnProperty.call(o, k)) {ar[ar.length] = k;}}
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) {return mod;}
const result = {};
if (mod != null) {for (let k = ownKeys(mod), i = 0; i < k.length; i++) {if (k[i] !== "default") {__createBinding(result, mod, k[i]);}}}
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
const globals_1 = require("@jest/globals");
const firestore_1 = require("firebase-admin/firestore");
// Mock Firebase Admin
globals_1.jest.mock('firebase-admin/firestore', () => ({
getFirestore: globals_1.jest.fn(),
FieldValue: {
arrayUnion: globals_1.jest.fn((value) => ({ arrayUnion: value }))
}
}));
// Mock Stripe
globals_1.jest.mock('stripe');
(0, globals_1.describe)('Stripe Connect Hardened Implementation', () => {
let mockDb;
let mockTransaction;
let mockStripe;
(0, globals_1.beforeEach)(() => {
// Reset all mocks
globals_1.jest.clearAllMocks();
// Mock Firestore transaction
mockTransaction = {
get: globals_1.jest.fn(),
set: globals_1.jest.fn(),
update: globals_1.jest.fn()
};
// Mock Firestore database
mockDb = {
collection: globals_1.jest.fn(() => ({
doc: globals_1.jest.fn(() => ({
get: globals_1.jest.fn(),
set: globals_1.jest.fn(),
update: globals_1.jest.fn()
})),
where: globals_1.jest.fn(() => ({
get: globals_1.jest.fn()
}))
})),
runTransaction: globals_1.jest.fn((callback) => callback(mockTransaction)),
batch: globals_1.jest.fn(() => ({
set: globals_1.jest.fn(),
update: globals_1.jest.fn(),
commit: globals_1.jest.fn()
}))
};
firestore_1.getFirestore.mockReturnValue(mockDb);
// Mock Stripe
mockStripe = {
webhooks: {
constructEvent: globals_1.jest.fn()
},
refunds: {
create: globals_1.jest.fn()
}
};
});
(0, globals_1.describe)('Idempotency Protection', () => {
(0, globals_1.test)('should skip processing if session already processed', async () => {
// Mock existing processed session
const mockProcessedDoc = {
exists: true,
data: () => ({
sessionId: 'cs_test_123',
status: 'completed',
processedAt: '2024-01-01T00:00:00Z'
})
};
mockTransaction.get.mockResolvedValue(mockProcessedDoc);
const session = {
id: 'cs_test_123',
metadata: {
orgId: 'org_123',
eventId: 'event_123',
ticketTypeId: 'tt_123',
quantity: '2'
},
customer_details: { email: 'test@example.com' },
amount_total: 10000
};
// Import the function under test
const { handleTicketPurchaseCompleted } = await Promise.resolve().then(() => __importStar(require('./stripeConnect')));
await (0, globals_1.expect)(handleTicketPurchaseCompleted(session, 'acct_123')).resolves.not.toThrow();
// Should only check for existing session, not create tickets
(0, globals_1.expect)(mockTransaction.get).toHaveBeenCalledTimes(1);
(0, globals_1.expect)(mockTransaction.set).not.toHaveBeenCalled();
(0, globals_1.expect)(mockTransaction.update).not.toHaveBeenCalled();
});
(0, globals_1.test)('should process new session and mark as processing', async () => {
// Mock non-existing processed session
const mockProcessedDoc = { exists: false };
const mockTicketTypeDoc = {
exists: true,
data: () => ({
inventory: 10,
sold: 5,
price: 5000
})
};
mockTransaction.get
.mockResolvedValueOnce(mockProcessedDoc) // processedSessions check
.mockResolvedValueOnce(mockTicketTypeDoc); // ticketTypes check
const session = {
id: 'cs_test_new',
metadata: {
orgId: 'org_123',
eventId: 'event_123',
ticketTypeId: 'tt_123',
quantity: '2'
},
customer_details: { email: 'test@example.com', name: 'Test User' },
amount_total: 10000,
currency: 'usd',
payment_intent: 'pi_123'
};
const { handleTicketPurchaseCompleted } = await Promise.resolve().then(() => __importStar(require('./stripeConnect')));
await (0, globals_1.expect)(handleTicketPurchaseCompleted(session, 'acct_123')).resolves.not.toThrow();
// Should mark session as processing
(0, globals_1.expect)(mockTransaction.set).toHaveBeenCalledWith(globals_1.expect.any(Object), globals_1.expect.objectContaining({
sessionId: 'cs_test_new',
status: 'processing'
}));
});
});
(0, globals_1.describe)('Inventory Concurrency Control', () => {
(0, globals_1.test)('should prevent overselling with insufficient inventory', async () => {
const mockProcessedDoc = { exists: false };
const mockTicketTypeDoc = {
exists: true,
data: () => ({
inventory: 1, // Only 1 ticket available
sold: 9,
price: 5000
})
};
mockTransaction.get
.mockResolvedValueOnce(mockProcessedDoc)
.mockResolvedValueOnce(mockTicketTypeDoc);
const session = {
id: 'cs_test_oversell',
metadata: {
orgId: 'org_123',
eventId: 'event_123',
ticketTypeId: 'tt_123',
quantity: '3' // Requesting 3 tickets but only 1 available
},
customer_details: { email: 'test@example.com' }
};
const { handleTicketPurchaseCompleted } = await Promise.resolve().then(() => __importStar(require('./stripeConnect')));
await (0, globals_1.expect)(handleTicketPurchaseCompleted(session, 'acct_123')).resolves.not.toThrow(); // Should not throw, but handle gracefully
// Should not create any tickets
(0, globals_1.expect)(mockTransaction.set).toHaveBeenCalledTimes(1); // Only the processing marker
});
(0, globals_1.test)('should update inventory atomically on successful purchase', async () => {
const mockProcessedDoc = { exists: false };
const mockTicketTypeDoc = {
exists: true,
data: () => ({
inventory: 10,
sold: 5,
price: 5000
})
};
mockTransaction.get
.mockResolvedValueOnce(mockProcessedDoc)
.mockResolvedValueOnce(mockTicketTypeDoc);
const session = {
id: 'cs_test_success',
metadata: {
orgId: 'org_123',
eventId: 'event_123',
ticketTypeId: 'tt_123',
quantity: '2'
},
customer_details: { email: 'test@example.com', name: 'Test User' },
amount_total: 10000,
currency: 'usd',
payment_intent: 'pi_123'
};
const { handleTicketPurchaseCompleted } = await Promise.resolve().then(() => __importStar(require('./stripeConnect')));
await (0, globals_1.expect)(handleTicketPurchaseCompleted(session, 'acct_123')).resolves.not.toThrow();
// Should update inventory: 10 - 2 = 8, sold: 5 + 2 = 7
(0, globals_1.expect)(mockTransaction.update).toHaveBeenCalledWith(globals_1.expect.any(Object), globals_1.expect.objectContaining({
inventory: 8,
sold: 7
}));
});
});
(0, globals_1.describe)('Platform Fee Configuration', () => {
(0, globals_1.test)('should calculate platform fee using configurable BPS', () => {
// Mock environment variables
process.env.PLATFORM_FEE_BPS = '250'; // 2.5%
process.env.PLATFORM_FEE_FIXED = '25'; // $0.25
const totalAmount = 10000; // $100.00
// Expected: (10000 * 250 / 10000) + 25 = 250 + 25 = 275 cents
const expectedFee = Math.round(totalAmount * (250 / 10000)) + 25;
(0, globals_1.expect)(expectedFee).toBe(275); // $2.75
});
(0, globals_1.test)('should use default platform fee when env vars not set', () => {
delete process.env.PLATFORM_FEE_BPS;
delete process.env.PLATFORM_FEE_FIXED;
const totalAmount = 10000; // $100.00
// Expected: (10000 * 300 / 10000) + 30 = 300 + 30 = 330 cents
const expectedFee = Math.round(totalAmount * (300 / 10000)) + 30;
(0, globals_1.expect)(expectedFee).toBe(330); // $3.30
});
});
(0, globals_1.describe)('Refund Safety', () => {
(0, globals_1.test)('should validate organization ownership before refund', async () => {
const mockOrgDoc = {
exists: true,
data: () => ({
payment: {
stripe: {
accountId: 'acct_123'
}
}
})
};
const mockOrderDocs = {
empty: false,
docs: [{
ref: { update: globals_1.jest.fn() },
data: () => ({
id: 'order_123',
orgId: 'org_123',
totalAmount: 10000,
metadata: { paymentIntentId: 'pi_123' },
ticketIds: ['ticket_1', 'ticket_2']
})
}]
};
mockDb.collection.mockImplementation((collection) => {
if (collection === 'orgs') {
return {
doc: () => ({
get: () => Promise.resolve(mockOrgDoc)
})
};
}
if (collection === 'orders') {
return {
where: () => ({
where: () => ({
get: () => Promise.resolve(mockOrderDocs)
})
})
};
}
return { doc: () => ({}) };
});
const mockRefund = {
id: 'ref_123',
status: 'succeeded',
amount: 10000
};
mockStripe.refunds.create.mockResolvedValue(mockRefund);
// Test would require importing and calling the refund function
// This demonstrates the validation logic structure
(0, globals_1.expect)(mockOrgDoc.exists).toBe(true);
(0, globals_1.expect)(mockOrderDocs.empty).toBe(false);
});
});
(0, globals_1.describe)('Connect Webhook Account Handling', () => {
(0, globals_1.test)('should extract account ID from event.account property', () => {
const mockEvent = {
id: 'evt_123',
type: 'checkout.session.completed',
account: 'acct_from_event_123',
data: {
object: {
id: 'cs_test_123',
metadata: { type: 'ticket_purchase' }
}
}
};
mockStripe.webhooks.constructEvent.mockReturnValue(mockEvent);
// Test would verify that account ID is correctly extracted from event.account
(0, globals_1.expect)(mockEvent.account).toBe('acct_from_event_123');
});
(0, globals_1.test)('should fallback to stripe-account header when event.account missing', () => {
const mockEvent = {
id: 'evt_123',
type: 'checkout.session.completed',
account: null, // No account in event
data: {
object: {
id: 'cs_test_123',
metadata: { type: 'ticket_purchase' }
}
}
};
mockStripe.webhooks.constructEvent.mockReturnValue(mockEvent);
const mockHeaders = {
'stripe-account': 'acct_from_header_123'
};
// Test would verify that header fallback works
const accountId = mockEvent.account || mockHeaders['stripe-account'];
(0, globals_1.expect)(accountId).toBe('acct_from_header_123');
});
});
(0, globals_1.describe)('Structured Logging', () => {
(0, globals_1.test)('should log with proper context structure', () => {
const consoleSpy = globals_1.jest.spyOn(console, 'log').mockImplementation();
// Mock the logWithContext function behavior
const logContext = {
sessionId: 'cs_test_123',
accountId: 'acct_123',
orgId: 'org_123',
eventId: 'event_123',
action: 'test_action'
};
const expectedLog = {
timestamp: globals_1.expect.any(String),
level: 'info',
message: 'Test message',
...logContext
};
// Test would verify structured logging format
(0, globals_1.expect)(expectedLog).toMatchObject(logContext);
consoleSpy.mockRestore();
});
});
(0, globals_1.afterEach)(() => {
// Clean up environment variables
delete process.env.PLATFORM_FEE_BPS;
delete process.env.PLATFORM_FEE_FIXED;
});
});
// # sourceMappingURL=stripeConnect.test.js.map