Added notifcation when new invoice
MODIFY
src/assets/i18n/en.json
src/assets/i18n/en.json
"title": "Invoice Synchro Failed",
"body": "Unable to synchronize {{nbrInvoicesInError}} invoice(s) with the billing service provider in organization '{{tenantName}}'"
},
+ "billingNewInvoice": {
+ "title": "New invoice",
+ "body": "A new invoice is now available"
+ },
"endUserErrorNotification": {
"title": "An user reported an error",
"body": "'{{userName}}' reported: '{{errorTitle}}' - '{{errorDescription}}' in organization '{{tenantName}}'"

ADD
src/assets/server/notification/email/en_US/billing-new-invoice.json
+{
+ "subject": "Billing - New invoice",
+ "tenant": "<%- tenantID %>",
+ "baseURL": "<%- evseDashboardURL %>",
+ "body": {
+ "header": {
+ "title": "Billing - New invoice",
+ "image": {
+ "left": {
+ "height": 60,
+ "url": "<%- evseDashboardURL %>/assets/img/logo-email.gif"
+ },
+ "right": {
+ "height": 50,
+ "url": "<%- evseDashboardURL %>/assets/img/info.png"
+ }
+ }
+ },
+ "beforeActionLines": [
+ "Dear <%- (user.firstName?user.firstName:user.name) %>,",
+ "",
+ "A new invoice is available : <%- invoice.number %>. Click <a href=\"<%- invoiceDownloadUrl %>\">here to download it</a>"
+ ],
+ "action": {
+ "title": "Consult",
+ "url": "<%- evseDashboardInvoiceURL %>"
+ },
+ "afterActionLines": [
+ "Best Regards,",
+ "EV Admins."
+ ],
+ "footer": {
+ }
+ }
+}

MODIFY
src/integration/billing/BillingIntegration.ts
src/integration/billing/BillingIntegration.ts
@@ -5,8 +5,10 @@ import { BillingSetting } from '../../types/Setting';
import BillingStorage from '../../storage/mongodb/BillingStorage';
import Constants from '../../utils/Constants';
import Logging from '../../utils/Logging';
+import NotificationHandler from '../../notification/NotificationHandler';
import { ServerAction } from '../../types/Server';
import SettingStorage from '../../storage/mongodb/SettingStorage';
+import TenantStorage from '../../storage/mongodb/TenantStorage';
import Transaction from '../../types/Transaction';
import { UserInErrorType } from '../../types/InError';
import UserStorage from '../../storage/mongodb/UserStorage';
@@ -350,6 +352,36 @@ export default abstract class BillingIntegration<T extends BillingSetting> {
if (invoice.downloadable) {
// Send link to the user using our notification framework (link to the front-end + download)
}
+ const tenant = await TenantStorage.getTenant(this.tenantID);
+ // Send async notification
+ await NotificationHandler.sendBillingNewInvoiceNotification(
+ this.tenantID,
+ Utils.generateGUID(),
+ invoice.user,
+ {
+ user: invoice.user,
+ evseDashboardInvoiceURL: await Utils.buildEvseBillingInvoicesURL(this.tenantID),
+ evseDashboardURL: Utils.buildEvseURL(tenant.subdomain),
+ invoiceDownloadUrl: await Utils.buildEvseBillingDownloadInvoicesURL(this.tenantID, invoice.id),
+ invoice: invoice
+ }
+ );
+ // .then(() => {
+ // Logging.logDebug({
+ // module: 'BillingIntegration',
+ // method: 'sendInvoiceToUser',
+ // message: 'Sent email to user',
+ // tenantID: this.tenantID
+ // });
+ // }).catch((e) => {
+ // Logging.logError({
+ // module: 'BillingIntegration',
+ // method: 'sendInvoiceToUser',
+ // message: 'Failed to send email to user',
+ // detailedMessages: { e },
+ // tenantID: this.tenantID
+ // });
+ // });
return invoice;
}

MODIFY
src/notification/NotificationHandler.ts
src/notification/NotificationHandler.ts
import User, { UserRole } from '../types/User';
-import UserNotifications, { BillingInvoiceSynchronizationFailedNotification, BillingUserSynchronizationFailedNotification, CarCatalogSynchronizationFailedNotification, ChargingStationRegisteredNotification, ChargingStationStatusErrorNotification, EndOfChargeNotification, EndOfSessionNotification, EndOfSignedSessionNotification, EndUserErrorNotification, NewRegisteredUserNotification, Notification, NotificationSeverity, NotificationSource, OCPIPatchChargingStationsStatusesErrorNotification, OfflineChargingStationNotification, OptimalChargeReachedNotification, PreparingSessionNotStartedNotification, RequestPasswordNotification, SessionNotStartedNotification, SmtpAuthErrorNotification, TransactionStartedNotification, UnknownUserBadgedNotification, UserAccountInactivityNotification, UserAccountStatusChangedNotification, UserNotificationKeys, VerificationEmailNotification } from '../types/UserNotifications';
+import UserNotifications, { BillingInvoiceSynchronizationFailedNotification, BillingNewInvoiceNotification, BillingUserSynchronizationFailedNotification, CarCatalogSynchronizationFailedNotification, ChargingStationRegisteredNotification, ChargingStationStatusErrorNotification, EndOfChargeNotification, EndOfSessionNotification, EndOfSignedSessionNotification, EndUserErrorNotification, NewRegisteredUserNotification, NotificationSeverity, NotificationSource, OCPIPatchChargingStationsStatusesErrorNotification, OfflineChargingStationNotification, OptimalChargeReachedNotification, PreparingSessionNotStartedNotification, RequestPasswordNotification, SessionNotStartedNotification, SmtpAuthErrorNotification, TransactionStartedNotification, UnknownUserBadgedNotification, UserAccountInactivityNotification, UserAccountStatusChangedNotification, UserNotificationKeys, VerificationEmailNotification } from '../types/UserNotifications';
import ChargingStation from '../types/ChargingStation';
import Configuration from '../utils/Configuration';
@@ -892,4 +892,28 @@ export default class NotificationHandler {
}
}
}
+
+ static async sendBillingNewInvoiceNotification(tenantID: string, notificationID: string, user: User,
+ sourceData: BillingNewInvoiceNotification): Promise<void> {
+ if (tenantID !== Constants.DEFAULT_TENANT) {
+ // Get the Tenant
+ const tenant = await TenantStorage.getTenant(tenantID);
+ // For each Sources
+ for (const notificationSource of NotificationHandler.notificationSources) {
+ // Active?
+ if (notificationSource.enabled) {
+ try {
+ // Save
+ await NotificationHandler.saveNotification(
+ tenantID, notificationSource.channel, notificationID, ServerAction.BILLING_NEW_INVOICE, { user });
+ // Send
+ await notificationSource.notificationTask.sendBillingNewInvoice(
+ sourceData, user, tenant, NotificationSeverity.INFO);
+ } catch (error) {
+ Logging.logActionExceptionMessage(tenantID, ServerAction.BILLING_NEW_INVOICE, error);
+ }
+ }
+ }
+ }
+ }
}

