bp - onboarding flow rework
MODIFY
src/integration/billing/BillingIntegration.ts
src/integration/billing/BillingIntegration.ts
@@ -710,6 +710,8 @@ export default abstract class BillingIntegration {
abstract createConnectedAccount(): Promise<Partial<BillingAccount>>;
+ abstract refreshConnectedAccount(billingAccount: BillingAccount, url: string): Promise<Partial<BillingAccount>>;
+
abstract billPlatformFee(transfer: BillingTransfer, user: User): Promise<BillingPlatformInvoice>;
abstract sendTransfer(transfer: BillingTransfer, user: User): Promise<string>;

MODIFY
src/integration/billing/stripe/StripeBillingIntegration.ts
src/integration/billing/stripe/StripeBillingIntegration.ts
@@ -1704,10 +1704,10 @@ export default class StripeBillingIntegration extends BillingIntegration {
public async createConnectedAccount(): Promise<Partial<BillingAccount>> {
await this.checkConnection();
- let billingAccount: Stripe.Account;
+ let stripeAccount: Stripe.Account;
// Create the account
try {
- billingAccount = await this.stripe.accounts.create({
+ stripeAccount = await this.stripe.accounts.create({
type: 'standard'
});
} catch (e) {
@@ -1718,13 +1718,20 @@ export default class StripeBillingIntegration extends BillingIntegration {
method: 'createConnectedAccount',
});
}
+ return {
+ accountExternalID: stripeAccount.id
+ };
+ }
+
+ public async refreshConnectedAccount(billingAccount: BillingAccount, accountActivationURL: string): Promise<Partial<BillingAccount>> {
+ await this.checkConnection();
// Generate the link to activate the account
let activationLink: Stripe.AccountLink;
try {
activationLink = await this.stripe.accountLinks.create({
- account: billingAccount.id,
- return_url: Utils.buildEvseBillingAccountActivationURL(this.tenant, billingAccount.id),
- refresh_url: Utils.buildEvseURL(),
+ account: billingAccount.accountExternalID,
+ return_url: accountActivationURL + '&OperationResult=Success',
+ refresh_url: accountActivationURL + '&OperationResult=Refresh',
type: 'account_onboarding',
});
} catch (e) {
@@ -1736,7 +1743,7 @@ export default class StripeBillingIntegration extends BillingIntegration {
});
}
return {
- accountExternalID: billingAccount.id,
+ id: billingAccount.id,
activationLink: activationLink.url
};
}

MODIFY
src/server/rest/v1/router/util/UtilRouter.ts
src/server/rest/v1/router/util/UtilRouter.ts
@@ -28,6 +28,7 @@ export default class UtilRouter {
this.buildRouteChargingStationDownloadFirmware();
this.buildRouteGetSiteImage();
this.buildRouteGetTenantLogo();
+ this.buildRouteBillingRefreshAccount();
this.buildRouteBillingActivateAccount();
return this.router;
}
@@ -87,6 +88,13 @@ export default class UtilRouter {
});
}
+ private buildRouteBillingRefreshAccount(): void {
+ this.router.patch(`/${RESTServerRoute.REST_BILLING_ACCOUNT_REFRESH}`, (req: Request, res: Response, next: NextFunction) => {
+ req.params.ID = req.params.id;
+ void RouterUtils.handleRestServerAction(BillingService.handleRefreshAccount.bind(this), ServerAction.BILLING_ACCOUNT_ACTIVATE, req, res, next);
+ });
+ }
+
private buildRouteBillingActivateAccount(): void {
this.router.patch(`/${RESTServerRoute.REST_BILLING_ACCOUNT_ACTIVATE}`, (req: Request, res: Response, next: NextFunction) => {
req.params.ID = req.params.id;

MODIFY
src/server/rest/v1/service/BillingService.ts
src/server/rest/v1/service/BillingService.ts
@@ -476,6 +476,66 @@ export default class BillingService {
next();
}
+ public static async handleOnboardAccount(action: ServerAction, req: Request, res: Response, next: NextFunction): Promise<void> {
+ // Check if component is active
+ UtilsService.assertComponentIsActiveFromToken(req.user, TenantComponents.BILLING_PLATFORM,
+ Action.BILLING_ONBOARD_ACCOUNT, Entity.BILLING_ACCOUNT, MODULE_NAME, 'handleOnboardAccount');
+ const filteredRequest = BillingValidatorRest.getInstance().validateBillingAccountGetReq(req.params);
+ // Check authorization
+ await AuthorizationService.checkAndGetBillingAccountAuthorizations(req.tenant, req.user, filteredRequest, Action.BILLING_ONBOARD_ACCOUNT);
+ // Get the billing impl
+ const billingImpl = await BillingFactory.getBillingImpl(req.tenant);
+ if (!billingImpl) {
+ throw new AppError({
+ errorCode: HTTPError.GENERAL_ERROR,
+ message: 'Billing service is not configured',
+ module: MODULE_NAME, method: 'handleCreateAccount',
+ action: action,
+ user: req.user
+ });
+ }
+ // Fetch the current account data
+ let billingAccount = await BillingStorage.getAccountByID(req.tenant, filteredRequest.ID);
+ UtilsService.assertObjectExists(action, billingAccount, `Account ID '${filteredRequest.ID}' does not exist`, MODULE_NAME, 'handleOnboardAccount', req.user);
+ // Check if the account onboarding is already sent
+ if (billingAccount.status !== BillingAccountStatus.IDLE) {
+ throw new AppError({
+ errorCode: HTTPError.GENERAL_ERROR,
+ message: 'Account onboarding aborted - current status should be IDLE',
+ module: MODULE_NAME, method: 'handleOnboardAccount',
+ action: action,
+ user: req.user
+ });
+ }
+ // Get the account owner
+ const user = await UserStorage.getUser(req.tenant, billingAccount.businessOwnerID);
+ UtilsService.assertObjectExists(action, user, `User ID '${billingAccount.businessOwnerID}' does not exist`, MODULE_NAME, 'handleOnboardAccount', req.user);
+ // Trigger the creation of the connected account
+ const connectedAccount = await billingImpl.createConnectedAccount();
+ // Update account properties
+ billingAccount = {
+ ...billingAccount,
+ ...connectedAccount,
+ status: BillingAccountStatus.PENDING,
+ lastChangedBy: { 'id': req.user.id },
+ lastChangedOn: new Date()
+ };
+ // Save the account
+ await BillingStorage.saveAccount(req.tenant, billingAccount);
+ // URL to the DASHBOARD Onboarding Page
+ const onboardingURL = Utils.buildEvseBillingAccountOnboardingURL(req.tenant, billingAccount.id);
+ // Notify the user
+ const notificationData : BillingAccountCreationLinkNotification = {
+ onboardingLink: onboardingURL,
+ evseDashboardURL: Utils.buildEvseURL(req.tenant.subdomain),
+ user
+ };
+ // Send the notification
+ void NotificationHandler.sendBillingAccountCreationLink(req.tenant, Utils.generateUUID(), user, notificationData);
+ res.json(Object.assign({ id: billingAccount.id }, Constants.REST_RESPONSE_SUCCESS));
+ next();
+ }
+
public static async handleCreateAccount(action: ServerAction, req: Request, res: Response, next: NextFunction): Promise<void> {
// Check if component is active
UtilsService.assertComponentIsActiveFromToken(req.user, TenantComponents.BILLING_PLATFORM,
@@ -512,6 +572,61 @@ export default class BillingService {
next();
}
+ public static async handleRefreshAccount(action: ServerAction, req: Request, res: Response, next: NextFunction): Promise<void> {
+ // Check Tenant
+ if (!req.tenant) {
+ throw new AppError({
+ errorCode: StatusCodes.BAD_REQUEST,
+ message: 'Tenant must be provided',
+ module: MODULE_NAME, method: 'handleRefreshAccount', action: action,
+ });
+ }
+ const filteredRequest = BillingValidatorRest.getInstance().validateBillingActivateAccountReq({ ...req.params, ...req.body });
+ let billingAccount = await BillingStorage.getAccountByID(req.tenant, filteredRequest.ID);
+ UtilsService.assertObjectExists(action, billingAccount, `Account ID '${filteredRequest.ID}' does not exist`, MODULE_NAME, 'handleRefreshAccount', req.user);
+ // Check if the account onboarding has been sent
+ if (billingAccount.status !== BillingAccountStatus.PENDING) {
+ throw new AppError({
+ errorCode: HTTPError.GENERAL_ERROR,
+ message: 'Account onboarding aborted - current status should be PENDING',
+ module: MODULE_NAME, method: 'handleRefreshAccount',
+ action: action,
+ user: req.user
+ });
+ }
+ // Get the account owner
+ const user = await UserStorage.getUser(req.tenant, billingAccount.businessOwnerID);
+ UtilsService.assertObjectExists(action, user, `User ID '${billingAccount.businessOwnerID}' does not exist`, MODULE_NAME, 'handleRefreshAccount', req.user);
+ // Get the billing impl
+ const billingImpl = await BillingFactory.getBillingImpl(req.tenant);
+ if (!billingImpl) {
+ throw new AppError({
+ errorCode: HTTPError.GENERAL_ERROR,
+ message: 'Billing service is not configured',
+ module: MODULE_NAME, method: 'handleRefreshAccount',
+ action: action,
+ user: req.user
+ });
+ }
+ // URL to the DASHBOARD Onboarding Page
+ const onboardingURL = Utils.buildEvseBillingAccountOnboardingURL(req.tenant, billingAccount.id);
+ // Refresh the activation link
+ const refreshedAccount = await billingImpl.refreshConnectedAccount(billingAccount, onboardingURL);
+ // Update account properties
+ billingAccount = {
+ ...billingAccount,
+ activationLink: refreshedAccount.activationLink, // URL to the STRIPE Onboarding Wizard
+ };
+ // Save the account
+ await BillingStorage.saveAccount(req.tenant, billingAccount);
+ // Send back the activation link
+ res.json(Object.assign({
+ id: billingAccount.id,
+ activationLink: billingAccount.activationLink
+ }, Constants.REST_RESPONSE_SUCCESS));
+ next();
+ }
+
public static async handleActivateAccount(action: ServerAction, req: Request, res: Response, next: NextFunction): Promise<void> {
// Check Tenant
if (!req.tenant) {
@@ -587,64 +702,6 @@ export default class BillingService {
next();
}
- public static async handleOnboardAccount(action: ServerAction, req: Request, res: Response, next: NextFunction): Promise<void> {
- // Check if component is active
- UtilsService.assertComponentIsActiveFromToken(req.user, TenantComponents.BILLING_PLATFORM,
- Action.BILLING_ONBOARD_ACCOUNT, Entity.BILLING_ACCOUNT, MODULE_NAME, 'handleOnboardAccount');
- const filteredRequest = BillingValidatorRest.getInstance().validateBillingAccountGetReq(req.params);
- // Check authorization
- await AuthorizationService.checkAndGetBillingAccountAuthorizations(req.tenant, req.user, filteredRequest, Action.BILLING_ONBOARD_ACCOUNT);
- // Get the billing impl
- const billingImpl = await BillingFactory.getBillingImpl(req.tenant);
- if (!billingImpl) {
- throw new AppError({
- errorCode: HTTPError.GENERAL_ERROR,
- message: 'Billing service is not configured',
- module: MODULE_NAME, method: 'handleCreateAccount',
- action: action,
- user: req.user
- });
- }
- // Fetch the current account data
- let billingAccount = await BillingStorage.getAccountByID(req.tenant, filteredRequest.ID);
- UtilsService.assertObjectExists(action, billingAccount, `Account ID '${filteredRequest.ID}' does not exist`, MODULE_NAME, 'handleOnboardAccount', req.user);
- // Check if the account onboarding is already sent
- if (billingAccount.status !== BillingAccountStatus.IDLE) {
- throw new AppError({
- errorCode: HTTPError.GENERAL_ERROR,
- message: 'Account onboarding aborted - current status should be IDLE',
- module: MODULE_NAME, method: 'handleOnboardAccount',
- action: action,
- user: req.user
- });
- }
- // Get the account owner
- const user = await UserStorage.getUser(req.tenant, billingAccount.businessOwnerID);
- UtilsService.assertObjectExists(action, user, `User ID '${billingAccount.businessOwnerID}' does not exist`, MODULE_NAME, 'handleOnboardAccount', req.user);
- // Trigger the creation of the connected account
- const connectedAccount = await billingImpl.createConnectedAccount();
- // Update account properties
- billingAccount = {
- ...billingAccount,
- ...connectedAccount,
- status: BillingAccountStatus.PENDING,
- lastChangedBy: { 'id': req.user.id },
- lastChangedOn: new Date()
- };
- // Save the account
- await BillingStorage.saveAccount(req.tenant, billingAccount);
- // Notify the user
- const notificationData : BillingAccountCreationLinkNotification = {
- onboardingLink: billingAccount.activationLink,
- evseDashboardURL: Utils.buildEvseURL(req.tenant.subdomain),
- user
- };
- // Send the notification
- void NotificationHandler.sendBillingAccountCreationLink(req.tenant, Utils.generateUUID(), user, notificationData);
- res.json(Object.assign({ id: billingAccount.id }, Constants.REST_RESPONSE_SUCCESS));
- next();
- }
-
public static async handleBillingGetTransfers(action: ServerAction, req: Request, res: Response, next: NextFunction): Promise<void> {
// Filter
const filteredRequest = BillingValidatorRest.getInstance().validateBillingTransfersGetReq(req.query);

MODIFY
src/types/Server.ts
src/types/Server.ts
@@ -454,6 +454,7 @@ export enum ServerAction {
BILLING_TEST_DATA_CLEANUP = 'BillingTestDataCleanup',
BILLING_BILL_PENDING_TRANSACTION = 'BillingBillPendingTransaction',
BILLING_ACCOUNT_CREATE = 'BillingAccountCreate',
+ BILLING_ACCOUNT_REFRESH = 'BillingAccountRefresh',
BILLING_ACCOUNT_ACTIVATE = 'BillingAccountActivate',
BILLING_ACCOUNT_ONBOARD = 'BillingAccountOnboard',
BILLING_ACCOUNT = 'BillingAccount',
@@ -692,6 +693,7 @@ export enum RESTServerRoute {
REST_BILLING_ACCOUNTS = 'billing/accounts',
REST_BILLING_ACCOUNT = 'billing/accounts/:id',
REST_BILLING_ACCOUNT_ONBOARD = 'billing/accounts/:id/onboard',
+ REST_BILLING_ACCOUNT_REFRESH = 'billing/accounts/:id/refresh',
REST_BILLING_ACCOUNT_ACTIVATE = 'billing/accounts/:id/activate',
// BILLING URLs for CRUD operations on INVOICES

MODIFY
src/utils/Utils.ts
src/utils/Utils.ts
@@ -966,8 +966,8 @@ export default class Utils {
return `${Utils.buildEvseURL(tenantSubdomain)}/invoices?InvoiceID=${invoiceID}#all`;
}
- public static buildEvseBillingAccountActivationURL(tenant: Tenant, billingAccountID: string): string {
- return `${Utils.buildEvseURL(tenant.subdomain)}/billing/accounts/${billingAccountID}?TenantID=${tenant.id}`;
+ public static buildEvseBillingAccountOnboardingURL(tenant: Tenant, billingAccountID: string): string {
+ return `${Utils.buildEvseURL(tenant.subdomain)}/auth/account-onboarding?TenantID=${tenant.id}&AccountID=${billingAccountID}`;
}
public static buildEvseUserToVerifyURL(tenantSubdomain: string, userId: string): string {

MODIFY
test/api/BillingTestHelper.ts
test/api/BillingTestHelper.ts
@@ -932,15 +932,45 @@ export default class BillingTestHelper {
public async createActivatedAccount(): Promise<BillingAccount> {
const accountID = await this.createBillingAccount();
- // Send the activation link
- const billingAccountOnboardResponse = await this.getCurrentUserService().billingApi.onboardBillingAccount(accountID);
- expect(billingAccountOnboardResponse.status).to.be.eq(StatusCodes.OK);
- let response = await this.getCurrentUserService().billingApi.activateBillingAccount({ accountID, TenantID: this.getTenantID() });
+ // ----------------------------------------------------
+ // Send the onboarding mail to the business user
+ // ----------------------------------------------------
+ let response = await this.getCurrentUserService().billingApi.onboardBillingAccount(accountID);
+ expect(response.status).to.be.eq(StatusCodes.OK);
+ // --------------------------------------------------------------
+ // Account is now in a PENDING state
+ // --------------------------------------------------------------
+ response = await this.getCurrentUserService().billingApi.readBillingAccount(accountID);
+ let billingAccount = response.data as BillingAccount ;
+ expect(billingAccount.status).to.be.eq(BillingAccountStatus.PENDING);
+ // ----------------------------------------------------
+ // Generate the link to the onboarding page
+ // ----------------------------------------------------
+ response = await this.getCurrentUserService().billingApi.refreshBillingAccount({ accountID, TenantID: this.getTenantID() });
+ expect(response.status).to.be.eq(StatusCodes.OK);
+ // --------------------------------------------------------------
+ // Account is now still in a PENDING state
+ // --------------------------------------------------------------
+ response = await this.getCurrentUserService().billingApi.readBillingAccount(accountID);
+ billingAccount = response.data as BillingAccount ;
+ expect(billingAccount.status).to.be.eq(BillingAccountStatus.PENDING);
+ expect(billingAccount.activationLink).to.be.not.null;
+ // --------------------------------------------------------------
+ // Navigate to the Onboarding page
+ // --------------------------------------------------------------
+ // TODO - how to test it!
+ // --------------------------------------------------------------
+ // Let's assume the onboarding has been completed successfully
+ // --------------------------------------------------------------
+ response = await this.getCurrentUserService().billingApi.activateBillingAccount({ accountID, TenantID: this.getTenantID() });
expect(response.status).to.be.eq(StatusCodes.OK);
expect(response.data?.id).to.be.eq(accountID);
+ // --------------------------------------------------------------
+ // Account is now in a ACTIVE state
+ // --------------------------------------------------------------
response = await this.getCurrentUserService().billingApi.readBillingAccount(accountID);
expect(response.status).to.be.eq(StatusCodes.OK);
- const billingAccount = response.data as BillingAccount ;
+ billingAccount = response.data as BillingAccount ;
expect(billingAccount.status).to.be.eq(BillingAccountStatus.ACTIVE);
return billingAccount;
}

MODIFY
test/api/client/BillingApi.ts
test/api/client/BillingApi.ts
@@ -60,6 +60,10 @@ export default class BillingApi extends CrudApi {
return super.create(params, super.buildRestEndpointUrl(RESTServerRoute.REST_BILLING_ACCOUNTS));
}
+ public async refreshBillingAccount(params) {
+ return super.patch(params, super.buildUtilRestEndpointUrl(RESTServerRoute.REST_BILLING_ACCOUNT_REFRESH, { id: params.accountID }));
+ }
+
public async activateBillingAccount(params) {
return super.patch(params, super.buildUtilRestEndpointUrl(RESTServerRoute.REST_BILLING_ACCOUNT_ACTIVATE, { id: params.accountID }));
}

bp - onboarding flow rework