Billing - scheduler - finalize/charge invoice
MODIFY
src/integration/billing/BillingIntegration.ts
src/integration/billing/BillingIntegration.ts
-import { BillingDataTransactionStart, BillingDataTransactionStop, BillingDataTransactionUpdate, BillingInvoice, BillingInvoiceDocument, BillingInvoiceItem, BillingTax, BillingUser, BillingUserSynchronizeAction } from '../../types/Billing';
+import { BillingChargeInvoiceAction, BillingDataTransactionStart, BillingDataTransactionStop, BillingDataTransactionUpdate, BillingInvoice, BillingInvoiceDocument, BillingInvoiceItem, BillingInvoiceStatus, BillingTax, BillingUser, BillingUserSynchronizeAction } from '../../types/Billing';
import User, { UserStatus } from '../../types/User';
import BackendError from '../../exception/BackendError';
@@ -350,6 +350,80 @@ export default abstract class BillingIntegration<T extends BillingSetting> {
return actionsDone;
}
+ public async finalizeInvoices(): Promise<BillingChargeInvoiceAction> {
+ const actionsDone: BillingChargeInvoiceAction = {
+ inSuccess: 0,
+ inError: 0
+ };
+ await this.checkConnection();
+ // Get the draft invoices
+ const draftInvoices = await BillingStorage.getDraftInvoices(this.tenantID);
+ // Let's first finalize the invoices
+ for (const draftInvoice of draftInvoices.result) {
+ // Synchronize user
+ try {
+ await this.finalizeInvoice(draftInvoice);
+ // TODO - update the billing invoice state to reflect the change?
+ Logging.logInfo({
+ tenantID: this.tenantID,
+ source: Constants.CENTRAL_SERVER,
+ action: ServerAction.BILLING_CHARGE_INVOICE,
+ module: MODULE_NAME, method: 'chargeInvoices',
+ message: `Succesfully finalized invoice '${draftInvoice.id}'`
+ });
+ actionsDone.inSuccess++;
+ } catch (error) {
+ actionsDone.inError++;
+ Logging.logError({
+ tenantID: this.tenantID,
+ source: Constants.CENTRAL_SERVER,
+ action: ServerAction.BILLING_SYNCHRONIZE_USERS,
+ module: MODULE_NAME, method: 'chargeInvoices',
+ message: `Failed to finalize invoice '${draftInvoice.id}'`,
+ detailedMessages: { error: error.message, stack: error.stack }
+ });
+ }
+ }
+ return actionsDone;
+ }
+
+ public async chargeInvoices(): Promise<BillingChargeInvoiceAction> {
+ const actionsDone: BillingChargeInvoiceAction = {
+ inSuccess: 0,
+ inError: 0
+ };
+ await this.checkConnection();
+
+ const openedInvoices = await BillingStorage.getOpenInvoices(this.tenantID);
+ // Let's now try to pay opened invoices
+ for (const openInvoice of openedInvoices.result) {
+ // Synchronize user
+ try {
+ await this.chargeInvoice(openInvoice);
+ // TODO - update the billing invoice state to reflect the change?
+ Logging.logInfo({
+ tenantID: this.tenantID,
+ source: Constants.CENTRAL_SERVER,
+ action: ServerAction.BILLING_CHARGE_INVOICE,
+ module: MODULE_NAME, method: 'chargeInvoices',
+ message: `Succesfully charge invoice '${openInvoice.id}'`
+ });
+ actionsDone.inSuccess++;
+ } catch (error) {
+ actionsDone.inError++;
+ Logging.logError({
+ tenantID: this.tenantID,
+ source: Constants.CENTRAL_SERVER,
+ action: ServerAction.BILLING_SYNCHRONIZE_USERS,
+ module: MODULE_NAME, method: 'chargeInvoices',
+ message: `Failed to charge invoice '${openInvoice.id}'`,
+ detailedMessages: { error: error.message, stack: error.stack }
+ });
+ }
+ }
+ return actionsDone;
+ }
+
public async sendInvoiceToUser(invoice: BillingInvoice): Promise<BillingInvoice> {
// Send link to the user using our notification framework (link to the front-end + download)
const tenant = await TenantStorage.getTenant(this.tenantID);

MODIFY
src/scheduler/tasks/SynchronizeBillingInvoicesTask.ts
src/scheduler/tasks/SynchronizeBillingInvoicesTask.ts
import BillingFactory from '../../integration/billing/BillingFactory';
+import { BillingInvoiceSynchonizationTaskConfig } from '../../types/TaskConfig';
import LockingHelper from '../../locking/LockingHelper';
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> {
+ async processTenant(tenant: Tenant, taskConfig: BillingInvoiceSynchonizationTaskConfig): Promise<void> {
// Get the lock
const billingLock = await LockingHelper.createBillingSyncInvoicesLock(tenant.id);
if (billingLock) {
try {
const billingImpl = await BillingFactory.getBillingImpl(tenant.id);
if (billingImpl) {
- const synchronizeAction = await billingImpl.synchronizeInvoices();
- if (synchronizeAction.inError === 0) {
+ // Synchronize new Invoices and Invoices changes
+ const synchronizeActionResults = await billingImpl.synchronizeInvoices();
+ if (synchronizeActionResults.inError > 0) {
await NotificationHandler.sendBillingInvoicesSynchronizationFailed(
tenant.id,
{
- nbrInvoicesInError: synchronizeAction.inError,
+ nbrInvoicesInError: synchronizeActionResults.inError,
evseDashboardURL: Utils.buildEvseURL(tenant.subdomain),
evseDashboardBillingURL: Utils.buildEvseBillingSettingsURL(tenant.subdomain)
}
);
}
+ // Attempt payment - once a month!
+ if (taskConfig?.attemptPayment) {
+ // Attempt to finalize invoices with status DRAFT
+ const finalizeActionResults = await billingImpl.finalizeInvoices();
+ if (finalizeActionResults.inError > 0) {
+ // TODO - dedicated notification type is required here!!!
+ await NotificationHandler.sendBillingInvoicesSynchronizationFailed(
+ tenant.id,
+ {
+ nbrInvoicesInError: finalizeActionResults.inError,
+ evseDashboardURL: Utils.buildEvseURL(tenant.subdomain),
+ evseDashboardBillingURL: Utils.buildEvseBillingSettingsURL(tenant.subdomain)
+ }
+ );
+ }
+ // Attempt to pay invoices with status OPEN
+ const chargeActionResults = await billingImpl.chargeInvoices();
+ if (chargeActionResults.inError > 0) {
+ // TODO - dedicated notification type is required here!!!
+ await NotificationHandler.sendBillingInvoicesSynchronizationFailed(
+ tenant.id,
+ {
+ nbrInvoicesInError: chargeActionResults.inError,
+ evseDashboardURL: Utils.buildEvseURL(tenant.subdomain),
+ evseDashboardBillingURL: Utils.buildEvseBillingSettingsURL(tenant.subdomain)
+ }
+ );
+ }
+ }
}
} catch (error) {
// Log error

MODIFY
src/storage/mongodb/BillingStorage.ts
src/storage/mongodb/BillingStorage.ts
@@ -26,6 +26,20 @@ export default class BillingStorage {
return invoicesMDB.count === 1 ? invoicesMDB.result[0] : null;
}
+ public static async getDraftInvoices(tenantID: string): Promise<DataResult<BillingInvoice>> {
+ const invoicesMDB = await BillingStorage.getInvoices(tenantID, {
+ invoiceStatus: [BillingInvoiceStatus.DRAFT]
+ }, Constants.DB_PARAMS_SINGLE_RECORD);
+ return invoicesMDB;
+ }
+
+ public static async getOpenInvoices(tenantID: string): Promise<DataResult<BillingInvoice>> {
+ const invoicesMDB = await BillingStorage.getInvoices(tenantID, {
+ invoiceStatus: [BillingInvoiceStatus.OPEN]
+ }, Constants.DB_PARAMS_SINGLE_RECORD);
+ return invoicesMDB;
+ }
+
public static async getInvoices(tenantID: string,
params: {
invoiceIDs?: string[]; billingInvoiceID?: string; search?: string; userIDs?: string[]; invoiceStatus?: BillingInvoiceStatus[];

MODIFY
src/types/Billing.ts
src/types/Billing.ts
@@ -54,6 +54,10 @@ export interface BillingUserSynchronizeAction extends ActionsResponse {
billingData?: BillingUserData;
}
+export interface BillingChargeInvoiceAction extends ActionsResponse {
+ billingData?: BillingUserData;
+}
+
export interface BillingTax {
id: string;
description: string;

MODIFY
src/types/TaskConfig.ts
src/types/TaskConfig.ts
@@ -25,6 +25,10 @@ export interface CheckSessionNotStartedAfterAuthorizeTaskConfig extends TaskConf
checkPastAuthorizeMins?: number;
}
+export interface BillingInvoiceSynchonizationTaskConfig extends TaskConfig {
+ attemptPayment?: boolean;
+}
+
export interface OCPIGetTokensTaskConfig extends TaskConfig {
partial?: boolean;
}

Billing - scheduler - finalize/charge invoice