MODIFY
src/notification/NotificationTask.ts
src/notification/NotificationTask.ts
-import { BillingInvoiceSynchronizationFailedNotification, BillingUserSynchronizationFailedNotification, CarCatalogSynchronizationFailedNotification, ChargingStationRegisteredNotification, ChargingStationStatusErrorNotification, EndOfChargeNotification, EndOfSessionNotification, EndOfSignedSessionNotification, EndUserErrorNotification, NewRegisteredUserNotification, NotificationSeverity, OCPIPatchChargingStationsStatusesErrorNotification, OfflineChargingStationNotification, OptimalChargeReachedNotification, PreparingSessionNotStartedNotification, RequestPasswordNotification, SessionNotStartedNotification, SmtpAuthErrorNotification, TransactionStartedNotification, UnknownUserBadgedNotification, UserAccountInactivityNotification, UserAccountStatusChangedNotification, VerificationEmailNotification } from '../types/UserNotifications';
+import { BillingInvoiceSynchronizationFailedNotification, BillingNewInvoiceNotification, BillingUserSynchronizationFailedNotification, CarCatalogSynchronizationFailedNotification, ChargingStationRegisteredNotification, ChargingStationStatusErrorNotification, EndOfChargeNotification, EndOfSessionNotification, EndOfSignedSessionNotification, EndUserErrorNotification, NewRegisteredUserNotification, NotificationSeverity, OCPIPatchChargingStationsStatusesErrorNotification, OfflineChargingStationNotification, OptimalChargeReachedNotification, PreparingSessionNotStartedNotification, RequestPasswordNotification, SessionNotStartedNotification, SmtpAuthErrorNotification, TransactionStartedNotification, UnknownUserBadgedNotification, UserAccountInactivityNotification, UserAccountStatusChangedNotification, VerificationEmailNotification } from '../types/UserNotifications';
import Tenant from '../types/Tenant';
import User from '../types/User';
@@ -26,4 +26,5 @@ export default interface NotificationTask {
sendCarCatalogSynchronizationFailed(data: CarCatalogSynchronizationFailedNotification, user: User, tenant: Tenant, severity: NotificationSeverity): Promise<void>;
sendSessionNotStarted(data: SessionNotStartedNotification, user: User, tenant: Tenant, severity: NotificationSeverity): Promise<void>;
sendEndUserErrorNotification(data: Partial<EndUserErrorNotification>, user: User, tenant: Tenant, severity: NotificationSeverity): Promise<void>;
+ sendBillingNewInvoice(data: BillingNewInvoiceNotification, user: User, tenant: Tenant, severity: NotificationSeverity): Promise<void>;
}

