billing - asyncTask changed to prevent invoicing conflicts
MODIFY
src/async-task/tasks/BillTransactionAsyncTask.ts
src/async-task/tasks/BillTransactionAsyncTask.ts
@@ -21,8 +21,9 @@ export default class BillTransactionAsyncTask extends AbstractAsyncTask {
if (billingImpl) {
// Get the Transaction to bill
const transactionID: string = this.asyncTask.parameters.transactionID;
- const transactionLock = await LockingHelper.acquireBillTransactionLock(tenant.id, Number(transactionID));
- if (transactionLock) {
+ const userID: string = this.asyncTask.parameters.userID;
+ const lock = await LockingHelper.acquireBillUserLock(tenant.id, userID);
+ if (lock) {
try {
const transaction = await TransactionStorage.getTransaction(tenant.id, Number(transactionID), { withUser: true, withChargingStation: true });
if (!transaction) {
@@ -45,8 +46,10 @@ export default class BillTransactionAsyncTask extends AbstractAsyncTask {
await TransactionStorage.saveTransaction(tenant.id, transaction);
} finally {
// Release the lock
- await LockingManager.release(transactionLock);
+ await LockingManager.release(lock);
}
+ } else {
+ throw new Error(`Unexpected situation - concurrent billing - transaction ID: '${this.asyncTask.parameters.transactionID}' - user: ${this.asyncTask.parameters.userID}`);
}
}
} catch (error) {

MODIFY
src/integration/billing/stripe/StripeBillingIntegration.ts
src/integration/billing/stripe/StripeBillingIntegration.ts
@@ -966,6 +966,7 @@ export default class StripeBillingIntegration extends BillingIntegration {
tenantID: this.tenant.id,
parameters: {
transactionID: String(transaction.id),
+ userID: transaction.userID
},
module: MODULE_NAME,
method: 'stopTransaction',

MODIFY
src/locking/LockingHelper.ts
src/locking/LockingHelper.ts
@@ -153,9 +153,16 @@ export default class LockingHelper {
return lock;
}
- public static async acquireBillTransactionLock(tenantID: string, transactionID: number): Promise<Lock | null> {
- const lock = LockingManager.createExclusiveLock(tenantID, LockEntity.TRANSACTION, `bill-transaction-${transactionID}`);
- if (!(await LockingManager.acquire(lock))) {
+ public static async acquireBillUserLock(tenantID: string, userID: string): Promise<Lock | null> {
+ const lock = LockingManager.createExclusiveLock(tenantID, LockEntity.USER, `bill-user-${userID}`);
+ // ----------------------------------------------------------------------------------------
+ // We may have concurrent attempts to create an invoice when running billing async tasks.
+ // On the otherhand, we cannot just give up too early in case of conflicts
+ // To prevent such situation, we have here a timeout of 60 seconds.
+ // This means that we assume here that the billing concrete layer (stripe) will be able to
+ // create within a minute both the invoice item and the corresponding invoice.
+ // ----------------------------------------------------------------------------------------
+ if (!(await LockingManager.acquire(lock, 60 /* , timeoutSecs) */))) {
return null;
}
return lock;

billing - asyncTask changed to prevent invoicing conflicts