// Mock CouchDB service for testing const mockCouchdbService = { createDocument: jest.fn(), getDocument: jest.fn(), find: jest.fn(), initialize: jest.fn().mockResolvedValue(true), isReady: jest.fn().mockReturnValue(true), isConnected: true, isConnecting: false, shutdown: jest.fn().mockResolvedValue(true), }; // Mock the service module jest.mock('../../services/couchdbService', () => mockCouchdbService); const couchdbService = require('../../services/couchdbService'); const PointTransaction = require('../../models/PointTransaction'); describe('PointTransaction Model', () => { beforeEach(() => { mockCouchdbService.createDocument.mockReset(); mockCouchdbService.getDocument.mockReset(); mockCouchdbService.find.mockReset(); }); describe('Schema Validation', () => { it('should create a valid point transaction', async () => { const transactionData = { user: 'user_123', amount: 50, transactionType: 'earned', description: 'Completed street cleaning task', relatedEntity: { type: 'task_completion', referenceId: 'task_123' }, balanceAfter: 150 }; 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({ rev: '1-abc' }); const transaction = await PointTransaction.create(transactionData); expect(transaction._id).toBeDefined(); expect(transaction.user).toBe(transactionData.user); expect(transaction.amount).toBe(transactionData.amount); expect(transaction.transactionType).toBe(transactionData.transactionType); expect(transaction.description).toBe(transactionData.description); expect(transaction.relatedEntity.type).toBe(transactionData.relatedEntity.type); expect(transaction.relatedEntity.referenceId).toBe(transactionData.relatedEntity.referenceId); expect(transaction.balanceAfter).toBe(transactionData.balanceAfter); }); it('should require user field', async () => { const transactionData = { amount: 50, transactionType: 'earned', description: 'Transaction without user', }; expect(() => new PointTransaction(transactionData)).toThrow(); }); it('should require points field', async () => { const transactionData = { user: 'user_123', transactionType: 'earned', description: 'Transaction without points', }; expect(() => new PointTransaction(transactionData)).toThrow(); }); it('should require type field', async () => { const transactionData = { user: 'user_123', amount: 50, description: 'Transaction without type', }; expect(() => new PointTransaction(transactionData)).toThrow(); }); it('should require description field', async () => { const transactionData = { user: 'user_123', amount: 50, transactionType: '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', amount: type === 'spent' || type === 'penalty' ? -50 : 50, transactionType: 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({ rev: '1-abc' }); const transaction = await PointTransaction.create(transactionData); expect(transaction.transactionType).toBe(type); }); }); it('should reject invalid transaction type', async () => { const transactionData = { user: 'user_123', amount: 50, transactionType: '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', amount: 100, transactionType: 'earned', description: 'Earned points transaction', }; mockCouchdbService.createDocument.mockResolvedValue({ rev: '1-abc' }); const transaction = await PointTransaction.create(transactionData); expect(transaction.amount).toBe(100); }); it('should accept negative points for spent transactions', async () => { const transactionData = { user: 'user_123', amount: -50, transactionType: 'spent', description: 'Spent points transaction', }; mockCouchdbService.createDocument.mockResolvedValue({ rev: '1-abc' }); const transaction = await PointTransaction.create(transactionData); expect(transaction.amount).toBe(-50); }); it('should accept positive points for bonus transactions', async () => { const transactionData = { user: 'user_123', amount: 25, transactionType: 'bonus', description: 'Bonus points transaction', }; mockCouchdbService.createDocument.mockResolvedValue({ rev: '1-abc' }); const transaction = await PointTransaction.create(transactionData); expect(transaction.amount).toBe(25); }); it('should accept negative points for penalty transactions', async () => { const transactionData = { user: 'user_123', amount: -10, transactionType: 'penalty', description: 'Penalty points transaction', }; mockCouchdbService.createDocument.mockResolvedValue({ rev: '1-abc' }); const transaction = await PointTransaction.create(transactionData); expect(transaction.amount).toBe(-10); }); }); describe('Source Information', () => { it('should allow source information', async () => { const transactionData = { user: 'user_123', amount: 50, transactionType: 'earned', description: 'Transaction with source', relatedEntity: { type: 'task_completion', referenceId: 'task_123', additionalInfo: 'Street cleaning task completed' } }; mockCouchdbService.createDocument.mockResolvedValue({ rev: '1-abc' }); const transaction = await PointTransaction.create(transactionData); expect(transaction.relatedEntity.type).toBe('task_completion'); expect(transaction.relatedEntity.referenceId).toBe('task_123'); expect(transaction.relatedEntity.additionalInfo).toBe('Street cleaning task completed'); }); it('should not require source information', async () => { const transactionData = { user: 'user_123', amount: 50, transactionType: 'earned', description: 'Transaction without source', }; mockCouchdbService.createDocument.mockResolvedValue({ rev: '1-abc' }); const transaction = await PointTransaction.create(transactionData); expect(transaction.relatedEntity).toBeNull(); }); }); 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', amount: 50, transactionType: 'earned', description: `Testing ${sourceType} source`, relatedEntity: { type: sourceType, referenceId: 'ref_123' } }; mockCouchdbService.createDocument.mockResolvedValue({ rev: '1-abc' }); const transaction = await PointTransaction.create(transactionData); expect(transaction.relatedEntity.type).toBe(sourceType); }); }); }); describe('Relationships', () => { it('should reference user ID', async () => { const transactionData = { user: 'user_123', amount: 50, transactionType: 'earned', description: 'User transaction', }; mockCouchdbService.createDocument.mockResolvedValue({ rev: '1-abc' }); 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', amount: 50, transactionType: 'earned', description: 'Timestamp test transaction', }; mockCouchdbService.createDocument.mockResolvedValue({ rev: '1-abc' }); 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'); }); }); 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', amount: 50, transactionType: 'earned', description: 'Test transaction', createdAt: '2023-01-01T00:00:00.000Z', updatedAt: '2023-01-01T00:00:00.000Z' }; mockCouchdbService.getDocument.mockResolvedValue(mockTransaction); const transaction = await PointTransaction.findById('point_transaction_123'); expect(transaction).toBeDefined(); expect(transaction._id).toBe('point_transaction_123'); }); it('should return null when transaction not found', async () => { mockCouchdbService.getDocument.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', type: 'point_transaction', user: 'user_123', amount: 100, transactionType: 'earned', description: 'Transaction 1', createdAt: '2023-01-01T00:00:00.000Z', updatedAt: '2023-01-01T00:00:00.000Z' }, { _id: 'point_transaction_2', type: 'point_transaction', user: 'user_123', amount: -25, transactionType: 'spent', description: 'Transaction 2', createdAt: '2023-01-02T00:00:00.000Z', updatedAt: '2023-01-02T00:00:00.000Z' } ]; mockCouchdbService.find.mockResolvedValue({ docs: 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'); }); it('should return null when transaction not found', async () => { mockCouchdbService.getDocument.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.find.mockResolvedValue({ docs: 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_3', type: 'point_transaction', user: 'user_123', balanceAfter: 125 } ]; mockCouchdbService.find.mockResolvedValue({ docs: mockTransactions }); const balance = await PointTransaction.getUserBalance('user_123'); expect(balance).toBe(125); }); it('should return 0 for user with no transactions', async () => { mockCouchdbService.find.mockResolvedValue({ docs: [] }); const balance = await PointTransaction.getUserBalance('user_456'); expect(balance).toBe(0); }); }); });