billing/pricing - item descriptions
MODIFY
src/assets/i18n/en.json
src/assets/i18n/en.json
"billing": {
"generatedUser": "Generated user for '{{email}}'",
"chargingAtSiteArea": "Charging session: {{sessionID}} - Start Date: {{startDate}} at {startTime} - Energy Consumption: {{totalConsumption}} kWh at {{siteAreaName}}",
- "chargingAtChargeBox": "Charging session: {{sessionID}} - Start Date: {{startDate}} at {{startTime}} - Energy Consumption: {{totalConsumption}} kWh at charging station {{chargeBoxID}}"
+ "chargingAtChargeBox": "Charging session: {{sessionID}} - Start Date: {{startDate}} at {{startTime}} - Energy Consumption: {{totalConsumption}} kWh at charging station {{chargeBoxID}}",
+
+ "flatFee-itemDescription": "Session: {{sessionID}} - Start Date: {{startDate}} at {{startTime}} at charging station {{chargeBoxID}} - Flat Fee",
+ "energy-itemDescription": "Session: {{sessionID}} - Start Date: {{startDate}} at {{startTime}} at charging station {{chargeBoxID}} - Energy Consumption: {{quantity}} kWh",
+ "chargingTime-itemDescription": "Session: {{sessionID}} - Start Date: {{startDate}} at {{startTime}} at charging station {{chargeBoxID}} - Charging Time: {{duration}}",
+ "parkingTime-itemDescription": "Session: {{sessionID}} - Start Date: {{startDate}} at {{startTime}} at charging station {{chargeBoxID}} - Parking Time: {{duration}}",
+
+ "header-itemDescription": "Session: {{sessionID}} - Start Date: {{startDate}} at {{startTime}} at charging station {{chargeBoxID}}",
+ "flatFee-shortItemDescription": "Flat Fee",
+ "energy-shortItemDescription": "Energy Consumption: {{quantity}} kWh",
+ "chargingTime-shortItemDescription": "Charging Time: {{duration}}",
+ "parkingTime-shortItemDescription": "Parking Time: {{duration}}"
},
"chargers": {
"chargeBoxSN": "Charge Box S/N",

MODIFY
src/assets/i18n/it.json
src/assets/i18n/it.json
"billing": {
"generatedUser": "Utenza generata per '{{email}}'",
"chargingAtSiteArea": "Charging session: {{sessionID}} - Start Date: {{startDate}} at {startTime} - Energy Consumption: {{totalConsumption}} kWh at {{siteAreaName}}",
- "chargingAtChargeBox": "Charging session: {{sessionID}} - Start Date: {{startDate}} at {{startTime}} - Energy Consumption: {{totalConsumption}} kWh at charging station {{chargeBoxID}}"
+ "chargingAtChargeBox": "Charging session: {{sessionID}} - Start Date: {{startDate}} at {{startTime}} - Energy Consumption: {{totalConsumption}} kWh at charging station {{chargeBoxID}}",
+
+ "flatFee-itemDescription": "Session: {{sessionID}} - Start Date: {{startDate}} at {{startTime}} at charging station {{chargeBoxID}} - Flat Fee",
+ "energy-itemDescription": "Session: {{sessionID}} - Start Date: {{startDate}} at {{startTime}} at charging station {{chargeBoxID}} - Energy Consumption: {{quantity}} kWh",
+ "chargingTime-itemDescription": "Session: {{sessionID}} - Start Date: {{startDate}} at {{startTime}} at charging station {{chargeBoxID}} - Charging Time: {{duration}}",
+ "parkingTime-itemDescription": "Session: {{sessionID}} - Start Date: {{startDate}} at {{startTime}} at charging station {{chargeBoxID}} - Parking Time: {{duration}}",
+
+ "header-itemDescription": "Session: {{sessionID}} - Start Date: {{startDate}} at {{startTime}} at charging station {{chargeBoxID}}",
+ "flatFee-shortItemDescription": "Flat Fee",
+ "energy-shortItemDescription": "Energy Consumption: {{quantity}} kWh",
+ "chargingTime-shortItemDescription": "Charging Time: {{duration}}",
+ "parkingTime-shortItemDescription": "Parking Time: {{duration}}"
},
"chargers": {
"chargeBoxSN": "Charge Box S/N",

MODIFY
src/assets/i18n/pt.json
src/assets/i18n/pt.json
"billing": {
"generatedUser": "Utilizador criado para '{{email}}'",
"chargingAtSiteArea": "Charging session: {{sessionID}} - Start Date: {{startDate}} at {startTime} - Energy Consumption: {{totalConsumption}} kWh at {{siteAreaName}}",
- "chargingAtChargeBox": "Charging session: {{sessionID}} - Start Date: {{startDate}} at {{startTime}} - Energy Consumption: {{totalConsumption}} kWh at charging station {{chargeBoxID}}"
+ "chargingAtChargeBox": "Charging session: {{sessionID}} - Start Date: {{startDate}} at {{startTime}} - Energy Consumption: {{totalConsumption}} kWh at charging station {{chargeBoxID}}",
+
+ "flatFee-itemDescription": "Session: {{sessionID}} - Start Date: {{startDate}} at {{startTime}} at charging station {{chargeBoxID}} - Flat Fee",
+ "energy-itemDescription": "Session: {{sessionID}} - Start Date: {{startDate}} at {{startTime}} at charging station {{chargeBoxID}} - Energy Consumption: {{quantity}} kWh",
+ "chargingTime-itemDescription": "Session: {{sessionID}} - Start Date: {{startDate}} at {{startTime}} at charging station {{chargeBoxID}} - Charging Time: {{duration}}",
+ "parkingTime-itemDescription": "Session: {{sessionID}} - Start Date: {{startDate}} at {{startTime}} at charging station {{chargeBoxID}} - Parking Time: {{duration}}",
+
+ "header-itemDescription": "Session: {{sessionID}} - Start Date: {{startDate}} at {{startTime}} at charging station {{chargeBoxID}}",
+ "flatFee-shortItemDescription": "Flat Fee",
+ "energy-shortItemDescription": "Energy Consumption: {{quantity}} kWh",
+ "chargingTime-shortItemDescription": "Charging Time: {{duration}}",
+ "parkingTime-shortItemDescription": "Parking Time: {{duration}}"
},
"chargers": {
"chargeBoxSN": "Charge Box S/N",

MODIFY
src/integration/billing/stripe/StripeBillingIntegration.ts
src/integration/billing/stripe/StripeBillingIntegration.ts
@@ -855,25 +855,41 @@ export default class StripeBillingIntegration extends BillingIntegration {
// session on friday night and keeps charging during the week-end, the two tariffs are used.
// The invoice will show the detail for each tariff and for each billed dimension
// ---------------------------------------------------------------------------------------------------------
+ await this._createStripeInvoiceItemHeader(customerID, billingInvoiceItem, invoiceID);
+ // Generate an invoice item for each tariff and each dimension!
let counter = 0;
for (const pricingConsumptionData of billingInvoiceItem.pricingData) {
await this._createStripeInvoiceItems4PricingConsumptionData(customerID, billingInvoiceItem, pricingConsumptionData, ++counter, invoiceID);
}
}
+ private async _createStripeInvoiceItemHeader(customerID: string,
+ billingInvoiceItem: BillingInvoiceItem, invoiceID?: string): Promise<void> {
+ if (!billingInvoiceItem.headerDescription) {
+ return;
+ }
+ const currency = billingInvoiceItem.currency.toLowerCase();
+ // Build stripe parameters for the parking time
+ const parameters: Stripe.InvoiceItemCreateParams = {
+ invoice: invoiceID,
+ customer: customerID,
+ currency,
+ description: billingInvoiceItem.headerDescription,
+ tax_rates: [],
+ // quantity: 1, //Cannot be set separately
+ amount: 0,
+ metadata: { ...billingInvoiceItem?.metadata }
+ };
+ if (!parameters.invoice) {
+ // STRIPE throws an exception when invoice is set to null.
+ delete parameters.invoice;
+ }
+ // Make sure to generate a unique idem potency key per pricing definition and dimension
+ await this._createStripeInvoiceItem(parameters, this.buildIdemPotencyKey(billingInvoiceItem.transactionID, 'invoice', 'items-header'));
+ }
+
private async _createStripeInvoiceItems4PricingConsumptionData(customerID: string,
billingInvoiceItem: BillingInvoiceItem, pricedData: PricedConsumptionData, counter: number, invoiceID?: string): Promise<void> {
- /* --------------------------------------------------------------------------------
- Convert pricing information to STRIPE expected data
- -----------------------------------------------------------------------------------
- Example:
- Consumption 1000 Kw.h - total amount: 4 euros
- Unit price should be (4 / 1000) ==> 0.004
- Stripe expects 'unit_amount' as an Integer, in Cents
- unit_amount: 0.4 ==> Not an integer - throws an exception
- Stripe alternative - 'unit_amount_decimal' in Cents, with 2 decimals, as a string!
- unit_amount_decimal: '004.00' (in Cents, with 2 decimals, as a string)
- ----------------------------------------------------------------------------------- */
// A stripe invoice item per dimension
await this._createStripeInvoiceItem4Dimension(customerID, DimensionType.FLAT_FEE, billingInvoiceItem, pricedData, counter, invoiceID);
await this._createStripeInvoiceItem4Dimension(customerID, DimensionType.CHARGING_TIME, billingInvoiceItem, pricedData, counter, invoiceID);
@@ -891,7 +907,7 @@ export default class StripeBillingIntegration extends BillingIntegration {
}
const currency = billingInvoiceItem.currency.toLowerCase();
// Tax rates
- const tax_rates = pricedData[dimension].taxes || [];
+ const tax_rates = dimensionData.taxes || [];
// Build stripe parameters for the parking time
const parameters: Stripe.InvoiceItemCreateParams = {
invoice: invoiceID,
@@ -900,7 +916,7 @@ export default class StripeBillingIntegration extends BillingIntegration {
description: dimensionData.itemDescription,
tax_rates,
// quantity: 1, //Cannot be set separately
- amount: Utils.createDecimal(pricedData[dimension].roundedAmount).times(100).toNumber(),
+ amount: Utils.createDecimal(dimensionData.roundedAmount).times(100).toNumber(),
metadata: { ...billingInvoiceItem?.metadata }
};
if (!parameters.invoice) {
@@ -1055,7 +1071,7 @@ export default class StripeBillingIntegration extends BillingIntegration {
const billingInvoiceItem: BillingInvoiceItem = {
transactionID,
currency,
- pricingData: pricingData,
+ pricingData,
metadata: {
// Let's keep track of the initial data for troubleshooting purposes
tenantID: this.tenant.id,
@@ -1064,6 +1080,10 @@ export default class StripeBillingIntegration extends BillingIntegration {
begin: transaction.timestamp?.valueOf(),
}
};
+ if (FeatureToggles.isFeatureActive(Feature.BILLING_SHOW_SESSION_HEADER)) {
+ // To be clarified - do we put general information on top
+ billingInvoiceItem.headerDescription = this.buildLineItemDescription(transaction, true);
+ }
// Returns a item representing the complete charging session (energy + parking information)
return billingInvoiceItem ;
}
@@ -1071,6 +1091,10 @@ export default class StripeBillingIntegration extends BillingIntegration {
private _extractTransactionPricingData(transaction: Transaction) : PricedConsumptionData[] {
const pricingModel = Object.freeze(transaction.pricingModel);
let pricingData: PricedConsumptionData[] = PricingEngine.extractFinalPricingData(pricingModel);
+ if (!FeatureToggles.isFeatureActive(Feature.BILLING_SHOW_PRICING_DETAIL)) {
+ // Accumulate data per dimensions = less details, less disputes
+ pricingData = [ this._accumulatePricingDimensions(pricingData) ] ;
+ }
pricingData = pricingData.map((pricingConsumptionData) => this._enrichTransactionPricingData(transaction, pricingConsumptionData));
return pricingData;
}
@@ -1085,33 +1109,62 @@ export default class StripeBillingIntegration extends BillingIntegration {
// -------------------------------------------------------------------------------
const flatFee = pricingConsumptionData.flatFee;
if (flatFee) {
- flatFee.itemDescription = flatFee.sourceName + ' - Flat Fee'; // TODO! - generate proper description
+ flatFee.itemDescription = this.buildLineItemDescription4PricedDimension(transaction, DimensionType.FLAT_FEE, flatFee);
flatFee.taxes = taxes;
}
const energy = pricingConsumptionData.energy;
if (energy) {
- energy.itemDescription = this.buildLineItemDescription4PricedDimension(transaction, energy);
+ energy.itemDescription = this.buildLineItemDescription4PricedDimension(transaction, DimensionType.ENERGY, energy);
energy.taxes = taxes;
}
const chargingTime = pricingConsumptionData.chargingTime;
if (chargingTime) {
- chargingTime.itemDescription = chargingTime.sourceName + ' - Charging Time'; // TODO! - generate proper description
+ chargingTime.itemDescription = this.buildLineItemDescription4PricedDimension(transaction, DimensionType.CHARGING_TIME, chargingTime);
chargingTime.taxes = taxes;
}
const parkingTime = pricingConsumptionData.parkingTime;
if (parkingTime) {
- parkingTime.itemDescription = parkingTime.sourceName + ' - Parking Time'; // TODO! - generate proper description
+ parkingTime.itemDescription = this.buildLineItemDescription4PricedDimension(transaction, DimensionType.PARKING_TIME, parkingTime);
parkingTime.taxes = taxes;
}
return pricingConsumptionData;
}
+
+ private _accumulatePricingDimensions(pricingData: PricedConsumptionData[]): PricedConsumptionData {
+ const accumulatedConsumptionData: PricedConsumptionData = {};
+ for (const pricingConsumptionData of pricingData) {
+ this._accumulatePricingDimension(accumulatedConsumptionData, pricingConsumptionData, DimensionType.FLAT_FEE);
+ this._accumulatePricingDimension(accumulatedConsumptionData, pricingConsumptionData, DimensionType.ENERGY);
+ this._accumulatePricingDimension(accumulatedConsumptionData, pricingConsumptionData, DimensionType.CHARGING_TIME);
+ this._accumulatePricingDimension(accumulatedConsumptionData, pricingConsumptionData, DimensionType.PARKING_TIME);
+ }
+ return accumulatedConsumptionData;
+ }
+
+ private _accumulatePricingDimension(accumulatedConsumptionData: PricedConsumptionData, pricingConsumptionData: PricedConsumptionData, dimensionType: DimensionType): void {
+ if (pricingConsumptionData[dimensionType]) {
+ if (!accumulatedConsumptionData[dimensionType]) {
+ const emptyDimensionData: PricedDimensionData = {
+ unitPrice: 0,
+ quantity:0,
+ amount: 0,
+ roundedAmount: 0
+ };
+ accumulatedConsumptionData[dimensionType] = emptyDimensionData;
+ }
+ accumulatedConsumptionData[dimensionType].quantity += pricingConsumptionData[dimensionType].quantity;
+ accumulatedConsumptionData[dimensionType].amount += pricingConsumptionData[dimensionType].amount;
+ accumulatedConsumptionData[dimensionType].roundedAmount += pricingConsumptionData[dimensionType].roundedAmount;
+ }
+ }
+
private _convertToBillingInvoiceItem(transaction: Transaction) : BillingInvoiceItem {
// Destructuring transaction.stop
const { price: unitPrice, priceUnit: currency, 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 quantity = Utils.createDecimal(transaction.stop.totalConsumptionWh).toNumber(); // Wh
const amount = roundedPrice; // Total amount for the line item
// -------------------------------------------------------------------------------
const taxes = this.getTaxRateIds();
@@ -1217,27 +1270,51 @@ export default class StripeBillingIntegration extends BillingIntegration {
return null;
}
- private buildLineItemDescription4PricedDimension(transaction: Transaction, pricedData: PricedDimensionData) {
- const chargeBox = transaction.chargeBox;
+ private buildLineItemDescription4PricedDimension(transaction: Transaction, dimensionType: DimensionType, pricedData: PricedDimensionData) {
const i18nManager = I18nManager.getInstanceForLocale(transaction.user.locale);
+ const userLocale = this.getUserLocale(transaction);
+ const chargeBox = transaction.chargeBox;
const sessionID = String(transaction?.id);
const startDate = i18nManager.formatDateTime(transaction.timestamp, 'LL', transaction.timezone);
const startTime = i18nManager.formatDateTime(transaction.timestamp, 'LT', transaction.timezone);
- const formattedConsumptionkWh = Utils.createDecimal(pricedData.quantity).toNumber().toLocaleString(this.getUserLocale(transaction));
- const descriptionPattern = (chargeBox?.siteArea?.name) ? 'billing.chargingAtSiteArea' : 'billing.chargingAtChargeBox';
+ // const currency = transaction.priceUnit;
+ // const tariffName = pricedData.sourceName;
+ // const unitPrice = Utils.createDecimal(pricedData.unitPrice).toNumber().toLocaleString(userLocale);
+ // const stepSize = pricedData.stepSize ?? 1;
+ let quantity: string, duration: string;
+ if (dimensionType === DimensionType.ENERGY) {
+ quantity = Utils.createDecimal(pricedData.quantity).div(1000).toNumber().toLocaleString(userLocale); // kWh
+ duration = '';
+ } else if (dimensionType === DimensionType.PARKING_TIME || dimensionType === DimensionType.CHARGING_TIME) {
+ quantity = Utils.createDecimal(pricedData.quantity).toNumber().toLocaleString(userLocale); // seconds
+ duration = moment.duration(pricedData.quantity, 's').humanize();
+ // duration = moment.duration(quantity, 's').format('HH:mm:ss');
+ } else {
+ quantity = Utils.createDecimal(pricedData.quantity).toNumber().toLocaleString(userLocale); // Sessions
+ duration = '';
+ }
// Get the translated line item description
+ let descriptionPattern = `billing.${dimensionType}-shortItemDescription`;
+ if (!FeatureToggles.isFeatureActive(Feature.BILLING_SHOW_SESSION_HEADER)) {
+ descriptionPattern = `billing.${dimensionType}-itemDescription`;
+ }
const description = i18nManager.translate(descriptionPattern, {
sessionID,
startDate,
startTime,
- totalConsumption: formattedConsumptionkWh,
+ quantity,
+ duration,
siteAreaName: chargeBox?.siteArea?.name,
- chargeBoxID: transaction?.chargeBoxID
+ chargeBoxID: transaction?.chargeBoxID,
+ // unitPrice,
+ // currency,
+ // stepSize,
+ // tariffName,
});
return description;
}
- private buildLineItemDescription(transaction: Transaction) {
+ private buildLineItemDescription(transaction: Transaction, headerMode = false) {
const chargeBox = transaction.chargeBox;
const i18nManager = I18nManager.getInstanceForLocale(transaction.user.locale);
const sessionID = String(transaction?.id);
@@ -1246,7 +1323,10 @@ export default class StripeBillingIntegration extends BillingIntegration {
const stopTime = i18nManager.formatDateTime(transaction.stop.timestamp, 'LT', transaction.timezone);
const formattedConsumptionkWh = this.formatConsumptionToKWh(transaction);
const timeSpent = this.convertTimeSpentToString(transaction);
- const descriptionPattern = (chargeBox?.siteArea?.name) ? 'billing.chargingAtSiteArea' : 'billing.chargingAtChargeBox';
+ let descriptionPattern = (chargeBox?.siteArea?.name) ? 'billing.chargingAtSiteArea' : 'billing.chargingAtChargeBox';
+ if (headerMode) {
+ descriptionPattern = 'billing.header-itemDescription';
+ }
// Get the translated line item description
const description = i18nManager.translate(descriptionPattern, {
sessionID,

MODIFY
src/integration/pricing/ConsumptionPricer.ts
src/integration/pricing/ConsumptionPricer.ts
@@ -98,7 +98,7 @@ export default class ConsumptionPricer {
}
private priceFlatFeeConsumption(): PricedDimensionData {
- // Flat fee must not be priced only once
+ // Flat fee must be priced only once
if (!this.pricingModel.pricerContext.flatFeeAlreadyPriced) {
const activePricingDefinition = this.getActiveDefinition4Dimension(this.actualPricingDefinitions, DimensionType.FLAT_FEE);
if (activePricingDefinition) {
@@ -239,7 +239,7 @@ export default class ConsumptionPricer {
unitPrice: 0,
amount: 0,
roundedAmount: 0,
- quantity: 0
+ quantity: 0 // Session
};
}
// First call for this dimension
@@ -247,20 +247,22 @@ export default class ConsumptionPricer {
unitPrice: unitPrice,
amount: unitPrice,
roundedAmount: Utils.truncTo(unitPrice, 2),
- quantity: 1
+ quantity: 1 // Session
};
return pricingDimension.pricedData;
}
private priceConsumptionStep(pricingDimension: PricingDimension, steps: number): PricedDimensionData {
- const unitPrice = pricingDimension.price || 0;
- const amount = Utils.createDecimal(unitPrice).times(steps).times(pricingDimension.stepSize).div(1000).toNumber();
+ const unitPrice = pricingDimension.price || 0; // Eur/wWh
+ const quantity = Utils.createDecimal(steps).times(pricingDimension.stepSize).toNumber(); // Wh
+ const amount = Utils.createDecimal(unitPrice).times(quantity).div(1000).toNumber(); // Eur
// Price the consumption
const pricedData: PricedDimensionData = {
unitPrice: unitPrice,
amount,
roundedAmount: Utils.truncTo(amount, 2),
- quantity: steps
+ quantity,
+ stepSize: pricingDimension.stepSize,
};
// Add the consumption to the previous data (if any) - for the billing
this.addPricedData(pricingDimension, pricedData);
@@ -269,15 +271,16 @@ export default class ConsumptionPricer {
}
private priceConsumptionWh(pricingDimension: PricingDimension, consumptionWh: number): PricedDimensionData {
- const unitPrice = pricingDimension.price || 0;
- const amount = Utils.createDecimal(unitPrice).times(consumptionWh).div(1000).toNumber();
- const consumptionkWh = Utils.createDecimal(consumptionWh).div(1000).toNumber();
+ const unitPrice = pricingDimension.price || 0; // Eur/kWh
+ const quantity = Utils.createDecimal(consumptionWh).toNumber(); // Wh
+ const amount = Utils.createDecimal(unitPrice).times(consumptionWh).div(1000).toNumber(); // Eur
+ // const consumptionkWh = Utils.createDecimal(consumptionWh).div(1000).toNumber();
// Price the consumption
const pricedData: PricedDimensionData = {
unitPrice: unitPrice,
amount,
roundedAmount: Utils.truncTo(amount, 2),
- quantity: consumptionkWh
+ quantity
};
// Add the consumption to the previous data (if any) - for the billing
this.addPricedData(pricingDimension, pricedData);
@@ -304,14 +307,16 @@ export default class ConsumptionPricer {
}
private priceTimeSteps(pricingDimension: PricingDimension, steps: number): PricedDimensionData {
- const unitPrice = pricingDimension.price || 0;
- const amount = Utils.createDecimal(unitPrice).times(steps).times(pricingDimension.stepSize).div(3600).toNumber();
+ const unitPrice = pricingDimension.price || 0; // Eur/hour
+ const quantity = Utils.createDecimal(steps).times(pricingDimension.stepSize).toNumber(); // seconds
+ const amount = Utils.createDecimal(unitPrice).times(quantity).div(3600).toNumber(); // Eur
// Price the consumption
const pricedData: PricedDimensionData = {
unitPrice: unitPrice,
amount,
roundedAmount: Utils.truncTo(amount, 2),
- quantity: steps
+ quantity,
+ stepSize: pricingDimension.stepSize
};
// Add the consumption to the previous data (if any) - for the billing
this.addPricedData(pricingDimension, pricedData);
@@ -320,15 +325,14 @@ export default class ConsumptionPricer {
}
private priceTimeSpent(pricingDimension: PricingDimension, seconds: number): PricedDimensionData {
- const unitPrice = pricingDimension.price || 0;
- const amount = Utils.createDecimal(unitPrice).times(seconds).div(3600).toNumber();
- const hours = Utils.createDecimal(seconds).div(3600).toNumber();
+ const unitPrice = pricingDimension.price || 0; // Eur/hour
+ const amount = Utils.createDecimal(unitPrice).times(seconds).div(3600).toNumber(); // Eur
// Price the consumption
const pricedData: PricedDimensionData = {
unitPrice: unitPrice,
amount,
roundedAmount: Utils.truncTo(amount, 2),
- quantity: hours
+ quantity: seconds
};
// Add the consumption to the previous data (if any) - for the billing
this.addPricedData(pricingDimension, pricedData);

MODIFY
src/types/Billing.ts
src/types/Billing.ts
@@ -85,6 +85,7 @@ export interface BillingInvoiceItem {
transactionID: number;
currency: string;
pricingData: PricedConsumptionData[]
+ headerDescription?: string,
metadata?: {
// Just a flat list of key/value pairs!
[name: string]: string | number | null;

MODIFY
src/types/Pricing.ts
src/types/Pricing.ts
@@ -116,6 +116,7 @@ export interface PricedDimensionData {
amount: number;
roundedAmount: number;
quantity: number;
+ stepSize?: number;
// Name of the tariff this dimension belongs to - mainly for troubleshooting purposes
sourceName?: string
// The item description is generated while billing the transaction

MODIFY
src/utils/FeatureToggles.ts
src/utils/FeatureToggles.ts
@@ -5,6 +5,8 @@ export enum Feature {
BILLING_CHECK_THRESHOLD_ON_STOP,
BILLING_PREVENT_CUSTOMER_DELETION,
PRICING_NEW_MODEL,
+ BILLING_SHOW_PRICING_DETAIL,
+ BILLING_SHOW_SESSION_HEADER,
}
export default class FeatureToggles {
@@ -14,6 +16,8 @@ export default class FeatureToggles {
Feature.BILLING_CHECK_THRESHOLD_ON_STOP,
Feature.BILLING_PREVENT_CUSTOMER_DELETION,
Feature.PRICING_NEW_MODEL,
+ // Feature.BILLING_SHOW_PRICING_DETAIL,
+ Feature.BILLING_SHOW_SESSION_HEADER,
];
// Check whether the feature is active or not!

MODIFY
test/api/BillingStripeTestData.ts
test/api/BillingStripeTestData.ts
@@ -198,7 +198,7 @@ export default class StripeIntegrationTestData {
const dynamicInvoice = await this.billInvoiceItem({
energyConsumptionkWh: 4, // kWh
energyAmount: 1, // EUR
- partingTime: 10, // Minutes
+ parkingTime: 10, // Minutes
parkingAmount: 5, // EUR
taxId
});
@@ -207,7 +207,7 @@ export default class StripeIntegrationTestData {
const updatedInvoice = await this.billInvoiceItem({
energyConsumptionkWh: 8, // kWh
energyAmount: 2, // EUR
- partingTime: 10, // Minutes
+ parkingTime: 10, // Minutes
parkingAmount: 5, // EUR
taxId
});
@@ -242,7 +242,7 @@ export default class StripeIntegrationTestData {
const dynamicInvoice = await this.billInvoiceItem({
energyConsumptionkWh: 16, // kWh
energyAmount: 4, // EUR
- partingTime: 10, // Minutes
+ parkingTime: 10, // Minutes
parkingAmount: 5, // EUR
taxId
});
@@ -268,7 +268,7 @@ export default class StripeIntegrationTestData {
public async billInvoiceItem(consumptionTestData: {
energyConsumptionkWh: number, // kWh
energyAmount: number, // EUR
- partingTime: number, // Minutes
+ parkingTime: number, // Minutes
parkingAmount: number // EUR
taxId: string
}) : Promise<BillingInvoice> {
@@ -282,15 +282,15 @@ export default class StripeIntegrationTestData {
unitPrice: Utils.createDecimal(consumptionTestData.energyAmount).div(consumptionTestData.energyConsumptionkWh).toNumber(),
amount: consumptionTestData.energyAmount, // total amount to bill - not yet in cents
roundedAmount: Utils.truncTo(consumptionTestData.energyAmount, 2),
- quantity: consumptionTestData.energyConsumptionkWh, // kW.h
+ quantity: Utils.createDecimal(consumptionTestData.energyConsumptionkWh).div(1000).toNumber(), // Wh
taxes // Array of taxes - cannot be null
},
parkingTime: {
- itemDescription: `Parking time - ${consumptionTestData.partingTime} minutes`,
- unitPrice: Utils.createDecimal(consumptionTestData.parkingAmount).div(consumptionTestData.partingTime).toNumber(),
+ itemDescription: `Parking time - ${consumptionTestData.parkingTime} minutes`,
+ unitPrice: Utils.createDecimal(consumptionTestData.parkingAmount).div(consumptionTestData.parkingTime).toNumber(),
amount: consumptionTestData.parkingAmount, // Euros
roundedAmount: Utils.truncTo(consumptionTestData.parkingAmount, 2),
- quantity: consumptionTestData.partingTime, // minutes
+ quantity: Utils.createDecimal(consumptionTestData.parkingTime).div(60).toNumber(), // seconds
taxes // Array of taxes - cannot be null
}
};

MODIFY
test/api/BillingTest.ts
test/api/BillingTest.ts
@@ -112,7 +112,7 @@ class TestData {
this.siteAreaContext = this.siteContext.getSiteAreaContext(ContextDefinition.SITE_AREA_CONTEXTS.WITH_SMART_CHARGING_THREE_PHASED);
this.chargingStationContext = this.siteAreaContext.getChargingStationContext(ContextDefinition.CHARGING_STATION_CONTEXTS.ASSIGNED_OCPP16 + '-' + ContextDefinition.SITE_CONTEXTS.SITE_BASIC + '-' + ContextDefinition.SITE_AREA_CONTEXTS.WITH_SMART_CHARGING_THREE_PHASED + '-singlePhased');
assert(!!this.chargingStationContext, 'Charging station context should not be null');
- await this.createTariff4ChargingStation('test CT', this.chargingStationContext.getChargingStation(), {
+ await this.createTariff4ChargingStation('FF+CT', this.chargingStationContext.getChargingStation(), {
flatFee: {
price: 1,
active: true
@@ -159,7 +159,7 @@ class TestData {
}
};
}
- await this.createTariff4ChargingStation('test FF+E', this.chargingStationContext.getChargingStation(), dimensions);
+ await this.createTariff4ChargingStation(testMode, this.chargingStationContext.getChargingStation(), dimensions);
return this.chargingStationContext;
}
@@ -186,7 +186,7 @@ class TestData {
active: true
}
};
- } else if (testMode === 'CT+PT+STEP') {
+ } else if (testMode === 'CT(STEP)+PT(STEP)') {
dimensions = {
chargingTime: {
price: 12, // Euro per hour
@@ -199,7 +199,7 @@ class TestData {
active: true
}
};
- } else if (testMode === 'E+PT+STEP') {
+ } else if (testMode === 'E+PT(STEP)') {
dimensions = {
energy: {
price: 0.50,
@@ -618,8 +618,8 @@ class TestData {
const tariff: Partial<PricingDefinition> = {
entityID: chargingStation.id, // a pricing model for the site
entityType: PricingEntity.CHARGING_STATION,
- name: 'CS Tariff - ' + testMode + ' - ' + chargingStation.id,
- description: 'Tariff for CS - ' + testMode + ' - ' + connectorType,
+ name: testMode,
+ description: 'Tariff for CS ' + chargingStation.id + ' - ' + testMode + ' - ' + connectorType,
staticRestrictions: {
connectorType,
validFrom: new Date(),
@@ -1365,7 +1365,7 @@ describe('Billing Service', function() {
});
it('should bill the CT(STEP)+PT(STEP) on COMBO CCS - DC', async () => {
- await testData.initChargingStationContext2TestFastCharger('CT+PT+STEP');
+ await testData.initChargingStationContext2TestFastCharger('CT(STEP)+PT(STEP)');
await testData.userService.billingApi.forceSynchronizeUser({ id: testData.userContext.id });
const userWithBillingData = await testData.billingImpl.getUser(testData.userContext);
await testData.assignPaymentMethod(userWithBillingData, 'tok_fr');
@@ -1376,7 +1376,7 @@ describe('Billing Service', function() {
});
it('should bill the ENERGY + PT(STEP) on COMBO CCS - DC', async () => {
- await testData.initChargingStationContext2TestFastCharger('E+PT+STEP');
+ await testData.initChargingStationContext2TestFastCharger('E+PT(STEP)');
await testData.userService.billingApi.forceSynchronizeUser({ id: testData.userContext.id });
const userWithBillingData = await testData.billingImpl.getUser(testData.userContext);
await testData.assignPaymentMethod(userWithBillingData, 'tok_fr');

billing/pricing - item descriptions