pricing - allow cross pricing definition sessions
MODIFY
src/integration/billing/stripe/StripeBillingIntegration.ts
src/integration/billing/stripe/StripeBillingIntegration.ts
@@ -2,7 +2,7 @@ import { AsyncTaskType, AsyncTasks } from '../../../types/AsyncTask';
/* eslint-disable @typescript-eslint/member-ordering */
import { BillingDataTransactionStart, BillingDataTransactionStop, BillingDataTransactionUpdate, BillingInvoice, BillingInvoiceItem, BillingInvoiceStatus, BillingOperationResult, BillingPaymentMethod, BillingStatus, BillingTax, BillingUser, BillingUserData } from '../../../types/Billing';
import FeatureToggles, { Feature } from '../../../utils/FeatureToggles';
-import { PricingConsumptionData, PricingDimensionData } from '../../../types/Pricing';
+import PricingModel, { PricingConsumptionData, PricingDimensionData } from '../../../types/Pricing';
import StripeHelpers, { StripeChargeOperationResult } from './StripeHelpers';
import Transaction, { StartTransactionErrorCode } from '../../../types/Transaction';
@@ -17,6 +17,7 @@ import Constants from '../../../utils/Constants';
import Cypher from '../../../utils/Cypher';
import I18nManager from '../../../utils/I18nManager';
import Logging from '../../../utils/Logging';
+import PricingEngine from '../../pricing/PricingEngine';
import { Request } from 'express';
import { ServerAction } from '../../../types/Server';
import SettingStorage from '../../../storage/mongodb/SettingStorage';
@@ -1046,16 +1047,13 @@ export default class StripeBillingIntegration extends BillingIntegration {
}
private _convertPricingDataToBillingInvoiceItem(transaction: Transaction) : BillingInvoiceItem {
- if (!transaction.pricingConsumptionData) {
- throw new Error('Unexpected situation - no pricing data!');
- }
const transactionID = transaction.id;
const currency = transaction.stop.priceUnit;
- const pricingData: PricingConsumptionData = this._enrichTransactionPricingData(transaction);
+ const pricingData: PricingConsumptionData[] = this._extractTransactionPricingData(transaction);
const billingInvoiceItem: BillingInvoiceItem = {
transactionID,
currency,
- pricingData,
+ pricingData: pricingData[0],
metadata: {
// Let's keep track of the initial data for troubleshooting purposes
tenantID: this.tenant.id,
@@ -1068,8 +1066,14 @@ export default class StripeBillingIntegration extends BillingIntegration {
return billingInvoiceItem ;
}
- private _enrichTransactionPricingData(transaction: Transaction) : PricingConsumptionData {
- const pricingConsumptionData: PricingConsumptionData = Object.freeze(transaction.pricingConsumptionData);
+ private _extractTransactionPricingData(transaction: Transaction) : PricingConsumptionData[] {
+ const pricingModel = Object.freeze(transaction.pricingModel);
+ let pricingData: PricingConsumptionData[] = PricingEngine.extractFinalPricingData(pricingModel);
+ pricingData = pricingData.map((pricingConsumptionData) => this._enrichTransactionPricingData(transaction, pricingConsumptionData));
+ return pricingData;
+ }
+
+ private _enrichTransactionPricingData(transaction: Transaction, pricingConsumptionData: PricingConsumptionData) : PricingConsumptionData {
// -------------------------------------------------------------------------------
// TODO - so far we use the same tax rates for all invoice items!
// -------------------------------------------------------------------------------

MODIFY
src/integration/pricing/PricingEngine.ts
src/integration/pricing/PricingEngine.ts
@@ -153,6 +153,7 @@ export default class PricingEngine {
return Promise.resolve(pricingConsumptionData);
}
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
private static priceFlatFeeConsumption(pricingDefinitions: PricingDefinition[], consumptionData: Consumption): PricingDimensionData {
let pricingDimensionData: PricingDimensionData = null;
const quantity = 1; // To be clarified - Flat Fee is billing billed once per sessions
@@ -229,8 +230,73 @@ export default class PricingEngine {
amount,
quantity
};
+ if (!pricingDimension.pricedData) {
+ pricingDimension.pricedData = pricingDimensionData;
+ } else {
+ // TODO - to be clarified - we should not update the transaction data directly!
+ pricingDimension.pricedData.amount = Utils.createDecimal(pricingDimension.pricedData.amount).plus(pricingDimensionData.amount).toNumber();
+ pricingDimension.pricedData.quantity = Utils.createDecimal(pricingDimension.pricedData.quantity).plus(pricingDimensionData.quantity).toNumber();
+ }
return pricingDimensionData;
}
+ static priceConsumption(tenant: Tenant, transaction: Transaction, consumptionData: Consumption): number {
+ // Check the restrictions to find the pricing definition matching the current context
+ let actualPricingDefinitions = transaction.pricingModel.pricingDefinitions.filter((pricingDefinition) =>
+ PricingEngine.checkPricingDefinitionRestrictions(pricingDefinition, consumptionData)
+ );
+ // Having more than one pricing definition this NOT a normal situation.
+ // This means that two different tariff matches the same criteria. This should not happen!
+ if (actualPricingDefinitions.length > 1) {
+ // TODO - to be clarified! - Shall we mix several pricing definition for a single transaction?
+ actualPricingDefinitions = [ actualPricingDefinitions?.[0] ];
+ }
+ // ----------------------------------------------------------------------------------------------
+ // TODO - POC - to be clarified - temporary solution
+ // - we shouldn't update the transaction object directly from this layer
+ // ----------------------------------------------------------------------------------------------
+ // Build the consumption data for each dimension
+ const flatFee: PricingDimensionData = PricingEngine.priceFlatFeeConsumption(actualPricingDefinitions, consumptionData);
+ const energy: PricingDimensionData = PricingEngine.priceEnergyConsumption(actualPricingDefinitions, consumptionData);
+ const chargingTime: PricingDimensionData = PricingEngine.priceChargingTimeConsumption(actualPricingDefinitions, consumptionData);
+ const parkingTime: PricingDimensionData = PricingEngine.priceParkingTimeConsumption(actualPricingDefinitions, consumptionData);
+
+ const amount = flatFee?.amount + energy?.amount + chargingTime?.amount + parkingTime?.amount;
+ return amount;
+ }
+
+ static extractFinalPricingData(pricingModel: ResolvedPricingModel): PricingConsumptionData[] {
+ // Iterate throw the list of pricing definitions
+ const pricedData: PricingConsumptionData[] = pricingModel.pricingDefinitions.map((pricingDefinition) =>
+ PricingEngine.extractFinalPricedConsumptionData(pricingDefinition)
+ );
+ // Remove null/undefined entries (if any)
+ return pricedData.filter((pricingConsumptionData) => !!pricingConsumptionData);
+ }
+
+ static extractFinalPricedConsumptionData(pricingDefinition: PricingDefinition): PricingConsumptionData {
+ const flatFee = pricingDefinition.dimensions.flatFee?.pricedData;
+ const energy = pricingDefinition.dimensions.energy?.pricedData;
+ const chargingTime = pricingDefinition.dimensions.chargingTime?.pricedData;
+ const parkingTime = pricingDefinition.dimensions.parkingTime?.pricedData;
+ if (flatFee || energy || chargingTime || parkingTime) {
+ return {
+ flatFee,
+ energy,
+ chargingTime,
+ parkingTime
+ };
+ }
+ // Nothing to bill for the current pricing definition
+ return null;
+ }
+
+ static extractPricedDataFromDimension(pricedData: PricingDimensionData[], pricingDefinition: PricingDefinition, dimensionType: string): void {
+ const pricingDimensionData = pricingDefinition.dimensions[dimensionType]?.pricedData;
+ if (pricingDimensionData) {
+ pricedData.push(pricingDimensionData);
+ }
+ }
+
}

MODIFY
src/integration/pricing/simple-pricing/BuiltInPricingIntegration.ts
src/integration/pricing/simple-pricing/BuiltInPricingIntegration.ts
@@ -29,9 +29,6 @@ export default class BuiltInPricingIntegration extends PricingIntegration<Simple
public async stopSession(transaction: Transaction, consumptionData: Consumption): Promise<PricedConsumption> {
const pricedConsumption = await this.computePrice(transaction, consumptionData);
- if (transaction.pricingModel) {
- pricedConsumption.pricingConsumptionData = await PricingEngine.priceFinalConsumption(this.tenant, transaction, consumptionData);
- }
return Promise.resolve(pricedConsumption);
}
@@ -39,7 +36,7 @@ export default class BuiltInPricingIntegration extends PricingIntegration<Simple
let amount: number;
let roundedAmount: number;
if (consumptionData.consumptionWh && typeof consumptionData.consumptionWh === 'number') {
- amount = Utils.computeSimplePrice(this.settings.price, consumptionData.consumptionWh);
+ amount = PricingEngine.priceConsumption(this.tenant, transaction, consumptionData);
roundedAmount = Utils.truncTo(amount, 2);
} else {
amount = 0;

MODIFY
src/server/ocpp/utils/OCPPUtils.ts
src/server/ocpp/utils/OCPPUtils.ts
@@ -285,10 +285,6 @@ export default class OCPPUtils {
consumption.pricingSource = pricedConsumption.pricingSource;
consumption.cumulatedAmount = pricedConsumption.cumulatedAmount;
transaction.currentCumulatedPrice = consumption.cumulatedAmount;
- if (FeatureToggles.isFeatureActive(Feature.PRICING_NEW_MODEL)) {
- // TODO - To be clarified - check for the PricingSource instead?
- transaction.pricingConsumptionData = pricedConsumption.pricingConsumptionData;
- }
}
break;
}

MODIFY
src/storage/mongodb/TransactionStorage.ts
src/storage/mongodb/TransactionStorage.ts
@@ -70,7 +70,6 @@ export default class TransactionStorage {
priceUnit: transactionToSave.priceUnit,
pricingSource: transactionToSave.pricingSource,
pricingModel: transactionToSave.pricingModel,
- pricingConsumptionData: transactionToSave.pricingConsumptionData,
stateOfCharge: transactionToSave.stateOfCharge,
timezone: transactionToSave.timezone,
signedData: transactionToSave.signedData,

MODIFY
src/types/Pricing.ts
src/types/Pricing.ts
@@ -28,6 +28,7 @@ export interface PricingDimension {
active: boolean, // Lets the user switch OFF that price without loosing the value (if any)
price: number, // Price per unit (excluding VAT) for this tariff dimension
stepSize?: number, // Minimum amount to be billed. This unit will be billed in this step_size blocks. For example: if type is time and step_size is 300, then time will be billed in blocks of 5 minutes, so if 6 minutes is used, 10 minutes (2 blocks of step_size) will be billed.
+ pricedData?: PricingDimensionData // Information set dynamically while charging
}
export interface PricingRestriction {

MODIFY
src/types/Transaction.ts
src/types/Transaction.ts
@@ -83,7 +83,6 @@ export default interface Transaction extends AbstractCurrentConsumption {
priceUnit?: string;
pricingSource?: string;
pricingModel?: ResolvedPricingModel,
- pricingConsumptionData?: PricingConsumptionData,
stateOfCharge: number;
timezone: string;
currentTimestamp?: Date;

pricing - allow cross pricing definition sessions