feat(subaccount): getOne endpoint
ADD
src/assets/server/rest/v1/schemas/billing/billing-sub-account-get.json
+{
+ "$id": "billing-sub-account-get",
+ "title": "Get billing sub account Request",
+ "type": "object",
+ "properties": {
+ "ID": {
+ "$ref": "common#/definitions/id"
+ }
+ }
+}

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: [Action.BILLING_CREATE_SUB_ACCOUNT, Action.LIST, Action.READ],
attributes: [
'id', 'accountID', 'userID', 'user.id', 'user.firstName', 'user.lastName', 'user.email'
]

MODIFY
src/server/rest/v1/router/api/BillingRouter.ts
src/server/rest/v1/router/api/BillingRouter.ts
@@ -45,6 +45,7 @@ export default class BillingRouter {
// -----------------------------------
this.buildRouteBillingCreateSubAccount();
this.buildRouteBillingGetSubAccounts();
+ this.buildRouteBillingGetSubAccount();
return this.router;
}
@@ -155,4 +156,11 @@ export default class BillingRouter {
void RouterUtils.handleRestServerAction(BillingService.handleBillingGetSubAccounts.bind(this), ServerAction.BILLING_SUB_ACCOUNTS, req, res, next);
});
}
+
+ private buildRouteBillingGetSubAccount(): void {
+ this.router.get(`/${RESTServerRoute.REST_BILLING_SUB_ACCOUNT}`, (req: Request, res: Response, next: NextFunction) => {
+ req.params.ID = req.params.id;
+ void RouterUtils.handleRestServerAction(BillingService.handleBillingGetSubAccount.bind(this), ServerAction.BILLING_SUB_ACCOUNT, req, res, next);
+ });
+ }
}

