bp - transfers - adding a job for dispatching funds
MODIFY
src/integration/billing/BillingIntegration.ts
src/integration/billing/BillingIntegration.ts
/* eslint-disable @typescript-eslint/member-ordering */
-import { BillingAccount, BillingChargeInvoiceAction, BillingDataTransactionStart, BillingDataTransactionStop, BillingDataTransactionUpdate, BillingInvoice, BillingInvoiceItem, BillingInvoiceStatus, BillingOperationResult, BillingPaymentMethod, BillingPlatformFeeStrategy, BillingPlatformInvoice, BillingSessionAccountData, BillingStatus, BillingTax, BillingTransfer, BillingTransferStatus, BillingUser } from '../../types/Billing';
+import { BillingAccount, BillingDataTransactionStart, BillingDataTransactionStop, BillingDataTransactionUpdate, BillingInvoice, BillingInvoiceItem, BillingInvoiceStatus, BillingOperationResult, BillingPaymentMethod, BillingPlatformFeeStrategy, BillingPlatformInvoice, BillingSessionAccountData, BillingStatus, BillingTax, BillingTransfer, BillingTransferStatus, BillingUser } from '../../types/Billing';
import { BillingPeriodicOperationTaskConfig, DispatchFundsTaskConfig } from '../../types/TaskConfig';
import Tenant, { TenantComponents } from '../../types/Tenant';
import Transaction, { CollectedFundReport, StartTransactionErrorCode } from '../../types/Transaction';
import User, { UserStatus } from '../../types/User';
+import { ActionsResponse } from '../../types/GlobalType';
import BackendError from '../../exception/BackendError';
import { BillingSettings } from '../../types/Setting';
import BillingStorage from '../../storage/mongodb/BillingStorage';
@@ -97,8 +98,8 @@ export default abstract class BillingIntegration {
return billingUser;
}
- public async chargeInvoices(taskConfig: BillingPeriodicOperationTaskConfig): Promise<BillingChargeInvoiceAction> {
- const actionsDone: BillingChargeInvoiceAction = {
+ public async chargeInvoices(taskConfig: BillingPeriodicOperationTaskConfig): Promise<ActionsResponse> {
+ const actionsDone: ActionsResponse = {
inSuccess: 0,
inError: 0
};
@@ -715,14 +716,35 @@ export default abstract class BillingIntegration {
public abstract sendTransfer(transfer: BillingTransfer, user: User): Promise<string>;
- public async dispatchCollectedFunds(taskConfig: DispatchFundsTaskConfig): Promise<void> {
+ public async dispatchCollectedFunds(taskConfig: DispatchFundsTaskConfig): Promise<ActionsResponse> {
+ const actionsDone: ActionsResponse = {
+ inSuccess: 0,
+ inError: 0
+ };
+ if (taskConfig.forceOperation && Utils.isDevelopmentEnv()) {
+ Logging.logConsoleDebug('Funds dispatching is being forced for testing purposes reasons!');
+ }
const collectedFunds = await TransactionStorage.getCollectedFunds(this.tenant);
if (collectedFunds.count) {
for (const collectedFundReport of collectedFunds.result) {
+ try {
await this.dispatchCollectedFundsToAccount(collectedFundReport);
+ // successfully dispatched funds to the account
+ actionsDone.inSuccess++;
+ } catch (error) {
+ actionsDone.inError++;
+ await Logging.logError({
+ tenantID: this.tenant.id,
+ action: ServerAction.BILLING_TRANSFER_DISPATCH_FUNDS,
+ module: MODULE_NAME, method: 'dispatchCollectedFunds',
+ message: `Failed to dispatch funds to account ID: '${collectedFundReport.key?.accountID}'`,
+ detailedMessages: { error: error.stack }
+ });
}
}
}
+ return actionsDone;
+ }
public async dispatchCollectedFundsToAccount(collectedFundReport: CollectedFundReport): Promise<void> {
const accountID = collectedFundReport.key.accountID;
@@ -745,6 +767,12 @@ export default abstract class BillingIntegration {
};
const transferID = await BillingStorage.saveTransfer(this.tenant, transferToSave);
await TransactionStorage.updateTransactionsWithTransferData(this.tenant, collectedFundReport.transactionIDs, transferID);
+ await Logging.logInfo({
+ tenantID: this.tenant.id,
+ action: ServerAction.BILLING_TRANSFER_DISPATCH_FUNDS,
+ module: MODULE_NAME, method: 'dispatchCollectedFunds',
+ message: `Funds dispatched - account ID: '${collectedFundReport.key?.accountID}' - Collected funds: ${collectedFunds} - Collected fees: ${collectedFees}`,
+ });
} finally {
// Release the lock
await LockingManager.release(lock);
@@ -759,17 +787,15 @@ export default abstract class BillingIntegration {
private async getDraftTransferForAccount(accountID: string, currency: string) : Promise<BillingTransfer> {
const filter = {
// TODO - add filtering on the dates - we should have a transfer per month !?!
+ // TODO - filter on the currency as well?
accountIDs: [accountID],
status: [BillingTransferStatus.DRAFT],
};
const sort = { createdOn: -1 };
const transfers = await BillingStorage.getTransfers(this.tenant, filter, { skip: 0, limit: 1, sort });
- if (transfers.result[0]) {
- // Return the existing DRAFT transfer
- return transfers.result[0];
- }
- // Create a new DRAFT transfer for the account
- return {
+ const transfer = transfers.result[0];
+ // Return the existing DRAFT transfer or a new one!
+ return (transfer) ? transfer : {
accountID, status: BillingTransferStatus.DRAFT, sessionCounter: 0,
collectedFunds: 0, collectedFlatFees: 0, collectedFees: 0, totalConsumptionWh: 0, totalDurationSecs: 0, transferAmount: 0,
platformFeeData: null, transferExternalID: null,

MODIFY
src/locking/LockingHelper.ts
src/locking/LockingHelper.ts
@@ -80,8 +80,8 @@ export default class LockingHelper {
return lock;
}
- public static async acquireDispatchCollectedFundsLock(tenantID: string, transactionID: string): Promise<Lock | null> {
- const lock = LockingManager.createExclusiveLock(tenantID, LockEntity.INVOICE, `dispatch-funds-${transactionID}`);
+ public static async acquireDispatchCollectedFundsLock(tenantID: string): Promise<Lock | null> {
+ const lock = LockingManager.createExclusiveLock(tenantID, LockEntity.INVOICE, 'dispatch-collected-funds');
if (!(await LockingManager.acquire(lock))) {
return null;
}

MODIFY
src/scheduler/tasks/BillingPeriodicOperationTask.ts
src/scheduler/tasks/BillingPeriodicOperationTask.ts
@@ -32,7 +32,7 @@ export default class BillingPeriodicOperationTask extends TenantSchedulerTask {
}
} catch (error) {
// Log error
- await Logging.logActionExceptionMessage(tenant.id, ServerAction.BILLING_PERFORM_OPERATIONS, error);
+ await Logging.logActionExceptionMessage(tenant.id, ServerAction.BILLING_PERFORM_OPERATIONS, error as Error);
} finally {
// Release the lock
await LockingManager.release(billingLock);

ADD
src/scheduler/tasks/DistpatchCollectedFundsTask.ts
+import BillingFactory from '../../integration/billing/BillingFactory';
+import { DispatchFundsTaskConfig } from '../../types/TaskConfig';
+import LockingHelper from '../../locking/LockingHelper';
+import LockingManager from '../../locking/LockingManager';
+import Logging from '../../utils/Logging';
+import NotificationHandler from '../../notification/NotificationHandler';
+import { ServerAction } from '../../types/Server';
+import Tenant from '../../types/Tenant';
+import TenantSchedulerTask from '../TenantSchedulerTask';
+import Utils from '../../utils/Utils';
+
+export default class DispatchCollectedFundsTask extends TenantSchedulerTask {
+ public async processTenant(tenant: Tenant, taskConfig: DispatchFundsTaskConfig): Promise<void> {
+ // Get the lock
+ const billingLock = await LockingHelper.acquireDispatchCollectedFundsLock(tenant.id);
+ if (billingLock) {
+ try {
+ const billingImpl = await BillingFactory.getBillingImpl(tenant);
+ if (billingImpl) {
+ // Attempt to finalize and pay invoices
+ const actionResults = await billingImpl.dispatchCollectedFunds(taskConfig);
+ if (actionResults.inError > 0) {
+ // TODO - send a notification to the ADMINS
+ // void NotificationHandler.sendBillingPeriodicOperationFailed(
+ // tenant,
+ // {
+ // nbrInvoicesInError: actionResults.inError,
+ // evseDashboardURL: Utils.buildEvseURL(tenant.subdomain),
+ // evseDashboardBillingURL: Utils.buildEvseBillingSettingsURL(tenant.subdomain)
+ // }
+ // );
+ }
+ }
+ } catch (error) {
+ // Log error
+ await Logging.logActionExceptionMessage(tenant.id, ServerAction.BILLING_TRANSFER_DISPATCH_FUNDS, error as Error);
+ } finally {
+ // Release the lock
+ await LockingManager.release(billingLock);
+ }
+ }
+ }
+}

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

MODIFY
src/types/Server.ts
src/types/Server.ts
@@ -463,6 +463,7 @@ export enum ServerAction {
BILLING_TRANSFER_PREPARE = 'BillingTransferPrepare',
BILLING_TRANSFER_FINALIZE = 'BillingTransferFinalize',
BILLING_TRANSFER_SEND = 'BillingTransferSend',
+ BILLING_TRANSFER_DISPATCH_FUNDS = 'BillingTransferDispatchFunds',
PRICING = 'Pricing',
PRICING_DEFINITION = 'PricingDefinition',

MODIFY
test/api/BillingPlatformTest.ts
test/api/BillingPlatformTest.ts
/* eslint-disable max-len */
-import { BillingAccount, BillingAccountStatus, BillingChargeInvoiceAction, BillingInvoiceStatus, BillingTransfer, BillingTransferStatus } from '../../src/types/Billing';
+import { BillingAccount, BillingAccountStatus, BillingInvoiceStatus, BillingTransfer, BillingTransferStatus } from '../../src/types/Billing';
import { BillingPlatformFeeStrategyFactory, BillingTransferFactory } from '../factories/BillingFactory';
import chai, { expect } from 'chai';
@@ -291,7 +291,7 @@ describeif(isBillingProperlyConfigured)('Billing Platform (utbillingplatform)',
onlyProcessUnpaidInvoices: false,
forceOperation: true
};
- const operationResult: BillingChargeInvoiceAction = await billingTestHelper.billingImpl.chargeInvoices(taskConfiguration);
+ const operationResult = await billingTestHelper.billingImpl.chargeInvoices(taskConfiguration);
assert(operationResult.inSuccess > 0, 'The operation should have been able to process at least one invoice');
assert(operationResult.inError === 0, 'The operation should detect any errors');
// // Explicit call to dispatch collected funds

MODIFY
test/api/BillingTest.ts
test/api/BillingTest.ts
-/* eslint-disable max-len */
-import { BillingChargeInvoiceAction, BillingInvoiceStatus } from '../../src/types/Billing';
import { BillingSettings, BillingSettingsType } from '../../src/types/Setting';
import StripeTestHelper, { BillingTestConfigHelper } from './StripeTestHelper';
import chai, { expect } from 'chai';
+/* eslint-disable max-len */
+import { BillingInvoiceStatus } from '../../src/types/Billing';
import { BillingPeriodicOperationTaskConfig } from '../../src/types/TaskConfig';
import BillingTestHelper from './BillingTestHelper';
import Constants from '../../src/utils/Constants';
-import ContextDefinition from './context/ContextDefinition';
import Factory from '../factories/Factory';
import MongoDBStorage from '../../src/storage/mongodb/MongoDBStorage';
import { StatusCodes } from 'http-status-codes';
@@ -824,7 +823,7 @@ describeif(isBillingProperlyConfigured)('Billing', () => {
onlyProcessUnpaidInvoices: false,
forceOperation: true
};
- const operationResult: BillingChargeInvoiceAction = await billingTestHelper.billingImpl.chargeInvoices(taskConfiguration);
+ const operationResult = await billingTestHelper.billingImpl.chargeInvoices(taskConfiguration);
assert(operationResult.inSuccess > 0, 'The operation should have been able to process at least one invoice');
assert(operationResult.inError === 0, 'The operation should detect any errors');
// The transaction should now have a different status and know the final invoice number

MODIFY
test/api/StripeTestHelper.ts
test/api/StripeTestHelper.ts
-import { BillingAccount, BillingChargeInvoiceAction, BillingInvoice, BillingInvoiceItem, BillingInvoiceStatus, BillingOperationResult, BillingUser, BillingUserData } from '../../src/types/Billing';
+import { BillingAccount, BillingInvoice, BillingInvoiceItem, BillingInvoiceStatus, BillingOperationResult, BillingUser, BillingUserData } from '../../src/types/Billing';
import { BillingSettings, BillingSettingsType, SettingDB } from '../../src/types/Setting';
import Tenant, { TenantComponents } from '../../src/types/Tenant';
import chai, { expect } from 'chai';
@@ -269,7 +269,7 @@ export default class StripeTestHelper {
forceOperation: true
};
// Here we simulate the periodic operation which is supposed to try to pay again after a payment failure
- const operationResult: BillingChargeInvoiceAction = await this.billingImpl.chargeInvoices(taskConfiguration);
+ const operationResult = await this.billingImpl.chargeInvoices(taskConfiguration);
assert(operationResult.inSuccess > 0, 'The operation should have been able to process at least one invoice');
assert(operationResult.inError === 0, 'The operation should detect any errors');
// Let's check whether the nb of paid invoices has changed

bp - transfers - adding a job for dispatching funds