billing - itemDescription per dimension + cleanup unused description
MODIFY
src/integration/billing/stripe/StripeBillingIntegration.ts
src/integration/billing/stripe/StripeBillingIntegration.ts
@@ -14,6 +14,7 @@ import { BillingSettings } from '../../../types/Setting';
import BillingStorage from '../../../storage/mongodb/BillingStorage';
import Constants from '../../../utils/Constants';
import Cypher from '../../../utils/Cypher';
+import { EffectivePricingData } from '../../../types/Pricing';
import I18nManager from '../../../utils/I18nManager';
import Logging from '../../../utils/Logging';
import { Request } from 'express';
@@ -857,6 +858,17 @@ export default class StripeBillingIntegration extends BillingIntegration {
}
private async _createStripeInvoiceItems(customerID: string, billingInvoiceItem: BillingInvoiceItem, 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 dimemsion
await this._createStripeInvoiceItem4Dimension(customerID, 'flatFee', billingInvoiceItem, invoiceID);
await this._createStripeInvoiceItem4Dimension(customerID, 'chargingTime', billingInvoiceItem, invoiceID);
@@ -866,8 +878,10 @@ export default class StripeBillingIntegration extends BillingIntegration {
private async _createStripeInvoiceItem4Dimension(customerID: string, pricingDimension: string,
billingInvoiceItem: BillingInvoiceItem, invoiceID?: string): Promise<Stripe.InvoiceItemCreateParams> {
- const { effectivePricing } = billingInvoiceItem;
- if (!effectivePricing[pricingDimension]?.amount || !effectivePricing[pricingDimension]?.quantity) {
+ const effectivePricing = billingInvoiceItem.effectivePricing;
+ // data for the current dimension (energy | parkingTime, etc)
+ const effectivePricingData: EffectivePricingData = effectivePricing[pricingDimension];
+ if (!effectivePricingData || !effectivePricingData.amount || !effectivePricingData.quantity) {
// Do not bill that dimension
return null;
}
@@ -879,7 +893,7 @@ export default class StripeBillingIntegration extends BillingIntegration {
invoice: invoiceID,
customer: customerID,
currency,
- description: effectivePricing[pricingDimension].description,
+ description: effectivePricingData.itemDescription,
tax_rates,
// quantity: 1, //Cannot be set separately
amount: Utils.createDecimal(effectivePricing[pricingDimension].amount).times(100).round().toNumber(),
@@ -893,61 +907,6 @@ export default class StripeBillingIntegration extends BillingIntegration {
return parameters;
}
- private _buildInvoiceItemParameters(customerID: string, billingInvoiceItem: BillingInvoiceItem, invoiceID?: string): Stripe.InvoiceItemCreateParams {
- /* --------------------------------------------------------------------------------
- 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)
- ----------------------------------------------------------------------------------- */
- const { description, effectivePricing } = billingInvoiceItem;
- const currency = effectivePricing.currency.toLowerCase();
- // Build stripe parameters for the item
- const parameters: Stripe.InvoiceItemCreateParams = {
- invoice: invoiceID,
- customer: customerID,
- currency,
- description,
- tax_rates: effectivePricing.energy?.taxes,
- // quantity: 1, //Cannot be set separately
- amount: Utils.createDecimal(effectivePricing.energy.amount).times(100).round().toNumber(), // In cents
- metadata: { ...billingInvoiceItem?.metadata }
- };
-
- // // ----------------------------------------------------------------------------------------
- // // INVESTIGATIONS - Attempts to set both the quantity and the unit_amount
- // // ----------------------------------------------------------------------------------------
- // Quantity must be an Integer! - STRIPE does not support decimals
- // const quantity = Utils.createDecimal(pricingData.quantity).round().toNumber(); // kW.h -
- // if (quantity === 0) {
- // // ----------------------------------------------------------------------------------------
- // // The quantity was too small - let's prevent dividing by zero
- // // parameters.quantity = 0; // Not an option for STRIPE
- // // ----------------------------------------------------------------------------------------
- // parameters.amount = Utils.createDecimal(pricingData.amount).times(100).round().toNumber();
- // } else {
- // // ----------------------------------------------------------------------------------------
- // // STRIPE expects either "unit_amount" in Cents - or unit_amount_decimal (with 4 decimals)
- // // ----------------------------------------------------------------------------------------
- // const unit_amount_in_cents = Utils.createDecimal(pricingData.amount).times(100).dividedBy(quantity);
- // // Let's use the more precise option
- // const unit_amount_decimal: string = unit_amount_in_cents.times(100).round().dividedBy(100).toNumber().toFixed(2);
- // parameters.quantity = quantity;
- // parameters.unit_amount_decimal = unit_amount_decimal;
- // }
-
- if (!parameters.invoice) {
- // STRIPE throws an exception when invoice is set to null.
- delete parameters.invoice;
- }
- return parameters;
- }
-
public async stopTransaction(transaction: Transaction): Promise<BillingDataTransactionStop> {
// Check whether the billing was activated on start transaction
if (!transaction.billingData?.withBillingActive) {
@@ -1065,10 +1024,9 @@ export default class StripeBillingIntegration extends BillingIntegration {
private shrinkInvoiceItem(fatInvoiceItem: BillingInvoiceItem): BillingInvoiceItem {
// The initial invoice item includes redundant transaction data
- const { description, transactionID, effectivePricing } = fatInvoiceItem;
+ const { transactionID, effectivePricing } = fatInvoiceItem;
// Let's return only essential information
const lightInvoiceItem: BillingInvoiceItem = {
- description,
transactionID,
effectivePricing
};
@@ -1093,7 +1051,6 @@ export default class StripeBillingIntegration extends BillingIntegration {
const taxes = this.getTaxRateIds();
// Build a billing invoice item based on the transaction
const billingInvoiceItem: BillingInvoiceItem = {
- description: itemDescription,
transactionID,
effectivePricing: {
currency,

MODIFY
src/integration/billing/stripe/StripeHelpers.ts
src/integration/billing/stripe/StripeHelpers.ts
@@ -32,7 +32,6 @@ export default class StripeHelpers {
if (billingInvoiceItem) {
session = {
transactionID: billingInvoiceItem.transactionID,
- description: billingInvoiceItem.description,
effectivePricing: billingInvoiceItem.effectivePricing,
};
}

MODIFY
src/types/Billing.ts
src/types/Billing.ts
@@ -82,7 +82,7 @@ export interface BillingInvoice {
}
export interface BillingInvoiceItem {
- description: string;
+ // description: string;
transactionID: number;
// pricingData: BillingPricingData;
effectivePricing: EffectivePricing
@@ -94,7 +94,6 @@ export interface BillingInvoiceItem {
export interface BillingSessionData {
transactionID: number;
- description: string;
// pricingData: BillingPricingData;
effectivePricing: EffectivePricing;
}

MODIFY
test/api/BillingStripeTestData.ts
test/api/BillingStripeTestData.ts
@@ -8,6 +8,7 @@ import CentralServerService from './client/CentralServerService';
import ContextDefinition from './context/ContextDefinition';
import ContextProvider from './context/ContextProvider';
import Cypher from '../../src/utils/Cypher';
+import { EffectivePricing } from '../../src/types/Pricing';
import Factory from '../factories/Factory';
import Stripe from 'stripe';
import StripeBillingIntegration from '../../src/integration/billing/stripe/StripeBillingIntegration';
@@ -196,10 +197,22 @@ export default class StripeIntegrationTestData {
// The user should have no DRAFT invoices
await this.checkForDraftInvoices(this.dynamicUser.id, 0);
// Let's create an Invoice with a first Item
- const dynamicInvoice = await this.billInvoiceItem(1000 /* kW.h */, 4 /* EUR */, taxId);
+ const dynamicInvoice = await this.billInvoiceItem({
+ energyConsumptionkWh: 1000, // kWh
+ energyAmount: 1, // EUR
+ partingTime: 10, // Minutes
+ parkingAmount: 5, // EUR
+ taxId
+ });
assert(dynamicInvoice, 'Invoice should not be null');
// Let's add an second item to the same invoice
- const updatedInvoice = await this.billInvoiceItem(2000 /* kW.h */, 8 /* EUR */, taxId);
+ const updatedInvoice = await this.billInvoiceItem({
+ energyConsumptionkWh: 2000, // kWh
+ energyAmount: 2, // EUR
+ partingTime: 10, // Minutes
+ parkingAmount: 5, // EUR
+ taxId
+ });
assert(updatedInvoice, 'Invoice should not be null');
// User should have a DRAFT invoice
const draftInvoices = await this.getInvoicesByState(this.dynamicUser.id, BillingInvoiceStatus.DRAFT);
@@ -220,9 +233,7 @@ export default class StripeIntegrationTestData {
public async checkImmediateBillingWithTaxes() : Promise<void> {
// Inputs / Expected Outputs
- const transactionPrice = 4 /* EUR */;
const tax20percent = 20 /* VAT 20 % */;
- const expectedTotal = 480; /* in cents, including taxes */
// PREREQUISITE - immediateBillingAllowed MUST BE ON!
const taxRate: Stripe.TaxRate = await this.assignTaxRate(tax20percent);
const taxId = taxRate.id;
@@ -230,7 +241,14 @@ export default class StripeIntegrationTestData {
await this.checkForDraftInvoices(this.dynamicUser.id, 0);
// 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(500 /* kW.h */, transactionPrice /* EUR */, taxId);
+ const dynamicInvoice = await this.billInvoiceItem({
+ energyConsumptionkWh: 500, // kWh
+ energyAmount: 4, // EUR
+ partingTime: 10, // Minutes
+ parkingAmount: 5, // EUR
+ taxId
+ });
+ const expectedTotal = 1080; /* in cents, including taxes */
assert(dynamicInvoice, 'Invoice should not be null');
// User should have a PAID invoice
const paidInvoices = await this.getInvoicesByState(this.dynamicUser.id, BillingInvoiceStatus.PAID);
@@ -249,30 +267,36 @@ export default class StripeIntegrationTestData {
expect(nbDraftInvoice).to.be.eql(0);
}
- public async billInvoiceItem(quantity: number, amount: number, taxId?: string) : Promise<BillingInvoice> {
+ public async billInvoiceItem(consumptionTestData: {
+ energyConsumptionkWh: number, // kWh
+ energyAmount: number, // EUR
+ partingTime: number, // Minutes
+ parkingAmount: number // EUR
+ taxId: string
+ }) : Promise<BillingInvoice> {
assert(this.billingUser, 'Billing user cannot be null');
- const price = amount / quantity;
-
- const itemDescription = `Stripe Integration - ${quantity} kWh * ${price} Eur`;
// array of tax ids to apply to the line item
- const taxes = (taxId) ? [ taxId ] : [];
- const invoiceItem:BillingInvoiceItem = {
- description: itemDescription,
- transactionID: Utils.getRandomIntSafe(),
- // pricingData: {
- // quantity, // kW.h
- // amount, // total amount to bill - not yet in cents
- // currency: 'EUR'
- // }
- effectivePricing: {
+ const taxes = (consumptionTestData.taxId) ? [ consumptionTestData.taxId ] : [];
+ // Princing/Consumption Data
+ const effectivePricing: EffectivePricing = {
currency: 'EUR',
energy: {
- itemDescription,
- amount, // total amount to bill - not yet in cents
- quantity, // kW.h
+ itemDescription: `Energy consumption - ${consumptionTestData.energyConsumptionkWh} kWh * ${consumptionTestData.energyAmount / consumptionTestData.energyConsumptionkWh} Eur`,
+ 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`,
+ amount: consumptionTestData.parkingAmount, // Euros
+ quantity: consumptionTestData.partingTime, // minutes
taxes // Array of taxes - cannot be null
}
- }
+ };
+ // Invoice Item
+ const invoiceItem:BillingInvoiceItem = {
+ transactionID: Utils.getRandomIntSafe(),
+ effectivePricing
};
// Let's attempt to bill the line item
const billingInvoice: BillingInvoice = await this.billingImpl.billInvoiceItem(this.dynamicUser, invoiceItem);

billing - itemDescription per dimension + cleanup unused description