diff --git a/backend/docs/specs/monthly_report/checklist.md b/backend/docs/specs/monthly_report/checklist.md new file mode 100644 index 00000000..1d922dc7 --- /dev/null +++ b/backend/docs/specs/monthly_report/checklist.md @@ -0,0 +1,9 @@ +# Checklist + +- [x] `pdfkit`, `nodemailer`, `node-cron` installed. +- [x] `reportService.js` implemented and generating valid PDFs. +- [x] `monthlyReportJob.js` scheduling correctly (1st of month). +- [x] Email sending functionality verified (mocked or tested). +- [x] Report contains correct data aggregation (tokens claimed per employee). +- [x] Environment variables documented in `.env.example`. +- [x] Unit tests passing. diff --git a/backend/docs/specs/monthly_report/spec.md b/backend/docs/specs/monthly_report/spec.md new file mode 100644 index 00000000..79c029e8 --- /dev/null +++ b/backend/docs/specs/monthly_report/spec.md @@ -0,0 +1,34 @@ +# Monthly Claims Report Specification + +## Overview +This feature implements a monthly PDF report that summarizes token claims by employees. The report is automatically generated and emailed to the DAO Admin on the 1st of every month. + +## Requirements +1. **PDF Generation**: Use `pdfkit` to generate a PDF document. +2. **Scheduling**: Use `node-cron` to schedule the task for the 1st of every month. +3. **Email Delivery**: Use `nodemailer` to send the PDF as an email attachment. +4. **Content**: The report must show the total tokens claimed by employees in the previous month. + +## Architecture +### New Components +1. **`src/services/reportService.js`**: Responsible for querying claim data and generating the PDF. +2. **`src/jobs/monthlyReportJob.js`**: Responsible for scheduling the monthly execution and triggering the email service. +3. **Email Configuration**: Add environment variables for email service credentials. + +### Data Flow +1. Cron job triggers on the 1st of the month. +2. `reportService` queries `ClaimsHistory` for claims in the previous month. +3. `reportService` aggregates the data (total claimed per user/token). +4. `reportService` generates a PDF stream/buffer. +5. `nodemailer` sends an email with the PDF attached to the DAO Admin. + +## Database +- **Table**: `claims_history` +- **Query**: Select claims where `claim_timestamp` is within the previous month range. + +## Configuration +New environment variables: +- `EMAIL_SERVICE`: The email service provider (e.g., 'gmail'). +- `EMAIL_USER`: The email address used to send the report. +- `EMAIL_PASS`: The password or app password for the email account. +- `DAO_ADMIN_EMAIL`: The recipient email address for the report. diff --git a/backend/docs/specs/monthly_report/tasks.md b/backend/docs/specs/monthly_report/tasks.md new file mode 100644 index 00000000..98706bd5 --- /dev/null +++ b/backend/docs/specs/monthly_report/tasks.md @@ -0,0 +1,13 @@ +# Tasks + +1. [x] Install dependencies: `pdfkit`, `nodemailer`, `node-cron`. +2. [x] Create `src/services/reportService.js` with methods to: + - Fetch monthly claim data from `ClaimsHistory`. + - Generate a PDF summary using `pdfkit`. +3. [x] Create `src/jobs/monthlyReportJob.js` to: + - Schedule the task using `node-cron`. + - Configure `nodemailer` transporter. + - Send the email with the generated PDF. +4. [x] Update `src/index.js` to initialize the cron job on server start. +5. [x] Update `.env.example` with new email configuration variables. +6. [x] Add unit tests for `reportService`. diff --git a/backend/package-lock.json b/backend/package-lock.json index d2266ef9..ca605978 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -22,6 +22,7 @@ "graphql-ws": "^5.14.3", "node-cron": "^4.2.1", "nodemailer": "^8.0.1", + "pdfkit": "^0.17.2", "pg": "^8.11.3", "redis": "^4.6.12", "sequelize": "^6.35.2", @@ -1698,6 +1699,15 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@swc/helpers": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.19.tgz", + "integrity": "sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -2241,6 +2251,26 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/baseline-browser-mapping": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", @@ -2314,6 +2344,15 @@ "node": ">=8" } }, + "node_modules/brotli": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz", + "integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.1.2" + } + }, "node_modules/browserslist": { "version": "4.28.1", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", @@ -2558,6 +2597,15 @@ "node": ">=12" } }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, "node_modules/cluster-key-slot": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", @@ -2741,6 +2789,12 @@ "node": ">= 8" } }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", + "license": "MIT" + }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -2830,6 +2884,12 @@ "node": ">=8" } }, + "node_modules/dfa": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz", + "integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==", + "license": "MIT" + }, "node_modules/diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", @@ -3274,6 +3334,23 @@ } } }, + "node_modules/fontkit": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.4.tgz", + "integrity": "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==", + "license": "MIT", + "dependencies": { + "@swc/helpers": "^0.5.12", + "brotli": "^1.3.2", + "clone": "^2.1.2", + "dfa": "^1.2.0", + "fast-deep-equal": "^3.1.3", + "restructure": "^3.0.0", + "tiny-inflate": "^1.0.3", + "unicode-properties": "^1.4.0", + "unicode-trie": "^2.0.0" + } + }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -4564,6 +4641,13 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/jpeg-exif": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/jpeg-exif/-/jpeg-exif-1.1.4.tgz", + "integrity": "sha512-a+bKEcCjtuW5WTdgeXFzswSrdqi0jk4XlEtZlx5A94wCoBpFjfFTbo/Tra5SpNCl/YFZPvcV1dJc+TAYeg6ROQ==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -4638,6 +4722,25 @@ "node": ">=6" } }, + "node_modules/linebreak": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz", + "integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==", + "license": "MIT", + "dependencies": { + "base64-js": "0.0.8", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/linebreak/node_modules/base64-js": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz", + "integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -5254,6 +5357,12 @@ "node": ">=6" } }, + "node_modules/pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", + "license": "MIT" + }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -5324,6 +5433,19 @@ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "license": "MIT" }, + "node_modules/pdfkit": { + "version": "0.17.2", + "resolved": "https://registry.npmjs.org/pdfkit/-/pdfkit-0.17.2.tgz", + "integrity": "sha512-UnwF5fXy08f0dnp4jchFYAROKMNTaPqb/xgR8GtCzIcqoTnbOqtp3bwKvO4688oHI6vzEEs8Q6vqqEnC5IUELw==", + "license": "MIT", + "dependencies": { + "crypto-js": "^4.2.0", + "fontkit": "^2.0.4", + "jpeg-exif": "^1.1.4", + "linebreak": "^1.1.0", + "png-js": "^1.0.0" + } + }, "node_modules/pg": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/pg/-/pg-8.18.0.tgz", @@ -5456,6 +5578,11 @@ "node": ">=8" } }, + "node_modules/png-js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/png-js/-/png-js-1.0.0.tgz", + "integrity": "sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g==" + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -5729,6 +5856,12 @@ "node": ">=10" } }, + "node_modules/restructure": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.2.tgz", + "integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==", + "license": "MIT" + }, "node_modules/retry": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", @@ -6380,6 +6513,12 @@ "node": ">=8" } }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", + "license": "MIT" + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -6529,6 +6668,26 @@ "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", "license": "MIT" }, + "node_modules/unicode-properties": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz", + "integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.0", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/unicode-trie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", + "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==", + "license": "MIT", + "dependencies": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", diff --git a/backend/package.json b/backend/package.json index 07c4bc7c..bab85c1a 100644 --- a/backend/package.json +++ b/backend/package.json @@ -24,6 +24,7 @@ "graphql-ws": "^5.14.3", "node-cron": "^4.2.1", "nodemailer": "^8.0.1", + "pdfkit": "^0.17.2", "pg": "^8.11.3", "redis": "^4.6.12", "sequelize": "^6.35.2", diff --git a/backend/src/index.js b/backend/src/index.js index fcbfc3d6..e77a92b2 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -274,6 +274,14 @@ const startServer = async () => { console.log('Continuing without Discord bot...'); } + // Initialize Monthly Report Job + try { + monthlyReportJob.start(); + } catch (jobError) { + console.error('Failed to initialize Monthly Report Job:', jobError); + } + + // Start the HTTP server httpServer.listen(PORT, () => { console.log(`Server is running on port ${PORT}`); console.log(`REST API available at: http://localhost:${PORT}`); diff --git a/backend/src/jobs/monthlyReportJob.js b/backend/src/jobs/monthlyReportJob.js new file mode 100644 index 00000000..913b9fa3 --- /dev/null +++ b/backend/src/jobs/monthlyReportJob.js @@ -0,0 +1,56 @@ +const cron = require('node-cron'); +const nodemailer = require('nodemailer'); +const reportService = require('../services/reportService'); + +class MonthlyReportJob { + constructor() { + this.cronSchedule = '0 0 1 * *'; // Run at 00:00 on the 1st day of every month + } + + start() { + console.log('Initializing Monthly Report Job...'); + cron.schedule(this.cronSchedule, async () => { + console.log('Running Monthly Report Job...'); + try { + await this.generateAndSendReport(); + } catch (error) { + console.error('Error running Monthly Report Job:', error); + } + }); + } + + async generateAndSendReport() { + const pdfBuffer = await reportService.generateMonthlyClaimsPDF(); + + // Configure transporter + const transporter = nodemailer.createTransport({ + service: process.env.EMAIL_SERVICE, + auth: { + user: process.env.EMAIL_USER, + pass: process.env.EMAIL_PASS + } + }); + + const previousMonth = new Date(); + previousMonth.setMonth(previousMonth.getMonth() - 1); + const monthName = previousMonth.toLocaleString('default', { month: 'long', year: 'numeric' }); + + const mailOptions = { + from: process.env.EMAIL_USER, + to: process.env.DAO_ADMIN_EMAIL, + subject: `Monthly Claims Report - ${monthName}`, + text: `Please find attached the monthly claims report for ${monthName}.`, + attachments: [ + { + filename: `claims-report-${monthName.replace(' ', '-')}.pdf`, + content: pdfBuffer + } + ] + }; + + await transporter.sendMail(mailOptions); + console.log(`Monthly report sent to ${process.env.DAO_ADMIN_EMAIL}`); + } +} + +module.exports = new MonthlyReportJob(); diff --git a/backend/src/models/subSchedule.js b/backend/src/models/subSchedule.js index b3787706..31744cfb 100644 --- a/backend/src/models/subSchedule.js +++ b/backend/src/models/subSchedule.js @@ -22,6 +22,7 @@ const SubSchedule = sequelize.define('SubSchedule', { allowNull: false, comment: 'Amount of tokens added in this top-up', }, + created_at: { type: DataTypes.DATE, allowNull: false, diff --git a/backend/src/services/reportService.js b/backend/src/services/reportService.js new file mode 100644 index 00000000..34e8383c --- /dev/null +++ b/backend/src/services/reportService.js @@ -0,0 +1,104 @@ +const PDFDocument = require('pdfkit'); +const { Op } = require('sequelize'); +const { ClaimsHistory } = require('../models'); + +class ReportService { + async generateMonthlyClaimsPDF() { + // 1. Fetch data + const now = new Date(); + // Previous month start + const start = new Date(now.getFullYear(), now.getMonth() - 1, 1); + // Previous month end (last day of previous month) + const end = new Date(now.getFullYear(), now.getMonth(), 0, 23, 59, 59, 999); + + const claims = await ClaimsHistory.findAll({ + where: { + claim_timestamp: { + [Op.between]: [start, end] + } + }, + order: [['claim_timestamp', 'ASC']] + }); + + // 2. Generate PDF + return new Promise((resolve, reject) => { + try { + const doc = new PDFDocument({ margin: 50 }); + const buffers = []; + + doc.on('data', buffers.push.bind(buffers)); + doc.on('end', () => { + const pdfData = Buffer.concat(buffers); + resolve(pdfData); + }); + + // Title + doc.fontSize(20).text(`Monthly Claims Report`, { align: 'center' }); + doc.fontSize(14).text(`${start.toLocaleString('default', { month: 'long', year: 'numeric' })}`, { align: 'center' }); + doc.moveDown(); + + if (claims.length === 0) { + doc.fontSize(12).text('No claims found for this period.', { align: 'center' }); + } else { + // Table Headers + const tableTop = 150; + const userX = 50; + const tokenX = 200; + const amountX = 350; + const dateX = 450; + + doc.fontSize(10).font('Helvetica-Bold'); + doc.text('User Address', userX, tableTop); + doc.text('Token Address', tokenX, tableTop); + doc.text('Amount', amountX, tableTop); + doc.text('Date', dateX, tableTop); + + doc.moveTo(userX, tableTop + 15).lineTo(550, tableTop + 15).stroke(); + + let y = tableTop + 25; + let totalTokens = 0; + doc.font('Helvetica'); + + for (const claim of claims) { + if (y > 700) { + doc.addPage(); + y = 50; + // Reprint headers + doc.fontSize(10).font('Helvetica-Bold'); + doc.text('User Address', userX, y); + doc.text('Token Address', tokenX, y); + doc.text('Amount', amountX, y); + doc.text('Date', dateX, y); + doc.moveTo(userX, y + 15).lineTo(550, y + 15).stroke(); + doc.font('Helvetica'); + y += 25; + } + + const amount = parseFloat(claim.amount_claimed); + totalTokens += amount; + + doc.fontSize(8).text(claim.user_address.substring(0, 15) + '...', userX, y); + doc.text(claim.token_address.substring(0, 15) + '...', tokenX, y); + doc.text(amount.toFixed(2), amountX, y); + doc.text(new Date(claim.claim_timestamp).toLocaleDateString(), dateX, y); + + y += 20; + } + + doc.moveDown(); + if (y > 700) { + doc.addPage(); + y = 50; + } + doc.fontSize(12).font('Helvetica-Bold').text(`Total Tokens Claimed: ${totalTokens.toFixed(2)}`, userX, y + 20); + } + + doc.end(); + } catch (error) { + reject(error); + } + }); + } +} + +module.exports = new ReportService(); diff --git a/backend/src/services/reportService.test.js b/backend/src/services/reportService.test.js new file mode 100644 index 00000000..fa167357 --- /dev/null +++ b/backend/src/services/reportService.test.js @@ -0,0 +1,34 @@ +const reportService = require('./reportService'); +const { ClaimsHistory } = require('../models'); + +jest.mock('../models', () => ({ + ClaimsHistory: { + findAll: jest.fn() + } +})); + +describe('ReportService', () => { + it('should generate a PDF buffer when claims exist', async () => { + ClaimsHistory.findAll.mockResolvedValue([ + { + user_address: '0x1234567890123456789012345678901234567890', + token_address: '0xabcdef1234567890abcdef1234567890abcdef12', + amount_claimed: '100.50', + claim_timestamp: new Date() + } + ]); + + const pdfBuffer = await reportService.generateMonthlyClaimsPDF(); + expect(Buffer.isBuffer(pdfBuffer)).toBe(true); + // PDF header signature %PDF-1.3 is 8 bytes, so it should be larger + expect(pdfBuffer.length).toBeGreaterThan(100); + }); + + it('should generate a PDF buffer when no claims exist', async () => { + ClaimsHistory.findAll.mockResolvedValue([]); + + const pdfBuffer = await reportService.generateMonthlyClaimsPDF(); + expect(Buffer.isBuffer(pdfBuffer)).toBe(true); + expect(pdfBuffer.length).toBeGreaterThan(100); + }); +}); diff --git a/backend/test-report.js b/backend/test-report.js new file mode 100644 index 00000000..39eefefe --- /dev/null +++ b/backend/test-report.js @@ -0,0 +1,47 @@ +const fs = require('fs'); +const reportService = require('./src/services/reportService'); +const { sequelize } = require('./src/database/connection'); +const { ClaimsHistory } = require('./src/models'); + +// Uncomment the following block to mock data if DB is unavailable +/* +ClaimsHistory.findAll = async () => { + return [ + { + user_address: '0x1234567890123456789012345678901234567890', + token_address: '0xTokenAddress', + amount_claimed: '100.50', + claim_timestamp: new Date() + }, + { + user_address: '0x0987654321098765432109876543210987654321', + token_address: '0xTokenAddress', + amount_claimed: '200.00', + claim_timestamp: new Date() + } + ]; +}; +*/ + +async function testReport() { + try { + // If not mocking, ensure DB connection + if (ClaimsHistory.findAll.toString().includes('native code') || !ClaimsHistory.findAll.toString().includes('return [')) { + await sequelize.authenticate(); + console.log('Database connected.'); + } + + console.log('Generating report...'); + const pdfBuffer = await reportService.generateMonthlyClaimsPDF(); + + fs.writeFileSync('test-report.pdf', pdfBuffer); + console.log('Report generated and saved to test-report.pdf'); + + process.exit(0); + } catch (error) { + console.error('Error generating report:', error); + process.exit(1); + } +} + +testReport(); diff --git a/backend/test-report.pdf b/backend/test-report.pdf new file mode 100644 index 00000000..f1b5dd99 Binary files /dev/null and b/backend/test-report.pdf differ