bp - transfer preparation amountAsDecimal
ADD
src/integration/billing/BillingHelpers.ts
+import { BillingInvoiceItem } from '../../types/Billing';
+import Decimal from 'decimal.js';
+import { PricedConsumptionData } from '../../types/Pricing';
+import Utils from '../../utils/Utils';
+
+export default class BillingHelpers {
+
+ public static getItemPrice(billingInvoiceItem: BillingInvoiceItem): Decimal {
+ return BillingHelpers.getBilledPrice(billingInvoiceItem.pricingData);
+ }
+
+ public static getBilledPrice(pricedData: PricedConsumptionData[]): Decimal {
+ let price = Utils.createDecimal(0);
+ if (pricedData) {
+ pricedData.forEach((pricedConsumptionData) => {
+ price = price.plus(pricedConsumptionData.flatFee?.amountAsDecimal || 0);
+ price = price.plus(pricedConsumptionData.energy?.amountAsDecimal || 0);
+ price = price.plus(pricedConsumptionData.parkingTime?.amountAsDecimal || 0);
+ price = price.plus(pricedConsumptionData.chargingTime?.amountAsDecimal || 0);
+ });
+ }
+ return price;
+ }
+}

MODIFY
src/integration/billing/BillingIntegration.ts
src/integration/billing/BillingIntegration.ts
+/* eslint-disable @typescript-eslint/member-ordering */
import { AsyncTaskType, AsyncTasks } from '../../types/AsyncTask';
-import { BillingAccount, BillingChargeInvoiceAction, BillingDataTransactionStart, BillingDataTransactionStop, BillingDataTransactionUpdate, BillingInvoice, BillingInvoiceItem, BillingInvoiceStatus, BillingOperationResult, BillingPaymentMethod, BillingSessionAccountData, BillingStatus, BillingTax, BillingUser } from '../../types/Billing';
+import { BillingAccount, BillingChargeInvoiceAction, BillingDataTransactionStart, BillingDataTransactionStop, BillingDataTransactionUpdate, BillingInvoice, BillingInvoiceItem, BillingInvoiceStatus, BillingOperationResult, BillingPaymentMethod, BillingSessionAccountData, BillingSessionData, BillingStatus, BillingTax, BillingTransfer, BillingTransferSession, BillingTransferStatus, BillingUser } from '../../types/Billing';
import Tenant, { TenantComponents } from '../../types/Tenant';
import Transaction, { StartTransactionErrorCode } from '../../types/Transaction';
import User, { UserStatus } from '../../types/User';
import AsyncTaskBuilder from '../../async-task/AsyncTaskBuilder';
import BackendError from '../../exception/BackendError';
+import BillingHelpers from './BillingHelpers';
import { BillingPeriodicOperationTaskConfig } from '../../types/TaskConfig';
import { BillingSettings } from '../../types/Setting';
import BillingStorage from '../../storage/mongodb/BillingStorage';
@@ -291,29 +293,6 @@ export default abstract class BillingIntegration {
return true;
}
- protected async triggerTransferPreparation(billingInvoice: BillingInvoice): Promise<void> {
- if (!billingInvoice.sessions) {
- // This should not happen!
- return;
- }
- // The invoice may include several sessions - let's check if at least one of these needs a transfer
- const sessions = billingInvoice.sessions.filter((session) => session?.accountData?.withTransferActive);
- if (sessions.length > 0) {
- // Create async task to proceed with the transfer of funds
- await AsyncTaskBuilder.createAndSaveAsyncTasks({
- name: AsyncTasks.PREPARE_INVOICE_TRANSFER,
- action: ServerAction.BILLING_PREPARE_TRANSFER,
- type: AsyncTaskType.TASK,
- tenantID: this.tenant.id,
- parameters: {
- invoiceID: billingInvoice.id
- },
- module: MODULE_NAME,
- method: 'triggerTransferPreparation',
- });
- }
- }
-
protected async updateTransactionsBillingData(billingInvoice: BillingInvoice): Promise<void> {
if (!billingInvoice.sessions) {
// This should not happen - but it happened once!
@@ -730,6 +709,89 @@ export default abstract class BillingIntegration {
abstract createSubAccount(): Promise<BillingAccount>;
- abstract prepareInvoiceTransfer(invoice: BillingInvoice): Promise<void>;
+ protected async triggerTransferPreparation(billingInvoice: BillingInvoice): Promise<void> {
+ if (!billingInvoice.sessions) {
+ // This should not happen!
+ return;
+ }
+ // The invoice may include several sessions - let's check if at least one of these needs a transfer
+ const sessions = billingInvoice.sessions.filter((session) => session?.accountData?.withTransferActive);
+ if (sessions.length > 0) {
+ // Create async task to proceed with the transfer of funds
+ await AsyncTaskBuilder.createAndSaveAsyncTasks({
+ name: AsyncTasks.PREPARE_INVOICE_TRANSFER,
+ action: ServerAction.BILLING_PREPARE_TRANSFER,
+ type: AsyncTaskType.TASK,
+ tenantID: this.tenant.id,
+ parameters: {
+ invoiceID: billingInvoice.id
+ },
+ module: MODULE_NAME,
+ method: 'triggerTransferPreparation',
+ });
+ }
+ }
+
+ public async prepareInvoiceTransfer(billingInvoice: BillingInvoice): Promise<void> {
+ if (!billingInvoice.sessions) {
+ // This should not happen!
+ return;
+ }
+ // The invoice may include several sessions - let's check if at least one of these needs a transfer
+ const sessions = billingInvoice.sessions.filter((session) => session?.accountData?.withTransferActive);
+ if (sessions.length > 0) {
+ // Get the list of account IDs
+ const allAccountIDs = sessions.map((session) => session?.accountData?.accountID);
+ // Remove duplicates
+ const accountIDs = [ ...new Set(allAccountIDs)];
+ if (accountIDs.length > 0) {
+ for (const accountID of accountIDs) {
+ const filteredSessions = billingInvoice.sessions.filter((session) => accountID === session?.accountData?.accountID);
+ await this.processTransfer4Account(accountID, filteredSessions);
+ }
+ }
+ }
+ }
+
+ public async processTransfer4Account(accountID: string, sessions: BillingSessionData[]): Promise<void> {
+ // Get the existing DRAFT transfer (if any)
+ const transfers = await BillingStorage.getTransfers(
+ this.tenant, {
+ accountIDs: [accountID],
+ status: [BillingTransferStatus.DRAFT],
+ }, Constants.DB_PARAMS_SINGLE_RECORD
+ );
+ let transfer: BillingTransfer = transfers.result[0];
+ if (!transfer) {
+ transfer = {
+ accountID, status: BillingTransferStatus.DRAFT, sessions: [], amount: 0, transferredAmount: 0,
+ platformFeeData: null, transferExternalID: null,
+ };
+ }
+ // Transfer amount as decimal to preserve the precision
+ const transferAmountAsDecimal = Utils.createDecimal(transfer.amount);
+ // Process all sessions of the invoice matching the current account ID
+ for (const session of sessions) {
+ if (accountID !== session.accountData.accountID) {
+ throw new Error('Unexpected situation - accountID is inconsistent!');
+ }
+ // Compute the session amount (adding the 4 pricing dimensions)
+ const amountAsDecimal = BillingHelpers.getBilledPrice(session.pricingData);
+ // Extract current session data
+ const sessionData: BillingTransferSession = {
+ transactionID: session.transactionID,
+ amountAsDecimal,
+ amount: amountAsDecimal.toNumber(),
+ roundedAmount: Utils.roundTo(amountAsDecimal, 2),
+ platformFee: session.accountData.platformFeeStrategy
+ };
+ // Update existing DRAFT transfer
+ transfer.sessions.push(sessionData);
+ }
+
+ transfer.amount = transferAmountAsDecimal.toNumber();
+ // Finally - crate/update a DRAFT transfer
+ await BillingStorage.saveTransfer(this.tenant, transfer);
+ }
}

MODIFY
src/integration/billing/stripe/StripeBillingIntegration.ts
src/integration/billing/stripe/StripeBillingIntegration.ts
import { AsyncTaskType, AsyncTasks } from '../../../types/AsyncTask';
/* eslint-disable @typescript-eslint/member-ordering */
-import { BillingAccount, BillingAccountStatus, BillingDataTransactionStart, BillingDataTransactionStop, BillingDataTransactionUpdate, BillingInvoice, BillingInvoiceItem, BillingInvoiceStatus, BillingOperationResult, BillingPaymentMethod, BillingSessionAccountData, BillingSessionData, BillingStatus, BillingTax, BillingTransfer, BillingTransferSession, BillingTransferStatus, BillingUser, BillingUserData } from '../../../types/Billing';
+import { BillingAccount, BillingAccountStatus, BillingDataTransactionStart, BillingDataTransactionStop, BillingDataTransactionUpdate, BillingInvoice, BillingInvoiceItem, BillingInvoiceStatus, BillingOperationResult, BillingPaymentMethod, BillingSessionAccountData, BillingStatus, BillingTax, BillingUser, BillingUserData } from '../../../types/Billing';
import { DimensionType, PricedConsumptionData, PricedDimensionData } from '../../../types/Pricing';
import FeatureToggles, { Feature } from '../../../utils/FeatureToggles';
import StripeHelpers, { StripeChargeOperationResult } from './StripeHelpers';
@@ -1735,72 +1735,10 @@ export default class StripeBillingIntegration extends BillingIntegration {
method: 'createSubAccount',
});
}
-
return {
accountExternalID: subAccount.id,
activationLink: activationLink.url,
status: BillingAccountStatus.IDLE
};
}
-
- public async prepareInvoiceTransfer(billingInvoice: BillingInvoice): Promise<void> {
- if (!billingInvoice.sessions) {
- // This should not happen!
- return;
- }
- // The invoice may include several sessions - let's check if at least one of these needs a transfer
- const sessions = billingInvoice.sessions.filter((session) => session?.accountData?.withTransferActive);
- if (sessions.length > 0) {
- // Get the list of account IDs
- const allAccountIDs = sessions.map((session) => session?.accountData?.accountID);
- // Remove duplicates
- const accountIDs = [ ...new Set(allAccountIDs)];
- if (accountIDs.length > 0) {
- for (const accountID of accountIDs) {
- const filteredSessions = billingInvoice.sessions.filter((session) => accountID === session?.accountData?.accountID);
- await this.processTransfer4Account(accountID, filteredSessions);
- }
- }
- }
- }
-
- public async processTransfer4Account(accountID: string, sessions: BillingSessionData[]): Promise<void> {
- // Get the existing DRAFT transfer (if any)
- const transfers = await BillingStorage.getTransfers(
- this.tenant, {
- accountIDs: [accountID],
- status: [BillingTransferStatus.DRAFT],
- }, Constants.DB_PARAMS_SINGLE_RECORD
- );
- let transfer: BillingTransfer = transfers.result[0];
- if (!transfer) {
- transfer = {
- accountID,
- status: BillingTransferStatus.DRAFT,
- sessions: [],
- amount: 0, // Todo
- transferredAmount: 0,
- platformFeeData: null, // Will be computed when finalizing the transfer
- transferExternalID: null,
- };
- }
- // Process all sessions of the invoice matching the current account ID
- for (const session of sessions) {
- if (accountID !== session.accountData.accountID) {
- throw new Error('Unexpected situation - accountID is inconsistent!');
- }
- // Extract current session data
- const sessionData: BillingTransferSession = {
- transactionID: session.transactionID,
- amount: 0, // Todo
- platformFee: session.accountData.platformFeeStrategy
- };
- // Update existing DRAFT transfer
- transfer.sessions.push(sessionData);
- transfer.amount += sessionData.amount;
- }
- // Finally - crate/update a DRAFT transfer
- await BillingStorage.saveTransfer(this.tenant, transfer);
- }
-
}

