feat(subaccounts): enndpoint to send onboarding
MODIFY
src/authorization/AuthorizationsDefinition.ts
src/authorization/AuthorizationsDefinition.ts
@@ -393,7 +393,7 @@ export const AUTHORIZATION_DEFINITION: AuthorizationDefinition = {
{ resource: Entity.BILLING, action: [Action.CHECK_CONNECTION, Action.CLEAR_BILLING_TEST_DATA] },
{
resource: Entity.BILLING_PLATFORM,
- action: [Action.BILLING_CREATE_SUB_ACCOUNT, Action.LIST, Action.READ],
+ action: [Action.BILLING_CREATE_SUB_ACCOUNT, Action.BILLING_SEND_SUB_ACCOUNT_ONBOARDING, Action.LIST, Action.READ],
attributes: [
'id', 'accountID', 'userID', 'user.id', 'user.firstName', 'user.lastName', 'user.email', 'status'
]

MODIFY
src/server/rest/v1/router/api/BillingRouter.ts
src/server/rest/v1/router/api/BillingRouter.ts
@@ -46,6 +46,7 @@ export default class BillingRouter {
this.buildRouteBillingCreateSubAccount();
this.buildRouteBillingGetSubAccounts();
this.buildRouteBillingGetSubAccount();
+ this.buildRouteBillingSendSubAccountOnboarding();
return this.router;
}
@@ -163,4 +164,11 @@ export default class BillingRouter {
void RouterUtils.handleRestServerAction(BillingService.handleBillingGetSubAccount.bind(this), ServerAction.BILLING_SUB_ACCOUNT, req, res, next);
});
}
+
+ private buildRouteBillingSendSubAccountOnboarding(): void {
+ this.router.patch(`/${RESTServerRoute.REST_BILLING_SUB_ACCOUNT_ONBOARD}`, (req: Request, res: Response, next: NextFunction) => {
+ req.params.ID = req.params.id;
+ void RouterUtils.handleRestServerAction(BillingService.handleSendSubAccountOnboarding.bind(this), ServerAction.BILLING_SUB_ACCOUNT_SEND_ONBOARDING, req, res, next);
+ });
+ }
}

