feat: added stripe sub-accounts creation
MODIFY
src/integration/billing/BillingIntegration.ts
src/integration/billing/BillingIntegration.ts
-import { BillingChargeInvoiceAction, BillingDataTransactionStart, BillingDataTransactionStop, BillingDataTransactionUpdate, BillingInvoice, BillingInvoiceItem, BillingInvoiceStatus, BillingOperationResult, BillingPaymentMethod, BillingStatus, BillingTax, BillingUser } from '../../types/Billing';
+import { BillingAccount, BillingChargeInvoiceAction, BillingDataTransactionStart, BillingDataTransactionStop, BillingDataTransactionUpdate, BillingInvoice, BillingInvoiceItem, BillingInvoiceStatus, BillingOperationResult, BillingPaymentMethod, BillingStatus, BillingTax, BillingUser } from '../../types/Billing';
import Tenant, { TenantComponents } from '../../types/Tenant';
import Transaction, { StartTransactionErrorCode } from '../../types/Transaction';
import User, { UserStatus } from '../../types/User';
@@ -685,4 +685,6 @@ export default abstract class BillingIntegration {
abstract deletePaymentMethod(user: User, paymentMethodId: string): Promise<BillingOperationResult>;
abstract precheckStartTransactionPrerequisites(user: User): Promise<StartTransactionErrorCode[]>;
+
+ abstract createSubAccount(): Promise<BillingAccount>;
}

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 { BillingDataTransactionStart, BillingDataTransactionStop, BillingDataTransactionUpdate, BillingInvoice, BillingInvoiceItem, BillingInvoiceStatus, BillingOperationResult, BillingPaymentMethod, BillingStatus, BillingTax, BillingUser, BillingUserData } from '../../../types/Billing';
+import { BillingAccount, BillingDataTransactionStart, BillingDataTransactionStop, BillingDataTransactionUpdate, BillingInvoice, BillingInvoiceItem, BillingInvoiceStatus, BillingOperationResult, BillingPaymentMethod, 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';
@@ -1684,4 +1684,44 @@ export default class StripeBillingIntegration extends BillingIntegration {
}
}));
}
+
+ public async createSubAccount(): Promise<BillingAccount> {
+ await this.checkConnection();
+ let subAccount: Stripe.Account;
+ // Create the sub-account
+ try {
+ subAccount = await this.stripe.accounts.create({
+ type: 'standard'
+ });
+ } catch (e) {
+ throw new BackendError({
+ message: 'Unexpected situation - unable to create sub-account',
+ detailedMessages: { e },
+ module: MODULE_NAME, action: ServerAction.BILLING_CREATE_SUB_ACCOUNT,
+ method: 'createSubAccount',
+ });
+ }
+ // Generate the link to activate the sub-account
+ let activationLink: Stripe.AccountLink;
+ try {
+ activationLink = await this.stripe.accountLinks.create({
+ account: subAccount.id,
+ return_url: Utils.buildEvseBillingSubAccountActivationURL(this.tenant.subdomain, subAccount.id),
+ refresh_url: Utils.buildEvseURL(),
+ type: 'account_onboarding',
+ });
+ } catch (e) {
+ throw new BackendError({
+ message: 'Unexpected situation - unable to create activation link',
+ detailedMessages: { e },
+ module: MODULE_NAME, action: ServerAction.BILLING_CREATE_SUB_ACCOUNT,
+ method: 'createSubAccount',
+ });
+ }
+
+ return {
+ id: subAccount.id,
+ activationLink: activationLink.url
+ };
+ }
}

MODIFY
src/types/Billing.ts
src/types/Billing.ts
@@ -153,3 +153,8 @@ export interface BillingAdditionalData {
lastError?: BillingError,
}
+export interface BillingAccount {
+ id: string;
+ activationLink?: string;
+}
+

