feat(billing): added endpoint to activate pending
ADD
src/assets/server/rest/v1/schemas/billing/billing-activate-sub-account.json
+{
+ "$id": "billing-activate-sub-account",
+ "title": "Activate a Billing sub-account Request",
+ "type": "object",
+ "properties": {
+ "subAccountID": {
+ "type": "string",
+ "sanitize": "mongo"
+ }
+ },
+ "required": [
+ "subAccountID"
+ ]
+}

MODIFY
src/server/rest/v1/router/util/UtilRouter.ts
src/server/rest/v1/router/util/UtilRouter.ts
@@ -2,6 +2,7 @@ import { RESTServerRoute, ServerAction } from '../../../../../types/Server';
import express, { NextFunction, Request, Response } from 'express';
import AssetService from '../../service/AssetService';
+import BillingService from '../../service/BillingService';
import CarService from '../../service/CarService';
import ChargingStationService from '../../service/ChargingStationService';
import CompanyService from '../../service/CompanyService';
@@ -27,6 +28,7 @@ export default class UtilRouter {
this.buildRouteChargingStationDownloadFirmware();
this.buildRouteGetSiteImage();
this.buildRouteGetTenantLogo();
+ this.buildRouteBillingActivateSubAccount();
return this.router;
}
@@ -84,4 +86,11 @@ export default class UtilRouter {
void RouterUtils.handleRestServerAction(SiteService.handleGetSiteImage.bind(this), ServerAction.SITE_IMAGE, req, res, next);
});
}
+
+ private buildRouteBillingActivateSubAccount(): void {
+ this.router.patch(`/${RESTServerRoute.REST_BILLING_ACTIVATE_SUB_ACCOUNT}`, (req: Request, res: Response, next: NextFunction) => {
+ req.params.subAccountID = req.params.id;
+ void RouterUtils.handleRestServerAction(BillingService.handleActivateSubAccount.bind(this), ServerAction.BILLING_ACTIVATE_SUB_ACCOUNT, req, res, next);
+ });
+ }
}

