From 5a330c5826bb1e7b1e91fa736d70c8a4da18bab7 Mon Sep 17 00:00:00 2001 From: urbantech Date: Tue, 5 Nov 2024 23:11:47 -0800 Subject: [PATCH] Add Financial Report API: controller, model, route, test, and update global setup --- .../financialReportingController.test.js | 67 ++++++++++----- __tests__/globalSetup.js | 81 +++++++------------ controllers/financialReportingController.js | 30 ++++--- models/financialReport.js | 46 +++++++++++ 4 files changed, 144 insertions(+), 80 deletions(-) diff --git a/__tests__/financialReportingController.test.js b/__tests__/financialReportingController.test.js index 168db29..7e018bd 100644 --- a/__tests__/financialReportingController.test.js +++ b/__tests__/financialReportingController.test.js @@ -1,10 +1,8 @@ const { createFinancialReport } = require('../controllers/financialReportingController'); const FinancialReport = require('../models/financialReport'); - const httpMocks = require('node-mocks-http'); -const mongoose = require('mongoose'); -// Mock the FinancialReport model +// Mock the entire model jest.mock('../models/financialReport'); describe('createFinancialReport Controller', () => { @@ -14,38 +12,69 @@ describe('createFinancialReport Controller', () => { req = httpMocks.createRequest(); res = httpMocks.createResponse(); next = jest.fn(); + jest.clearAllMocks(); }); it('should create a new financial report and return 201 status', async () => { - // Mock request body - req.body = { + // Setup request body with fixed timestamp + const testData = { ReportID: 'test-id', - Type: 'test-type', - Data: 'test-data', - Timestamp: new Date(), + Type: 'Annual', + Data: { key: 'test-data' }, + TotalRevenue: '1000.00', + TotalExpenses: '500.00', + NetIncome: '500.00', + EquitySummary: ['uuid1', 'uuid2'], + Timestamp: new Date().toISOString() // Convert to ISO string format }; - // Mock the save function - FinancialReport.prototype.save = jest.fn().mockResolvedValue(req.body); + req.body = testData; + + // Mock the constructor and save method + const mockSave = jest.fn().mockResolvedValue(testData); + const mockInstance = { save: mockSave }; + FinancialReport.mockImplementation(() => mockInstance); + // Execute controller await createFinancialReport(req, res, next); + // Get response data and parse it + const responseData = JSON.parse(res._getData()); + // Assertions - expect(FinancialReport).toHaveBeenCalledWith(req.body); - expect(FinancialReport.prototype.save).toHaveBeenCalled(); + expect(FinancialReport).toHaveBeenCalledWith(testData); + expect(mockSave).toHaveBeenCalled(); expect(res.statusCode).toBe(201); - expect(res._getJSONData()).toEqual(req.body); + expect(responseData).toEqual(testData); // Compare with the original test data expect(next).not.toHaveBeenCalled(); }); it('should handle errors and pass them to the error handling middleware', async () => { - const errorMessage = 'Failed to create financial report'; - const rejectedPromise = Promise.reject(new Error(errorMessage)); - FinancialReport.prototype.save = jest.fn().mockReturnValue(rejectedPromise); + // Setup request body with fixed timestamp + const testData = { + ReportID: 'test-id', + Type: 'Annual', + Data: { key: 'test-data' }, + TotalRevenue: '1000.00', + TotalExpenses: '500.00', + NetIncome: '500.00', + EquitySummary: ['uuid1', 'uuid2'], + Timestamp: new Date().toISOString() // Convert to ISO string format + }; + + req.body = testData; + // Mock constructor and save method to throw error + const error = new Error('Failed to create financial report'); + const mockSave = jest.fn().mockRejectedValue(error); + FinancialReport.mockImplementation(() => ({ save: mockSave })); + + // Execute controller await createFinancialReport(req, res, next); - expect(FinancialReport.prototype.save).toHaveBeenCalled(); - expect(next).toHaveBeenCalledWith(expect.objectContaining({ message: errorMessage })); + // Assertions + expect(FinancialReport).toHaveBeenCalledWith(testData); + expect(mockSave).toHaveBeenCalled(); + expect(next).toHaveBeenCalledWith(error); }); -}); +}); \ No newline at end of file diff --git a/__tests__/globalSetup.js b/__tests__/globalSetup.js index 61917c8..c84474f 100644 --- a/__tests__/globalSetup.js +++ b/__tests__/globalSetup.js @@ -1,59 +1,38 @@ -require('dotenv').config(); // Load the .env file -const mongoose = require("mongoose"); -const { Client } = require('pg'); +require('dotenv').config(); // Ensure dotenv loads +const mongoose = require('mongoose'); -module.exports = async () => { - // Set up MongoDB - const mongoUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/opencap_test'; // Fallback to hardcoded URI +async function connectToMongoDB() { + const mongoUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/opencap_test'; if (!mongoUri) { throw new Error('MongoDB URI is not defined.'); } - - await mongoose.connect(mongoUri, { - useNewUrlParser: true, - useUnifiedTopology: true, - }); - - // Drop the MongoDB database to clean up before tests - await mongoose.connection.dropDatabase(); - - // Set up PostgreSQL for metadata management - const pgClient = new Client({ - user: process.env.PG_USER || 'lakehouse_user', - host: process.env.PG_HOST || 'localhost', - database: process.env.PG_DATABASE || 'lakehouse_metadata', - password: process.env.PG_PASSWORD || 'password', - port: process.env.PG_PORT || 5432, - }); - - try { - await pgClient.connect(); - console.log('Connected to PostgreSQL database.'); - - // Clean up PostgreSQL data before tests (if necessary) - await pgClient.query('TRUNCATE TABLE datasets, dataset_schema, ingestion_logs RESTART IDENTITY CASCADE;'); - console.log('PostgreSQL tables truncated.'); - - } catch (error) { - console.error('Error connecting to PostgreSQL:', error); - throw new Error('PostgreSQL connection failed.'); - } finally { - await pgClient.end(); + await mongoose.connect(mongoUri, { useNewUrlParser: true, useUnifiedTopology: true }); + console.log('Connected to MongoDB for setup.'); +} + +async function dropDatabaseWithRetry() { + let attempts = 3; + while (attempts > 0) { + try { + await mongoose.connection.dropDatabase(); + console.log('MongoDB test database dropped successfully.'); + return; + } catch (error) { + if (error.codeName === 'DatabaseDropPending') { + console.log('Database drop pending, retrying...'); + attempts -= 1; + await new Promise((resolve) => setTimeout(resolve, 1000)); + } else { + throw error; + } + } } + throw new Error('Failed to drop MongoDB test database.'); +} - // Set up MinIO client (mock or real) - process.env.MINIO_ENDPOINT = process.env.MINIO_ENDPOINT || 'localhost'; - process.env.MINIO_PORT = process.env.MINIO_PORT || '9000'; - process.env.MINIO_ACCESS_KEY = process.env.MINIO_ACCESS_KEY || 'your-access-key'; - process.env.MINIO_SECRET_KEY = process.env.MINIO_SECRET_KEY || 'your-secret-key'; - console.log('MinIO environment variables set.'); - - // Set up Airflow (mock or real) - process.env.AIRFLOW_BASE_URL = process.env.AIRFLOW_BASE_URL || 'http://localhost:8080'; - process.env.AIRFLOW_USERNAME = process.env.AIRFLOW_USERNAME || 'admin'; - process.env.AIRFLOW_PASSWORD = process.env.AIRFLOW_PASSWORD || 'admin_password'; - console.log('Airflow environment variables set.'); - - // Close the MongoDB connection +module.exports = async () => { + await connectToMongoDB(); + await dropDatabaseWithRetry(); await mongoose.connection.close(); + console.log('MongoDB connection closed after setup.'); }; diff --git a/controllers/financialReportingController.js b/controllers/financialReportingController.js index ca174d5..d526573 100644 --- a/controllers/financialReportingController.js +++ b/controllers/financialReportingController.js @@ -1,18 +1,31 @@ -// Import necessary modules and models +// controllers/financialReportingController.js const FinancialReport = require("../models/financialReport"); // Controller function to create a new financial report -async function createFinancialReport(req, res, next) { +const createFinancialReport = async (req, res, next) => { try { // Extract financial report data from the request body - const { ReportID, Type, Data, Timestamp } = req.body; + const { + ReportID, + Type, + Data, + TotalRevenue, + TotalExpenses, + NetIncome, + EquitySummary, + Timestamp + } = req.body; // Create a new financial report document const financialReport = new FinancialReport({ ReportID, Type, Data, - Timestamp, + TotalRevenue, + TotalExpenses, + NetIncome, + EquitySummary, + Timestamp }); // Save the financial report document to the database @@ -24,11 +37,8 @@ async function createFinancialReport(req, res, next) { // Handle errors and pass them to the error handling middleware next(error); } -} - -// Add more controller functions for other routes related to Financial Reporting Tool here +}; module.exports = { - createFinancialReport, - // Add more controller functions here as needed -}; + createFinancialReport +}; \ No newline at end of file diff --git a/models/financialReport.js b/models/financialReport.js index e69de29..9ec06da 100644 --- a/models/financialReport.js +++ b/models/financialReport.js @@ -0,0 +1,46 @@ +// models/financialReport.js + +const mongoose = require('mongoose'); +const { Schema } = mongoose; +const { v4: uuidv4 } = require('uuid'); + +const FinancialReportSchema = new Schema({ + ReportID: { + type: String, + default: uuidv4, // Generate a UUID by default + unique: true, + required: true, + }, + Type: { + type: String, + enum: ['Annual', 'Quarterly'], // Enum values + required: true, + }, + Data: { + type: Schema.Types.Mixed, // Stores JSON data + required: true, + }, + TotalRevenue: { + type: Schema.Types.Decimal128, // Decimal for monetary values + required: true, + }, + TotalExpenses: { + type: Schema.Types.Decimal128, + required: true, + }, + NetIncome: { + type: Schema.Types.Decimal128, + required: true, + }, + EquitySummary: { + type: [String], // Array of UUIDs (shareClassId) + default: [], // Optional field + }, + Timestamp: { + type: Date, + default: Date.now, + required: true, + }, +}); + +module.exports = mongoose.model('FinancialReport', FinancialReportSchema);