bp - compute session fees
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, BillingPlatformInvoice, BillingSessionAccountData, BillingSessionData, BillingStatus, BillingTax, BillingTransfer, BillingTransferSession, BillingTransferStatus, BillingUser } from '../../types/Billing';
+import { BillingAccount, BillingAccountSessionFee, BillingChargeInvoiceAction, BillingDataTransactionStart, BillingDataTransactionStop, BillingDataTransactionUpdate, BillingInvoice, BillingInvoiceItem, BillingInvoiceStatus, BillingOperationResult, BillingPaymentMethod, BillingPlatformInvoice, 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';
@@ -749,13 +749,13 @@ export default abstract class BillingIntegration {
const accountIDs = [ ...new Set(allAccountIDs)];
if (accountIDs.length > 0) {
for (const accountID of accountIDs) {
- await this.processTransferForAccount(accountID, billingInvoice);
+ await this.dispatchFundsPerAccount(accountID, billingInvoice);
}
}
}
}
- public async processTransferForAccount(accountID: string, invoice: BillingInvoice): Promise<void> {
+ public async dispatchFundsPerAccount(accountID: string, invoice: BillingInvoice): Promise<void> {
try {
const sessions = invoice.sessions.filter((session) => accountID === session?.accountData?.accountID);
// Get the existing DRAFT transfer (if any)
@@ -776,16 +776,19 @@ export default abstract class BillingIntegration {
// Process all sessions of the invoice matching the current account ID
for (const session of sessions) {
// Compute the session amount (adding the 4 pricing dimensions)
- const amountAsDecimal = BillingHelpers.getBilledPrice(session.pricingData); // TODO - consider tax rate settings!
+ const amountAsDecimal = BillingHelpers.getBilledPrice(session.pricingData);
+ const amount = amountAsDecimal.toNumber();
+ const roundedAmount = Utils.roundTo(amountAsDecimal, 2);
+ const accountSessionFee = this.computeAccountSessionFee(session, roundedAmount);
// Extract current session data
const sessionData: BillingTransferSession = {
transactionID: session.transactionID,
invoiceID: invoice.id,
invoiceNumber: invoice.number,
amountAsDecimal,
- amount: amountAsDecimal.toNumber(),
- roundedAmount: Utils.roundTo(amountAsDecimal, 2),
- platformFeeStrategy: session.accountData.platformFeeStrategy
+ amount,
+ roundedAmount,
+ accountSessionFee
};
// Update the collection of sessions in the DRAFT transfer
transfer.sessions.push(sessionData);
@@ -797,6 +800,8 @@ export default abstract class BillingIntegration {
});
// Round the final result only!
transfer.totalAmount = Utils.roundTo(transferAmountAsDecimal, 2);
+ // Amount to transfer is not yet known - The invoice must be generated to take the tax rates into account
+ transfer.transferAmount = null;
// Finally - create or update the transfer
await BillingStorage.saveTransfer(this.tenant, transfer);
} catch (error) {
@@ -809,4 +814,15 @@ export default abstract class BillingIntegration {
});
}
}
+
+ private computeAccountSessionFee(session: BillingSessionData, sessionTotalAmount: number): BillingAccountSessionFee {
+ const { percentage, flatFeePerSession } = session.accountData.platformFeeStrategy;
+ const feeAmount = Utils.createDecimal(sessionTotalAmount).mul(percentage).div(100).plus(flatFeePerSession).toNumber();
+ return {
+ percentage,
+ flatFeePerSession,
+ taxExternalID: null, // TODO - consider tax rates
+ feeAmount
+ };
+ }
}

