async billing proposal
MODIFY
src/async-task/AsyncTaskManager.ts
src/async-task/AsyncTaskManager.ts
@@ -3,6 +3,7 @@ import AsyncTask, { AsyncTaskStatus, AsyncTasks } from '../types/AsyncTask';
import AbstractAsyncTask from './AsyncTask';
import { ActionsResponse } from '../types/GlobalType';
import AsyncTaskStorage from '../storage/mongodb/AsyncTaskStorage';
+import BillTransactionAsyncTask from './tasks/BillTransactionAsyncTask';
import Configuration from '../utils/Configuration';
import Constants from '../utils/Constants';
import LockingHelper from '../locking/LockingHelper';
@@ -75,6 +76,9 @@ export default class AsyncTaskManager {
async (asyncTask: AsyncTask) => {
// Tasks
switch (asyncTask.name) {
+ case AsyncTasks.BILL_TRANSACTION:
+ abstractAsyncTask = new BillTransactionAsyncTask(asyncTask);
+ break;
case AsyncTasks.TAGS_IMPORT:
abstractAsyncTask = new TagsImportAsyncTask(asyncTask);
break;

ADD
src/async-task/tasks/BillTransactionAsyncTask.ts
+import AbstractAsyncTask from '../AsyncTask';
+import { BillingDataTransactionStop } from '../../types/Billing';
+import BillingFactory from '../../integration/billing/BillingFactory';
+import LockingHelper from '../../locking/LockingHelper';
+import LockingManager from '../../locking/LockingManager';
+import Logging from '../../utils/Logging';
+import { ServerAction } from '../../types/Server';
+import TenantComponents from '../../types/TenantComponents';
+import TenantStorage from '../../storage/mongodb/TenantStorage';
+import TransactionStorage from '../../storage/mongodb/TransactionStorage';
+import Utils from '../../utils/Utils';
+
+export default class BillTransactionAsyncTask extends AbstractAsyncTask {
+ protected async executeAsyncTask(): Promise<void> {
+ const tenant = await TenantStorage.getTenant(this.asyncTask.tenantID);
+ // Check if OCPI component is active
+ if (Utils.isTenantComponentActive(tenant, TenantComponents.BILLING)) {
+ try {
+ const billingImpl = await BillingFactory.getBillingImpl(tenant.id);
+ if (billingImpl) {
+
+ // Get the Transaction to bill
+ const transactionID: string = this.asyncTask.parameters.transactionID;
+ const transactionLock = await LockingHelper.createBillTransactionLock(tenant.id, Number(transactionID));
+ if (transactionLock) {
+ try {
+ const transaction = await TransactionStorage.getTransaction(tenant.id, Number(transactionID));
+ if (!transaction) {
+ throw new Error(`Unknown Transaction ID '${this.asyncTask.parameters.transactionID}'`);
+ }
+ // Attempt to finalize and pay invoices
+ const billingDataStop: BillingDataTransactionStop = await billingImpl.billTransaction(transaction);
+ // Update
+ if (transaction.billingData) {
+ transaction.billingData.stop = billingDataStop;
+ transaction.billingData.lastUpdate = new Date();
+ }
+ // Save
+ await TransactionStorage.saveTransaction(tenant.id, transaction);
+ } finally {
+ // Release the lock
+ await LockingManager.release(transactionLock);
+ }
+ }
+ }
+ } catch (error) {
+ await Logging.logActionExceptionMessage(tenant.id, ServerAction.BILLING_TRANSACTION, error);
+ // TODO - to be clarified - Propagate the error to have the chance to run the task again!
+ throw error;
+ }
+ }
+ }
+}

MODIFY
src/integration/billing/BillingIntegration.ts
src/integration/billing/BillingIntegration.ts
@@ -369,6 +369,8 @@ export default abstract class BillingIntegration {
abstract stopTransaction(transaction: Transaction): Promise<BillingDataTransactionStop>;
+ abstract billTransaction(transaction: Transaction): Promise<BillingDataTransactionStop>;
+
abstract checkIfUserCanBeCreated(user: User): Promise<boolean>;
abstract checkIfUserCanBeUpdated(user: User): Promise<boolean>;

MODIFY
src/integration/billing/stripe/StripeBillingIntegration.ts
src/integration/billing/stripe/StripeBillingIntegration.ts
+import { AsyncTaskType, AsyncTasks } from '../../../types/AsyncTask';
/* eslint-disable @typescript-eslint/member-ordering */
import { BillingDataTransactionStart, BillingDataTransactionStop, BillingDataTransactionUpdate, BillingInvoice, BillingInvoiceDocument, BillingInvoiceItem, BillingInvoiceStatus, BillingOperationResult, BillingPaymentMethod, BillingStatus, BillingTax, BillingUser, BillingUserData } from '../../../types/Billing';
import { DocumentEncoding, DocumentType } from '../../../types/GlobalType';
import FeatureToggles, { Feature } from '../../../utils/FeatureToggles';
+import AsyncTaskManager from '../../../async-task/AsyncTaskManager';
import AxiosFactory from '../../../utils/AxiosFactory';
import { AxiosInstance } from 'axios';
import BackendError from '../../../exception/BackendError';
@@ -854,6 +856,31 @@ export default class StripeBillingIntegration extends BillingIntegration {
status: BillingStatus.UNBILLED
};
}
+ if (FeatureToggles.isFeatureActive(Feature.BILLING_ASYNC_BILL_TRANSACTION)) {
+ // Create and Save async task
+ await AsyncTaskManager.createAndSaveAsyncTasks({
+ name: AsyncTasks.BILL_TRANSACTION,
+ action: ServerAction.BILLING_TRANSACTION,
+ type: AsyncTaskType.TASK,
+ tenantID: this.tenantID,
+ parameters: {
+ transactionID: String(transaction.id),
+ },
+ module: MODULE_NAME,
+ method: 'stopTransaction',
+ });
+ // Inform tha calling layer that the operation has been postponed
+ if (!transaction.billingData?.withBillingActive) {
+ return {
+ status: BillingStatus.PENDING
+ };
+ }
+ } else {
+ return await this.billTransaction(transaction);
+ }
+ }
+
+ public async billTransaction(transaction: Transaction): Promise<BillingDataTransactionStop> {
// Check Stripe
await this.checkConnection();
// Check object
@@ -863,7 +890,7 @@ export default class StripeBillingIntegration extends BillingIntegration {
const customerID: string = transaction.user?.billingData?.customerID;
const customer = await this.getStripeCustomer(customerID);
if (customer) {
- const billingDataTransactionStop: BillingDataTransactionStop = await this.billTransaction(transaction);
+ const billingDataTransactionStop: BillingDataTransactionStop = await this._billTransaction(transaction);
return billingDataTransactionStop;
}
} catch (error) {
@@ -877,9 +904,8 @@ export default class StripeBillingIntegration extends BillingIntegration {
detailedMessages: { error: error.message, stack: error.stack }
});
}
-
return {
- status: BillingStatus.UNBILLED
+ status: BillingStatus.FAILED
};
}
@@ -901,7 +927,7 @@ export default class StripeBillingIntegration extends BillingIntegration {
return null;
}
- public async billTransaction(transaction: Transaction): Promise<BillingDataTransactionStop> {
+ private async _billTransaction(transaction: Transaction): Promise<BillingDataTransactionStop> {
// ACHTUNG: a single transaction may generate several lines in the invoice
const invoiceItem: BillingInvoiceItem = this.convertToBillingInvoiceItem(transaction);
const billingInvoice = await this.billInvoiceItem(transaction.user, invoiceItem, `${transaction.id}`);

MODIFY
src/locking/LockingHelper.ts
src/locking/LockingHelper.ts
@@ -160,6 +160,14 @@ export default class LockingHelper {
return lock;
}
+ public static async createBillTransactionLock(tenantID: string, transactionID: number): Promise<Lock | null> {
+ const lock = LockingManager.createExclusiveLock(tenantID, LockEntity.TRANSACTION, `bill-transaction-${transactionID}`);
+ if (!(await LockingManager.acquire(lock))) {
+ return null;
+ }
+ return lock;
+ }
+
private static async createOCPIEndpointActionLock(tenantID: string, ocpiEndpoint: OCPIEndpoint, action: string): Promise<Lock | null> {
const lock = LockingManager.createExclusiveLock(tenantID, LockEntity.OCPI_ENDPOINT, `${ocpiEndpoint.id}-${action}`);
if (!(await LockingManager.acquire(lock))) {

MODIFY
src/types/AsyncTask.ts
src/types/AsyncTask.ts
@@ -32,6 +32,7 @@ export enum AsyncTaskStatus {
export enum AsyncTasks {
+ BILL_TRANSACTION = 'BillTransactionAsyncTask',
TAGS_IMPORT = 'TagsImportAsyncTask',
USERS_IMPORT = 'UsersImportAsyncTask',
OCPI_PUSH_TOKENS = 'OCPIPushTokensAsyncTask',

MODIFY
src/types/Billing.ts
src/types/Billing.ts
@@ -19,6 +19,8 @@ export interface BillingDataTransactionUpdate {
export enum BillingStatus {
UNBILLED = 'unbilled',
BILLED = 'billed',
+ PENDING = 'pending',
+ FAILED = 'failed',
}
export interface BillingDataTransactionStop {
status?: string;

MODIFY
src/utils/FeatureToggles.ts
src/utils/FeatureToggles.ts
@@ -9,6 +9,7 @@ export enum Feature {
BILLING_WITH_PARKING_TIME,
BILLING_ITEM_WITH_PARKING_TIME,
BILLING_ITEM_WITH_START_DATE,
+ BILLING_ASYNC_BILL_TRANSACTION,
}
export default class FeatureToggles {
@@ -21,6 +22,7 @@ export default class FeatureToggles {
Feature.BILLING_CHECK_USER_DEFAULT_PAYMENT_METHOD,
// Feature.BILLING_ITEM_WITH_PARKING_TIME,
Feature.BILLING_ITEM_WITH_START_DATE,
+ // Feature.BILLING_ASYNC_BILL_TRANSACTION,
];
// Check whether the feature is active or not!

async billing proposal