MODIFY
src/notification/email/EMailNotificationTask.ts
src/notification/email/EMailNotificationTask.ts
-import { BillingInvoiceSynchronizationFailedNotification, BillingUserSynchronizationFailedNotification, CarCatalogSynchronizationFailedNotification, ChargingStationRegisteredNotification, ChargingStationStatusErrorNotification, EndOfChargeNotification, EndOfSessionNotification, EndOfSignedSessionNotification, EndUserErrorNotification, NewRegisteredUserNotification, NotificationSeverity, OCPIPatchChargingStationsStatusesErrorNotification, OfflineChargingStationNotification, OptimalChargeReachedNotification, PreparingSessionNotStartedNotification, RequestPasswordNotification, SessionNotStartedNotification, SmtpAuthErrorNotification, TransactionStartedNotification, UnknownUserBadgedNotification, UserAccountInactivityNotification, UserAccountStatusChangedNotification, VerificationEmailNotification } from '../../types/UserNotifications';
+import { BillingInvoiceSynchronizationFailedNotification, BillingNewInvoiceNotification, BillingUserSynchronizationFailedNotification, CarCatalogSynchronizationFailedNotification, ChargingStationRegisteredNotification, ChargingStationStatusErrorNotification, EndOfChargeNotification, EndOfSessionNotification, EndOfSignedSessionNotification, EndUserErrorNotification, NewRegisteredUserNotification, NotificationSeverity, OCPIPatchChargingStationsStatusesErrorNotification, OfflineChargingStationNotification, OptimalChargeReachedNotification, PreparingSessionNotStartedNotification, RequestPasswordNotification, SessionNotStartedNotification, SmtpAuthErrorNotification, TransactionStartedNotification, UnknownUserBadgedNotification, UserAccountInactivityNotification, UserAccountStatusChangedNotification, VerificationEmailNotification } from '../../types/UserNotifications';
import BackendError from '../../exception/BackendError';
import Configuration from '../../utils/Configuration';
@@ -125,6 +125,10 @@ export default class EMailNotificationTask implements NotificationTask {
return this.prepareAndSendEmail('billing-invoice-synchronization-failed', data, user, tenant, severity);
}
+ public async sendBillingNewInvoice(data: BillingNewInvoiceNotification, user: User, tenant: Tenant, severity: NotificationSeverity): Promise<void> {
+ return this.prepareAndSendEmail('billing-new-invoice', data, user, tenant, severity);
+ }
+
public async sendCarCatalogSynchronizationFailed(data: CarCatalogSynchronizationFailedNotification, user: User, tenant: Tenant, severity: NotificationSeverity): Promise<void> {
return this.prepareAndSendEmail('car-synchronization-failed', data, user, tenant, severity);
}