MODIFY
src/server/rest/v1/service/BillingService.ts
src/server/rest/v1/service/BillingService.ts
@@ -498,9 +498,6 @@ export default class BillingService {
subAccount.userID = user.id;
// Save the sub account
subAccount.id = await BillingStorage.saveSubAccount(req.tenant, subAccount);
- // Notify the user
- void NotificationHandler.sendBillingSubAccountCreationLink(
- req.tenant, Utils.generateUUID(), user, { onboardingLink: subAccount.activationLink, evseDashboardURL: Utils.buildEvseURL(req.tenant.subdomain), user });
res.status(StatusCodes.CREATED).json(subAccount);
next();
}
@@ -522,6 +519,16 @@ export default class BillingService {
}
const subAccount = await BillingStorage.getSubAccountByID(tenant, filteredRequest.ID);
UtilsService.assertObjectExists(action, subAccount, `Sub account ID '${filteredRequest.ID}' does not exist`, MODULE_NAME, 'handleActivateSubAccount', req.user);
+ // Check if the sub account onboarding has been sent
+ if (subAccount.status === BillingAccountStatus.IDLE) {
+ throw new AppError({
+ errorCode: HTTPError.GENERAL_ERROR,
+ message: 'Sub account onboarding has not been sent',
+ module: MODULE_NAME, method: 'handleActivateSubAccount',
+ action: action,
+ user: req.user
+ });
+ }
// Check if the sub account is already activated
if (subAccount.status === BillingAccountStatus.ACTIVE) {
throw new AppError({
@@ -587,6 +594,48 @@ export default class BillingService {
next();
}
+ public static async handleSendSubAccountOnboarding(action: ServerAction, req: Request, res: Response, next: NextFunction): Promise<void> {
+ // Check if component is active
+ UtilsService.assertComponentIsActiveFromToken(req.user, TenantComponents.BILLING_PLATFORM,
+ Action.BILLING_SEND_SUB_ACCOUNT_ONBOARDING, Entity.BILLING_PLATFORM, MODULE_NAME, 'handleSendSubAccountOnboarding');
+ const filteredRequest = BillingValidatorRest.getInstance().validateBillingSubAccountGetReq(req.params);
+ // Check authorization
+ await AuthorizationService.checkAndGetBillingPlatformAuthorizations(req.tenant, req.user, Action.BILLING_SEND_SUB_ACCOUNT_ONBOARDING);
+ const subAccount = await BillingStorage.getSubAccountByID(req.tenant, filteredRequest.ID);
+ UtilsService.assertObjectExists(action, subAccount, `Sub account ID '${filteredRequest.ID}' does not exist`, MODULE_NAME, 'handleSendSubAccountOnboarding', req.user);
+ // Check if the sub account onboarding is already sent
+ if (subAccount.status === BillingAccountStatus.PENDING) {
+ throw new AppError({
+ errorCode: HTTPError.GENERAL_ERROR,
+ message: 'Sub account onboarding is already sent',
+ module: MODULE_NAME, method: 'handleSendSubAccountOnboarding',
+ action: action,
+ user: req.user
+ });
+ }
+ // Check if the sub account is already activated
+ if (subAccount.status === BillingAccountStatus.ACTIVE) {
+ throw new AppError({
+ errorCode: HTTPError.GENERAL_ERROR,
+ message: 'Sub account is already activated',
+ module: MODULE_NAME, method: 'handleSendSubAccountOnboarding',
+ action: action,
+ user: req.user
+ });
+ }
+ // Activate and save the sub account
+ subAccount.status = BillingAccountStatus.PENDING;
+ await BillingStorage.saveSubAccount(req.tenant, subAccount);
+ // Get the sub account owner
+ const user = await UserStorage.getUser(req.tenant, subAccount.userID);
+ UtilsService.assertObjectExists(action, user, `User ID '${subAccount.userID}' does not exist`, MODULE_NAME, 'handleSendSubAccountOnboarding', req.user);
+ // Notify the user
+ void NotificationHandler.sendBillingSubAccountCreationLink(
+ req.tenant, Utils.generateUUID(), user, { onboardingLink: subAccount.activationLink, evseDashboardURL: Utils.buildEvseURL(req.tenant.subdomain), user });
+ res.status(StatusCodes.OK).json(subAccount);
+ next();
+ }
+
private static async checkActivationPrerequisites(action: ServerAction, req: Request): Promise<void> {
const billingImpl = await BillingFactory.getBillingImpl(req.tenant);
if (!billingImpl) {

MODIFY
src/types/Authorization.ts
src/types/Authorization.ts
@@ -137,6 +137,7 @@ export enum Action {
BILLING_CHARGE_INVOICE = 'BillingChargeInvoice',
BILLING_CREATE_SUB_ACCOUNT = 'BillingSubAccountCreate',
BILLING_ACTIVATE_SUB_ACCOUNT = 'BillingSubAccountActivate',
+ BILLING_SEND_SUB_ACCOUNT_ONBOARDING = 'BillingSendSubAccountOnboarding',
CHECK_CONNECTION = 'CheckConnection',
CLEAR_BILLING_TEST_DATA = 'ClearBillingTestData',
RETRIEVE_CONSUMPTION = 'RetrieveConsumption',

MODIFY
src/types/Server.ts
src/types/Server.ts
@@ -454,6 +454,7 @@ export enum ServerAction {
BILLING_BILL_PENDING_TRANSACTION = 'BillingBillPendingTransaction',
BILLING_SUB_ACCOUNT_CREATE = 'BillingSubAccountCreate',
BILLING_SUB_ACCOUNT_ACTIVATE = 'BillingSubAccountActivate',
+ BILLING_SUB_ACCOUNT_SEND_ONBOARDING = 'BillingSubAccountSendOnboarding',
BILLING_SUB_ACCOUNT = 'BillingSubAccount',
BILLING_SUB_ACCOUNTS = 'BillingSubAccounts',
@@ -685,6 +686,7 @@ export enum RESTServerRoute {
REST_BILLING_SUB_ACCOUNTS = 'billing/sub-accounts',
REST_BILLING_SUB_ACCOUNT = 'billing/sub-accounts/:id',
+ REST_BILLING_SUB_ACCOUNT_ONBOARD = 'billing/sub-accounts/:id/onboard',
REST_BILLING_SUB_ACCOUNT_ACTIVATE = 'billing/sub-accounts/:id/activate',
// BILLING URLs for CRUD operations on INVOICES

MODIFY
test/api/BillingTest.ts
test/api/BillingTest.ts
@@ -990,11 +990,16 @@ describeif(isBillingProperlyConfigured)('Billing', () => {
});
it('should create a sub account and activate it', async () => {
+ // Create a sub account
const response = await billingTestHelper.userService.billingApi.createSubAccount({
userID: billingTestHelper.userContext.id
});
expect(response.status).to.be.eq(StatusCodes.CREATED);
-
+ // Send the activation link
+ const subAccountOnboardResponse = await billingTestHelper.userService.billingApi.sendSubAccountOnboarding(response.data.id);
+ expect(subAccountOnboardResponse.status).to.be.eq(StatusCodes.OK);
+ expect(subAccountOnboardResponse.data.status).to.be.eq(BillingAccountStatus.PENDING);
+ // Activate the sub account
const activationResponse = await billingTestHelper.userService.billingApi.activateSubAccount({ accountID: response.data.id, TenantID: billingTestHelper.tenantContext.getTenant().id });
expect(activationResponse.status).to.be.eq(StatusCodes.OK);
expect(activationResponse.data.status).to.be.eq(BillingAccountStatus.ACTIVE);
@@ -1006,11 +1011,15 @@ describeif(isBillingProperlyConfigured)('Billing', () => {
});
it('should not activate a sub account twice', async () => {
+ // Create a sub account
const response = await billingTestHelper.userService.billingApi.createSubAccount({
userID: billingTestHelper.userContext.id
});
expect(response.status).to.be.eq(StatusCodes.CREATED);
-
+ // Send the activation link
+ const subAccountOnboardResponse = await billingTestHelper.userService.billingApi.sendSubAccountOnboarding(response.data.id);
+ expect(subAccountOnboardResponse.status).to.be.eq(StatusCodes.OK);
+ // Activate the sub account
let activationResponse = await billingTestHelper.userService.billingApi.activateSubAccount({ accountID: response.data.id, TenantID: billingTestHelper.tenantContext.getTenant().id });
expect(activationResponse.status).to.be.eq(StatusCodes.OK);
activationResponse = await billingTestHelper.userService.billingApi.activateSubAccount({ accountID: response.data.id, TenantID: billingTestHelper.tenantContext.getTenant().id });
@@ -1130,6 +1139,50 @@ describeif(isBillingProperlyConfigured)('Billing', () => {
expect(subAccountResponse.data.userID).to.be.eq(billingTestHelper.userContext.id);
expect(subAccountResponse.data.status).to.be.eq(BillingAccountStatus.IDLE);
});
+
+ it('should send sub-account onboarding', async () => {
+ const subAccountCreateResponse = await billingTestHelper.userService.billingApi.createSubAccount({
+ userID: billingTestHelper.userContext.id
+ });
+ expect(subAccountCreateResponse.status).to.be.eq(StatusCodes.CREATED);
+
+ const subAccountOnboardResponse = await billingTestHelper.userService.billingApi.sendSubAccountOnboarding(subAccountCreateResponse.data.id);
+ expect(subAccountOnboardResponse.status).to.be.eq(StatusCodes.OK);
+ expect(subAccountOnboardResponse.data.status).to.be.eq(BillingAccountStatus.PENDING);
+ });
+
+ it('should not able to send sub-account onboarding twice', async () => {
+ const subAccountCreateResponse = await billingTestHelper.userService.billingApi.createSubAccount({
+ userID: billingTestHelper.userContext.id
+ });
+ expect(subAccountCreateResponse.status).to.be.eq(StatusCodes.CREATED);
+
+ let subAccountOnboardResponse = await billingTestHelper.userService.billingApi.sendSubAccountOnboarding(subAccountCreateResponse.data.id);
+ expect(subAccountOnboardResponse.status).to.be.eq(StatusCodes.OK);
+ expect(subAccountOnboardResponse.data.status).to.be.eq(BillingAccountStatus.PENDING);
+
+ subAccountOnboardResponse = await billingTestHelper.userService.billingApi.sendSubAccountOnboarding(subAccountCreateResponse.data.id);
+ expect(subAccountOnboardResponse.status).to.be.eq(StatusCodes.INTERNAL_SERVER_ERROR);
+ });
+
+ it('should not able to send sub-account onboarding for an activated sub-account', async () => {
+ // Create the sub account
+ const subAccountCreateResponse = await billingTestHelper.userService.billingApi.createSubAccount({
+ userID: billingTestHelper.userContext.id
+ });
+ expect(subAccountCreateResponse.status).to.be.eq(StatusCodes.CREATED);
+ // Send onboarding
+ let subAccountOnboardResponse = await billingTestHelper.userService.billingApi.sendSubAccountOnboarding(subAccountCreateResponse.data.id);
+ expect(subAccountOnboardResponse.status).to.be.eq(StatusCodes.OK);
+ expect(subAccountOnboardResponse.data.status).to.be.eq(BillingAccountStatus.PENDING);
+ // Activate it
+ const activationResponse = await billingTestHelper.userService.billingApi.activateSubAccount({ accountID: subAccountCreateResponse.data.id, TenantID: billingTestHelper.tenantContext.getTenant().id });
+ expect(activationResponse.status).to.be.eq(StatusCodes.OK);
+ expect(activationResponse.data.status).to.be.eq(BillingAccountStatus.ACTIVE);
+ // Try to re-send onboarding
+ subAccountOnboardResponse = await billingTestHelper.userService.billingApi.sendSubAccountOnboarding(subAccountCreateResponse.data.id);
+ expect(subAccountOnboardResponse.status).to.be.eq(StatusCodes.INTERNAL_SERVER_ERROR);
+ });
});
});
@@ -1167,6 +1220,12 @@ describeif(isBillingProperlyConfigured)('Billing', () => {
const subAccountResponse = await billingTestHelper.userService.billingApi.readSubAccount('62978713f146ea8cb3bf8a95');
expect(subAccountResponse.status).to.be.eq(StatusCodes.FORBIDDEN);
});
+
+ it('should not be able to send sub-account onboarding', async () => {
+ // List sub-accounts
+ const subAccountResponse = await billingTestHelper.userService.billingApi.sendSubAccountOnboarding('62978713f146ea8cb3bf8a95');
+ expect(subAccountResponse.status).to.be.eq(StatusCodes.FORBIDDEN);
+ });
});
});

MODIFY
test/api/client/BillingApi.ts
test/api/client/BillingApi.ts
@@ -67,4 +67,8 @@ export default class BillingApi extends CrudApi {
public async readSubAccount(id: string) {
return super.read({}, super.buildRestEndpointUrl(RESTServerRoute.REST_BILLING_SUB_ACCOUNT, { id }));
}
+
+ public async sendSubAccountOnboarding(id: string) {
+ return super.patch({}, super.buildRestEndpointUrl(RESTServerRoute.REST_BILLING_SUB_ACCOUNT_ONBOARD, { id }));
+ }
}

MODIFY
test/api/client/utils/CrudApi.ts
test/api/client/utils/CrudApi.ts
@@ -65,6 +65,16 @@ export default class CrudApi {
});
}
+ public async patch(id, path): Promise<any> {
+ return this._authenticatedApi.send({
+ method: 'PATCH',
+ url: path,
+ params: {
+ ID: id
+ }
+ });
+ }
+
protected buildUtilRestEndpointUrl(urlPatternAsString: RESTServerRoute, params: { [name: string]: string | number | null } = {}): string {
return this.buildRestEndpointUrl(urlPatternAsString, params, 'util');
}

feat(subaccounts): enndpoint to send onboarding