bp - tax rate per account
MODIFY
src/assets/server/rest/v1/docs/e-mobility-oas.json
src/assets/server/rest/v1/docs/e-mobility-oas.json
"Account": {
"type": "object",
"properties": {
- "accountID": {
- "type": "string"
- },
- "userID": {
- "type": "string"
- },
"id": {
"type": "string",
"readOnly": true
},
- "user": {
+ "businessOwnerID": {
+ "type": "string"
+ },
+ "businessOwner": {
"$ref": "#/components/schemas/User"
+ },
+ "status": {
+ "type": "string",
+ "enum": [
+ "idle",
+ "pending",
+ "active"
+ ]
+ },
+ "activationLink": {
+ "type": "string"
+ },
+ "accountExternalID": {
+ "type": "string"
+ },
+ "taxID": {
+ "type": "string"
}
}
},

MODIFY
src/assets/server/rest/v1/schemas/billing/billing-create-account.json
src/assets/server/rest/v1/schemas/billing/billing-create-account.json
"properties": {
"businessOwnerID": {
"$ref": "common#/definitions/id"
+ },
+ "taxID": {
+ "type": "string",
+ "sanitize": "mongo"
}
},
"required": [

MODIFY
src/authorization/AuthorizationsDefinition.ts
src/authorization/AuthorizationsDefinition.ts
@@ -505,7 +505,7 @@ export const AUTHORIZATION_DEFINITION: AuthorizationDefinition = {
resource: Entity.BILLING_ACCOUNT,
action: [Action.LIST, Action.CREATE, Action.READ, Action.UPDATE, Action.DELETE, Action.BILLING_ONBOARD_ACCOUNT],
attributes: [
- 'id', 'accountExternalID', 'businessOwnerID', 'businessOwner.id', 'businessOwner.firstName', 'businessOwner.name', 'businessOwner.email', 'status'
+ 'id', 'accountExternalID', 'taxID', 'businessOwnerID', 'businessOwner.id', 'businessOwner.firstName', 'businessOwner.name', 'businessOwner.email', 'status'
]
},
{

MODIFY
src/integration/billing/BillingIntegration.ts
src/integration/billing/BillingIntegration.ts
@@ -712,7 +712,7 @@ export default abstract class BillingIntegration {
public abstract refreshConnectedAccount(billingAccount: BillingAccount, url: string): Promise<Partial<BillingAccount>>;
- public abstract billPlatformFee(transfer: BillingTransfer, user: User): Promise<BillingPlatformInvoice>;
+ public abstract billPlatformFee(transfer: BillingTransfer, user: User, billingAccount: BillingAccount): Promise<BillingPlatformInvoice>;
public abstract sendTransfer(transfer: BillingTransfer, user: User): Promise<string>;

MODIFY
src/integration/billing/stripe/StripeBillingIntegration.ts
src/integration/billing/stripe/StripeBillingIntegration.ts
@@ -1748,7 +1748,7 @@ export default class StripeBillingIntegration extends BillingIntegration {
};
}
- public async billPlatformFee(billingTransfer: BillingTransfer, user: User): Promise<BillingPlatformInvoice> {
+ public async billPlatformFee(billingTransfer: BillingTransfer, user: User, billingAccount: BillingAccount): Promise<BillingPlatformInvoice> {
await this.checkConnection();
if (!user.billingData || !user.billingData.customerID) {
// Synchronize owner if needed
@@ -1765,7 +1765,7 @@ export default class StripeBillingIntegration extends BillingIntegration {
});
}
// Add items to the invoice
- await this.addItemsToPlatformFeeInvoice(stripeInvoice, billingTransfer, user);
+ await this.addItemsToPlatformFeeInvoice(stripeInvoice, billingTransfer, user, billingAccount);
// Mark the invoice as paid
const stripePaidInvoice = await this.markStripeInvoiceAsPaid(stripeInvoice);
// Preserve some information
@@ -1799,7 +1799,7 @@ export default class StripeBillingIntegration extends BillingIntegration {
}
}
- private async addItemsToPlatformFeeInvoice(stripeInvoice: Stripe.Invoice, billingTransfer: BillingTransfer, user: User): Promise<void> {
+ private async addItemsToPlatformFeeInvoice(stripeInvoice: Stripe.Invoice, billingTransfer: BillingTransfer, user: User, billingAccount: BillingAccount): Promise<void> {
// Create invoice items
try {
// Extract session amounts
@@ -1821,7 +1821,7 @@ export default class StripeBillingIntegration extends BillingIntegration {
// Generate the invoice item
const description = this.buildTransferFeeItemDescription(user, amounts.length);
// A single tax rate per session
- const tax_rates = [];
+ const tax_rates = (billingAccount.taxID) ? [billingAccount.taxID] : [] ;
// Prepare item parameters
const parameters: Stripe.InvoiceItemCreateParams = {
invoice: stripeInvoice.id,

MODIFY
src/server/rest/v1/service/BillingService.ts
src/server/rest/v1/service/BillingService.ts
@@ -563,6 +563,7 @@ export default class BillingService {
businessOwnerID: user.id,
status: BillingAccountStatus.IDLE,
accountExternalID: null,
+ taxID: filteredRequest.taxID,
createdBy: { id: req.user.id },
createdOn: new Date()
};
@@ -781,7 +782,7 @@ export default class BillingService {
const user = await UserStorage.getUser(req.tenant, billingAccount.businessOwnerID);
UtilsService.assertObjectExists(action, user, `User ID '${billingAccount.businessOwnerID}' does not exist`, MODULE_NAME, 'handleSendTransferInvoice', req.user);
// Generate the invoice with a fee per session
- const invoice = await billingImpl.billPlatformFee(transfer, user);
+ const invoice = await billingImpl.billPlatformFee(transfer, user, billingAccount);
// Funds to transfer
const transferAmount = Utils.createDecimal(transfer.totalAmount).minus(invoice.totalAmount).toNumber();
// Update transfer properties

MODIFY
src/storage/mongodb/BillingStorage.ts
src/storage/mongodb/BillingStorage.ts
@@ -213,6 +213,7 @@ export default class BillingStorage {
status: billingAccount.status,
businessOwnerID: DatabaseUtils.convertToObjectID(billingAccount.businessOwnerID),
accountExternalID: billingAccount.accountExternalID,
+ taxID: billingAccount.taxID,
activationLink: billingAccount.activationLink, // Should not be persisted - added here only for troubleshooting purposes
};
// Check Created/Last Changed By

MODIFY
src/types/Billing.ts
src/types/Billing.ts
@@ -169,6 +169,7 @@ export interface BillingAccount extends CreatedUpdatedProps, BillingTransferAuth
status: BillingAccountStatus;
activationLink?: string;
accountExternalID: string;
+ taxID: string;
}
export interface BillingAccountData {

MODIFY
test/api/BillingPlatformTest.ts
test/api/BillingPlatformTest.ts
@@ -306,9 +306,10 @@ describeif(isBillingProperlyConfigured)('Billing Platform (utbillingplatform)',
// Check Transfer Data for the current account ID
const nbDraftTransfers = await billingTestHelper.checkForDraftTransfers();
assert(nbDraftTransfers === 1, 'The expected number of DRAFT transfers is not correct');
- await billingTestHelper.finalizeDraftTransfer();
+ const transferID = await billingTestHelper.finalizeDraftTransfer();
+ await billingTestHelper.checkTransferData(transferID, BillingTransferStatus.FINALIZED, 2, 20.16, 1.68, 18.48);
// Make sure we have the more than the required amount
- await billingTestHelper.addFundsToBalance(20.16);
+ await billingTestHelper.addFundsToBalance(18.48);
// Send the the money to to sub account
await billingTestHelper.sendFinalizedTransfer();
});
@@ -335,9 +336,10 @@ describeif(isBillingProperlyConfigured)('Billing Platform (utbillingplatform)',
// Check Transfer Data for the current account ID
const nbDraftTransfers = await billingTestHelper.checkForDraftTransfers();
assert(nbDraftTransfers === 1, 'The expected number of DRAFT transfers is not correct');
- await billingTestHelper.finalizeDraftTransfer();
+ const transferID = await billingTestHelper.finalizeDraftTransfer();
+ await billingTestHelper.checkTransferData(transferID, BillingTransferStatus.FINALIZED, 1, 10.08, 0.84, 9.24);
// Make sure we have the more than the required amount
- await billingTestHelper.addFundsToBalance(10.08);
+ await billingTestHelper.addFundsToBalance(9.24);
// Send the the money to to sub account
await billingTestHelper.sendFinalizedTransfer();
});

MODIFY
test/api/BillingTestHelper.ts
test/api/BillingTestHelper.ts
import AsyncTask, { AsyncTaskStatus } from '../../src/types/AsyncTask';
import { BillingAccount, BillingAccountStatus, BillingDataTransactionStop, BillingInvoice, BillingInvoiceStatus, BillingStatus, BillingTransfer, BillingTransferStatus, BillingUser } from '../../src/types/Billing';
import { BillingSettings, BillingSettingsType, SettingDB } from '../../src/types/Setting';
+import { BillingTestConfigHelper, StripeTaxHelper } from './StripeTestHelper';
import { ChargePointErrorCode, ChargePointStatus, OCPPStatusNotificationRequest } from '../../src/types/ocpp/OCPPServer';
import ChargingStation, { ConnectorType } from '../../src/types/ChargingStation';
import PricingDefinition, { DayOfWeek, PricingDimension, PricingDimensions, PricingEntity, PricingRestriction } from '../../src/types/Pricing';
@@ -11,7 +12,6 @@ import chai, { expect } from 'chai';
import AsyncTaskStorage from '../../src/storage/mongodb/AsyncTaskStorage';
import BillingFacade from '../../src/integration/billing/BillingFacade';
import { BillingPlatformFeeStrategyFactory } from '../factories/BillingFactory';
-import { BillingTestConfigHelper } from './StripeTestHelper';
import CentralServerService from './client/CentralServerService';
import ChargingStationContext from './context/ChargingStationContext';
import Company from '../../src/types/Company';
@@ -885,8 +885,11 @@ export default class BillingTestHelper {
}
public async createBillingAccount(): Promise<string> {
+ // Assign a tax rate (5% exclusive) to apply to the platform fee
+ const taxRate = StripeTaxHelper.fetchOrCreateTaxRate(this.billingImpl, 5 /* 5% */);
let response = await this.getCurrentUserService().billingApi.createBillingAccount({
- businessOwnerID: this.getCurrentUserContext().id
+ businessOwnerID: this.getCurrentUserContext().id,
+ taxID: (await taxRate).id
});
if (response.status !== StatusCodes.OK) {
await this.dumpLastErrors();
@@ -995,7 +998,7 @@ export default class BillingTestHelper {
return (draftTransfer) ? draftTransfer.sessions?.length : 0;
}
- public async finalizeDraftTransfer(): Promise<void> {
+ public async finalizeDraftTransfer(): Promise<string> {
const transfer = await this.getLatestTransfer(BillingTransferStatus.DRAFT);
assert(transfer?.id, 'transfer ID should not be null');
let response = await this.getCurrentUserService().billingApi.finalizeTransfer(transfer.id);
@@ -1006,6 +1009,24 @@ export default class BillingTestHelper {
response = await this.adminUserService.billingApi.readTransfer(transfer.id);
expect(response.status).to.be.eq(StatusCodes.OK);
expect(response.data.status).to.eq(BillingTransferStatus.FINALIZED);
+ expect(response.data.id).not.to.be.null;
+ return response.data.id;
+ }
+
+ public async checkTransferData(transferID: string,
+ expectedStatus: BillingTransferStatus,
+ expectedSessionCounter: number,
+ expectedCollectedFunds: number,
+ expectedCollectedFee: number,
+ expectedTransferAmount: number) : Promise<void> {
+ const response = await this.getCurrentUserService().billingApi.readTransfer(transferID);
+ expect(response.status).to.equal(StatusCodes.OK);
+ const transfer = response.data as BillingTransfer;
+ expect(transfer.status).to.equal(expectedStatus);
+ expect(transfer.sessions?.length).to.equal(expectedSessionCounter);
+ expect(transfer.totalAmount).to.equal(expectedCollectedFunds);
+ expect(transfer.transferAmount).to.equal(expectedTransferAmount);
+ expect(transfer.invoice?.totalAmount).to.equal(expectedCollectedFee); // Includes the taxes (c.f.: taxID - 5% exclusive at the account level)
}
public async sendFinalizedTransfer(): Promise<void> {

MODIFY
test/api/StripeTestHelper.ts
test/api/StripeTestHelper.ts
@@ -77,6 +77,39 @@ export class BillingTestConfigHelper {
}
}
+export class StripeTaxHelper {
+ public static async fetchOrCreateTaxRate(billingImplementation: StripeBillingIntegration, rate: number) : Promise<Stripe.TaxRate> {
+ // Get the stripe facade
+ const stripeInstance = await billingImplementation.getStripeInstance();
+ // Get the list of tax rates
+ const taxRates = await stripeInstance.taxRates.list({
+ limit: 10,
+ active: true,
+ inclusive: false
+ });
+ let taxRate = null;
+ // Iterate the list to find one having the expected rate
+ for (const existingTaxRate of taxRates.data) {
+ if (existingTaxRate.percentage === rate && !existingTaxRate.inclusive) {
+ taxRate = existingTaxRate;
+ break;
+ }
+ }
+ if (!taxRate) {
+ // Not found - let's create it!
+ taxRate = await stripeInstance.taxRates.create({
+ display_name: 'TVA',
+ description: `TVA France - ${rate}%`,
+ jurisdiction: 'FR',
+ percentage: rate,
+ inclusive: false
+ });
+ }
+ expect(taxRate).to.not.be.null;
+ return taxRate;
+ }
+}
+
export default class StripeTestHelper {
// Tenant: utbilling
private tenantContext: TenantContext;
@@ -171,7 +204,7 @@ export default class StripeTestHelper {
// TODO: check this is not the default pm as here we are dealing with source and not pm
const operationResult: BillingOperationResult = await concreteImplementation.deletePaymentMethod(this.dynamicUser, newSourceId);
expect(operationResult.internalData).to.not.be.null;
- const paymentMethod = operationResult.internalData as any;
+ const paymentMethod = operationResult.internalData as { id: string };
await this.retrievePaymentMethod(paymentMethod.id);
}
@@ -187,18 +220,7 @@ export default class StripeTestHelper {
}
public async assignTaxRate(rate: number) : Promise<Stripe.TaxRate> {
- // Let's create a tax rate
- const concreteImplementation : StripeBillingIntegration = this.billingImpl ;
- const stripeInstance = await concreteImplementation.getStripeInstance();
- const taxRate = await stripeInstance.taxRates.create({
- display_name: 'TVA',
- description: `TVA France - ${rate}%`,
- jurisdiction: 'FR',
- percentage: rate,
- inclusive: false
- });
- expect(taxRate).to.not.be.null;
- return taxRate;
+ return StripeTaxHelper.fetchOrCreateTaxRate(this.billingImpl, rate);
}
public async checkBusinessProcessBillToPay(paymentShouldFail: boolean, withTax?:boolean) : Promise<void> {

bp - tax rate per account