MODIFY
src/server/rest/v1/service/AuthorizationService.ts
src/server/rest/v1/service/AuthorizationService.ts
import { Action, AuthorizationActions, AuthorizationContext, AuthorizationFilter, Entity } from '../../../../types/Authorization';
import { AssetDataResult, BillingInvoiceDataResult, BillingPaymentMethodDataResult, BillingSubaccountsDataResult, CarCatalogDataResult, CarDataResult, CompanyDataResult, DataResult, LogDataResult, PricingDefinitionDataResult, RegistrationTokenDataResult, SiteAreaDataResult, SiteDataResult, TagDataResult, UserDataResult } from '../../../../types/DataResult';
-import { BillingInvoice, BillingPaymentMethod } from '../../../../types/Billing';
+import { BillingAccount, BillingInvoice, BillingPaymentMethod } from '../../../../types/Billing';
import { Car, CarCatalog } from '../../../../types/Car';
import { HttpAssetGetRequest, HttpAssetsGetRequest } from '../../../../types/requests/HttpAssetRequest';
-import { HttpBillingInvoiceRequest, HttpBillingInvoicesRequest, HttpDeletePaymentMethod, HttpPaymentMethods, HttpSetupPaymentMethod } from '../../../../types/requests/HttpBillingRequest';
+import { HttpBillingInvoiceRequest, HttpBillingInvoicesRequest, HttpBillingSubAccountGetRequest, HttpDeletePaymentMethod, HttpPaymentMethods, HttpSetupPaymentMethod } from '../../../../types/requests/HttpBillingRequest';
import { HttpCarCatalogGetRequest, HttpCarCatalogsGetRequest, HttpCarGetRequest, HttpCarsGetRequest } from '../../../../types/requests/HttpCarRequest';
import { HttpChargingStationGetRequest, HttpChargingStationsGetRequest } from '../../../../types/requests/HttpChargingStationRequest';
import { HttpCompaniesGetRequest, HttpCompanyGetRequest } from '../../../../types/requests/HttpCompanyRequest';
@@ -605,6 +605,12 @@ export default class AuthorizationService {
return AuthorizationService.checkAndGetEntityAuthorizations(tenant, Entity.BILLING_PLATFORM, userToken, {}, {}, authAction, entityData);
}
+ public static async checkAndGetBillingSubAccountAuthorizations(tenant: Tenant, userToken: UserToken,
+ filteredRequest: Partial<HttpBillingSubAccountGetRequest>, authAction: Action, entityData?: EntityData): Promise<AuthorizationFilter> {
+ return AuthorizationService.checkAndGetEntityAuthorizations(
+ tenant, Entity.BILLING_PLATFORM, userToken, filteredRequest, filteredRequest.ID ? { id: filteredRequest.ID } : {}, authAction, entityData);
+ }
+
public static async checkAndGetTaxesAuthorizations(tenant: Tenant, userToken: UserToken, failsWithException = true): Promise<AuthorizationFilter> {
const authorizations: AuthorizationFilter = {
filters: {},
@@ -665,6 +671,11 @@ export default class AuthorizationService {
tenant, userToken, Entity.USER, Action.LIST, authorizationFilter);
}
+ public static addSubAccountAuthorizations(tenant: Tenant, userToken: UserToken, billingSubAccount: BillingAccount): void {
+ // Add Meta Data
+ billingSubAccount.canRead = true;
+ }
+
public static async checkAndGetPaymentMethodsAuthorizations(tenant: Tenant, userToken: UserToken,
filteredRequest: Partial<HttpPaymentMethods>, failsWithException = true): Promise<AuthorizationFilter> {
const authorizations: AuthorizationFilter = {

MODIFY
src/server/rest/v1/service/BillingService.ts
src/server/rest/v1/service/BillingService.ts
@@ -570,18 +570,33 @@ export default class BillingService {
const subAccounts = await BillingStorage.getSubAccounts(req.tenant, {
subAccountIDs: filteredRequest.SubAccountID ? filteredRequest.SubAccountID.split('|') : null,
userIDs: filteredRequest.UserID ? filteredRequest.UserID.split('|') : null,
+ search: filteredRequest.Search ? filteredRequest.Search : null,
+ status: filteredRequest.Status ? filteredRequest.Status.split('|') : null,
}, {
sort: UtilsService.httpSortFieldsToMongoDB(filteredRequest.SortFields),
skip: filteredRequest.Skip,
limit: filteredRequest.Limit,
onlyRecordCount: filteredRequest.OnlyRecordCount
},
- authorizations.projectFields);
+ authorizations.projectFields
+ );
await AuthorizationService.addSubAccountsAuthorizations(req.tenant, req.user, subAccounts, authorizations);
res.json(subAccounts);
next();
}
+ public static async handleBillingGetSubAccount(action: ServerAction, req: Request, res: Response, next: NextFunction): Promise<void> {
+ // Filter
+ const filteredRequest = BillingValidatorRest.getInstance().validateBillingSubAccountGetReq(req.params);
+ // Check if component is active
+ UtilsService.assertComponentIsActiveFromToken(req.user, TenantComponents.BILLING_PLATFORM,
+ Action.READ, Entity.BILLING_PLATFORM, MODULE_NAME, 'handleBillingGetSubAccount');
+ // Get the billing subacounts
+ const subAccount = await UtilsService.checkAndGetBillingSubAccountAuthorization(req.tenant, req.user, filteredRequest.ID, Action.READ, action);
+ res.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/service/UtilsService.ts
src/server/rest/v1/service/UtilsService.ts
import { Action, AuthorizationFilter, Entity } from '../../../../types/Authorization';
+import { BillingAccount, BillingInvoice } from '../../../../types/Billing';
import { Car, CarCatalog } from '../../../../types/Car';
import ChargingStation, { ChargePoint } from '../../../../types/ChargingStation';
import { HTTPAuthError, HTTPError } from '../../../../types/HTTPError';
@@ -13,7 +14,6 @@ import AssetStorage from '../../../../storage/mongodb/AssetStorage';
import AuthorizationService from './AuthorizationService';
import Authorizations from '../../../../authorization/Authorizations';
import AxiosFactory from '../../../../utils/AxiosFactory';
-import { BillingInvoice } from '../../../../types/Billing';
import { BillingSettings } from '../../../../types/Setting';
import BillingStorage from '../../../../storage/mongodb/BillingStorage';
import CarStorage from '../../../../storage/mongodb/CarStorage';
@@ -806,6 +806,41 @@ export default class UtilsService {
authAction, action, entityData, additionalFilters, applyProjectFields);
}
+ public static async checkAndGetBillingSubAccountAuthorization(tenant: Tenant, userToken: UserToken, subAccountID: string, authAction: Action,
+ action: ServerAction, entityData?: EntityData, additionalFilters: Record<string, any> = {}, applyProjectFields = false): Promise<BillingAccount> {
+ // Check mandatory fields
+ UtilsService.assertIdIsProvided(action, subAccountID, MODULE_NAME, 'checkAndGetBillingSubAccountAuthorization', userToken);
+ // Get dynamic auth
+ const authorizations = await AuthorizationService.checkAndGetBillingSubAccountAuthorizations(
+ tenant, userToken, { ID: subAccountID }, authAction, entityData);
+ // Get Invoice
+ const subAccount = await BillingStorage.getSubAccountByID(tenant, subAccountID,
+ applyProjectFields ? authorizations.projectFields : null
+ );
+ UtilsService.assertObjectExists(action, subAccount, `Billing sub-account ID '${subAccountID}' does not exist`,
+ MODULE_NAME, 'checkAndGetBillingSubAccountAuthorization', userToken);
+ // Assign projected fields
+ if (authorizations.projectFields && applyProjectFields) {
+ subAccount.projectFields = authorizations.projectFields;
+ }
+ // Assign Metadata
+ if (authorizations.metadata) {
+ subAccount.metadata = authorizations.metadata;
+ }
+ AuthorizationService.addSubAccountAuthorizations(tenant, userToken, subAccount);
+ const authorized = AuthorizationService.canPerformAction(subAccount, authAction);
+ if (!authorized) {
+ throw new AppAuthError({
+ errorCode: HTTPAuthError.FORBIDDEN,
+ user: userToken,
+ action: authAction, entity: Entity.INVOICE,
+ module: MODULE_NAME, method: 'checkAndGetInvoiceAuthorization',
+ value: subAccountID
+ });
+ }
+ return subAccount;
+ }
+
public static sendEmptyDataResult(res: Response, next: NextFunction): void {
res.json(Constants.DB_EMPTY_DATA_RESULT);
next();

MODIFY
src/server/rest/v1/validator/BillingValidatorRest.ts
src/server/rest/v1/validator/BillingValidatorRest.ts
-import { HttpBillingInvoiceRequest, HttpBillingInvoicesRequest, HttpBillingSubAccountActivateRequest, HttpBillingSubAccountCreateRequest, HttpBillingSubAccountsGetRequest, HttpDeletePaymentMethod, HttpPaymentMethods, HttpSetupPaymentMethod } from '../../../../types/requests/HttpBillingRequest';
+import { HttpBillingInvoiceRequest, HttpBillingInvoicesRequest, HttpBillingSubAccountActivateRequest, HttpBillingSubAccountCreateRequest, HttpBillingSubAccountGetRequest, HttpBillingSubAccountsGetRequest, HttpDeletePaymentMethod, HttpPaymentMethods, HttpSetupPaymentMethod } from '../../../../types/requests/HttpBillingRequest';
import { BillingSettings } from '../../../../types/Setting';
import Schema from '../../../../types/validator/Schema';
@@ -17,6 +17,7 @@ export default class BillingValidatorRest extends SchemaValidator {
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 billingSubAccountsGet: Schema = JSON.parse(fs.readFileSync(`${global.appRoot}/assets/server/rest/v1/schemas/billing/billing-sub-accounts-get.json`, 'utf8'));
+ private billingSubAccountGet: Schema = JSON.parse(fs.readFileSync(`${global.appRoot}/assets/server/rest/v1/schemas/billing/billing-sub-account-get.json`, 'utf8'));
private constructor() {
super('BillingValidatorRest');
@@ -64,4 +65,8 @@ export default class BillingValidatorRest extends SchemaValidator {
public validateBillingSubAccountsGetReq(data: Record<string, unknown>): HttpBillingSubAccountsGetRequest {
return this.validate(this.billingSubAccountsGet, data);
}
+
+ public validateBillingSubAccountGetReq(data: Record<string, unknown>): HttpBillingSubAccountGetRequest {
+ return this.validate(this.billingSubAccountGet, data);
+ }
}

MODIFY
src/storage/mongodb/BillingStorage.ts
src/storage/mongodb/BillingStorage.ts
@@ -239,7 +239,7 @@ export default class BillingStorage {
public static async getSubAccounts(tenant: Tenant,
params: {
- subAccountIDs?: string[], subAccountAccountIDs?: string[], search?: string, userIDs?: string[]
+ subAccountIDs?: string[], subAccountAccountIDs?: string[], search?: string, userIDs?: string[], status?: string[]
} = {},
dbParams: DbParams, projectFields?: string[]): Promise<DataResult<BillingAccount>> {
const startTime = Logging.traceDatabaseRequestStart();
@@ -339,10 +339,10 @@ export default class BillingStorage {
};
}
- public static async getSubAccountByID(tenant: Tenant, id: string): Promise<BillingAccount> {
+ public static async getSubAccountByID(tenant: Tenant, id: string, projectFields?: string[]): Promise<BillingAccount> {
const subAccountMDB = await BillingStorage.getSubAccounts(tenant, {
subAccountIDs: [id]
- }, Constants.DB_PARAMS_SINGLE_RECORD);
+ }, Constants.DB_PARAMS_SINGLE_RECORD, projectFields);
return subAccountMDB.count === 1 ? subAccountMDB.result[0] : null;
}
}

MODIFY
src/types/Billing.ts
src/types/Billing.ts
@@ -159,7 +159,7 @@ export enum BillingAccountStatus {
ACTIVE = 'active'
}
-export interface BillingAccount {
+export interface BillingAccount extends AuthorizationActions {
id?: string;
accountID: string;
activationLink?: string;

MODIFY
src/types/requests/HttpBillingRequest.ts
src/types/requests/HttpBillingRequest.ts
@@ -41,8 +41,12 @@ export interface HttpBillingSubAccountActivateRequest {
export interface HttpBillingSubAccountsGetRequest extends HttpDatabaseRequest {
SubAccountID?: string;
UserID?: string;
- Status?: BillingInvoiceStatus;
+ Status?: string;
StartDateTime?: Date;
EndDateTime?: Date;
Search?: string;
}
+
+export interface HttpBillingSubAccountGetRequest {
+ ID: string;
+}

MODIFY
test/api/BillingTest.ts
test/api/BillingTest.ts
@@ -1126,6 +1126,20 @@ describeif(isBillingProperlyConfigured)('Billing', () => {
expect(subAccountsResponse.status).to.be.eq(StatusCodes.OK);
expect(subAccountsResponse.data.result.map((subAccount: BillingAccount) => subAccount.id)).to.include(subAccountResponse.data.id);
});
+
+ it('should read sub-account', async () => {
+ const subAccountCreateResponse = await billingTestHelper.userService.billingApi.createSubAccount({
+ userID: billingTestHelper.userContext.id
+ });
+ expect(subAccountCreateResponse.status).to.be.eq(StatusCodes.CREATED);
+
+ // List sub-accounts
+ const subAccountResponse = await billingTestHelper.userService.billingApi.readSubAccount(subAccountCreateResponse.data.id);
+ expect(subAccountResponse.status).to.be.eq(StatusCodes.OK);
+ expect(subAccountResponse.data.id).to.be.eq(subAccountCreateResponse.data.id);
+ expect(subAccountResponse.data.userID).to.be.eq(billingTestHelper.userContext.id);
+ expect(subAccountResponse.data.status).to.be.eq(BillingAccountStatus.IDLE);
+ });
});
});

MODIFY
test/api/client/BillingApi.ts
test/api/client/BillingApi.ts
@@ -63,4 +63,8 @@ export default class BillingApi extends CrudApi {
public async readAllSubAccounts(params, paging = TestConstants.DEFAULT_PAGING, ordering = TestConstants.DEFAULT_ORDERING) {
return super.readAll(params, paging, ordering, super.buildRestEndpointUrl(RESTServerRoute.REST_BILLING_SUB_ACCOUNTS));
}
+
+ public async readSubAccount(id: string) {
+ return super.read({}, super.buildRestEndpointUrl(RESTServerRoute.REST_BILLING_SUB_ACCOUNT, { id }));
+ }
}

feat(subaccount): getOne endpoint