MODIFY
src/server/rest/v1/service/BillingService.ts
src/server/rest/v1/service/BillingService.ts
@@ -514,11 +514,51 @@ export default class BillingService {
// Save the sub account
subAccount.id = await BillingStorage.saveSubAccount(req.tenant, subAccount);
// Notify the user by mail
- void NotificationHandler.sendBillingSubAccountCreationLink(req.tenant, Utils.generateUUID(), user, { onboardingLink: subAccount.activationLink, evseDashboardURL: Utils.buildEvseURL(req.tenant.subdomain), 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();
}
+ public static async handleActivateSubAccount(action: ServerAction, req: Request, res: Response, next: NextFunction): Promise<void> {
+ // Check if component is active
+ UtilsService.assertComponentIsActiveFromToken(req.user, TenantComponents.BILLING,
+ Action.CREATE, Entity.BILLING, MODULE_NAME, 'handleActivateSubAccount');
+ const filteredRequest = BillingValidator.getInstance().validateBillingActivateSubAccountReq(req.params);
+ // 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: 'handleCreateSubAccount',
+ action: action,
+ user: req.user
+ });
+ }
+ const subAccount = await BillingStorage.getSubAccountByAccountID(req.tenant, filteredRequest.subAccountID);
+ UtilsService.assertObjectExists(action, subAccount, `Sub account ID '${filteredRequest.subAccountID}' does not exist`, MODULE_NAME, 'handleActivateSubAccount', req.user);
+ // Check if the sub account is already activated
+ if (!subAccount.pending) {
+ throw new AppError({
+ errorCode: HTTPError.GENERAL_ERROR,
+ message: 'Sub account is already activated',
+ module: MODULE_NAME, method: 'handleActivateSubAccount',
+ action: action,
+ user: req.user
+ });
+ }
+ // Activate and save the sub account
+ subAccount.pending = false;
+ await BillingStorage.saveSubAccount(req.tenant, subAccount);
+
+ // Notify the user by mail
+ // 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/server/rest/v1/validator/BillingValidator.ts
src/server/rest/v1/validator/BillingValidator.ts
-import { HttpBillingCreateSubAccount, HttpBillingInvoiceRequest, HttpBillingInvoicesRequest, HttpDeletePaymentMethod, HttpPaymentMethods, HttpSetupPaymentMethod } from '../../../../types/requests/HttpBillingRequest';
+import { HttpBillingActivateSubAccount, HttpBillingCreateSubAccount, HttpBillingInvoiceRequest, HttpBillingInvoicesRequest, HttpDeletePaymentMethod, HttpPaymentMethods, HttpSetupPaymentMethod } from '../../../../types/requests/HttpBillingRequest';
import { BillingSettings } from '../../../../types/Setting';
import Schema from '../../../../types/validator/Schema';
@@ -15,6 +15,7 @@ export default class BillingValidator extends SchemaValidator {
private billingInvoicesGet: Schema = JSON.parse(fs.readFileSync(`${global.appRoot}/assets/server/rest/v1/schemas/billing/invoices-get.json`, 'utf8'));
private billingInvoiceGet: Schema = JSON.parse(fs.readFileSync(`${global.appRoot}/assets/server/rest/v1/schemas/billing/invoice-get.json`, 'utf8'));
private billingCreateSubAccount: Schema = JSON.parse(fs.readFileSync(`${global.appRoot}/assets/server/rest/v1/schemas/billing/billing-create-sub-account.json`, 'utf8'));
+ private billingActivateSubAccount: Schema = JSON.parse(fs.readFileSync(`${global.appRoot}/assets/server/rest/v1/schemas/billing/billing-activate-sub-account.json`, 'utf8'));
private constructor() {
super('BillingValidator');
@@ -54,4 +55,8 @@ export default class BillingValidator extends SchemaValidator {
public validateBillingCreateSubAccountReq(data: Record<string, unknown>): HttpBillingCreateSubAccount {
return this.validate(this.billingCreateSubAccount, data);
}
+
+ public validateBillingActivateSubAccountReq(data: Record<string, unknown>): HttpBillingActivateSubAccount {
+ return this.validate(this.billingActivateSubAccount, data);
+ }
}

MODIFY
src/storage/mongodb/BillingStorage.ts
src/storage/mongodb/BillingStorage.ts
@@ -238,4 +238,113 @@ export default class BillingStorage {
await Logging.traceDatabaseRequestEnd(tenant, MODULE_NAME, 'saveInvoice', startTime, subAccountMDB);
return subAccountMDB._id.toString();
}
+
+ public static async getSubAccounts(tenant: Tenant,
+ params: {
+ subAccountIDs?: string[], subAccountAccountIDs?: string[], search?: string, userIDs?: string[]
+ } = {},
+ dbParams: DbParams, projectFields?: string[]): Promise<DataResult<BillingAccount>> {
+ const startTime = Logging.traceDatabaseRequestStart();
+ DatabaseUtils.checkTenantObject(tenant);
+ // Clone before updating the values
+ dbParams = Utils.cloneObject(dbParams);
+ // Check Limit
+ dbParams.limit = Utils.checkRecordLimit(dbParams.limit);
+ // Check Skip
+ dbParams.skip = Utils.checkRecordSkip(dbParams.skip);
+ // Create Aggregation
+ const aggregation = [];
+ // Search filters
+ const filters: FilterParams = {};
+ // Search
+ // Filter by other properties
+ if (params.search) {
+ filters.$or = [
+ { 'accountID': { $regex: params.search, $options: 'i' } }
+ ];
+ }
+ if (!Utils.isEmptyArray(params.subAccountIDs)) {
+ filters._id = {
+ $in: params.subAccountIDs.map((subAccountID) => DatabaseUtils.convertToObjectID(subAccountID))
+ };
+ }
+ if (!Utils.isEmptyArray(params.subAccountAccountIDs)) {
+ filters.accountID = {
+ $in: params.subAccountAccountIDs
+ };
+ }
+ if (!Utils.isEmptyArray(params.userIDs)) {
+ filters.userID = {
+ $in: params.userIDs.map((userID) => DatabaseUtils.convertToObjectID(userID))
+ };
+ }
+ // Set filters
+ if (!Utils.isEmptyJSon(filters)) {
+ aggregation.push({
+ $match: filters
+ });
+ }
+ // Limit records?
+ if (!dbParams.onlyRecordCount) {
+ aggregation.push({ $limit: Constants.DB_RECORD_COUNT_CEIL });
+ }
+ // Count Records
+ const subAccountCountMDB = await global.database.getCollection<any>(tenant.id, 'billingsubaccounts')
+ .aggregate([...aggregation, { $count: 'count' }], DatabaseUtils.buildAggregateOptions())
+ .toArray() as DatabaseCount[];
+ // Check if only the total count is requested
+ if (dbParams.onlyRecordCount) {
+ await Logging.traceDatabaseRequestEnd(tenant, MODULE_NAME, 'getSubAccounts', startTime, aggregation, subAccountCountMDB);
+ return {
+ count: (subAccountCountMDB.length > 0 ? subAccountCountMDB[0].count : 0),
+ result: []
+ };
+ }
+ // Remove the limit
+ aggregation.pop();
+ // Sort
+ if (!dbParams.sort) {
+ dbParams.sort = { _id: 1 };
+ }
+ aggregation.push({
+ $sort: dbParams.sort
+ });
+ // Skip
+ aggregation.push({
+ $skip: dbParams.skip
+ });
+ // Limit
+ aggregation.push({
+ $limit: dbParams.limit
+ });
+ // Add Users
+ DatabaseUtils.pushUserLookupInAggregation({
+ tenantID: tenant.id, aggregation: aggregation, asField: 'user', localField: 'userID',
+ foreignField: '_id', oneToOneCardinality: true, oneToOneCardinalityNotNull: false
+ });
+ // Add Last Changed / Created
+ DatabaseUtils.pushCreatedLastChangedInAggregation(tenant.id, aggregation);
+ // Handle the ID
+ DatabaseUtils.pushRenameDatabaseID(aggregation);
+ // Convert Object ID to string
+ DatabaseUtils.pushConvertObjectIDToString(aggregation, 'userID');
+ // Project
+ DatabaseUtils.projectFields(aggregation, projectFields);
+ // Read DB
+ const subAccountMDB = await global.database.getCollection<any>(tenant.id, 'billingsubaccounts')
+ .aggregate<any>(aggregation, DatabaseUtils.buildAggregateOptions())
+ .toArray() as BillingAccount[];
+ await Logging.traceDatabaseRequestEnd(tenant, MODULE_NAME, 'getSubAccounts', startTime, aggregation, subAccountMDB);
+ return {
+ count: DatabaseUtils.getCountFromDatabaseCount(subAccountCountMDB[0]),
+ result: subAccountMDB
+ };
+ }
+
+ public static async getSubAccountByAccountID(tenant: Tenant, accountID: string): Promise<BillingAccount> {
+ const subAccountMDB = await BillingStorage.getSubAccounts(tenant, {
+ subAccountAccountIDs: [accountID]
+ }, Constants.DB_PARAMS_SINGLE_RECORD);
+ return subAccountMDB.count === 1 ? subAccountMDB.result[0] : null;
+ }
}

MODIFY
src/types/Server.ts
src/types/Server.ts
@@ -453,6 +453,7 @@ export enum ServerAction {
BILLING_TEST_DATA_CLEANUP = 'BillingTestDataCleanup',
BILLING_BILL_PENDING_TRANSACTION = 'BillingBillPendingTransaction',
BILLING_CREATE_SUB_ACCOUNT = 'BillingCreateSubAccount',
+ BILLING_ACTIVATE_SUB_ACCOUNT = 'BillingCreateSubAccount',
PRICING = 'Pricing',
PRICING_DEFINITION = 'PricingDefinition',
@@ -680,6 +681,7 @@ export enum RESTServerRoute {
REST_BILLING_TAXES = 'billing/taxes',
REST_BILLING_SUB_ACCOUNTS = 'billing/sub-accounts',
+ REST_BILLING_ACTIVATE_SUB_ACCOUNT = 'billing/sub-accounts/:id/activate',
// BILLING URLs for CRUD operations on INVOICES
REST_BILLING_INVOICES = 'invoices',

MODIFY
src/types/requests/HttpBillingRequest.ts
src/types/requests/HttpBillingRequest.ts
@@ -34,3 +34,7 @@ export interface HttpBillingCreateSubAccount {
siteID?: string;
companyID: string;
}
+
+export interface HttpBillingActivateSubAccount {
+ subAccountID: string;
+}

feat(billing): added endpoint to activate pending