billing - billing new pricing consumption data
MODIFY
src/integration/billing/stripe/StripeBillingIntegration.ts
src/integration/billing/stripe/StripeBillingIntegration.ts
@@ -2,6 +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 StripeHelpers, { StripeChargeOperationResult } from './StripeHelpers';
import Transaction, { StartTransactionErrorCode } from '../../../types/Transaction';
@@ -16,7 +17,6 @@ import Constants from '../../../utils/Constants';
import Cypher from '../../../utils/Cypher';
import I18nManager from '../../../utils/I18nManager';
import Logging from '../../../utils/Logging';
-import { PricingDimensionData } from '../../../types/Pricing';
import { Request } from 'express';
import { ServerAction } from '../../../types/Server';
import SettingStorage from '../../../storage/mongodb/SettingStorage';
@@ -1035,20 +1035,76 @@ export default class StripeBillingIntegration extends BillingIntegration {
}
private convertToBillingInvoiceItem(transaction: Transaction) : BillingInvoiceItem {
- // Destructuring transaction.stop
+
+ if (FeatureToggles.isFeatureActive(Feature.PRICING_NEW_MODEL) && transaction.pricingModel) {
+ // Built-in Pricing
+ return this._convertPricingDataToBillingInvoiceItem(transaction);
+ }
+ // Simple Pricing - Do it the old way!
+ return this._convertToBillingInvoiceItem(transaction);
+
+ }
+
+ private _convertPricingDataToBillingInvoiceItem(transaction: Transaction) : BillingInvoiceItem {
+ if (!transaction.pricingConsumptionData) {
+ throw new Error('Unexpected situation - no pricing data!');
+ }
const transactionID = transaction.id;
- const { price, priceUnit, roundedPrice, totalConsumptionWh, timestamp } = transaction.stop;
- // TODO - we need a description per dimension type
- const itemDescription = this.buildLineItemDescription(transaction);
+ const currency = transaction.stop.priceUnit;
+ const pricingData: PricingConsumptionData = this._enrichTransactionPricingData(transaction);
+ const billingInvoiceItem: BillingInvoiceItem = {
+ transactionID,
+ currency,
+ pricingData,
+ metadata: {
+ // Let's keep track of the initial data for troubleshooting purposes
+ tenantID: this.tenant.id,
+ transactionID: transaction.id,
+ userID: transaction.userID,
+ begin: transaction.timestamp?.valueOf(),
+ }
+ };
+ // Returns a item representing the complete charging session (energy + parking information)
+ return billingInvoiceItem ;
+ }
+
+ private _enrichTransactionPricingData(transaction: Transaction) : PricingConsumptionData {
+ const pricingConsumptionData: PricingConsumptionData = Object.freeze(transaction.pricingConsumptionData);
+ // -------------------------------------------------------------------------------
+ // TODO - so far we use the same tax rates for all invoice items!
+ // -------------------------------------------------------------------------------
+ const taxes = this.getTaxRateIds();
// -------------------------------------------------------------------------------
- // ACHTUNG - STRIPE expects the amount and prices in CENTS!
+ // Enrich the consumption data with information required for billing it
// -------------------------------------------------------------------------------
+ if (pricingConsumptionData.flatFee) {
+ pricingConsumptionData.flatFee.itemDescription += ' - Flat Fee'; // TODO! - generate proper description
+ pricingConsumptionData.flatFee.taxes = taxes;
+ }
+ if (pricingConsumptionData.energy) {
+ pricingConsumptionData.energy.itemDescription = this.buildLineItemDescription(transaction);
+ pricingConsumptionData.energy.taxes = taxes;
+ }
+ if (pricingConsumptionData.chargingTime) {
+ pricingConsumptionData.chargingTime.itemDescription += ' - Charging Time'; // TODO! - generate proper description
+ pricingConsumptionData.chargingTime.taxes = taxes;
+ }
+ if (pricingConsumptionData.parkingTime) {
+ pricingConsumptionData.parkingTime.itemDescription += ' - Parking Time'; // TODO! - generate proper description
+ pricingConsumptionData.parkingTime.taxes = taxes;
+ }
+ return pricingConsumptionData;
+ }
+
+ private _convertToBillingInvoiceItem(transaction: Transaction) : BillingInvoiceItem {
+ // Destructuring transaction.stop
+ const { price, priceUnit, roundedPrice, totalConsumptionWh, timestamp } = transaction.stop;
+ const transactionID = transaction.id;
+ const itemDescription = this.buildLineItemDescription(transaction);
const quantity = Utils.createDecimal(transaction.stop.totalConsumptionWh).dividedBy(1000).toNumber(); // Total consumption in kW.h
const amount = roundedPrice; // Total amount for the line item
const currency = priceUnit;
// -------------------------------------------------------------------------------
- // TODO - take into account SITE settings
- // TODO - so far we use the same tax rates for all invoice items!
const taxes = this.getTaxRateIds();
// Build a billing invoice item based on the transaction
const billingInvoiceItem: BillingInvoiceItem = {

MODIFY
src/integration/pricing/PricingEngine.ts
src/integration/pricing/PricingEngine.ts
@@ -74,44 +74,29 @@ export default class PricingEngine {
private static priceFlatFeeConsumption(tenant: Tenant, transaction: Transaction, consumptionData: Consumption): PricingDimensionData {
let pricingDimensionData: PricingDimensionData = null;
- const quantity = 1;
+ const quantity = 1; // To be clarified - Flat Fee is billing billed once per sessions
pricingDimensionData = PricingEngine.PriceDimensionConsumption(transaction.pricingModel, 'flatFee', quantity);
return pricingDimensionData;
}
private static priceEnergyConsumption(tenant: Tenant, transaction: Transaction, consumptionData: Consumption): PricingDimensionData {
let pricingDimensionData: PricingDimensionData = null;
- const totalConsumptionWh = transaction.currentTotalConsumptionWh;
- if (transaction.pricingModel) {
- // Apply the new Pricing Engine
- const quantity = Utils.createDecimal(consumptionData.cumulatedConsumptionWh).dividedBy(1000).toNumber(); // Total consumption in kW.h
+ const quantity = Utils.createDecimal(consumptionData?.cumulatedConsumptionWh).dividedBy(1000).toNumber(); // Total consumption in kW.h
pricingDimensionData = PricingEngine.PriceDimensionConsumption(transaction.pricingModel, 'energy', quantity);
- } else {
- // -------------------------------------------------------------------------------
- // Let's do it the old way - using the Simple Pricing logic
- // -------------------------------------------------------------------------------
- const quantity = Utils.createDecimal(totalConsumptionWh).dividedBy(1000).toNumber(); // Total consumption in kW.h
- const roundedPrice = Utils.truncTo(transaction.currentCumulatedPrice, 2);
- const amount = roundedPrice; // Total amount for the line item
- // TODO - POC - take the pricingModel into consideration
- // Build the consumption data for each dimension
- pricingDimensionData = {
- amount,
- quantity
- };
- }
return pricingDimensionData;
}
private static priceParkingTimeConsumption(tenant: Tenant, transaction: Transaction, consumptionData: Consumption): PricingDimensionData {
let pricingDimensionData: PricingDimensionData = null;
- pricingDimensionData = PricingEngine.PriceDimensionConsumption(transaction.pricingModel, 'parkingTime', consumptionData.totalDurationSecs);
+ const hours = Utils.createDecimal(consumptionData?.totalDurationSecs).dividedBy(3600).toNumber();
+ pricingDimensionData = PricingEngine.PriceDimensionConsumption(transaction.pricingModel, 'parkingTime', hours);
return pricingDimensionData;
}
private static priceChargingTimeConsumption(tenant: Tenant, transaction: Transaction, consumptionData: Consumption): PricingDimensionData {
let pricingDimensionData: PricingDimensionData = null;
- pricingDimensionData = PricingEngine.PriceDimensionConsumption(transaction.pricingModel, 'chargingTime', consumptionData.totalInactivitySecs);
+ const hours = Utils.createDecimal(consumptionData?.totalInactivitySecs).dividedBy(60).toNumber();
+ pricingDimensionData = PricingEngine.PriceDimensionConsumption(transaction.pricingModel, 'chargingTime', hours);
return pricingDimensionData;
}
@@ -128,6 +113,8 @@ export default class PricingEngine {
if (dimensionToPrice) {
pricingDimensionData = PricingEngine.applyPricingDefinition(dimensionToPrice, quantity);
if (pricingDimensionData) {
+ // TODO - clarify where to show the actual tariff name
+ pricingDimensionData.itemDescription = activePricingDefinition.name;
break;
}
}
@@ -140,6 +127,9 @@ export default class PricingEngine {
if (pricingDimension?.active) {
return pricingDefinition;
}
+ // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ // TODO - check for pricing restriction and power thresholds
+ // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
return null;
}

MODIFY
test/api/BillingStripeTestData.ts
test/api/BillingStripeTestData.ts
@@ -451,8 +451,8 @@ export default class StripeIntegrationTestData {
maxPowerkW: 40000,
};
const tariff1: PricingDefinition = {
- name: 'Tesla Tariff',
- description: 'Tesla Tariff for low EVSE',
+ name: 'BLUE Tariff',
+ description: 'Tariff for low EVSE',
restrictions: lowConsumptionRestrictions,
dimensions: {
chargingTime: parkingPrice,
@@ -468,8 +468,8 @@ export default class StripeIntegrationTestData {
active: true
};
const tariff2: PricingDefinition = {
- name: 'Tesla Tariff',
- description: 'Tesla Tariff for fast chargers',
+ name: 'GREEN Tariff',
+ description: 'Tariff for fast chargers',
restrictions: fastChargerRestrictions,
dimensions: {
// chargingTime: parkingPrice, // parking time is free while charging

billing - billing new pricing consumption data