MODIFY
src/notification/remote-push-notification/RemotePushNotificationTask.ts
src/notification/remote-push-notification/RemotePushNotificationTask.ts
-import { BillingInvoiceSynchronizationFailedNotification, BillingUserSynchronizationFailedNotification, CarCatalogSynchronizationFailedNotification, ChargingStationRegisteredNotification, ChargingStationStatusErrorNotification, EndOfChargeNotification, EndOfSessionNotification, EndOfSignedSessionNotification, EndUserErrorNotification, NewRegisteredUserNotification, NotificationSeverity, OCPIPatchChargingStationsStatusesErrorNotification, OfflineChargingStationNotification, OptimalChargeReachedNotification, PreparingSessionNotStartedNotification, RequestPasswordNotification, SessionNotStartedNotification, SmtpAuthErrorNotification, TransactionStartedNotification, UnknownUserBadgedNotification, UserAccountInactivityNotification, UserAccountStatusChangedNotification, UserNotificationType, VerificationEmailNotification } from '../../types/UserNotifications';
+import { BillingInvoiceSynchronizationFailedNotification, BillingNewInvoiceNotification, BillingUserSynchronizationFailedNotification, CarCatalogSynchronizationFailedNotification, ChargingStationRegisteredNotification, ChargingStationStatusErrorNotification, EndOfChargeNotification, EndOfSessionNotification, EndOfSignedSessionNotification, EndUserErrorNotification, NewRegisteredUserNotification, NotificationSeverity, OCPIPatchChargingStationsStatusesErrorNotification, OfflineChargingStationNotification, OptimalChargeReachedNotification, PreparingSessionNotStartedNotification, RequestPasswordNotification, SessionNotStartedNotification, SmtpAuthErrorNotification, TransactionStartedNotification, UnknownUserBadgedNotification, UserAccountInactivityNotification, UserAccountStatusChangedNotification, UserNotificationType, VerificationEmailNotification } from '../../types/UserNotifications';
import User, { UserStatus } from '../../types/User';
import Configuration from '../../utils/Configuration';
@@ -319,6 +319,18 @@ export default class RemotePushNotificationTask implements NotificationTask {
title, body, user, { 'error': data.nbrInvoicesInError.toString() }, severity);
}
+ public async sendBillingNewInvoice(data: BillingNewInvoiceNotification, user: User, tenant: Tenant, severity: NotificationSeverity): Promise<void> {
+ // Set the locale
+ const i18nManager = new I18nManager(user.locale);
+ // Get Message Text
+ const title = i18nManager.translate('notifications.billingNewInvoice.title');
+ const body = i18nManager.translate('notifications.billingNewInvoice.body',
+ { invoiceNumber: data.invoice.number });
+ // Send Notification
+ return this.sendRemotePushNotificationToUser(tenant, UserNotificationType.BILLING_NEW_INVOICE,
+ title, body, user, { 'invoiceNumber': data.invoice.number.toString() }, severity);
+ }
+
private async sendRemotePushNotificationToUser(tenant: Tenant, notificationType: UserNotificationType, title: string, body: string, user: User, data?: object, severity?: NotificationSeverity) {
// Checks
if (!this.initialized) {

MODIFY
src/types/Server.ts
src/types/Server.ts
@@ -349,6 +349,7 @@ export enum ServerAction {
BILLING_FORCE_SYNCHRONIZE_USER_INVOICES = 'BillingForceSynchronizeUserInvoices',
BILLING_DOWNLOAD_INVOICE = 'BillingDownloadInvoice',
BILLING_CREATE_TRANSACTION_INVOICE = 'BillingCreateTransactionInvoice',
+ BILLING_NEW_INVOICE = 'BillingNewInvoice',
MONGO_DB = 'MongoDB',

MODIFY
src/types/UserNotifications.ts
src/types/UserNotifications.ts
+import { BillingInvoice } from './Billing';
import NotificationTask from '../notification/NotificationTask';
import User from './User';
@@ -58,6 +59,7 @@ export enum UserNotificationType {
OFFLINE_CHARGING_STATION = 'OfflineChargingStation',
BILLING_USER_SYNCHRONIZATION_FAILED = 'BillingUserSynchronizationFailed',
BILLING_INVOICE_SYNCHRONIZATION_FAILED = 'BillingInvoiceSynchronizationFailed',
+ BILLING_NEW_INVOICE = 'BillingNewInvoice',
CAR_CATALOG_SYNCHRONIZATION_FAILED = 'CarCatalogSynchronizationFailed',
SESSION_NOT_STARTED_AFTER_AUTHORIZE = 'SessionNotStartedAfterAuthorize',
END_USER_ERROR_NOTIFICATION = 'EndUserErrorNotification'
@@ -222,6 +224,15 @@ export interface BillingInvoiceSynchronizationFailedNotification extends BaseNot
evseDashboardURL: string;
evseDashboardBillingURL: string;
}
+
+export interface BillingNewInvoiceNotification extends BaseNotification {
+ evseDashboardURL: string;
+ evseDashboardInvoiceURL: string;
+ user: User;
+ invoice: BillingInvoice;
+ invoiceDownloadUrl: string;
+}
+
export interface CarCatalogSynchronizationFailedNotification extends BaseNotification {
nbrCarsInError: number;
evseDashboardURL: string;

MODIFY
src/utils/Utils.ts
src/utils/Utils.ts
-import { AxiosError } from 'axios';
-import bcrypt from 'bcryptjs';
-import crypto from 'crypto';
-import { Request } from 'express';
-import fs from 'fs';
-import http from 'http';
-import _ from 'lodash';
-import moment from 'moment';
-import { ObjectID } from 'mongodb';
-import passwordGenerator from 'password-generator';
-import path from 'path';
-import tzlookup from 'tz-lookup';
-import url from 'url';
-import { v4 as uuid } from 'uuid';
-import validator from 'validator';
-import Authorizations from '../authorization/Authorizations';
-import AppError from '../exception/AppError';
-import BackendError from '../exception/BackendError';
-import ChargingStationStorage from '../storage/mongodb/ChargingStationStorage';
-import SiteAreaStorage from '../storage/mongodb/SiteAreaStorage';
-import TenantStorage from '../storage/mongodb/TenantStorage';
-import UserStorage from '../storage/mongodb/UserStorage';
+import { AnalyticsSettingsType, AssetSettingsType, BillingSettingsType, PricingSettingsType, RefundSettingsType, RoamingSettingsType, SettingDBContent, SmartChargingContentType } from '../types/Setting';
+import { Car, CarCatalog, CarType } from '../types/Car';
+import { ChargePointStatus, OCPPProtocol, OCPPVersion } from '../types/ocpp/OCPPServer';
+import ChargingStation, { ChargePoint, Connector, ConnectorCurrentLimitSource, CurrentType, SiteAreaLimitSource } from '../types/ChargingStation';
+import Transaction, { InactivityStatus } from '../types/Transaction';
+import User, { UserRole, UserStatus } from '../types/User';
+
+import { ActionsResponse } from '../types/GlobalType';
import Address from '../types/Address';
+import AppError from '../exception/AppError';
import Asset from '../types/Asset';
-import { Car, CarCatalog, CarType } from '../types/Car';
+import Authorizations from '../authorization/Authorizations';
+import { AxiosError } from 'axios';
+import BackendError from '../exception/BackendError';
import { ChargingProfile } from '../types/ChargingProfile';
-import ChargingStation, { ChargePoint, Connector, ConnectorCurrentLimitSource, CurrentType, SiteAreaLimitSource } from '../types/ChargingStation';
+import ChargingStationStorage from '../storage/mongodb/ChargingStationStorage';
import Company from '../types/Company';
+import Configuration from './Configuration';
import ConnectorStats from '../types/ConnectorStats';
+import Constants from './Constants';
import Consumption from '../types/Consumption';
-import { ActionsResponse } from '../types/GlobalType';
+import Cypher from './Cypher';
import { HTTPError } from '../types/HTTPError';
-import OCPIEndpoint from '../types/ocpi/OCPIEndpoint';
-import { ChargePointStatus, OCPPProtocol, OCPPVersion } from '../types/ocpp/OCPPServer';
import { HttpEndUserErrorNotificationRequest } from '../types/requests/HttpNotificationRequest';
+import Logging from './Logging';
+import OCPIEndpoint from '../types/ocpi/OCPIEndpoint';
+import { ObjectID } from 'mongodb';
+import { Request } from 'express';
import { ServerAction } from '../types/Server';
-import { AnalyticsSettingsType, AssetSettingsType, BillingSettingsType, PricingSettingsType, RefundSettingsType, RoamingSettingsType, SettingDBContent, SmartChargingContentType } from '../types/Setting';
import Site from '../types/Site';
import SiteArea from '../types/SiteArea';
+import SiteAreaStorage from '../storage/mongodb/SiteAreaStorage';
import Tag from '../types/Tag';
import Tenant from '../types/Tenant';
import TenantComponents from '../types/TenantComponents';
-import Transaction, { InactivityStatus } from '../types/Transaction';
-import User, { UserRole, UserStatus } from '../types/User';
+import TenantStorage from '../storage/mongodb/TenantStorage';
+import UserStorage from '../storage/mongodb/UserStorage';
import UserToken from '../types/UserToken';
-import Configuration from './Configuration';
-import Constants from './Constants';
-import Cypher from './Cypher';
-import Logging from './Logging';
+import _ from 'lodash';
+import bcrypt from 'bcryptjs';
+import crypto from 'crypto';
+import fs from 'fs';
+import http from 'http';
+import moment from 'moment';
+import passwordGenerator from 'password-generator';
+import path from 'path';
+import tzlookup from 'tz-lookup';
+import url from 'url';
+import { v4 as uuid } from 'uuid';
+import validator from 'validator';
const MODULE_NAME = 'Utils';
@@ -1038,6 +1039,18 @@ export default class Utils {
return _evseBaseURL + '/settings#billing';
}
+ public static async buildEvseBillingInvoicesURL(tenantID: string): Promise<string> {
+ const tenant = await TenantStorage.getTenant(tenantID);
+ const _evseBaseURL = Utils.buildEvseURL(tenant.subdomain);
+ return _evseBaseURL + '/invoices';
+ }
+
+ public static async buildEvseBillingDownloadInvoicesURL(tenantID: string, invoiceID: string): Promise<string> {
+ const tenant = await TenantStorage.getTenant(tenantID);
+ const _evseBillingInvoiceURL = await Utils.buildEvseBillingInvoicesURL(tenant.subdomain);
+ return _evseBillingInvoiceURL + '?invoiceID=' + invoiceID + '#all';
+ }
+
public static isServerInProductionMode(): boolean {
const env = process.env.NODE_ENV || 'dev';
return (env === 'production');

Added notifcation when new invoice