MODIFY
src/integration/billing/stripe/StripeBillingIntegration.ts
src/integration/billing/stripe/StripeBillingIntegration.ts
@@ -1743,15 +1743,18 @@ export default class StripeBillingIntegration extends BillingIntegration {
public async billPlatformFee(billingTransfer: BillingTransfer, user: User): Promise<BillingPlatformInvoice> {
await this.checkConnection();
- // Create invoice items
try {
- await Promise.all(billingTransfer.sessions.map(async (session) =>
- this.createStripeInvoiceItem({
- amount: session.amount * 100, // TODO - bill the platform fee (not the session amount)
+ // Create invoice items
+ await Promise.all(billingTransfer.sessions.map(async (session) => {
+ // A single tax rate per session
+ const tax_rates = (session.accountSessionFee.taxExternalID) ? [ session.accountSessionFee.taxExternalID ] : [];
+ return this.createStripeInvoiceItem({
+ amount: session.accountSessionFee.feeAmount,
customer: user.billingData.customerID,
- currency: billingTransfer.currency
- }, this.buildIdemPotencyKey(session.transactionID, 'invoice', 'platformFee'))
- ));
+ currency: billingTransfer.currency,
+ tax_rates
+ }, this.buildIdemPotencyKey(session.transactionID, 'invoice', 'platformFee'));
+ }));
} catch (e) {
throw new BackendError({
message: 'Unexpected situation - unable to create invoice item',
@@ -1763,6 +1766,11 @@ export default class StripeBillingIntegration extends BillingIntegration {
// Create invoice
let stripeInvoice: Stripe.Invoice;
try {
+ // -------------------------------------------------------------------------------------------
+ // TODO - Assuming the business owner is also an EV driver, we have here a potential conflict
+ // -------------------------------------------------------------------------------------------
+ // > Consider the STRIPE's new options: 'pending_invoice_items_behavior'
+ // -------------------------------------------------------------------------------------------
stripeInvoice = await this.createStripeInvoice(user.billingData.customerID, user.id, this.buildIdemPotencyKey(billingTransfer.id, 'invoice', 'platformFee'));
} catch (e) {
throw new BackendError({

MODIFY
src/server/rest/v1/service/BillingService.ts
src/server/rest/v1/service/BillingService.ts
@@ -707,10 +707,15 @@ export default class BillingService {
user.billingData = (await billingImpl.forceSynchronizeUser(user)).billingData;
await UserStorage.saveUser(req.tenant, user);
}
+ // Generate the invoice with a fee per session
const invoice = await billingImpl.billPlatformFee(transfer, user);
+ // Let's keep track of the invoice data
+ transfer.invoice = invoice;
+ // We now know how much we can transfer to the sub-account!
+ const amountIncludingTaxes = invoice.totalAmount;
+ transfer.transferAmount = Utils.createDecimal(transfer.totalAmount).minus(amountIncludingTaxes).toNumber();
// Update the transfer status
transfer.status = BillingTransferStatus.FINALIZED;
- transfer.invoice = invoice;
await BillingStorage.saveTransfer(req.tenant, transfer);
res.json(Constants.REST_RESPONSE_SUCCESS);
next();

MODIFY
src/storage/mongodb/BillingStorage.ts
src/storage/mongodb/BillingStorage.ts
@@ -354,7 +354,7 @@ export default class BillingStorage {
amountAsDecimal: session.amountAsDecimal,
amount: session.amount,
roundedAmount: session.roundedAmount,
- platformFeeStrategy: session.platformFeeStrategy,
+ accountSessionFee: session.accountSessionFee,
})),
currency: transfer.currency,
};

MODIFY
src/types/Billing.ts
src/types/Billing.ts
@@ -193,6 +193,11 @@ export interface BillingPlatformFeeData {
invoiceExternalID?: string; // Invoice sent to the CPO
}
+export interface BillingAccountSessionFee extends BillingPlatformFeeStrategy {
+ taxExternalID: string; // Tax to apply on the platform fee
+ feeAmount: number;
+}
+
export interface BillingTransfer extends CreatedUpdatedProps, BillingTransferAuthorizationActions {
id?: string;
status: BillingTransferStatus;
@@ -218,7 +223,7 @@ export interface BillingTransferSession {
amountAsDecimal: BillingAmount
amount: number; // ACHTUNG - That one should not include any taxes
roundedAmount: number;
- platformFeeStrategy: BillingPlatformFeeStrategy;
+ accountSessionFee: BillingAccountSessionFee;
}
export interface BillingPlatformInvoice {

MODIFY
test/factories/BillingFactory.ts
test/factories/BillingFactory.ts
-import { BillingPlatformFeeStrategy, BillingTransfer, BillingTransferSession, BillingTransferStatus } from '../../src/types/Billing';
+import { BillingAccountSessionFee, BillingPlatformFeeStrategy, BillingTransfer, BillingTransferSession, BillingTransferStatus } from '../../src/types/Billing';
import { Factory } from 'rosie';
import faker from 'faker';
@@ -22,11 +22,19 @@ export const BillingTransferFactory = Factory.define<BillingTransfer>('billingtr
export const BillingTransferSessionFactory = Factory.define<BillingTransferSession>('billingtransfersession')
.attr('transactionID', () => faker.datatype.number())
.attr('amount', () => faker.datatype.number())
- .attr('platformFeeStrategy', () => ({
+ .attr('accountSessionFee', () => ({
flatFeePerSession: faker.datatype.number(),
percentage: faker.datatype.number(),
+ taxExternalID: faker.datatype.string(),
+ feeAmount: faker.datatype.number()
}));
export const BillingPlatformFeeStrategyFactory = Factory.define<BillingPlatformFeeStrategy>('billingplatformfeestrategy')
.attr('flatFeePerSession', () => faker.datatype.number())
.attr('percentage', () => faker.datatype.number());
+
+export const BillingPlatformSessionFeeFactory = Factory.define<BillingAccountSessionFee>('billingaccountSessionFee')
+ .attr('flatFeePerSession', () => faker.datatype.number())
+ .attr('percentage', () => faker.datatype.number())
+ .attr('taxExternalID', () => faker.datatype.string())
+ .attr('feeAmount', () => faker.datatype.number());

bp - compute session fees