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

Email notification of files that are about to expire #336

Merged
merged 7 commits into from
Dec 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
20 changes: 20 additions & 0 deletions app.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const merge = require('webpack-merge')
const config = [require('cozy-scripts/config/webpack.bundle.default.js')]

const extraConfig = {
resolve: {
alias: {
handlebars: 'handlebars/dist/handlebars.min.js'
}
},
module: {
// mjml-core/lib/helpers/mjmlconfig and encoding/lib/iconv-loader use
// expressions inside require. We do not need the functionality provided
// by the dynamic require
exprContextRegExp: /$^/,
exprContextCritical: false
}
}
config.push(extraConfig)

module.exports = [merge.apply(null, config)]
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@
"url": "https://github.com/cozy/mespapiers/issues"
},
"homepage": "https://github.com/cozy/mespapiers#readme",
"resolutions": {
"uglify-js": "npm:terser"
},
"devDependencies": {
"@testing-library/react": "11.2.7",
"@welldone-software/why-did-you-render": "^6.2.1",
Expand All @@ -45,6 +48,7 @@
"eslint-config-cozy-app": "^4.0.0",
"git-directory-deploy": "1.5.1",
"mockdate": "3.0.5",
"raw-loader": "3.1.0",
Merkur39 marked this conversation as resolved.
Show resolved Hide resolved
"react-test-renderer": "16.14.0",
"redux-mock-store": "1.5.4",
"stylint": "2.0.0"
Expand All @@ -57,6 +61,7 @@
"cozy-harvest-lib": "^9.26.6",
"cozy-intent": "^1.17.3",
"cozy-mespapiers-lib": "^10.0.4",
"cozy-notifications": "^0.13.2",
"cozy-realtime": "4.2.2",
"cozy-scripts": "6.3.0",
"cozy-sharing": "4.3.1",
Expand Down
2 changes: 2 additions & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ export const APP_SLUG = 'mespapiers'
export const EXPIRATION_SERVICE_NAME = 'expiration'
export const DEFAULT_NOTICE_PERIOD_DAYS = 90
export const PERSONAL_SPORTING_LICENCE_NOTICE_PERIOD_DAYS = 15
export const lang = process.env.COZY_LOCALE || 'fr'
Merkur39 marked this conversation as resolved.
Show resolved Hide resolved
export const dictRequire = lang => require(`locales/${lang}`)
57 changes: 37 additions & 20 deletions src/helpers/service.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,16 +66,16 @@ export const fetchOrCreateTriggerByName = async (client, serviceName) => {
/**
* @param {IOCozyFile} file - An CozyFile
* @param {string} dateLabel - Label of date
* @returns {Date} Normalize expiration date
* @returns {string} Normalize expiration date (ISO)
*/
export const computeNormalizeExpirationDate = (file, dateLabel) => {
if (file.metadata[dateLabel]) {
if (dateLabel === 'referencedDate') {
return add(new Date(file.metadata[dateLabel] ?? file.created_at), {
days: 365
})
}).toISOString()
}
return new Date(file.metadata[dateLabel])
return new Date(file.metadata[dateLabel]).toISOString()
}

return null
Expand All @@ -84,7 +84,7 @@ export const computeNormalizeExpirationDate = (file, dateLabel) => {
/**
* @param {IOCozyFile} file - An CozyFile
* @param {string} dateLabel - Label of date
* @returns {Date} Notice date
* @returns {string} Notice date (ISO)
*/
export const computeNoticeDate = (file, dateLabel) => {
let noticeDays
Expand All @@ -105,10 +105,11 @@ export const computeNoticeDate = (file, dateLabel) => {
file,
dateLabel
)

return normalizeExpirationDate
? sub(normalizeExpirationDate, {
? sub(new Date(normalizeExpirationDate), {
days: noticeDays
})
}).toISOString()
: null
}

Expand All @@ -135,22 +136,38 @@ const getPaperToNotify = file => {

/**
* @param {IOCozyFile[]} files - List of CozyFile
* @returns {IOCozyFile[]} List of CozyFile that must be notified
* @returns {{ file: IOCozyFile, noticeDate: string, expirationDate: string }[]} List of CozyFile that must be notified with their noticeDate & expirationDate
*/
export const getfilesNeedNotified = files => {
return files.filter(file => {
const paperToNotify = getPaperToNotify(file)

if (paperToNotify) {
const noticeDate = computeNoticeDate(
file,
paperToNotify.expirationDateAttribute
)
return noticeDate ? new Date() >= noticeDate : false
}

return false
})
return files
.map(file => {
const paperToNotify = getPaperToNotify(file)

if (paperToNotify) {
const noticeDate = computeNoticeDate(
file,
paperToNotify.expirationDateAttribute
)

if (!noticeDate) {
return null
}

return new Date() >= new Date(noticeDate)
? {
file,
noticeDate,
expirationDate: computeNormalizeExpirationDate(
file,
paperToNotify.expirationDateAttribute
)
}
: null
}

return null
})
.filter(Boolean)
}

/**
Expand Down
41 changes: 29 additions & 12 deletions src/helpers/service.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,20 +49,20 @@ describe('Service', () => {
created_at: '2022-09-01T00:00:00.000Z',
metadata: {
qualification: { label: 'personal_sporting_licence' },
referencedDate: '2022-09-23T11:35:58.118Z'
referencedDate: '2021-09-23T11:35:58.118Z'
}
}

describe('computeExpirationDate', () => {
it('should return expirationDate', () => {
const res = computeNormalizeExpirationDate(fakeFile01, 'expirationDate')

expect(res.toISOString()).toBe('2022-09-23T11:35:58.118Z')
expect(res).toBe('2022-09-23T11:35:58.118Z')
})
it('should return referencedDate plus 365 days', () => {
const res = computeNormalizeExpirationDate(fakeFile02, 'referencedDate')

expect(res.toISOString()).toBe('2023-09-23T11:35:58.118Z')
expect(res).toBe('2022-09-23T11:35:58.118Z')
})

it('should return "null" if metadata is not found', () => {
Expand All @@ -76,12 +76,12 @@ describe('Service', () => {
it('should return notice date for file with expirationDate metadata', () => {
const res = computeNoticeDate(fakeFile01, 'expirationDate')

expect(res.toISOString()).toBe('2022-06-25T11:35:58.118Z')
expect(res).toBe('2022-06-25T11:35:58.118Z')
})
it('should return notice date for file with referencedDate metadata', () => {
const res = computeNoticeDate(fakeFile02, 'referencedDate')

expect(res.toISOString()).toBe('2023-09-08T11:35:58.118Z')
expect(res).toBe('2022-09-08T11:35:58.118Z')
})
it('should return null for file without corresponding metadata', () => {
const res = computeNoticeDate(fakeFile02, 'expirationDate')
Expand All @@ -96,13 +96,30 @@ describe('Service', () => {

expect(res).toEqual([
{
_id: '01',
created_at: '2022-09-01T00:00:00.000Z',
metadata: {
expirationDate: '2022-09-23T11:35:58.118Z',
qualification: { label: 'national_id_card' }
},
name: 'national id card'
expirationDate: '2022-09-23T11:35:58.118Z',
noticeDate: '2022-06-25T11:35:58.118Z',
file: {
_id: '01',
created_at: '2022-09-01T00:00:00.000Z',
metadata: {
expirationDate: '2022-09-23T11:35:58.118Z',
qualification: { label: 'national_id_card' }
},
name: 'national id card'
}
},
{
expirationDate: '2022-09-23T11:35:58.118Z',
noticeDate: '2022-09-08T11:35:58.118Z',
file: {
_id: '02',
name: 'personal sporting licence',
created_at: '2022-09-01T00:00:00.000Z',
metadata: {
qualification: { label: 'personal_sporting_licence' },
referencedDate: '2021-09-23T11:35:58.118Z'
}
}
}
])
})
Merkur39 marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
17 changes: 16 additions & 1 deletion src/locales/en.json
Original file line number Diff line number Diff line change
@@ -1 +1,16 @@
{}
{
"notifications": {
"expiration": {
"email": {
"appName": "Mes papiers",
"button": "See",
"content": {
"hello": "Hello 👋,",
"info": "I would like to inform you that the document(s) in the following list are expiring. To view your document and be redirected to the Cozy Cloud My Papers application, simply click on the document name.",
"seeyou": "See you soon!",
"signature": "Your Cozy!"
}
}
}
}
}
17 changes: 16 additions & 1 deletion src/locales/fr.json
Original file line number Diff line number Diff line change
@@ -1 +1,16 @@
{}
{
"notifications": {
"expiration": {
"email": {
"appName": "Mes papiers",
"button": "Voir",
"content": {
"hello": "Bonjour 👋,",
"info": "Je vous informe que le/les documents dans la liste suivante arrivent à expiration. Pour consulter votre document et être redirigé vers l'application Mes Papiers de Cozy Cloud il vous suffit de cliquer sur le nom du document.",
"seeyou": "À très bientôt !",
"signature": "Votre Cozy !"
}
}
}
}
}
30 changes: 30 additions & 0 deletions src/notifications/helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { initTranslation } from 'cozy-ui/transpiled/react/I18n/translation'
import log from 'cozy-logger'

import { dictRequire, lang } from 'src/constants'
import ExpirationNotification from 'src/notifications'

const translation = initTranslation(lang, dictRequire)
const t = translation.t.bind(translation)

/**
* Build the notification for expiration serivce
*
* @param {CozyClient} client - Cozy client
* @param {object} options - Options
* @return {NotificationView} - The konnector alerts notification view
*/
export const buildNotification = (client, options) => {
log('info', `Build notification...`)
const notification = new ExpirationNotification({
client,
lang,
t,
data: {},
locales: {
[lang]: dictRequire(lang)
},
...options
})
return notification
}
104 changes: 104 additions & 0 deletions src/notifications/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import format from 'date-fns/format'

import { generateWebLink } from 'cozy-client'
import { NotificationView } from 'cozy-notifications'

import template from 'raw-loader!./template.hbs'
Merkur39 marked this conversation as resolved.
Show resolved Hide resolved

/**
* @typedef {object} FilesInfo
* @property {string} name - Filename
* @property {string} paperLink - Link leading to the file
* @property {string} expirationDate - Expiration date of the document
*/

/**
* @typedef {object} BuildDataReturns
* @property {string} date - Current date formatted
* @property {FilesInfo} filesInfo
* @property {string} homeUrl - Cozy Home url
* @property {string} mespapiersUrl - Cozy mesPapiers url
* @property {string} settingsUrl - Cozy settings url
*/

/**
* Manages the notification sent for expiration files
*/
class ExpirationNotification extends NotificationView {
constructor(options) {
super(options)
this.currentDate = options.currentDate
this.filesInfo = options.filesInfo
}

/**
* Required by cozy-notifications
* The current version of cozy-notifications does not allow to omit these methods
* @returns {boolean}
*/
shouldSend() {
return true
}
/**
* Required by cozy-notifications
Merkur39 marked this conversation as resolved.
Show resolved Hide resolved
* The current version of cozy-notifications does not allow to omit these methods
* @returns {string}
*/
getPushContent() {
return ' '
}
/**
* Required by cozy-notifications
* The current version of cozy-notifications does not allow to omit these methods
* @returns {string}
*/
getTitle() {
return ' '
}

/**
* Building data passed to the template
* @returns {BuildDataReturns}
*/
async buildData() {
return {
date: format(new Date(), 'dd/MM/yyyy'),
filesInfo: this.filesInfo.map(fileInfo => {
const { file, expirationDate } = fileInfo
const paperLink = generateWebLink({
slug: 'mespapiers',
cozyUrl: this.client.getStackClient().uri,
subDomainType: 'nested',
pathname: '/',
hash: `paper/file/${file.metadata.qualification.label}/${file._id}`
})

return {
name: file.name,
paperLink,
expirationDate
}
}),
homeUrl: this.client.getStackClient().uri,
mespapiersUrl: generateWebLink({
slug: 'mespapiers',
cozyUrl: this.client.getStackClient().uri,
subDomainType: 'nested',
pathname: '/',
hash: 'paper'
}),
settingsUrl: generateWebLink({
slug: 'settings',
cozyUrl: this.client.getStackClient().uri,
subDomainType: 'nested',
pathname: '/'
})
}
}
}

ExpirationNotification.template = template
ExpirationNotification.category = 'expiration'
ExpirationNotification.preferredChannels = ['mail']

export default ExpirationNotification
Loading