MODIFY
src/types/Billing.ts
src/types/Billing.ts
@@ -2,6 +2,7 @@ import { AuthorizationActions, BillingInvoiceAuthorizationActions } from './Auth
import { ActionsResponse } from './GlobalType';
import CreatedUpdatedProps from './CreatedUpdatedProps';
+import Decimal from 'decimal.js';
import { PricedConsumptionData } from './Pricing';
import User from './User';
@@ -202,9 +203,14 @@ export interface BillingTransfer {
transferExternalID?: string; // Transfer sent to the CPO
}
+// Very important - preserve maximal precision - Decimal type is persisted as an object in the DB
+export type BillingAmount = Decimal.Value;
+
export interface BillingTransferSession {
transactionID: number;
+ amountAsDecimal: BillingAmount
amount: number; // ACHTUNG - That one should not include any taxes
+ roundedAmount: number;
platformFee: BillingPlatformFeeStrategy;
}

MODIFY
src/utils/Utils.ts
src/utils/Utils.ts
@@ -1021,7 +1021,7 @@ export default class Utils {
return recordLimit;
}
- public static roundTo(value: number, scale: number): number {
+ public static roundTo(value: Decimal.Value, scale: number): number {
const roundPower = Math.pow(10, scale);
return Utils.createDecimal(value).mul(roundPower).round().div(roundPower).toNumber();
}

bp - transfer preparation amountAsDecimal