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

MODIFY
src/integration/billing/stripe/StripeBillingIntegration.ts
src/integration/billing/stripe/StripeBillingIntegration.ts
@@ -936,6 +936,51 @@ export default class StripeBillingIntegration extends BillingIntegration {
}
}
// Create and Save async task
+ // await AsyncTaskBuilder.createAndSaveAsyncTasks({
+ // name: AsyncTasks.BILL_TRANSACTION,
+ // action: ServerAction.BILLING_TRANSACTION,
+ // type: AsyncTaskType.TASK,
+ // tenantID: this.tenant.id,
+ // parameters: {
+ // transactionID: String(transaction.id),
+ // userID: transaction.userID
+ // },
+ // module: MODULE_NAME,
+ // method: 'stopTransaction',
+ // });
+ // Inform the calling layer that the operation has been postponed
+ return {
+ status: BillingStatus.PENDING
+ };
+ }
+
+ public async endTransaction(transaction: Transaction): Promise<BillingDataTransactionStop> {
+ // Check whether the billing was activated on start transaction
+ if (!transaction.billingData?.withBillingActive) {
+ return {
+ status: BillingStatus.UNBILLED
+ };
+ }
+ // Do not bill suspicious StopTransaction events
+ if (FeatureToggles.isFeatureActive(Feature.BILLING_CHECK_THRESHOLD_ON_STOP) && !Utils.isDevelopmentEnv()) {
+ // Suspicious StopTransaction may occur after a 'Housing temperature approaching limit' error on some charging stations
+ const timeSpent = this.computeTimeSpentInSeconds(transaction);
+ // TODO - make it part of the pricing or billing settings!
+ if (timeSpent < 60 /* seconds */ || transaction.stop.totalConsumptionWh < 1000 /* 1kWh */) {
+ await Logging.logWarning({
+ tenantID: this.tenant.id,
+ user: transaction.userID,
+ action: ServerAction.BILLING_TRANSACTION,
+ module: MODULE_NAME, method: 'endTransaction',
+ message: `Transaction data is suspicious - billing operation has been aborted - transaction ID: ${transaction.id}`,
+ ...LoggingHelper.getTransactionProperties(transaction)
+ });
+ return {
+ status: BillingStatus.UNBILLED
+ };
+ }
+ }
+ // Create and Save async task
await AsyncTaskBuilder.createAndSaveAsyncTasks({
name: AsyncTasks.BILL_TRANSACTION,
action: ServerAction.BILLING_TRANSACTION,
@@ -946,7 +991,7 @@ export default class StripeBillingIntegration extends BillingIntegration {
userID: transaction.userID
},
module: MODULE_NAME,
- method: 'stopTransaction',
+ method: 'endTransaction',
});
// Inform the calling layer that the operation has been postponed
return {

MODIFY
src/server/ocpp/services/OCPPService.ts
src/server/ocpp/services/OCPPService.ts
@@ -800,7 +800,7 @@ export default class OCPPService {
lastTransaction.stop.inactivityStatus = Utils.getInactivityStatusLevel(chargingStation, lastTransaction.connectorId,
lastTransaction.stop.totalInactivitySecs + lastTransaction.stop.extraInactivitySecs);
// Build extra inactivity consumption
- await OCPPUtils.buildExtraConsumptionInactivity(tenant, lastTransaction);
+ await OCPPUtils.buildExtraConsumptionInactivity(tenant, chargingStation, lastTransaction);
await Logging.logInfo({
tenantID: tenant.id,
...LoggingHelper.getChargingStationProperties(chargingStation),

MODIFY
src/server/ocpp/utils/OCPPUtils.ts
src/server/ocpp/utils/OCPPUtils.ts
@@ -195,7 +195,7 @@ export default class OCPPUtils {
}
}
- public static async buildExtraConsumptionInactivity(tenant: Tenant, transaction: Transaction): Promise<boolean> {
+ public static async buildExtraConsumptionInactivity(tenant: Tenant, chargingStation: ChargingStation, transaction: Transaction): Promise<boolean> {
// Extra inactivity
if (transaction.stop.extraInactivitySecs > 0) {
// Get the last Consumption
@@ -218,6 +218,10 @@ export default class OCPPUtils {
lastConsumption.instantWattsL1 = 0;
lastConsumption.instantWattsL2 = 0;
lastConsumption.instantWattsL3 = 0;
+ // Pricing
+ await OCPPUtils.processTransactionPricing(tenant, transaction, chargingStation, lastConsumption, TransactionAction.UPDATE);
+ // Billing
+ await OCPPUtils.processTransactionBilling(tenant, transaction, TransactionAction.END);
// Save
await ConsumptionStorage.saveConsumption(tenant, lastConsumption);
// Update the Stop transaction data
@@ -373,7 +377,7 @@ export default class OCPPUtils {
});
}
break;
- // Stop Transaction
+ // Stop Transaction (potentially we will have two calls the second is for the extra inactivity)
case TransactionAction.STOP:
try {
// Delegate
@@ -398,6 +402,30 @@ export default class OCPPUtils {
});
}
break;
+ case TransactionAction.END:
+ try {
+ // Delegate
+ const billingDataStop: BillingDataTransactionStop = await billingImpl.endTransaction(transaction);
+ // Update
+ if (transaction.billingData) {
+ transaction.billingData.stop = billingDataStop;
+ transaction.billingData.lastUpdate = new Date();
+ }
+ } catch (error) {
+ const message = `Billing - stopTransaction failed - transaction ID '${transaction.id}'`;
+ await Logging.logError({
+ tenantID: tenant.id,
+ siteID: transaction.siteID,
+ siteAreaID: transaction.siteAreaID,
+ companyID: transaction.companyID,
+ chargingStationID: transaction.chargeBoxID,
+ user: transaction.userID,
+ action: ServerAction.BILLING_TRANSACTION,
+ module: MODULE_NAME, method: 'processTransactionBilling',
+ message, detailedMessages: { error: error.stack }
+ });
+ }
+ break;
}
}
}

MODIFY
test/api/BillingTestHelper.ts
test/api/BillingTestHelper.ts
import AsyncTask, { AsyncTaskStatus } from '../../src/types/AsyncTask';
import { BillingDataTransactionStop, BillingInvoice, BillingInvoiceStatus, BillingStatus, BillingUser } from '../../src/types/Billing';
import { BillingSettings, BillingSettingsType, SettingDB } from '../../src/types/Setting';
+import { ChargePointErrorCode, ChargePointStatus, OCPPStatusNotificationRequest } from '../../src/types/ocpp/OCPPServer';
import ChargingStation, { ConnectorType } from '../../src/types/ChargingStation';
-import FeatureToggles, { Feature } from '../../src/utils/FeatureToggles';
import PricingDefinition, { DayOfWeek, PricingDimension, PricingDimensions, PricingEntity, PricingRestriction } from '../../src/types/Pricing';
import chai, { assert, expect } from 'chai';
@@ -489,7 +489,18 @@ export default class BillingTestHelper {
return roundedPrice;
}
- public async generateTransaction(user: any, expectedStatus = 'Accepted'): Promise<number> {
+ public async sendStatusNotification(connectorId: number, timestamp: Date, status: ChargePointStatus): Promise<void> {
+ const occpStatusFinishing: OCPPStatusNotificationRequest = {
+ connectorId,
+ status,
+ errorCode: ChargePointErrorCode.NO_ERROR,
+ timestamp: timestamp.toISOString()
+ };
+ await this.chargingStationContext.setConnectorStatus(occpStatusFinishing);
+ }
+
+
+ public async generateTransaction(user: any, expectedStatus = 'Accepted', withExtraNotificationStatus = true): Promise<number> {
const meterStart = 0;
const meterStop = 32325; // Unit: Wh
@@ -504,6 +515,8 @@ export default class BillingTestHelper {
const tagId = user.tags[0].id;
// # Begin
const startDate = moment();
+ // Let's send an OCCP status notification to simulate some extra inactivities
+ await this.sendStatusNotification(connectorId, startDate.toDate(), ChargePointStatus.PREPARING);
const startTransactionResponse = await this.chargingStationContext.startTransaction(connectorId, tagId, meterStart, startDate.toDate());
expect(startTransactionResponse).to.be.transactionStatus(expectedStatus);
const transactionId = startTransactionResponse.transactionId;
@@ -546,9 +559,12 @@ export default class BillingTestHelper {
if (expectedStatus === 'Accepted') {
const stopTransactionResponse = await this.chargingStationContext.stopTransaction(transactionId, tagId, meterStop, stopDate.toDate());
expect(stopTransactionResponse).to.be.transactionStatus('Accepted');
- }
+ // Let's send an OCCP status notification to simulate some extra inactivities
+ await this.sendStatusNotification(connectorId, stopDate.clone().add(29, 'minutes').toDate(), ChargePointStatus.FINISHING);
+ await this.sendStatusNotification(connectorId, stopDate.clone().add(30, 'minutes').toDate(), ChargePointStatus.AVAILABLE);
// Give some time to the asyncTask to bill the transaction
await this.waitForAsyncTasks();
+ }
return transactionId;
}

pricing - price on end transaction