// Mock CouchDB service for testing const mockCouchdbService = { createDocument: jest.fn(), findDocumentById: jest.fn(), updateDocument: jest.fn(), findByType: jest.fn(), initialize: jest.fn(), getDocument: jest.fn(), findUserById: jest.fn(), update: jest.fn(), }; // Mock the service module jest.mock('../../services/couchdbService', () => mockCouchdbService); mockCouchdbService.createDocument.mockReset(); mockCouchdbService.findDocumentById.mockReset(); mockCouchdbService.updateDocument.mockReset(); mockCouchdbService.findByType.mockReset(); }); describe('Schema Validation', () => { it('should create a valid point transaction', async () => { const transactionData = { user: 'user_123', points: 50, type: 'earned', description: 'Completed street cleaning task', source: { type: 'task_completion', referenceId: 'task_123' } }; const mockCreated = { _id: 'point_transaction_123', _rev: '1-abc', type: 'point_transaction', ...transactionData, createdAt: '2023-01-01T00:00:00.000Z', updatedAt: '2023-01-01T00:00:00.000Z' }; mockCouchdbService.createDocument.mockResolvedValue(mockCreated); const transaction = await PointTransaction.create(transactionData); expect(transaction._id).toBeDefined(); expect(transaction.user).toBe(transactionData.user); expect(transaction.points).toBe(transactionData.points); expect(transaction.type).toBe(transactionData.type); expect(transaction.description).toBe(transactionData.description); expect(transaction.source.type).toBe(transactionData.source.type); expect(transaction.source.referenceId).toBe(transactionData.source.referenceId); }); it('should require user field', async () => { const transactionData = { points: 50, type: 'earned', description: 'Transaction without user', }; expect(() => new PointTransaction(transactionData)).toThrow(); }); it('should require points field', async () => { const transactionData = { user: 'user_123', type: 'earned', description: 'Transaction without points', }; expect(() => new PointTransaction(transactionData)).toThrow(); }); it('should require type field', async () => { const transactionData = { user: 'user_123', points: 50, description: 'Transaction without type', }; expect(() => new PointTransaction(transactionData)).toThrow(); }); it('should require description field', async () => { const transactionData = { user: 'user_123', points: 50, type: 'earned', }; expect(() => new PointTransaction(transactionData)).toThrow(); }); }); describe('Transaction Types', () => { const validTypes = ['earned', 'spent', 'bonus', 'penalty', 'refund']; validTypes.forEach(type => { it(`should accept "${type}" as valid transaction type`, async () => { const transactionData = { user: 'user_123', points: 50, type, description: `Testing ${type} transaction`, }; const mockCreated = { _id: 'point_transaction_123', _rev: '1-abc', type: 'point_transaction', ...transactionData, createdAt: '2023-01-01T00:00:00.000Z', updatedAt: '2023-01-01T00:00:00.000Z' }; mockCouchdbService.createDocument.mockResolvedValue(mockCreated); const transaction = await PointTransaction.create(transactionData); expect(transaction.type).toBe(type); }); }); it('should reject invalid transaction type', async () => { const transactionData = { user: 'user_123', points: 50, type: 'invalid_type', description: 'Invalid type transaction', }; expect(() => new PointTransaction(transactionData)).toThrow(); }); }); describe('Points Validation', () => { it('should accept positive points for earned transactions', async () => { const transactionData = { user: 'user_123', points: 100, type: 'earned', description: 'Earned points transaction', }; const mockCreated = { _id: 'point_transaction_123', _rev: '1-abc', type: 'point_transaction', ...transactionData, createdAt: '2023-01-01T00:00:00.000Z', updatedAt: '2023-01-01T00:00:00.000Z' }; mockCouchdbService.createDocument.mockResolvedValue(mockCreated); const transaction = await PointTransaction.create(transactionData); expect(transaction.points).toBe(100); }); it('should accept negative points for spent transactions', async () => { const transactionData = { user: 'user_123', points: -50, type: 'spent', description: 'Spent points transaction', }; const mockCreated = { _id: 'point_transaction_123', _rev: '1-abc', type: 'point_transaction', ...transactionData, createdAt: '2023-01-01T00:00:00.000Z', updatedAt: '2023-01-01T00:00:00.000Z' }; mockCouchdbService.createDocument.mockResolvedValue(mockCreated); const transaction = await PointTransaction.create(transactionData); expect(transaction.points).toBe(-50); }); it('should accept positive points for bonus transactions', async () => { const transactionData = { user: 'user_123', points: 25, type: 'bonus', description: 'Bonus points transaction', }; const mockCreated = { _id: 'point_transaction_123', _rev: '1-abc', type: 'point_transaction', ...transactionData, createdAt: '2023-01-01T00:00:00.000Z', updatedAt: '2023-01-01T00:00:00.000Z' }; mockCouchdbService.createDocument.mockResolvedValue(mockCreated); const transaction = await PointTransaction.create(transactionData); expect(transaction.points).toBe(25); }); it('should accept negative points for penalty transactions', async () => { const transactionData = { user: 'user_123', points: -10, type: 'penalty', description: 'Penalty points transaction', }; const mockCreated = { _id: 'point_transaction_123', _rev: '1-abc', type: 'point_transaction', ...transactionData, createdAt: '2023-01-01T00:00:00.000Z', updatedAt: '2023-01-01T00:00:00.000Z' }; mockCouchdbService.createDocument.mockResolvedValue(mockCreated); const transaction = await PointTransaction.create(transactionData); expect(transaction.points).toBe(-10); }); }); describe('Source Information', () => { it('should allow source information', async () => { const transactionData = { user: 'user_123', points: 50, type: 'earned', description: 'Transaction with source', source: { type: 'task_completion', referenceId: 'task_123', additionalInfo: 'Street cleaning task completed' } }; const mockCreated = { _id: 'point_transaction_123', _rev: '1-abc', type: 'point_transaction', ...transactionData, createdAt: '2023-01-01T00:00:00.000Z', updatedAt: '2023-01-01T00:00:00.000Z' }; mockCouchdbService.createDocument.mockResolvedValue(mockCreated); const transaction = await PointTransaction.create(transactionData); expect(transaction.source.type).toBe('task_completion'); expect(transaction.source.referenceId).toBe('task_123'); expect(transaction.source.additionalInfo).toBe('Street cleaning task completed'); }); it('should not require source information', async () => { const transactionData = { user: 'user_123', points: 50, type: 'earned', description: 'Transaction without source', }; const mockCreated = { _id: 'point_transaction_123', _rev: '1-abc', type: 'point_transaction', ...transactionData, createdAt: '2023-01-01T00:00:00.000Z', updatedAt: '2023-01-01T00:00:00.000Z' }; mockCouchdbService.createDocument.mockResolvedValue(mockCreated); const transaction = await PointTransaction.create(transactionData); expect(transaction.source).toBeUndefined(); }); }); describe('Source Types', () => { const validSourceTypes = [ 'task_completion', 'street_adoption', 'event_participation', 'reward_redemption', 'badge_earned', 'manual_adjustment', 'system_bonus' ]; validSourceTypes.forEach(sourceType => { it(`should accept "${sourceType}" as valid source type`, async () => { const transactionData = { user: 'user_123', points: 50, type: 'earned', description: `Testing ${sourceType} source`, source: { type: sourceType, referenceId: 'ref_123' } }; const mockCreated = { _id: 'point_transaction_123', _rev: '1-abc', type: 'point_transaction', ...transactionData, createdAt: '2023-01-01T00:00:00.000Z', updatedAt: '2023-01-01T00:00:00.000Z' }; mockCouchdbService.createDocument.mockResolvedValue(mockCreated); const transaction = await PointTransaction.create(transactionData); expect(transaction.source.type).toBe(sourceType); }); }); }); describe('Relationships', () => { it('should reference user ID', async () => { const transactionData = { user: 'user_123', points: 50, type: 'earned', description: 'User transaction', }; const mockCreated = { _id: 'point_transaction_123', _rev: '1-abc', type: 'point_transaction', ...transactionData, createdAt: '2023-01-01T00:00:00.000Z', updatedAt: '2023-01-01T00:00:00.000Z' }; mockCouchdbService.createDocument.mockResolvedValue(mockCreated); const transaction = await PointTransaction.create(transactionData); expect(transaction.user).toBe('user_123'); }); }); describe('Timestamps', () => { it('should automatically set createdAt and updatedAt', async () => { const transactionData = { user: 'user_123', points: 50, type: 'earned', description: 'Timestamp test transaction', }; const mockCreated = { _id: 'point_transaction_123', _rev: '1-abc', type: 'point_transaction', ...transactionData, createdAt: '2023-01-01T00:00:00.000Z', updatedAt: '2023-01-01T00:00:00.000Z' }; mockCouchdbService.createDocument.mockResolvedValue(mockCreated); const transaction = await PointTransaction.create(transactionData); expect(transaction.createdAt).toBeDefined(); expect(transaction.updatedAt).toBeDefined(); expect(typeof transaction.createdAt).toBe('string'); expect(typeof transaction.updatedAt).toBe('string'); }); it('should update updatedAt on modification', async () => { const transactionData = { user: 'user_123', points: 50, type: 'earned', description: 'Update test transaction', }; const mockTransaction = { _id: 'point_transaction_123', _rev: '1-abc', type: 'point_transaction', ...transactionData, createdAt: '2023-01-01T00:00:00.000Z', updatedAt: '2023-01-01T00:00:00.000Z' }; mockCouchdbService.findDocumentById.mockResolvedValue(mockTransaction); mockCouchdbService.updateDocument.mockResolvedValue({ ...mockTransaction, description: 'Updated transaction description', _rev: '2-def', updatedAt: '2023-01-01T00:00:01.000Z' }); const transaction = await PointTransaction.findById('point_transaction_123'); const originalUpdatedAt = transaction.updatedAt; transaction.description = 'Updated transaction description'; await transaction.save(); expect(transaction.updatedAt).not.toBe(originalUpdatedAt); }); }); describe('Static Methods', () => { it('should find transaction by ID', async () => { const mockTransaction = { _id: 'point_transaction_123', _rev: '1-abc', type: 'point_transaction', user: 'user_123', points: 50, type: 'earned', description: 'Test transaction', createdAt: '2023-01-01T00:00:00.000Z', updatedAt: '2023-01-01T00:00:00.000Z' }; mockCouchdbService.findDocumentById.mockResolvedValue(mockTransaction); const transaction = await PointTransaction.findById('point_transaction_123'); expect(transaction).toBeDefined(); expect(transaction._id).toBe('point_transaction_123'); expect(transaction.user).toBe('user_123'); }); it('should return null when transaction not found', async () => { mockCouchdbService.findDocumentById.mockResolvedValue(null); const transaction = await PointTransaction.findById('nonexistent'); expect(transaction).toBeNull(); }); it('should find transactions by user ID', async () => { const mockTransactions = [ { _id: 'point_transaction_1', _rev: '1-abc', type: 'point_transaction', user: 'user_123', points: 50, type: 'earned', description: 'Transaction 1', createdAt: '2023-01-01T00:00:00.000Z', updatedAt: '2023-01-01T00:00:00.000Z' }, { _id: 'point_transaction_2', _rev: '1-abc', type: 'point_transaction', user: 'user_123', points: -25, type: 'spent', description: 'Transaction 2', createdAt: '2023-01-02T00:00:00.000Z', updatedAt: '2023-01-02T00:00:00.000Z' } ]; mockCouchdbService.findByType.mockResolvedValue(mockTransactions); const transactions = await PointTransaction.findByUser('user_123'); expect(transactions).toHaveLength(2); expect(transactions[0].user).toBe('user_123'); expect(transactions[1].user).toBe('user_123'); }); }); describe('Helper Methods', () => { it('should calculate user balance', async () => { const mockTransactions = [ { _id: 'point_transaction_1', type: 'point_transaction', user: 'user_123', points: 100, type: 'earned' }, { _id: 'point_transaction_2', type: 'point_transaction', user: 'user_123', points: -25, type: 'spent' }, { _id: 'point_transaction_3', type: 'point_transaction', user: 'user_123', points: 50, type: 'earned' } ]; mockCouchdbService.findByType.mockResolvedValue(mockTransactions); const balance = await PointTransaction.getUserBalance('user_123'); expect(balance).toBe(125); // 100 - 25 + 50 }); it('should return 0 for user with no transactions', async () => { mockCouchdbService.findByType.mockResolvedValue([]); const balance = await PointTransaction.getUserBalance('user_456'); expect(balance).toBe(0); }); }); });