Added invoice sync task
ADD
src/assets/server/notification/email/en_US/billing-invoice-synchronization-failed.json
+{
+ "subject": "Billing - Invoice Synchronization Failed",
+ "tenant": "<%- tenantID %>",
+ "baseURL": "<%- evseDashboardURL %>",
+ "body": {
+ "header": {
+ "title": "Billing - Invoice Synchronization Failed",
+ "image": {
+ "left": {
+ "height": 60,
+ "url": "<%- evseDashboardURL %>/assets/img/logo-email.gif"
+ },
+ "right": {
+ "height": 50,
+ "url": "<%- evseDashboardURL %>/assets/img/info.png"
+ }
+ }
+ },
+ "beforeActionLines": [
+ "Dear Administrator,",
+ "",
+ "Unable to synchronize <%- nbrInvoicesInError %> invoices(s) with Billing service provider."
+ ],
+ "action": {
+ "title": "View Settings",
+ "url": "<%- evseDashboardBillingURL %>"
+ },
+ "afterActionLines": [
+ "Best Regards,",
+ "EV Admins."
+ ],
+ "footer": {
+ }
+ }
+}

MODIFY
src/integration/billing/stripe/StripeBillingIntegration.ts
src/integration/billing/stripe/StripeBillingIntegration.ts
@@ -885,7 +885,7 @@ export default class StripeBillingIntegration extends BillingIntegration<StripeB
private async modifyUser(user: User): Promise<BillingUser> {
await this.checkConnection();
- const fullName = Utils.buildUserFullName(user);
+ const fullName = Utils.buildUserFullName(user, false, false, false);
const locale = Utils.getLanguageFromLocale(user.locale).toLocaleLowerCase();
const i18nManager = new I18nManager(user.locale);
const description = i18nManager.translate('billing.generatedUser', { email: user.email });

MODIFY
src/notification/NotificationHandler.ts
src/notification/NotificationHandler.ts
import User, { UserRole } from '../types/User';
-import UserNotifications, { BillingUserSynchronizationFailedNotification, CarCatalogSynchronizationFailedNotification, ChargingStationRegisteredNotification, ChargingStationStatusErrorNotification, EndOfChargeNotification, EndOfSessionNotification, EndOfSignedSessionNotification, NewRegisteredUserNotification, Notification, NotificationSeverity, NotificationSource, OCPIPatchChargingStationsStatusesErrorNotification, OfflineChargingStationNotification, OptimalChargeReachedNotification, PreparingSessionNotStartedNotification, RequestPasswordNotification, SessionNotStartedNotification, SmtpAuthErrorNotification, TransactionStartedNotification, UnknownUserBadgedNotification, UserAccountInactivityNotification, UserAccountStatusChangedNotification, UserNotificationKeys, VerificationEmailNotification } from '../types/UserNotifications';
+import UserNotifications, { BillingInvoiceSynchronizationFailedNotification, BillingUserSynchronizationFailedNotification, CarCatalogSynchronizationFailedNotification, ChargingStationRegisteredNotification, ChargingStationStatusErrorNotification, EndOfChargeNotification, EndOfSessionNotification, EndOfSignedSessionNotification, NewRegisteredUserNotification, Notification, 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';
@@ -734,7 +734,7 @@ export default class NotificationHandler {
// Send
for (const adminUser of adminUsers) {
// Enabled?
- if (adminUser.notificationsActive && adminUser.notifications.sendBillingUserSynchronizationFailed) {
+ if (adminUser.notificationsActive && adminUser.notifications.sendBillingSynchronizationFailed) {
await notificationSource.notificationTask.sendBillingUserSynchronizationFailed(
sourceData, adminUser, tenant, NotificationSeverity.ERROR);
}
@@ -749,6 +749,44 @@ export default class NotificationHandler {
}
}
+ static async sendBillingInvoicesSynchronizationFailed(tenantID: string, sourceData: BillingInvoiceSynchronizationFailedNotification): Promise<void> {
+ if (tenantID !== Constants.DEFAULT_TENANT) {
+ // Get the Tenant
+ const tenant = await TenantStorage.getTenant(tenantID);
+ // Enrich with admins
+ const adminUsers = await NotificationHandler.getAdminUsers(tenantID);
+ if (adminUsers && adminUsers.length > 0) {
+ // For each Sources
+ for (const notificationSource of NotificationHandler.notificationSources) {
+ // Active?
+ if (notificationSource.enabled) {
+ try {
+ // Check notification
+ const hasBeenNotified = await NotificationHandler.hasNotifiedSourceByID(
+ tenantID, notificationSource.channel, ServerAction.BILLING_INVOICE_SYNCHRONIZATION_FAILED);
+ // Notified?
+ if (!hasBeenNotified) {
+ // Save
+ await NotificationHandler.saveNotification(
+ tenantID, notificationSource.channel, null, ServerAction.BILLING_INVOICE_SYNCHRONIZATION_FAILED);
+ // Send
+ for (const adminUser of adminUsers) {
+ // Enabled?
+ if (adminUser.notificationsActive && adminUser.notifications.sendBillingSynchronizationFailed) {
+ await notificationSource.notificationTask.sendBillingInvoiceSynchronizationFailed(
+ sourceData, adminUser, tenant, NotificationSeverity.ERROR);
+ }
+ }
+ }
+ } catch (error) {
+ Logging.logActionExceptionMessage(tenantID, ServerAction.BILLING_USER_SYNCHRONIZATION_FAILED, error);
+ }
+ }
+ }
+ }
+ }
+ }
+
static async sendCarsSynchronizationFailed(sourceData: CarCatalogSynchronizationFailedNotification): Promise<void> {
// Enrich with admins
const adminUsers = await NotificationHandler.getAdminUsers(Constants.DEFAULT_TENANT);

MODIFY
src/notification/NotificationTask.ts
src/notification/NotificationTask.ts
-import { BillingUserSynchronizationFailedNotification, CarCatalogSynchronizationFailedNotification, ChargingStationRegisteredNotification, ChargingStationStatusErrorNotification, EndOfChargeNotification, EndOfSessionNotification, EndOfSignedSessionNotification, NewRegisteredUserNotification, NotificationSeverity, OCPIPatchChargingStationsStatusesErrorNotification, OfflineChargingStationNotification, OptimalChargeReachedNotification, PreparingSessionNotStartedNotification, RequestPasswordNotification, SessionNotStartedNotification, SmtpAuthErrorNotification, TransactionStartedNotification, UnknownUserBadgedNotification, UserAccountInactivityNotification, UserAccountStatusChangedNotification, VerificationEmailNotification } from '../types/UserNotifications';
+import { BillingInvoiceSynchronizationFailedNotification, BillingUserSynchronizationFailedNotification, CarCatalogSynchronizationFailedNotification, ChargingStationRegisteredNotification, ChargingStationStatusErrorNotification, EndOfChargeNotification, EndOfSessionNotification, EndOfSignedSessionNotification, 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';
@@ -22,6 +22,7 @@ export default interface NotificationTask {
sendPreparingSessionNotStarted(data: PreparingSessionNotStartedNotification, user: User, tenant: Tenant, severity: NotificationSeverity): Promise<void>;
sendOfflineChargingStations(data: OfflineChargingStationNotification, user: User, tenant: Tenant, severity: NotificationSeverity): Promise<void>;
sendBillingUserSynchronizationFailed(data: BillingUserSynchronizationFailedNotification, user: User, tenant: Tenant, severity: NotificationSeverity): Promise<void>;
+ sendBillingInvoiceSynchronizationFailed(data: BillingInvoiceSynchronizationFailedNotification, user: User, tenant: Tenant, severity: NotificationSeverity): Promise<void>;
sendCarCatalogSynchronizationFailed(data: CarCatalogSynchronizationFailedNotification, user: User, tenant: Tenant, severity: NotificationSeverity): Promise<void>;
sendSessionNotStarted(data: SessionNotStartedNotification, user: User, tenant: Tenant, severity: NotificationSeverity): Promise<void>;
}

MODIFY
src/notification/email/EMailNotificationTask.ts
src/notification/email/EMailNotificationTask.ts
-import { BillingUserSynchronizationFailedNotification, CarCatalogSynchronizationFailedNotification, ChargingStationRegisteredNotification, ChargingStationStatusErrorNotification, EndOfChargeNotification, EndOfSessionNotification, EndOfSignedSessionNotification, NewRegisteredUserNotification, NotificationSeverity, OCPIPatchChargingStationsStatusesErrorNotification, OfflineChargingStationNotification, OptimalChargeReachedNotification, PreparingSessionNotStartedNotification, RequestPasswordNotification, SessionNotStartedNotification, SmtpAuthErrorNotification, TransactionStartedNotification, UnknownUserBadgedNotification, UserAccountInactivityNotification, UserAccountStatusChangedNotification, VerificationEmailNotification } from '../../types/UserNotifications';
+import { BillingInvoiceSynchronizationFailedNotification, BillingUserSynchronizationFailedNotification, CarCatalogSynchronizationFailedNotification, ChargingStationRegisteredNotification, ChargingStationStatusErrorNotification, EndOfChargeNotification, EndOfSessionNotification, EndOfSignedSessionNotification, 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';
@@ -121,6 +121,10 @@ export default class EMailNotificationTask implements NotificationTask {
return this.prepareAndSendEmail('billing-user-synchronization-failed', data, user, tenant, severity);
}
+ public async sendBillingInvoiceSynchronizationFailed(data: BillingInvoiceSynchronizationFailedNotification, user: User, tenant: Tenant, severity: NotificationSeverity): Promise<void> {
+ return this.prepareAndSendEmail('billing-invoice-synchronization-failed', 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 { BillingUserSynchronizationFailedNotification, CarCatalogSynchronizationFailedNotification, ChargingStationRegisteredNotification, ChargingStationStatusErrorNotification, EndOfChargeNotification, EndOfSessionNotification, EndOfSignedSessionNotification, NewRegisteredUserNotification, NotificationSeverity, OCPIPatchChargingStationsStatusesErrorNotification, OfflineChargingStationNotification, OptimalChargeReachedNotification, PreparingSessionNotStartedNotification, RequestPasswordNotification, SessionNotStartedNotification, SmtpAuthErrorNotification, TransactionStartedNotification, UnknownUserBadgedNotification, UserAccountInactivityNotification, UserAccountStatusChangedNotification, UserNotificationType, VerificationEmailNotification } from '../../types/UserNotifications';
+import { BillingInvoiceSynchronizationFailedNotification, BillingUserSynchronizationFailedNotification, CarCatalogSynchronizationFailedNotification, ChargingStationRegisteredNotification, ChargingStationStatusErrorNotification, EndOfChargeNotification, EndOfSessionNotification, EndOfSignedSessionNotification, 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';
@@ -294,6 +294,18 @@ export default class RemotePushNotificationTask implements NotificationTask {
title, body, user, { 'error': data.nbrUsersInError + '', }, severity);
}
+ public async sendBillingInvoiceSynchronizationFailed(data: BillingInvoiceSynchronizationFailedNotification, 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.billingUserSynchronizationFailed.title');
+ const body = i18nManager.translate('notifications.billingUserSynchronizationFailed.body',
+ { nbrUsersInError: data.nbrInvoicesInError, tenantName: tenant.name });
+ // Send Notification
+ return this.sendRemotePushNotificationToUser(tenant, UserNotificationType.BILLING_USER_SYNCHRONIZATION_FAILED,
+ title, body, user, { 'error': data.nbrInvoicesInError + '', }, severity);
+ }
+
private async sendRemotePushNotificationToUser(tenant: Tenant, notificationType: UserNotificationType, title: string, body: string, user: User, data?: object, severity?: NotificationSeverity) {
// Checks
if (!this.initialized) {

MODIFY
src/scheduler/SchedulerManager.ts
src/scheduler/SchedulerManager.ts
@@ -17,6 +17,7 @@ import OCPIGetTokensTask from './tasks/ocpi/OCPIGetTokensTask';
import OCPIPatchLocationsTask from './tasks/ocpi/OCPIPatchLocationsTask';
import SchedulerTask from './SchedulerTask';
import { ServerAction } from '../types/Server';
+import SynchronizeBillingInvoicesTask from './tasks/SynchronizeBillingInvoicesTask';
import SynchronizeBillingUsersTask from './tasks/SynchronizeBillingUsersTask';
import SynchronizeCarsTask from './tasks/SynchronizeCarsTask';
import SynchronizeRefundTransactionsTask from './tasks/SynchronizeRefundTransactionsTask';
@@ -96,6 +97,9 @@ export default class SchedulerManager {
case 'SynchronizeBillingUsersTask':
schedulerTask = new SynchronizeBillingUsersTask();
break;
+ case 'SynchronizeBillingInvoicesTask':
+ schedulerTask = new SynchronizeBillingInvoicesTask();
+ break;
case 'SynchronizeCarsTask':
schedulerTask = new SynchronizeCarsTask();
break;

ADD
src/scheduler/tasks/SynchronizeBillingInvoicesTask.ts
+import BillingFactory from '../../integration/billing/BillingFactory';
+import { LockEntity } from '../../types/Locking';
+import LockingManager from '../../locking/LockingManager';
+import Logging from '../../utils/Logging';
+import NotificationHandler from '../../notification/NotificationHandler';
+import SchedulerTask from '../SchedulerTask';
+import { ServerAction } from '../../types/Server';
+import { TaskConfig } from '../../types/TaskConfig';
+import Tenant from '../../types/Tenant';
+import Utils from '../../utils/Utils';
+
+export default class SynchronizeBillingInvoicesTask extends SchedulerTask {
+ async processTenant(tenant: Tenant, config: TaskConfig): Promise<void> {
+ // Get the lock
+ const billingLock = LockingManager.createExclusiveLock(tenant.id, LockEntity.INVOICE, 'synchronize-billing');
+ if (await LockingManager.acquire(billingLock)) {
+ try {
+ const billingImpl = await BillingFactory.getBillingImpl(tenant.id);
+ if (billingImpl) {
+ const synchronizeAction = await billingImpl.synchronizeInvoices(tenant.id);
+ if (synchronizeAction.inError === 0) {
+ await NotificationHandler.sendBillingInvoicesSynchronizationFailed(
+ tenant.id,
+ {
+ nbrInvoicesInError: synchronizeAction.inError,
+ evseDashboardURL: Utils.buildEvseURL(tenant.subdomain),
+ evseDashboardBillingURL: await Utils.buildEvseBillingSettingsURL(tenant.id)
+ }
+ );
+ }
+ }
+ } catch (error) {
+ // Log error
+ Logging.logActionExceptionMessage(tenant.id, ServerAction.BILLING_SYNCHRONIZE_INVOICES, error);
+ } finally {
+ // Release the lock
+ await LockingManager.release(billingLock);
+ }
+ }
+ }
+}

MODIFY
src/server/rest/service/security/UserSecurity.ts
src/server/rest/service/security/UserSecurity.ts
@@ -223,7 +223,7 @@ export default class UserSecurity {
sendCarCatalogSynchronizationFailed: notifications ? UtilsSecurity.filterBoolean(notifications.sendCarCatalogSynchronizationFailed) : false,
sendUserAccountInactivity: notifications ? UtilsSecurity.filterBoolean(notifications.sendUserAccountInactivity) : false,
sendPreparingSessionNotStarted: notifications ? UtilsSecurity.filterBoolean(notifications.sendPreparingSessionNotStarted) : false,
- sendBillingUserSynchronizationFailed: false,
+ sendBillingSynchronizationFailed: notifications ? UtilsSecurity.filterBoolean(notifications.sendBillingSynchronizationFailed) : false,
sendNewRegisteredUser: false,
sendUnknownUserBadged: false,
sendChargingStationStatusError: false,
@@ -236,7 +236,7 @@ export default class UserSecurity {
if (role === UserRole.ADMIN) {
filteredNotifications = {
...filteredNotifications,
- sendBillingUserSynchronizationFailed: notifications ? UtilsSecurity.filterBoolean(notifications.sendBillingUserSynchronizationFailed) : false,
+ sendBillingSynchronizationFailed: notifications ? UtilsSecurity.filterBoolean(notifications.sendBillingSynchronizationFailed) : false,
sendNewRegisteredUser: notifications ? UtilsSecurity.filterBoolean(notifications.sendNewRegisteredUser) : false,
sendUnknownUserBadged: notifications ? UtilsSecurity.filterBoolean(notifications.sendUnknownUserBadged) : false,
sendChargingStationStatusError: notifications ? UtilsSecurity.filterBoolean(notifications.sendChargingStationStatusError) : false,

MODIFY
src/storage/mongodb/UserStorage.ts
src/storage/mongodb/UserStorage.ts
@@ -258,7 +258,7 @@ export default class UserStorage {
sendUserAccountInactivity: userToSave.notifications ? Utils.convertToBoolean(userToSave.notifications.sendUserAccountInactivity) : false,
sendPreparingSessionNotStarted: userToSave.notifications ? Utils.convertToBoolean(userToSave.notifications.sendPreparingSessionNotStarted) : false,
sendOfflineChargingStations: userToSave.notifications ? Utils.convertToBoolean(userToSave.notifications.sendOfflineChargingStations) : false,
- sendBillingUserSynchronizationFailed: userToSave.notifications ? Utils.convertToBoolean(userToSave.notifications.sendBillingUserSynchronizationFailed) : false,
+ sendBillingUserSynchronizationFailed: userToSave.notifications ? Utils.convertToBoolean(userToSave.notifications.sendBillingSynchronizationFailed) : false,
sendSessionNotStarted: userToSave.notifications ? Utils.convertToBoolean(userToSave.notifications.sendSessionNotStarted) : false,
sendCarCatalogSynchronizationFailed: userToSave.notifications ? Utils.convertToBoolean(userToSave.notifications.sendCarCatalogSynchronizationFailed) : false,
},
@@ -1054,7 +1054,7 @@ export default class UserStorage {
sendUserAccountInactivity: false,
sendPreparingSessionNotStarted: false,
sendOfflineChargingStations: false,
- sendBillingUserSynchronizationFailed: false,
+ sendBillingSynchronizationFailed: false,
sendSessionNotStarted: false,
sendCarCatalogSynchronizationFailed: false
},

MODIFY
src/types/Locking.ts
src/types/Locking.ts
@@ -22,4 +22,5 @@ export enum LockEntity {
LOGGING = 'logging',
TRANSACTION = 'transaction',
CAR = 'car',
+ INVOICE = 'invoice'
}

MODIFY
src/types/Server.ts
src/types/Server.ts
@@ -205,6 +205,7 @@ export enum ServerAction {
PREPARING_SESSION_NOT_STARTED = 'PreparingSessionNotStarted',
OFFLINE_CHARGING_STATIONS = 'OfflineChargingStations',
BILLING_USER_SYNCHRONIZATION_FAILED = 'BillingUserSynchronizationFailed',
+ BILLING_INVOICE_SYNCHRONIZATION_FAILED = 'BillingInvoiceSynchronizationFailed',
CAR_CATALOG_SYNCHRONIZATION_FAILED = 'CarCatalogSynchronizationFailed',
CAR_CATALOG_SYNCHRONIZATION = 'CarCatalogSynchronization',

MODIFY
src/types/UserNotifications.ts
src/types/UserNotifications.ts
@@ -16,7 +16,7 @@ export default interface UserNotifications {
sendUserAccountInactivity: boolean;
sendPreparingSessionNotStarted: boolean;
sendOfflineChargingStations: boolean;
- sendBillingUserSynchronizationFailed: boolean;
+ sendBillingSynchronizationFailed: boolean;
sendCarCatalogSynchronizationFailed: boolean;
sendSessionNotStarted: boolean;
}
@@ -213,6 +213,12 @@ export interface BillingUserSynchronizationFailedNotification extends BaseNotifi
evseDashboardURL: string;
evseDashboardBillingURL: string;
}
+
+export interface BillingInvoiceSynchronizationFailedNotification extends BaseNotification {
+ nbrInvoicesInError: number;
+ evseDashboardURL: string;
+ evseDashboardBillingURL: string;
+}
export interface CarCatalogSynchronizationFailedNotification extends BaseNotification {
nbrCarsInError: number;
evseDashboardURL: string;

MODIFY
test/api/BillingTest.ts
test/api/BillingTest.ts
@@ -180,7 +180,7 @@ describe('Billing Service', function() {
fakeUser,
false
);
- testData.createdUsers.push(fakeUser);
+ // TestData.createdUsers.push(fakeUser);
const billingUser = await billingImpl.getUserByEmail(fakeUser.email);
expect(billingUser.name).to.be.eq(fakeUser.firstName + ' ' + fakeUser.name);
});
@@ -387,7 +387,7 @@ describe('Billing Service', function() {
);
const response = await testData.userService.billingApi.readAll({}, TestConstants.DEFAULT_PAGING, TestConstants.DEFAULT_ORDERING, '/client/api/BillingUserInvoices');
for (let i = 0; i < response.data.result.length - 1; i++) {
- expect(response.data.result[i].userID).to.be.eq(basicUser.id);
+ expect(response.data.result[i].user.id).to.be.eq(basicUser.id);
}
});

MODIFY
test/factories/UserFactory.ts
test/factories/UserFactory.ts
@@ -28,7 +28,7 @@ const userFactory = Factory.define('user')
sendUserAccountInactivity: false,
sendPreparingSessionNotStarted: false,
sendOfflineChargingStations: false,
- sendBillingUserSynchronizationFailed: false,
+ sendBillingSynchronizationFailed: false,
sendSessionNotStarted: false,
sendCarCatalogSynchronizationFailed: false
}))

Added invoice sync task