MODIFY
src/types/Server.ts
src/types/Server.ts
@@ -452,6 +452,7 @@ export enum ServerAction {
BILLING_CHARGE_INVOICE = 'BillingChargeInvoice',
BILLING_TEST_DATA_CLEANUP = 'BillingTestDataCleanup',
BILLING_BILL_PENDING_TRANSACTION = 'BillingBillPendingTransaction',
+ BILLING_CREATE_SUB_ACCOUNT = 'BillingCreateSubAccount',
PRICING = 'Pricing',
PRICING_DEFINITION = 'PricingDefinition',

MODIFY
src/utils/Utils.ts
src/utils/Utils.ts
@@ -1038,11 +1038,15 @@ export default class Utils {
return `${Utils.buildEvseURL(tenantSubdomain)}/invoices?InvoiceID=${invoiceID}#all`;
}
- // TODO uodate the route once we handle the payment ui and delete other unused urls
+ // TODO update the route once we handle the payment ui and delete other unused urls
public static buildEvseBillingPayURL(tenantSubdomain: string, invoiceID: string): string {
return `${Utils.buildEvseURL(tenantSubdomain)}/invoices?InvoiceID=${invoiceID}#all`;
}
+ public static buildEvseBillingSubAccountActivationURL(tenantSubdomain: string, subAccountID: string): string {
+ return `${Utils.buildEvseURL(tenantSubdomain)}/billing/sub-account/${subAccountID}`;
+ }
+
public static buildEvseUserToVerifyURL(tenantSubdomain: string, userId: string): string {
return `${Utils.buildEvseURL(tenantSubdomain)}/users/${userId}`;
}

MODIFY
test/api/BillingTest.ts
test/api/BillingTest.ts
@@ -15,6 +15,7 @@ import { StatusCodes } from 'http-status-codes';
import StripeTestHelper from './StripeTestHelper';
import TestConstants from './client/utils/TestConstants';
import User from '../../src/types/User';
+import Utils from '../../src/utils/Utils';
import assert from 'assert';
import chaiSubset from 'chai-subset';
import config from '../config';
@@ -85,6 +86,14 @@ describeif(isBillingProperlyConfigured)('Billing', () => {
await stripeTestHelper.forceBillingSettings(immediateBilling);
});
+ describe('Sub-accounts', () => {
+ it('Should create a sub-account with its associated activation link', async () => {
+ const subAccount = await stripeTestHelper.createSubAccount();
+ expect(subAccount.id).to.exist;
+ expect(subAccount.activationLink).to.include('https://connect.stripe.com/setup/s/');
+ });
+ });
+
it('Should add a different source to BILLING-TEST user', async () => {
await stripeTestHelper.assignPaymentMethod('tok_fr');
});
@@ -950,7 +959,6 @@ describeif(isBillingProperlyConfigured)('Billing', () => {
await billingTestHelper.checkTransactionBillingData(transactionID, BillingInvoiceStatus.PAID, 19.49);
});
});
-
});
describe('Billing Test Data Cleanup (utbilling)', () => {

MODIFY
test/api/BillingTestHelper.ts
test/api/BillingTestHelper.ts
/* eslint-disable max-len */
import AsyncTask, { AsyncTaskStatus } from '../../src/types/AsyncTask';
-import { BillingDataTransactionStop, BillingInvoice, BillingInvoiceStatus, BillingStatus, BillingUser } from '../../src/types/Billing';
+import { BillingAccount, BillingDataTransactionStop, BillingInvoice, BillingInvoiceStatus, BillingStatus, BillingUser } from '../../src/types/Billing';
import { BillingSettings, BillingSettingsType, SettingDB } from '../../src/types/Setting';
import { ChargePointErrorCode, ChargePointStatus, OCPPStatusNotificationRequest } from '../../src/types/ocpp/OCPPServer';
import ChargingStation, { ConnectorType } from '../../src/types/ChargingStation';

MODIFY
test/api/StripeTestHelper.ts
test/api/StripeTestHelper.ts
-import { BillingChargeInvoiceAction, BillingInvoice, BillingInvoiceItem, BillingInvoiceStatus, BillingOperationResult, BillingUser, BillingUserData } from '../../src/types/Billing';
+import { BillingAccount, BillingChargeInvoiceAction, BillingInvoice, BillingInvoiceItem, BillingInvoiceStatus, BillingOperationResult, BillingUser, BillingUserData } from '../../src/types/Billing';
import { BillingSettings, BillingSettingsType, SettingDB } from '../../src/types/Setting';
import Tenant, { TenantComponents } from '../../src/types/Tenant';
import chai, { expect } from 'chai';
-import { BillingPeriodicOperationTaskConfig } from '../../src/types/TaskConfig';
+import { BillingPeriodicOperationTaskConfig } from '../../src/types/TaskConfig';
import BillingStorage from '../../src/storage/mongodb/BillingStorage';
import CentralServerService from './client/CentralServerService';
import ContextDefinition from './context/ContextDefinition';
@@ -451,4 +451,8 @@ export default class StripeTestHelper {
assert(response?.data?.error, 'error should not be null');
}
}
+
+ public async createSubAccount(): Promise<BillingAccount> {
+ return await this.billingImpl.createSubAccount();
+ }
}

feat: added stripe sub-accounts creation