Avoid session deletion when linked to invoice
MODIFY
src/integration/billing/stripe/StripeBillingIntegration.ts
src/integration/billing/stripe/StripeBillingIntegration.ts
@@ -601,6 +601,7 @@ export default class StripeBillingIntegration extends BillingIntegration<StripeB
}
return {
status: BillingStatus.BILLED,
+ invoiceID: invoice.invoice.id,
invoiceStatus: invoice.invoice.status,
invoiceItem: invoice.invoiceItem,
};

MODIFY
src/server/ocpp/utils/OCPPUtils.ts
src/server/ocpp/utils/OCPPUtils.ts
@@ -129,6 +129,7 @@ export default class OCPPUtils {
billingDataStop = await billingImpl.stopTransaction(transaction);
// Update
transaction.billingData.status = billingDataStop.status;
+ transaction.billingData.invoiceID = billingDataStop.invoiceID;
transaction.billingData.invoiceStatus = billingDataStop.invoiceStatus;
transaction.billingData.invoiceItem = billingDataStop.invoiceItem;
transaction.billingData.lastUpdate = new Date();

MODIFY
src/server/rest/service/TransactionService.ts
src/server/rest/service/TransactionService.ts
@@ -6,6 +6,8 @@ import { ActionsResponse } from '../../../types/GlobalType';
import AppAuthError from '../../../exception/AppAuthError';
import AppError from '../../../exception/AppError';
import Authorizations from '../../../authorization/Authorizations';
+import BillingFactory from '../../../integration/billing/BillingFactory';
+import BillingStorage from '../../../storage/mongodb/BillingStorage';
import ChargingStationStorage from '../../../storage/mongodb/ChargingStationStorage';
import Configuration from '../../../utils/Configuration';
import Constants from '../../../utils/Constants';
@@ -643,14 +645,17 @@ export default class TransactionService {
inSuccess: 0,
inError: 0
};
- const specificError: { refunded: number; notFound: number; refundedIDs: number[]; notFoundIDs: number[] } = {
+ const specificError: { refunded: number; notFound: number; billed: number, refundedIDs: number[]; notFoundIDs: number[], billedIDs: number[] } = {
refunded: 0,
notFound: 0,
+ billed: 0,
refundedIDs: [],
- notFoundIDs: []
+ notFoundIDs: [],
+ billedIDs: []
};
// Check if transaction has been refunded
const refundConnector = await RefundFactory.getRefundImpl(loggedUser.tenantID);
+ const billingImpl = await BillingFactory.getBillingImpl(loggedUser.tenantID);
for (const transactionId of transactionsIDs) {
// Get
const transaction = await TransactionStorage.getTransaction(loggedUser.tenantID, transactionId);
@@ -664,6 +669,10 @@ export default class TransactionService {
result.inError++;
specificError.refunded++;
specificError.refundedIDs.push(transactionId);
+ } else if (billingImpl && transaction.billingData.invoiceID && await BillingStorage.getInvoice(loggedUser.tenantID, transaction.billingData.invoiceID)) {
+ result.inError++;
+ specificError.billed++;
+ specificError.billedIDs.push(transactionId);
} else if (!transaction.stop) {
if (!transaction.chargeBox) {
transactionsIDsToDelete.push(transactionId);
@@ -696,6 +705,9 @@ export default class TransactionService {
if (specificError.refunded) {
errorDetails.push(`${specificError.refunded} session IDs has been refunded and cannot be deleted: ${specificError.refundedIDs.join(', ')}`);
}
+ if (specificError.billed) {
+ errorDetails.push(`${specificError.billed} session IDs has been billed and cannot be deleted: ${specificError.billedIDs.join(', ')}`);
+ }
Logging.logError({
tenantID: loggedUser.tenantID,
user: loggedUser,

MODIFY
src/server/rest/service/security/TransactionSecurity.ts
src/server/rest/service/security/TransactionSecurity.ts
@@ -206,6 +206,11 @@ export default class TransactionSecurity {
filteredTransaction.stop.priceUnit = transaction.stop.priceUnit;
filteredTransaction.stop.pricingSource = transaction.stop.pricingSource;
}
+ if (transaction.billingData) {
+ filteredTransaction.billingData = {
+ invoiceID: transaction.billingData.invoiceID
+ };
+ }
}
}
return filteredTransaction;

MODIFY
src/storage/mongodb/TransactionStorage.ts
src/storage/mongodb/TransactionStorage.ts
@@ -160,6 +160,7 @@ export default class TransactionStorage {
if (transactionToSave.billingData) {
transactionMDB.billingData = {
status: transactionToSave.billingData.status,
+ invoiceID: Utils.convertToObjectID(transactionToSave.billingData.invoiceID),
invoiceStatus: transactionToSave.billingData.invoiceStatus,
invoiceItem: transactionToSave.billingData.invoiceItem,
lastUpdate: Utils.convertToDate(transactionToSave.billingData.lastUpdate),
@@ -179,6 +180,9 @@ export default class TransactionStorage {
if (!transactionMDB.billingData.invoiceItem) {
delete transactionMDB.billingData.invoiceItem;
}
+ if (!transactionMDB.billingData.invoiceID) {
+ delete transactionMDB.billingData.invoiceID;
+ }
}
if (transactionToSave.ocpiData) {
transactionMDB.ocpiData = {

MODIFY
src/types/Billing.ts
src/types/Billing.ts
@@ -2,6 +2,7 @@ import { ActionsResponse } from './GlobalType';
export interface BillingTransactionData {
status?: string;
+ invoiceID?: string;
invoiceStatus?: BillingInvoiceStatus;
invoiceItem?: BillingInvoiceItem;
lastUpdate?: Date;
@@ -29,6 +30,7 @@ export enum BillingMethod {
export interface BillingDataTransactionStop {
status?: string;
+ invoiceID?: string;
invoiceStatus?: BillingInvoiceStatus;
invoiceItem?: BillingInvoiceItem;
}

MODIFY
test/api/BillingTest.ts
test/api/BillingTest.ts
@@ -37,6 +37,7 @@ class TestData {
public siteContext: SiteContext;
public siteAreaContext: any;
public chargingStationContext: ChargingStationContext;
+ public transactionUserService: CentralServerService;
public createdUsers: User[] = [];
public isForcedSynchro: boolean;
public pending = false;
@@ -82,7 +83,7 @@ class TestData {
}
-async function generateTransaction(user: User, chargingStationContext) {
+async function generateTransaction(user: User, chargingStationContext): Promise<number> {
const connectorId = 1;
const tagId = user.tags[0].id;
const meterStart = 0;
@@ -92,9 +93,10 @@ async function generateTransaction(user: User, chargingStationContext) {
const startTransactionResponse = await chargingStationContext.startTransaction(connectorId, tagId, meterStart, startDate);
// eslint-disable-next-line @typescript-eslint/unbound-method
expect(startTransactionResponse).to.be.transactionValid;
- const transactionId1 = startTransactionResponse.transactionId;
+ const transactionId1 = startTransactionResponse.transactionId as number;
const stopTransactionResponse = await chargingStationContext.stopTransaction(transactionId1, tagId, meterStop, stopDate);
expect(stopTransactionResponse).to.be.transactionStatus('Accepted');
+ return transactionId1;
}
const testData: TestData = new TestData();
@@ -414,6 +416,8 @@ describe('Billing Service', function() {
testData.siteContext = testData.tenantContext.getSiteContext(ContextDefinition.SITE_CONTEXTS.SITE_WITH_OTHER_USER_STOP_AUTHORIZATION);
testData.siteAreaContext = testData.siteContext.getSiteAreaContext(ContextDefinition.SITE_AREA_CONTEXTS.WITH_ACL);
testData.chargingStationContext = testData.siteAreaContext.getChargingStationContext(ContextDefinition.CHARGING_STATION_CONTEXTS.ASSIGNED_OCPP16);
+ testData.transactionUserService = new CentralServerService(
+ testData.tenantContext.getTenant().subdomain, testData.userContext);
});
describe('Where admin user', () => {
@@ -449,6 +453,13 @@ describe('Billing Service', function() {
expect(response.data).containSubset(Constants.REST_RESPONSE_SUCCESS);
expect(response.data.inSuccess).to.be.eq(1);
});
+
+ it('should not delete a transaction linked to an invoice', async () => {
+ const transactionID = await generateTransaction(testData.userContext, testData.chargingStationContext);
+ const transactionDeleted = await testData.transactionUserService.transactionApi.delete(transactionID);
+ expect(transactionDeleted.data.inError).to.be.eq(1);
+ expect(transactionDeleted.data.inSuccess).to.be.eq(0);
+ });
});
describe('Where basic user', () => {

Avoid session deletion when linked to invoice