pricing/billing - keep track of the unitPrice
MODIFY
src/integration/billing/stripe/StripeBillingIntegration.ts
src/integration/billing/stripe/StripeBillingIntegration.ts
@@ -1116,12 +1116,11 @@ export default class StripeBillingIntegration extends BillingIntegration {
private _convertToBillingInvoiceItem(transaction: Transaction) : BillingInvoiceItem {
// Destructuring transaction.stop
- const { price, priceUnit, roundedPrice, totalConsumptionWh, timestamp } = 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 amount = roundedPrice; // Total amount for the line item
- const currency = priceUnit;
// -------------------------------------------------------------------------------
const taxes = this.getTaxRateIds();
// Build a billing invoice item based on the transaction
@@ -1130,6 +1129,7 @@ export default class StripeBillingIntegration extends BillingIntegration {
currency,
pricingData: [{
energy: {
+ unitPrice,
itemDescription,
amount,
quantity,
@@ -1141,9 +1141,9 @@ export default class StripeBillingIntegration extends BillingIntegration {
tenantID: this.tenant.id,
transactionID: transaction.id,
userID: transaction.userID,
- price,
+ unitPrice,
roundedPrice,
- priceUnit,
+ currency,
totalConsumptionWh,
begin: transaction.timestamp?.valueOf(),
end: timestamp?.valueOf()

MODIFY
src/integration/pricing/PricingEngine.ts
src/integration/pricing/PricingEngine.ts
@@ -212,17 +212,20 @@ export default class PricingEngine {
}
static priceFlatFeeDimension(pricingDimension: PricingDimension): PricingDimensionData {
+ const unitPrice = pricingDimension.price || 0;
if (pricingDimension.pricedData) {
// This should not happen for the flatFee dimension - Flat Fee is billed only once per session
// throw new Error('Unexpected situation - priceFlatFeeDimension should be called only once per session');
return {
+ unitPrice: 0,
amount: 0,
quantity: 0
};
}
// First call for this dimension
pricingDimension.pricedData = {
- amount: pricingDimension.price,
+ unitPrice: unitPrice,
+ amount: unitPrice,
quantity: 1
};
return pricingDimension.pricedData;
@@ -231,18 +234,24 @@ export default class PricingEngine {
static priceEnergyDimension(pricingDimension: PricingDimension, cumulatedConsumptionWh: number): PricingDimensionData {
let amount: number;
let consumptionkWh: number;
+ const unitPrice = pricingDimension.price || 0;
if (pricingDimension.stepSize) { // In kWh
- const nbSteps = Utils.createDecimal(cumulatedConsumptionWh).div(1000).divToInt(pricingDimension.stepSize).plus(1).toNumber();
- amount = Utils.createDecimal(pricingDimension.price).mul(nbSteps).mul(pricingDimension.stepSize).toNumber();
+ // TODO - clarify the .plus(1) below - shall we bill the first step?
+ const nbSteps = Utils.createDecimal(cumulatedConsumptionWh).div(1000).divToInt(unitPrice).plus(1).toNumber();
consumptionkWh = Utils.createDecimal(nbSteps).mul(pricingDimension.stepSize).toNumber();
+ amount = Utils.createDecimal(unitPrice).mul(consumptionkWh).toNumber();
} else {
- amount = Utils.createDecimal(pricingDimension.price).times(cumulatedConsumptionWh).div(1000).toNumber();
+ // Convert to kWh
consumptionkWh = Utils.createDecimal(cumulatedConsumptionWh).div(1000).toNumber();
+ // Let's truncate to avoid pricing an energy which was not delivered
+ consumptionkWh = Utils.truncTo(consumptionkWh, 2);
+ amount = Utils.createDecimal(unitPrice).times(consumptionkWh).toNumber();
}
const previousData = pricingDimension.pricedData;
if (previousData) {
// The new priced data is the delta
const newData : PricingDimensionData = {
+ unitPrice: unitPrice,
amount: Utils.createDecimal(amount).minus(previousData?.amount || 0).toNumber(),
quantity: Utils.createDecimal(consumptionkWh).minus(previousData?.quantity || 0).toNumber(),
};
@@ -254,6 +263,7 @@ export default class PricingEngine {
}
// first call for this dimension
pricingDimension.pricedData = {
+ unitPrice: unitPrice,
amount,
quantity: consumptionkWh
};
@@ -263,20 +273,22 @@ export default class PricingEngine {
static priceTimeBasedDimension(pricingDimension: PricingDimension, seconds: number): PricingDimensionData {
let amount: number;
let hours: number;
+ const unitPrice = pricingDimension.price || 0;
if (pricingDimension.stepSize) { // stepSize is in second
// bill at least one step
const nbSteps = Utils.createDecimal(seconds).divToInt(pricingDimension.stepSize).plus(1).toNumber();
const nbSeconds = Utils.createDecimal(nbSteps).mul(pricingDimension.stepSize).toNumber();
hours = Utils.createDecimal(nbSeconds).div(3600).toNumber();
- amount = Utils.createDecimal(pricingDimension.price).mul(nbSeconds).div(3600).toNumber();
+ amount = Utils.createDecimal(unitPrice).mul(nbSeconds).div(3600).toNumber();
} else {
hours = Utils.createDecimal(seconds).div(3600).toNumber();
- amount = Utils.createDecimal(pricingDimension.price).mul(seconds).div(3600).toNumber();
+ amount = Utils.createDecimal(unitPrice).mul(seconds).div(3600).toNumber();
}
const previousData = pricingDimension.pricedData;
if (previousData) {
// The new priced data is the delta
const newData : PricingDimensionData = {
+ unitPrice: unitPrice,
amount: Utils.createDecimal(amount).minus(previousData?.amount || 0).toNumber(),
quantity: Utils.createDecimal(hours).minus(previousData?.quantity || 0).toNumber(),
};
@@ -288,6 +300,7 @@ export default class PricingEngine {
}
// first call for this dimension
pricingDimension.pricedData = {
+ unitPrice: unitPrice,
amount,
quantity: hours
};

MODIFY
src/types/Pricing.ts
src/types/Pricing.ts
@@ -90,6 +90,7 @@ export interface PricingConsumptionData {
}
export interface PricingDimensionData {
+ unitPrice: number,
amount: number;
quantity: number;
// Name of the tariff this dimension belongs to - mainly for troubleshooting purposes

MODIFY
test/api/BillingStripeTestData.ts
test/api/BillingStripeTestData.ts
@@ -197,7 +197,7 @@ export default class StripeIntegrationTestData {
await this.checkForDraftInvoices(this.dynamicUser.id, 0);
// Let's create an Invoice with a first Item
const dynamicInvoice = await this.billInvoiceItem({
- energyConsumptionkWh: 1000, // kWh
+ energyConsumptionkWh: 4, // kWh
energyAmount: 1, // EUR
partingTime: 10, // Minutes
parkingAmount: 5, // EUR
@@ -206,7 +206,7 @@ export default class StripeIntegrationTestData {
assert(dynamicInvoice, 'Invoice should not be null');
// Let's add an second item to the same invoice
const updatedInvoice = await this.billInvoiceItem({
- energyConsumptionkWh: 2000, // kWh
+ energyConsumptionkWh: 8, // kWh
energyAmount: 2, // EUR
partingTime: 10, // Minutes
parkingAmount: 5, // EUR
@@ -241,7 +241,7 @@ export default class StripeIntegrationTestData {
// Let's create an Invoice with a first Item
const beforeInvoiceDateTime = Utils.createDecimal(new Date().getTime()).div(1000).trunc().toNumber();
const dynamicInvoice = await this.billInvoiceItem({
- energyConsumptionkWh: 500, // kWh
+ energyConsumptionkWh: 16, // kWh
energyAmount: 4, // EUR
partingTime: 10, // Minutes
parkingAmount: 5, // EUR
@@ -280,12 +280,14 @@ export default class StripeIntegrationTestData {
const pricingConsumptionData: PricingConsumptionData = {
energy: {
itemDescription: `Energy consumption - ${consumptionTestData.energyConsumptionkWh} kWh * ${consumptionTestData.energyAmount / consumptionTestData.energyConsumptionkWh} Eur`,
+ unitPrice: Utils.createDecimal(consumptionTestData.energyAmount).div(consumptionTestData.energyConsumptionkWh).toNumber(),
amount: consumptionTestData.energyAmount, // total amount to bill - not yet in cents
quantity: consumptionTestData.energyConsumptionkWh, // kW.h
taxes // Array of taxes - cannot be null
},
parkingTime: {
itemDescription: `Parking time - ${consumptionTestData.partingTime} minutes * ${consumptionTestData.parkingAmount} Eur`,
+ unitPrice: Utils.createDecimal(consumptionTestData.parkingAmount).div(consumptionTestData.partingTime).toNumber(),
amount: consumptionTestData.parkingAmount, // Euros
quantity: consumptionTestData.partingTime, // minutes
taxes // Array of taxes - cannot be null

pricing/billing - keep track of the unitPrice