Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DOC-1658: reporting scheduled per scheme based on config #61

Open
wants to merge 70 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 67 commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
05e3487
DOC-1658: WIP reporting scheduled per scheme based on config
jbarnardeviden Dec 11, 2024
ff2c286
DOC-1658: handle scheduling for report
jbarnardeviden Dec 12, 2024
6bc858b
DOC-1658: create report csv and send with email via notify
jbarnardeviden Dec 13, 2024
41b4cdd
DOC-1658: unit tests
jbarnardeviden Dec 13, 2024
94f4992
Merge branch main into DOC-1658-reporting
jbarnardeviden Jan 6, 2025
afc9a94
Mocks for tests
jbarnardeviden Jan 6, 2025
f8cfe54
Correct use of object
jbarnardeviden Jan 6, 2025
cde3d2b
Bump version
jbarnardeviden Jan 6, 2025
676bd1d
Removed mocks from integration tests
jbarnardeviden Jan 6, 2025
9d2ab0e
Corrected changelog and sequelize operators usage
jbarnardeviden Jan 7, 2025
f7526b0
Tests for reporting index and additional filename test for publish by…
jbarnardeviden Jan 7, 2025
0294421
formatting
jbarnardeviden Jan 7, 2025
3c21cf3
Corrected report database and added integration tests
jbarnardeviden Jan 8, 2025
ef41894
removed console log
jbarnardeviden Jan 8, 2025
efff5b9
integration test for creating and sending report
jbarnardeviden Jan 8, 2025
5516fa1
Test report date ranges
jbarnardeviden Jan 8, 2025
3611392
Updated config to allow us to set the day of the month for montlhy ru…
jbarnardeviden Jan 8, 2025
c712179
Fix for running development
jbarnardeviden Jan 9, 2025
e3fd395
allow config of time of day to run report
jbarnardeviden Jan 9, 2025
61ee318
spacing fix
jbarnardeviden Jan 9, 2025
37c7bb3
load test
jbarnardeviden Jan 9, 2025
808f136
Config for report
jbarnardeviden Jan 9, 2025
8e71a6d
WIP save file to blob storage
jbarnardeviden Jan 10, 2025
72b4619
Stream data from database, transform into CSV, and save in blob stroa…
jbarnardeviden Jan 10, 2025
5afb20a
WIP stream from db to blob storage
jbarnardeviden Jan 13, 2025
2195a3c
WIP CSV creation
jbarnardeviden Jan 14, 2025
9f8cdcd
WIP tidy up reporting changes
jbarnardeviden Jan 14, 2025
05ecc8b
Removed test
jbarnardeviden Jan 14, 2025
21dc817
Updated tests
jbarnardeviden Jan 14, 2025
06f292c
Updated test
jbarnardeviden Jan 14, 2025
ee4851d
Removed snapshot
jbarnardeviden Jan 15, 2025
a45ee45
Merge branch 'main' into DOC-1658-reporting
jbarnardeviden Jan 15, 2025
acdca2a
Set report headers - order and names
jbarnardeviden Jan 16, 2025
96d3dcc
Set errors field based on the failure record
jbarnardeviden Jan 16, 2025
8096d68
Set status field value
jbarnardeviden Jan 16, 2025
34dc4c2
store paymentReference in statements table for reporting
jbarnardeviden Jan 16, 2025
163e9f0
order by deliveryId and method; added an index
jbarnardeviden Jan 16, 2025
29fd41a
Show address as one field
jbarnardeviden Jan 16, 2025
7c5a308
Version bump
jbarnardeviden Jan 16, 2025
f9aa3e0
Fixed tests
jbarnardeviden Jan 16, 2025
2086d20
Added some failures into the report load test
jbarnardeviden Jan 17, 2025
e020910
format dates
jbarnardeviden Jan 17, 2025
2b10f78
Failure date
jbarnardeviden Jan 17, 2025
f2fe1ca
Slightly less confusing short name for test data
jbarnardeviden Jan 17, 2025
531180c
Delinked report 15th of month
jbarnardeviden Jan 20, 2025
3da3162
Removed email from config and moved report status to constants
jbarnardeviden Jan 20, 2025
dfb35e7
Removed email config and added scheme reporting config with fallbacks…
jbarnardeviden Jan 20, 2025
e834554
Added default values
jbarnardeviden Jan 20, 2025
1c3f1b2
Azure storage report folder
jbarnardeviden Jan 20, 2025
afc9f7b
Moved numbers into config or constants
jbarnardeviden Jan 21, 2025
e693d30
Fixed whitespace issue
jbarnardeviden Jan 21, 2025
e2dd317
Removed redundant await
jbarnardeviden Jan 21, 2025
747fc2a
Use transaction for stream query
jbarnardeviden Jan 21, 2025
f4b0e41
Use constants
jbarnardeviden Jan 21, 2025
77a62d8
Reduce complexity by moving getRunDate logic into own function
jbarnardeviden Jan 21, 2025
f8b1a24
Logging
jbarnardeviden Jan 21, 2025
a98ec15
removed template and email as not used and added env vars to docker f…
jbarnardeviden Jan 21, 2025
9e81a8c
Reduced complexity
jbarnardeviden Jan 21, 2025
405513a
Remove transaction from streaming call as not supported and removed e…
jbarnardeviden Jan 22, 2025
1570be0
Storage config tests
jbarnardeviden Jan 22, 2025
9e67402
Test storage saveReportFile
jbarnardeviden Jan 22, 2025
2529395
Test get report file
jbarnardeviden Jan 22, 2025
5f696dd
Test handling stream errors
jbarnardeviden Jan 23, 2025
94a925d
Test rollback when no data
jbarnardeviden Jan 23, 2025
5ed27c7
Refactored send report to test the data row
jbarnardeviden Jan 23, 2025
a806c8f
Simplified code
jbarnardeviden Jan 23, 2025
69bd00f
Added newline
jbarnardeviden Jan 23, 2025
b315bf6
2.2.5
ffcplatform Jan 28, 2025
bfffa99
Moved headers to constants file
jbarnardeviden Jan 28, 2025
a83b6a8
Headers constant file
jbarnardeviden Jan 28, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ LABEL uk.gov.defra.ffc.parent-image=defradigital/node-development:${PARENT_VERSI

ARG PORT_DEBUG
EXPOSE ${PORT_DEBUG}
USER root
RUN apk add --update --no-cache openjdk17-jre

USER node

COPY --chown=node:node package*.json ./
RUN npm install
Expand Down
8 changes: 6 additions & 2 deletions app/config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,24 @@ const Joi = require('joi')
const mqConfig = require('./message')
const dbConfig = require('./database')
const storageConfig = require('./storage')
const reportConfig = require('./report')

const schema = Joi.object({
env: Joi.string().valid('development', 'test', 'production').default('development'),
deliveryCheckInterval: Joi.number().default(30000),
deliveryCheckInterval: Joi.number().required(),
reportingCheckInterval: Joi.number().required(),
notifyApiKey: Joi.string().required(),
notifyApiKeyLetter: Joi.string().required(),
notifyEmailTemplateKey: Joi.string().required(),
retentionPeriodInWeeks: Joi.number().default(78),
retentionPeriodInWeeks: Joi.number().required(),
statementReceiverApiVersion: Joi.string().required(),
statementReceiverEndpoint: Joi.string().required()
})

const config = {
env: process.env.NODE_ENV,
deliveryCheckInterval: process.env.DELIVERY_CHECK_INTERVAL,
reportingCheckInterval: process.env.REPORTING_CHECK_INTERVAL,
notifyApiKey: process.env.NOTIFY_API_KEY,
notifyApiKeyLetter: process.env.NOTIFY_API_KEY_LETTER,
notifyEmailTemplateKey: process.env.NOTIFY_EMAIL_TEMPLATE_KEY,
Expand All @@ -42,5 +45,6 @@ value.publishSubscription = mqConfig.publishSubscription
value.crmTopic = mqConfig.crmTopic
value.dbConfig = dbConfig
value.storageConfig = storageConfig
value.reportConfig = reportConfig

module.exports = value
67 changes: 67 additions & 0 deletions app/config/report.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
const Joi = require('joi')
const { DELINKED: DELINKED_SCHEME_NAME, SFI } = require('../constants/scheme-names').LONG_NAMES

// Define config schema
const schema = Joi.object({
schemes: Joi.array().items(
Joi.object({
schemeName: Joi.string().required(),
schedule: Joi.object({
dayOfMonth: Joi.number().integer(),
dayOfYear: Joi.number().integer(),
monthOfYear: Joi.number().integer(),
hour: Joi.number().integer(),
minute: Joi.number().integer(),
second: Joi.number().integer(),
intervalNumber: Joi.number().integer().min(1).required(),
intervalType: Joi.string().valid('days', 'weeks', 'months', 'years').required()
}).required(),
dateRange: Joi.object({
durationNumber: Joi.number().integer().min(1).required(),
durationType: Joi.string().valid('days', 'weeks', 'months', 'years').required()
}).required()
})
).required()
})

// Build config
const config = {
schemes: [
{
schemeName: process.env.DELINKED_SCHEME_NAME || DELINKED_SCHEME_NAME,
schedule: {
intervalNumber: process.env.DELINKED_INTERVAL_NUMBER,
intervalType: process.env.DELINKED_INTERVAL_TYPE,
dayOfMonth: process.env.DELINKED_DAY_OF_MONTH
},
dateRange: {
durationNumber: process.env.DELINKED_DURATION_NUMBER,
durationType: process.env.DELINKED_DURATION_TYPE
}
},
{
schemeName: process.env.SFI_SCHEME_NAME || SFI,
schedule: {
intervalNumber: process.env.SFI_INTERVAL_NUMBER,
intervalType: process.env.SFI_INTERVAL_TYPE,
dayOfMonth: process.env.SFI_DAY_OF_MONTH
},
dateRange: {
durationNumber: process.env.SFI_DURATION_NUMBER,
durationType: process.env.SFI_DURATION_TYPE
}
}
]
}

// Validate config
const result = schema.validate(config, {
abortEarly: false
})

// Throw if config is invalid
if (result.error) {
throw new Error(`The report config is invalid. ${result.error.message}`)
}

module.exports = result.value
2 changes: 2 additions & 0 deletions app/config/storage.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const schema = Joi.object({
storageAccount: Joi.string().required(),
container: Joi.string().default('statements'),
folder: Joi.string().default('outbound'),
reportFolder: Joi.string().default('reports'),
useConnectionStr: Joi.boolean().default(false),
createContainers: Joi.boolean().default(true)
})
Expand All @@ -16,6 +17,7 @@ const config = {
storageAccount: process.env.AZURE_STORAGE_ACCOUNT_NAME,
container: process.env.AZURE_STORAGE_CONTAINER,
folder: process.env.AZURE_STORAGE_FOLDER,
reportFolder: process.AZURE_STORAGE_REPORT_FOLDER,
useConnectionStr: process.env.AZURE_STORAGE_USE_CONNECTION_STRING,
createContainers: process.env.AZURE_STORAGE_CREATE_CONTAINERS
}
Expand Down
4 changes: 4 additions & 0 deletions app/constants/publish.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module.exports = {
RETRIES: 3,
RETRY_INTERVAL: 100
}
5 changes: 5 additions & 0 deletions app/constants/report.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = {
FAILED: 'Failed',
PENDING: 'Pending',
SUCCESS: 'Success'
}
3 changes: 2 additions & 1 deletion app/data/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const fs = require('fs')
const path = require('path')
const { Sequelize, DataTypes } = require('sequelize')
const { Sequelize, DataTypes, Op } = require('sequelize')
const config = require('../config')
const dbConfig = config.dbConfig[config.env]
const modelPath = path.join(__dirname, 'models')
Expand All @@ -25,5 +25,6 @@ Object.keys(db).forEach(modelName => {

db.sequelize = sequelize
db.Sequelize = Sequelize
db.Op = Op

module.exports = db
17 changes: 17 additions & 0 deletions app/data/models/report.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
module.exports = (sequelize, DataTypes) => {
const report = sequelize.define('report', {
reportId: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
lastDeliveryId: DataTypes.INTEGER,
schemeName: DataTypes.STRING,
reportStartDate: DataTypes.DATE,
reportEndDate: DataTypes.DATE,
requested: DataTypes.DATE,
sent: DataTypes.DATE
},
{
tableName: 'reports',
freezeTableName: true,
timestamps: false
})
return report
}
3 changes: 2 additions & 1 deletion app/data/models/statement.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ module.exports = (sequelize, DataTypes) => {
schemeFrequency: DataTypes.STRING,
received: DataTypes.DATE,
documentReference: DataTypes.INTEGER,
emailTemplate: DataTypes.STRING
emailTemplate: DataTypes.STRING,
paymentReference: DataTypes.STRING
},
{
tableName: 'statements',
Expand Down
2 changes: 2 additions & 0 deletions app/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ require('./insights').setup()
require('log-timestamp')
const messaging = require('./messaging')
const monitoring = require('./monitoring')
const reporting = require('./reporting')
const { initialiseContainers } = require('./storage')

process.on('SIGTERM', async () => {
Expand All @@ -18,4 +19,5 @@ module.exports = (async () => {
await initialiseContainers()
await messaging.start()
await monitoring.start()
await reporting.start()
})()
17 changes: 11 additions & 6 deletions app/publishing/publish-by-email.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const moment = require('moment')
const config = require('../config')
const { NotifyClient } = require('notifications-node-client')
const { retry } = require('../retry')
const { RETRIES, RETRY_INTERVAL } = require('../constants/publish')

function setupEmailRequest (template, email, linkToFile, personalisation, notifyClient) {
const latestDownloadDate = moment(new Date()).add(config.retentionPeriodInWeeks, 'weeks').format('LL')
Expand All @@ -16,18 +17,22 @@ function setupEmailRequest (template, email, linkToFile, personalisation, notify
}
}

const publishByEmail = async (template, email, file, personalisation) => {
const publishByEmail = async (template, email, file, personalisation, filename = null) => {
moment.locale('en-gb')
const notifyClient = new NotifyClient(config.notifyApiKey)
const fileOptions = {
confirmEmailBeforeDownload: true,
retentionPeriod: `${config.retentionPeriodInWeeks} weeks`
}
if (filename) {
fileOptions.filename = filename
}
const linkToFile = await notifyClient.prepareUpload(
file,
{
confirmEmailBeforeDownload: true,
retentionPeriod: `${config.retentionPeriodInWeeks} weeks`
}
fileOptions
)
const emailRequest = setupEmailRequest(template, email, linkToFile, personalisation, notifyClient)
return retry(emailRequest, 3, 100, true)
return retry(emailRequest, RETRIES, RETRY_INTERVAL, true)
.then(result => {
return result
})
Expand Down
10 changes: 10 additions & 0 deletions app/reporting/complete-report.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const db = require('../data')

const completeReport = async (reportId, transaction) => {
await db.report.update(
{ sent: new Date() },
{ where: { reportId } },
{ transaction })
}

module.exports = completeReport
13 changes: 13 additions & 0 deletions app/reporting/create-report.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const db = require('../data')

const createReport = async (schemeName, lastDeliveryId, reportStartDate, reportEndDate, requested, transaction) => {
return db.report.create({
lastDeliveryId,
schemeName,
reportStartDate,
reportEndDate,
requested
}, { transaction })
}

module.exports = createReport
27 changes: 27 additions & 0 deletions app/reporting/get-deliveries-for-report.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
const db = require('../data')
const QueryStream = require('pg-query-stream')

const getDeliveriesForReport = async (schemeName, start, end) => {
console.log('get deliveries', {
schemeName,
start,
end
})

const query = `
SELECT d.*, s.*, f.*
FROM deliveries d
INNER JOIN statements s ON d."statementId" = s."statementId"
LEFT JOIN failures f ON d."deliveryId" = f."deliveryId"
WHERE s."schemeName" = $1 AND d.requested BETWEEN $2 AND $3
ORDER BY d."deliveryId" ASC, d."method"
`

const client = await db.sequelize.connectionManager.getConnection()
const stream = new QueryStream(query, [schemeName, start, end])
const queryStream = client.query(stream)

return queryStream
}

module.exports = getDeliveriesForReport
23 changes: 23 additions & 0 deletions app/reporting/get-todays-report.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
const db = require('../data')
const hour = 23
const minute = 59
const second = 59
const millisecond = 999

const getTodaysReport = async (schemeName) => {
const today = new Date()
const startOfDay = new Date(today.setHours(0, 0, 0, 0))
const endOfDay = new Date(today.setHours(hour, minute, second, millisecond))

return db.report.findAll({
where: {
schemeName,
sent: {
[db.Op.between]: [startOfDay, endOfDay],
[db.Op.ne]: null
}
}
})
}

module.exports = getTodaysReport
67 changes: 67 additions & 0 deletions app/reporting/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
const config = require('../config')
const getTodaysReport = require('./get-todays-report')
const { sendReport } = require('./send-report')
const moment = require('moment')

const startSchemeReport = async (schemeName, startDate, endDate) => {
console.log('[REPORTING] Starting report for scheme: ', schemeName)
const existingReport = await getTodaysReport(schemeName)
if (!existingReport?.length) {
await sendReport(schemeName, startDate, endDate)
} else {
console.log('[REPORTING] A report has already run today for scheme: ', schemeName)
}
}

const isToday = (date) => {
return moment(date).isSame(moment(), 'day')
}

const getRunDate = (schedule) => {
const { intervalType, dayOfMonth, dayOfYear, monthOfYear, hour, minute, second } = schedule
const baseDate = moment().hour(hour || 0).minute(minute || 0).second(second || 0).startOf('day')

if (intervalType === 'months') {
return baseDate.date(dayOfMonth || 1)
} else if (intervalType === 'years') {
return baseDate.month((monthOfYear || 1) - 1).date(dayOfYear || 1)
} else {
return baseDate
}
}

const processScheme = async (scheme) => {
const { schemeName, schedule, dateRange } = scheme
const runDate = getRunDate(schedule)

if (isToday(runDate)) {
console.log('[REPORTING] A report is due to run today for scheme: ', schemeName)
const startDate = moment().subtract(dateRange.durationNumber, dateRange.durationType).startOf('day').toDate()
const endDate = moment().endOf('day').toDate()
await startSchemeReport(schemeName, startDate, endDate)
} else {
console.log('[REPORTING] No report is due to run today for scheme: ', schemeName)
}
}

const start = async () => {
try {
console.log('[REPORTING] Starting reporting')
const schemes = config.reportConfig.schemes
for (const scheme of schemes) {
try {
await processScheme(scheme)
} catch (error) {
console.error('Error processing scheme:', scheme.schemeName, error)
}
}
} catch (err) {
console.error(err)
} finally {
setTimeout(start, config.reportingCheckInterval)
}
}

module.exports = {
start
}
Loading
Loading