billing - STRIPE - isTransactionBillingActivated
MODIFY
src/integration/billing/BillingIntegration.ts
src/integration/billing/BillingIntegration.ts
@@ -282,16 +282,6 @@ export default abstract class BillingIntegration {
action: ServerAction.BILLING_TRANSACTION
});
}
- // Check Charging Station
- if (transaction?.billingData?.invoiceID) {
- throw new BackendError({
- message: 'Transaction has already billing data',
- source: Constants.CENTRAL_SERVER,
- module: MODULE_NAME,
- method: 'checkStopTransaction',
- action: ServerAction.BILLING_TRANSACTION
- });
- }
if (this.productionMode) {
// Check Billing Data (only in Live Mode)
if (!transaction.user.billingData) {

MODIFY
src/integration/billing/stripe/StripeBillingIntegration.ts
src/integration/billing/stripe/StripeBillingIntegration.ts
@@ -634,6 +634,13 @@ export default class StripeBillingIntegration extends BillingIntegration {
}
public async startTransaction(transaction: Transaction): Promise<BillingDataTransactionStart> {
+
+ if (!this.settings.billing.isTransactionBillingActivated) {
+ return {
+ isTransactionBillingActivated: false
+ };
+ }
+
// Check Stripe
await this.checkConnection();
// Check Transaction
@@ -663,7 +670,7 @@ export default class StripeBillingIntegration extends BillingIntegration {
});
}
return {
- cancelTransaction: false
+ isTransactionBillingActivated: true
};
}
@@ -680,7 +687,7 @@ export default class StripeBillingIntegration extends BillingIntegration {
});
}
return {
- cancelTransaction: false
+ isTransactionBillingActivated: true
};
}
@@ -740,6 +747,14 @@ export default class StripeBillingIntegration extends BillingIntegration {
}
public async stopTransaction(transaction: Transaction): Promise<BillingDataTransactionStop> {
+ if (!transaction.billingData.isTransactionBillingActivated) {
+ // --------------------------------------------------------------------
+ // The billing feature has been activated after the startTransaction
+ // --------------------------------------------------------------------
+ return {
+ status: BillingStatus.UNBILLED
+ };
+ }
// Check Stripe
await this.checkConnection();
// Check object

MODIFY
src/server/ocpp/utils/OCPPUtils.ts
src/server/ocpp/utils/OCPPUtils.ts
+import { BillingDataTransactionStart, BillingDataTransactionStop } from '../../../types/Billing';
import { ChargingProfile, ChargingProfilePurposeType } from '../../../types/ChargingProfile';
import ChargingStation, { ChargingStationCapabilities, ChargingStationOcppParameters, ChargingStationTemplate, Connector, ConnectorCurrentLimitSource, CurrentType, OcppParameter, SiteAreaLimitSource, StaticLimitAmps, TemplateUpdate, TemplateUpdateResult } from '../../../types/ChargingStation';
import { OCPPAuthorizeRequestExtended, OCPPMeasurand, OCPPMeterValue, OCPPNormalizedMeterValue, OCPPPhase, OCPPReadingContext, OCPPStopTransactionRequestExtended, OCPPUnitOfMeasure, OCPPValueFormat } from '../../../types/ocpp/OCPPServer';
@@ -6,7 +7,6 @@ import Transaction, { InactivityStatus, TransactionAction, TransactionStop } fro
import { ActionsResponse } from '../../../types/GlobalType';
import BackendError from '../../../exception/BackendError';
-import { BillingDataTransactionStop } from '../../../types/Billing';
import BillingFactory from '../../../integration/billing/BillingFactory';
import ChargingStationClientFactory from '../../../client/ocpp/ChargingStationClientFactory';
import ChargingStationStorage from '../../../storage/mongodb/ChargingStationStorage';
@@ -316,7 +316,6 @@ export default class OCPPUtils {
}
public static async billTransaction(tenantID: string, transaction: Transaction, action: TransactionAction): Promise<void> {
- let billingDataStop: BillingDataTransactionStop;
const billingImpl = await BillingFactory.getBillingImpl(tenantID);
if (billingImpl) {
// Check
@@ -325,9 +324,10 @@ export default class OCPPUtils {
case TransactionAction.START:
try {
// Delegate
- await billingImpl.startTransaction(transaction);
+ const billingDataTransactionStart: BillingDataTransactionStart = await billingImpl.startTransaction(transaction);
// Update
transaction.billingData = {
+ isTransactionBillingActivated: billingDataTransactionStart.isTransactionBillingActivated,
lastUpdate: new Date()
};
} catch (error) {
@@ -365,12 +365,9 @@ export default class OCPPUtils {
case TransactionAction.STOP:
try {
// Delegate
- billingDataStop = await billingImpl.stopTransaction(transaction);
+ const billingDataStop: BillingDataTransactionStop = 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.stop = billingDataStop;
transaction.billingData.lastUpdate = new Date();
} catch (error) {
await Logging.logError({

MODIFY
src/server/rest/v1/service/BillingService.ts
src/server/rest/v1/service/BillingService.ts
@@ -450,15 +450,16 @@ export default class BillingService {
UtilsService.assertObjectExists(action, transaction, `Transaction ID '${filteredRequest.transactionID}' does not exist`,
MODULE_NAME, 'handleCreateTransactionInvoice', req.user);
// Create an invoice for the transaction
+ // ----------------------------------------------------------------------
+ // TODO - Rethink that part!
+ // Calling StopTransaction without calling startTransaction may have
+ // unpredictable side-effects.
+ // ----------------------------------------------------------------------
const billingDataStop = await billingImpl.stopTransaction(transaction);
- // Update transaction
- transaction.billingData = {
- status: billingDataStop.status,
- invoiceID: billingDataStop.invoiceID,
- invoiceStatus: billingDataStop.invoiceStatus,
- invoiceItem: billingDataStop.invoiceItem,
- lastUpdate: new Date()
- };
+ // Update transaction billing data
+ transaction.billingData.stop = billingDataStop;
+ transaction.billingData.lastUpdate = new Date();
+ // Save it
await TransactionStorage.saveTransaction(req.user.tenantID, transaction);
// Ok
await Logging.logInfo({

MODIFY
src/server/rest/v1/service/TransactionService.ts
src/server/rest/v1/service/TransactionService.ts
@@ -8,6 +8,7 @@ import AppAuthError from '../../../../exception/AppAuthError';
import AppError from '../../../../exception/AppError';
import Authorizations from '../../../../authorization/Authorizations';
import BillingFactory from '../../../../integration/billing/BillingFactory';
+import { BillingStatus } from '../../../../types/Billing';
import ChargingStationStorage from '../../../../storage/mongodb/ChargingStationStorage';
import Configuration from '../../../../utils/Configuration';
import Constants from '../../../../utils/Constants';
@@ -15,7 +16,6 @@ import Consumption from '../../../../types/Consumption';
import ConsumptionStorage from '../../../../storage/mongodb/ConsumptionStorage';
import Cypher from '../../../../utils/Cypher';
import { DataResult } from '../../../../types/DataResult';
-import I18nManager from '../../../../utils/I18nManager';
import LockingHelper from '../../../../locking/LockingHelper';
import LockingManager from '../../../../locking/LockingManager';
import Logging from '../../../../utils/Logging';
@@ -980,7 +980,7 @@ export default class TransactionService {
detailedMessages: { transaction }
});
// Billed
- } else if (billingImpl && transaction.billingData && transaction.billingData.invoiceID) {
+ } else if (billingImpl && transaction.billingData?.stop?.status === BillingStatus.BILLED) {
result.inError++;
await Logging.logError({
tenantID: loggedUser.tenantID,

MODIFY
src/server/rest/v1/service/security/SettingSecurity.ts
src/server/rest/v1/service/security/SettingSecurity.ts
@@ -174,6 +174,7 @@ export default class SettingSecurity {
break;
case BillingSettingsType.STRIPE:
settings.content.billing = {
+ isTransactionBillingActivated : sanitize(request.content.billing.isTransactionBillingActivated),
immediateBillingAllowed: sanitize(request.content.billing.immediateBillingAllowed),
periodicBillingAllowed: sanitize(request.content.billing.periodicBillingAllowed),
taxID: sanitize(request.content.billing.taxID)

MODIFY
src/storage/mongodb/SettingStorage.ts
src/storage/mongodb/SettingStorage.ts
@@ -301,8 +301,9 @@ export default class SettingStorage {
billingSettings.backupSensitiveData = settings.result[0].backupSensitiveData;
// Billing Common Properties
if (config.billing) {
- const { immediateBillingAllowed, periodicBillingAllowed, taxID, usersLastSynchronizedOn } = config.billing;
+ const { isTransactionBillingActivated, immediateBillingAllowed, periodicBillingAllowed, taxID, usersLastSynchronizedOn } = config.billing;
billingSettings.billing = {
+ isTransactionBillingActivated,
immediateBillingAllowed,
periodicBillingAllowed,
usersLastSynchronizedOn,

MODIFY
src/storage/mongodb/TransactionStorage.ts
src/storage/mongodb/TransactionStorage.ts
@@ -168,11 +168,14 @@ 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,
+ isTransactionBillingActivated: transactionToSave.billingData.isTransactionBillingActivated,
lastUpdate: Utils.convertToDate(transactionToSave.billingData.lastUpdate),
+ stop: {
+ status: transactionToSave.billingData.stop?.status,
+ invoiceID: Utils.convertToObjectID(transactionToSave.billingData.stop?.invoiceID),
+ invoiceStatus: transactionToSave.billingData.stop?.invoiceStatus,
+ invoiceItem: transactionToSave.billingData.stop?.invoiceItem,
+ },
};
}
if (transactionToSave.ocpiData) {

MODIFY
src/types/Billing.ts
src/types/Billing.ts
@@ -3,19 +3,17 @@ import { ActionsResponse, DocumentEncoding, DocumentType } from './GlobalType';
import User from './User';
export interface BillingTransactionData {
- status?: string;
- invoiceID?: string;
- invoiceStatus?: BillingInvoiceStatus;
- invoiceItem?: BillingInvoiceItem;
+ isTransactionBillingActivated?: boolean;
lastUpdate?: Date;
+ stop?: BillingDataTransactionStop;
}
export interface BillingDataTransactionStart {
- cancelTransaction?: boolean;
+ isTransactionBillingActivated: boolean;
}
export interface BillingDataTransactionUpdate {
- cancelTransaction?: boolean;
+ isTransactionBillingActivated: boolean;
}
export enum BillingStatus {

MODIFY
src/types/Setting.ts
src/types/Setting.ts
@@ -237,6 +237,7 @@ export interface BillingSettings extends Setting {
}
export interface BillingSetting {
+ isTransactionBillingActivated: boolean;
immediateBillingAllowed: boolean;
periodicBillingAllowed: boolean;
taxID: string;

MODIFY
test/api/BillingStripeTestData.ts
test/api/BillingStripeTestData.ts
@@ -63,9 +63,8 @@ export default class StripeIntegrationTestData {
public async forceBillingSettings(immediateBilling: boolean): Promise<void> {
// The tests requires some settings to be forced
this.billingImpl = await this.setBillingSystemValidCredentials(immediateBilling);
- expect(this.billingImpl, 'Billing implementation should not ber null');
this.billingUser = await this.billingImpl.getUser(this.dynamicUser);
- expect(this.billingUser, 'Billing user should not ber null');
+ assert(this.billingUser, 'Billing user should not be null');
}
public async setBillingSystemValidCredentials(immediateBilling: boolean) : Promise<StripeBillingIntegration> {
@@ -73,21 +72,13 @@ export default class StripeIntegrationTestData {
await this.saveBillingSettings(billingSettings);
billingSettings.stripe.secretKey = await Cypher.encrypt(this.getTenantID(), billingSettings.stripe.secretKey);
const billingImpl = StripeBillingIntegration.getInstance(this.getTenantID(), billingSettings);
- expect(this.billingImpl).to.not.be.null;
+ assert(billingImpl, 'Billing implementation should not be null');
return billingImpl;
}
- public async setBillingSystemInvalidCredentials() : Promise<StripeBillingIntegration> {
- const billingSettings = this.getLocalSettings(false);
- billingSettings.stripe.secretKey = await Cypher.encrypt(this.getTenantID(), 'sk_test_' + 'invalid_credentials');
- await this.saveBillingSettings(billingSettings);
- const billingImpl = StripeBillingIntegration.getInstance(this.getTenantID(), billingSettings);
- expect(this.billingImpl).to.not.be.null;
- return billingImpl;
- }
-
- public getLocalSettings(immediateBilling: boolean): BillingSettings {
+ public getLocalSettings(immediateBillingAllowed: boolean): BillingSettings {
const billingProperties = {
+ isTransactionBillingActivated: config.get('billing.isTransactionBillingActivated'),
immediateBillingAllowed: config.get('billing.immediateBillingAllowed'),
periodicBillingAllowed: config.get('billing.periodicBillingAllowed'),
taxID: config.get('billing.taxID')
@@ -104,11 +95,11 @@ export default class StripeIntegrationTestData {
stripe: stripeProperties,
};
- // ---------------------------------------------------------
- // Our test needs the immediate billing to be switched off!
+ // -----------------------------------------------------------------
+ // Our test may need the immediate billing to be switched off!
// Because we want to check the DRAFT state of the invoice
- settings.billing.immediateBillingAllowed = immediateBilling;
- // ---------------------------------------------------------
+ settings.billing.immediateBillingAllowed = immediateBillingAllowed;
+ // -----------------------------------------------------------------
return settings;
}
@@ -177,10 +168,6 @@ export default class StripeIntegrationTestData {
inclusive: false
});
expect(taxRate).to.not.be.null;
- // concreteImplementation.alterStripeSettings({
- // taxID: taxRate?.id, // Default tax to apply when charging invoices
- // // immediateBillingAllowed: true // Activate immediate billing
- // });
return taxRate;
}

MODIFY
test/api/BillingTest.ts
test/api/BillingTest.ts
@@ -56,7 +56,7 @@ class TestData {
assert(!!tenantId, 'Tenant ID cannot be null');
billingSettings.stripe.secretKey = await Cypher.encrypt(tenantId, billingSettings.stripe.secretKey);
const billingImpl = StripeBillingIntegration.getInstance(tenantId, billingSettings);
- expect(billingImpl).to.not.be.null;
+ assert(billingImpl, 'Billing implementation should not be null');
return billingImpl;
}
@@ -67,12 +67,13 @@ class TestData {
billingSettings.stripe.secretKey = await Cypher.encrypt(tenantId, 'sk_test_' + 'invalid_credentials');
await this.saveBillingSettings(billingSettings);
const billingImpl = StripeBillingIntegration.getInstance(tenantId, billingSettings);
- expect(billingImpl).to.not.be.null;
+ assert(billingImpl, 'Billing implementation should not be null');
return billingImpl;
}
- public getLocalSettings(immediateBilling: boolean): BillingSettings {
+ public getLocalSettings(immediateBillingAllowed: boolean): BillingSettings {
const billingProperties = {
+ isTransactionBillingActivated: config.get('billing.isTransactionBillingActivated'),
immediateBillingAllowed: config.get('billing.immediateBillingAllowed'),
periodicBillingAllowed: config.get('billing.periodicBillingAllowed'),
taxID: config.get('billing.taxID')
@@ -89,11 +90,11 @@ class TestData {
stripe: stripeProperties,
};
- // ---------------------------------------------------------
+ // -----------------------------------------------------------------
// Our test needs the immediate billing to be switched off!
// Because we want to check the DRAFT state of the invoice
- settings.billing.immediateBillingAllowed = immediateBilling;
- // ---------------------------------------------------------
+ settings.billing.immediateBillingAllowed = immediateBillingAllowed;
+ // -----------------------------------------------------------------
return settings;
}
@@ -483,6 +484,7 @@ describe('Billing Service', function() {
} as User;
fakeUser.issuer = true;
testData.billingImpl = await testData.setBillingSystemInvalidCredentials();
+ assert(testData.billingImpl, 'Billing implementation should not be null');
await testData.userService.createEntity(
testData.userService.userApi,
fakeUser
@@ -504,6 +506,7 @@ describe('Billing Service', function() {
assert(!!testData.userService, 'User service cannot be null');
// Force INVALID STRIPE credentials
testData.billingImpl = await testData.setBillingSystemInvalidCredentials();
+ assert(testData.billingImpl, 'Billing implementation should not be null');
});
after(async () => {

MODIFY
test/api/context/BillingContext.ts
test/api/context/BillingContext.ts
@@ -21,6 +21,7 @@ export default class BillingContext {
private static getBillingSettings(): BillingSettings {
const billingProperties = {
+ isTransactionBillingActivated: config.get('billing.isTransactionBillingActivated'),
immediateBillingAllowed: config.get('billing.immediateBillingAllowed'),
periodicBillingAllowed: config.get('billing.periodicBillingAllowed'),
taxID: config.get('billing.taxID')

MODIFY
test/api/context/ContextDefinition.ts
test/api/context/ContextDefinition.ts
@@ -179,6 +179,7 @@ export default class ContextDefinition {
content: {
type: BillingSettingsType.STRIPE,
billing: {
+ isTransactionBillingActivated: true,
immediateBillingAllowed: true,
periodicBillingAllowed: true,
taxID: ''
@@ -307,6 +308,7 @@ export default class ContextDefinition {
content: {
type: BillingSettingsType.STRIPE,
billing: {
+ isTransactionBillingActivated: true,
immediateBillingAllowed: true,
periodicBillingAllowed: true,
taxID: ''

MODIFY
test/config-template.json
test/config-template.json
"debug": false
},
"billing": {
+ "isTransactionBillingActivated": true,
"immediateBillingAllowed": true,
"periodicBillingAllowed": true,
"taxID": ""

MODIFY
test/config.js
test/config.js
@@ -252,15 +252,20 @@ const config = convict({
}
},
billing: {
+ isTransactionBillingActivated: {
+ doc: 'Activates the billing of transactions feature',
+ format: Boolean,
+ default: true
+ },
immediateBillingAllowed: {
doc: 'Allow immediate billing',
format: Boolean,
- default: ''
+ default: true
},
periodicBillingAllowed: {
doc: 'Allow periodic billing',
format: Boolean,
- default: ''
+ default: false
},
taxID: {
doc: 'taxes to apply by default',

billing - STRIPE - isTransactionBillingActivated