feat(billing): added getTransfers endpoint
MODIFY
src/assets/server/rest/v1/docs/e-mobility-oas.json
src/assets/server/rest/v1/docs/e-mobility-oas.json
}
}
},
+ "/api/billing/transfers": {
+ "get": {
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "description": "Get Transfers",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "AccountID",
+ "description": "Account ID",
+ "required": false,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "in": "query",
+ "name": "TransferExternalID",
+ "description": "Transfer External ID",
+ "required": false,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "in": "query",
+ "name": "Status",
+ "description": "Pipe separated invoice status (draft, pending, transferred)",
+ "required": false,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "$ref": "#/components/parameters/StartDateTime"
+ },
+ {
+ "$ref": "#/components/parameters/EndDateTime"
+ },
+ {
+ "$ref": "#/components/parameters/Search"
+ },
+ {
+ "$ref": "#/components/parameters/Skip"
+ },
+ {
+ "$ref": "#/components/parameters/Limit"
+ },
+ {
+ "$ref": "#/components/parameters/SortFields"
+ }
+ ],
+ "tags": [
+ "Billing"
+ ],
+ "responses": {
+ "200": {
+ "description": "Return transfers",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "count": {
+ "type": "number",
+ "example": 1
+ },
+ "result": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Transfer"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "401": {
+ "$ref": "#/components/responses/UnauthorizedError"
+ },
+ "403": {
+ "$ref": "#/components/responses/ForbiddenError"
+ },
+ "500": {
+ "$ref": "#/components/responses/BackendError"
+ }
+ }
+ }
+ },
"/api/invoices": {
"get": {
"security": [
"$ref": "#/components/schemas/User"
}
}
+ },
+ "Transfer": {
+ "type": "object",
+ "properties": {
+ "accountID": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string",
+ "readOnly": true
+ },
+ "status": {
+ "type": "string",
+ "enum": [
+ "draft",
+ "pending",
+ "transferred"
+ ]
+ },
+ "sessions": {
+ "type": "object",
+ "properties": {
+ "transactionID": {
+ "type": "number"
+ },
+ "amount": {
+ "type": "number"
+ },
+ "platformFee": {
+ "type": "number"
+ }
+ }
+ },
+ "amount": {
+ "type": "number"
+ },
+ "transferredAmount": {
+ "type": "number"
+ },
+ "platformFeeData": {
+ "type": "object",
+ "properties": {
+ "taxExternalID": {
+ "type": "string"
+ },
+ "feeAmount": {
+ "type": "number"
+ },
+ "feeTaxAmount": {
+ "type": "number"
+ },
+ "invoiceExternalID": {
+ "type": "string"
+ }
+ }
+ },
+ "transferExternalID": {
+ "type": "string"
+ }
+ }
}
},
"parameters": {

ADD
src/assets/server/rest/v1/schemas/billing/billing-transfers-get.json
+{
+ "$id": "billing-transfers-get",
+ "title": "Get billing transfers Request",
+ "type": "object",
+ "properties": {
+ "IDs": {
+ "$ref": "common#/definitions/ids"
+ },
+ "Status": {
+ "type": "string",
+ "enum": [
+ "pending",
+ "draft",
+ "transferred"
+ ]
+ },
+ "AccountID": {
+ "$ref": "common#/definitions/ids"
+ },
+ "TransferExternalID": {
+ "type": "string"
+ },
+ "Search": {
+ "$ref": "common#/definitions/search"
+ },
+ "SortFields": {
+ "$ref": "common#/definitions/sortFields"
+ },
+ "Limit": {
+ "$ref": "common#/definitions/limit"
+ },
+ "Skip": {
+ "$ref": "common#/definitions/skip"
+ },
+ "OnlyRecordCount": {
+ "$ref": "common#/definitions/onlyRecordCount"
+ },
+ "ProjectFields": {
+ "$ref": "common#/definitions/projectFields"
+ }
+ }
+}

MODIFY
src/authorization/AuthorizationsDefinition.ts
src/authorization/AuthorizationsDefinition.ts
@@ -398,6 +398,10 @@ export const AUTHORIZATION_DEFINITION: AuthorizationDefinition = {
'id', 'accountExternalID', 'businessOwnerID', 'user.id', 'user.firstName', 'user.lastName', 'user.email', 'status'
]
},
+ {
+ resource: Entity.BILLING_TRANSFER,
+ action: [Action.LIST]
+ },
{ resource: Entity.TAX, action: [Action.LIST] },
{
resource: Entity.INVOICE, action: [Action.LIST],

MODIFY
src/server/rest/v1/router/api/BillingRouter.ts
src/server/rest/v1/router/api/BillingRouter.ts
@@ -47,6 +47,7 @@ export default class BillingRouter {
this.buildRouteBillingGetSubAccounts();
this.buildRouteBillingGetSubAccount();
this.buildRouteBillingSendSubAccountOnboarding();
+ this.buildRouteBillingGetTransfers();
return this.router;
}
@@ -171,4 +172,10 @@ export default class BillingRouter {
void RouterUtils.handleRestServerAction(BillingService.handleOnboardAccount.bind(this), ServerAction.BILLING_SUB_ACCOUNT_ONBOARD, req, res, next);
});
}
+
+ private buildRouteBillingGetTransfers(): void {
+ this.router.get(`/${RESTServerRoute.REST_BILLING_TRANSFERS}`, (req: Request, res: Response, next: NextFunction) => {
+ void RouterUtils.handleRestServerAction(BillingService.handleBillingGetTransfers.bind(this), ServerAction.BILLING_TRANSFERS, 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 { AssetDataResult, BillingInvoiceDataResult, BillingPaymentMethodDataResult, BillingSubaccountsDataResult, BillingTransfersDataResult, CarCatalogDataResult, CarDataResult, CompanyDataResult, DataResult, LogDataResult, PricingDefinitionDataResult, RegistrationTokenDataResult, SiteAreaDataResult, SiteDataResult, TagDataResult, UserDataResult } from '../../../../types/DataResult';
import { BillingAccount, BillingInvoice, BillingPaymentMethod } from '../../../../types/Billing';
import { Car, CarCatalog } from '../../../../types/Car';
import { HttpAssetGetRequest, HttpAssetsGetRequest } from '../../../../types/requests/HttpAssetRequest';
-import { HttpBillingInvoiceRequest, HttpBillingInvoicesRequest, HttpBillingSubAccountGetRequest, HttpBillingSubAccountsGetRequest, HttpDeletePaymentMethod, HttpPaymentMethods, HttpSetupPaymentMethod } from '../../../../types/requests/HttpBillingRequest';
+import { HttpBillingInvoiceRequest, HttpBillingInvoicesRequest, HttpBillingSubAccountGetRequest, HttpBillingSubAccountsGetRequest, HttpBillingTransfersGetRequest, 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';
@@ -625,6 +625,20 @@ export default class AuthorizationService {
return authorizations;
}
+ public static async checkAndGetBillingTransfersAuthorizations(tenant: Tenant, userToken: UserToken,
+ filteredRequest?: Partial<HttpBillingTransfersGetRequest>, failsWithException = true): Promise<AuthorizationFilter> {
+ const authorizations: AuthorizationFilter = {
+ filters: {},
+ dataSources: new Map(),
+ projectFields: [],
+ authorized: false
+ };
+ // Check static & dynamic authorization
+ await this.canPerformAuthorizationAction(
+ tenant, userToken, Entity.BILLING_TRANSFER, Action.LIST, authorizations, filteredRequest, null, failsWithException);
+ return authorizations;
+ }
+
public static async checkAndGetTaxesAuthorizations(tenant: Tenant, userToken: UserToken, failsWithException = true): Promise<AuthorizationFilter> {
const authorizations: AuthorizationFilter = {
filters: {},
@@ -690,6 +704,14 @@ export default class AuthorizationService {
billingSubAccount.canRead = true;
}
+ public static async addTransfersAuthorizations(tenant: Tenant, userToken: UserToken, billingSubAccounts: BillingTransfersDataResult,
+ authorizationFilter: AuthorizationFilter): Promise<void> {
+ // Add Meta Data
+ billingSubAccounts.metadata = authorizationFilter.metadata;
+ billingSubAccounts.canListSubAccount = await AuthorizationService.canPerformAuthorizationAction(
+ tenant, userToken, Entity.BILLING_SUB_ACCOUNT, Action.LIST, authorizationFilter);
+ }
+
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
@@ -604,6 +604,35 @@ export default class BillingService {
next();
}
+ public static async handleBillingGetTransfers(action: ServerAction, req: Request, res: Response, next: NextFunction): Promise<void> {
+ // Filter
+ const filteredRequest = BillingValidatorRest.getInstance().validateBillingTransfersGetReq(req.query);
+ // Check auth
+ const authorizations = await AuthorizationService.checkAndGetBillingTransfersAuthorizations(req.tenant, req.user, filteredRequest /* , false */);
+ if (!authorizations.authorized) {
+ UtilsService.sendEmptyDataResult(res, next);
+ return;
+ }
+ // Get the billing sub accounts
+ const transfers = await BillingStorage.getTransfers(req.tenant, {
+ IDs: filteredRequest.ID ? filteredRequest.ID.split('|') : null,
+ accountIDs: filteredRequest.AccountID ? filteredRequest.AccountID.split('|') : null,
+ transferExternalIDs: filteredRequest.TransferExternalID ? filteredRequest.TransferExternalID.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
+ );
+ await AuthorizationService.addTransfersAuthorizations(req.tenant, req.user, transfers, authorizations);
+ res.json(transfers);
+ 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/BillingValidatorRest.ts
src/server/rest/v1/validator/BillingValidatorRest.ts
-import { HttpBillingInvoiceRequest, HttpBillingInvoicesRequest, HttpBillingSubAccountActivateRequest, HttpBillingSubAccountCreateRequest, HttpBillingSubAccountGetRequest, HttpBillingSubAccountsGetRequest, HttpDeletePaymentMethod, HttpPaymentMethods, HttpSetupPaymentMethod } from '../../../../types/requests/HttpBillingRequest';
+import { HttpBillingInvoiceRequest, HttpBillingInvoicesRequest, HttpBillingSubAccountActivateRequest, HttpBillingSubAccountCreateRequest, HttpBillingSubAccountGetRequest, HttpBillingSubAccountsGetRequest, HttpBillingTransfersGetRequest, HttpDeletePaymentMethod, HttpPaymentMethods, HttpSetupPaymentMethod } from '../../../../types/requests/HttpBillingRequest';
import { BillingSettings } from '../../../../types/Setting';
import Schema from '../../../../types/validator/Schema';
@@ -18,6 +18,7 @@ export default class BillingValidatorRest extends SchemaValidator {
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 billingTransfersGet: Schema = JSON.parse(fs.readFileSync(`${global.appRoot}/assets/server/rest/v1/schemas/billing/billing-transfers-get.json`, 'utf8'));
private constructor() {
super('BillingValidatorRest');
@@ -69,4 +70,8 @@ export default class BillingValidatorRest extends SchemaValidator {
public validateBillingSubAccountGetReq(data: Record<string, unknown>): HttpBillingSubAccountGetRequest {
return this.validate(this.billingSubAccountGet, data);
}
+
+ public validateBillingTransfersGetReq(data: Record<string, unknown>): HttpBillingTransfersGetRequest {
+ return this.validate(this.billingTransfersGet, data);
+ }
}

MODIFY
src/types/Authorization.ts
src/types/Authorization.ts
@@ -78,6 +78,7 @@ export enum Entity {
BILLING = 'Billing',
BILLING_PLATFORM = 'BillingPlatform',
BILLING_SUB_ACCOUNT = 'BillingSubAccount',
+ BILLING_TRANSFER = 'BillingTransfer',
SETTING = 'Setting',
ASYNC_TASK = 'AsyncTask',
OCPI_ENDPOINT = 'OcpiEndpoint',

MODIFY
src/types/DataResult.ts
src/types/DataResult.ts
-import { BillingAccount, BillingInvoice, BillingPaymentMethod } from './Billing';
+import { BillingAccount, BillingInvoice, BillingPaymentMethod, BillingTransfer } from './Billing';
import { Car, CarCatalog } from './Car';
import Transaction, { TransactionStats } from './Transaction';
@@ -94,6 +94,10 @@ export interface BillingSubaccountsDataResult extends DataResult<BillingAccount>
canListUsers?: boolean;
}
+export interface BillingTransfersDataResult extends DataResult<BillingTransfer> {
+ canListSubAccount?: boolean;
+}
+
export interface TransactionRefundDataResult {
count: number;
result: Transaction[];

MODIFY
src/types/Server.ts
src/types/Server.ts
@@ -457,6 +457,7 @@ export enum ServerAction {
BILLING_SUB_ACCOUNT_ONBOARD = 'BillingSubAccountOnboard',
BILLING_SUB_ACCOUNT = 'BillingSubAccount',
BILLING_SUB_ACCOUNTS = 'BillingSubAccounts',
+ BILLING_TRANSFERS = 'BillingTransfers',
PRICING = 'Pricing',
PRICING_DEFINITION = 'PricingDefinition',
@@ -696,6 +697,8 @@ export enum RESTServerRoute {
// BILLING URLs for Non-CRUD operations on INVOICES
REST_BILLING_DOWNLOAD_INVOICE = 'invoices/:invoiceID/download',
+ REST_BILLING_TRANSFERS = 'billing/transfers',
+
// PRICING URLs for CRUD operations
REST_PRICING_DEFINITIONS = 'pricing-definitions',
REST_PRICING_DEFINITION = 'pricing-definitions/:id',

MODIFY
src/types/requests/HttpBillingRequest.ts
src/types/requests/HttpBillingRequest.ts
@@ -55,3 +55,13 @@ export interface HttpBillingSubAccountsGetRequest extends HttpDatabaseRequest {
export interface HttpBillingSubAccountGetRequest extends HttpByIDRequest {
ID: string;
}
+
+export interface HttpBillingTransfersGetRequest extends HttpDatabaseRequest {
+ ID?: string;
+ AccountID?: string;
+ Status?: string;
+ TransferExternalID?: string;
+ StartDateTime?: Date;
+ EndDateTime?: Date;
+ Search?: string;
+}

MODIFY
test/api/BillingTest.ts
test/api/BillingTest.ts
@@ -1190,6 +1190,18 @@ describeif(isBillingProperlyConfigured)('Billing', () => {
expect(subAccountOnboardResponse.status).to.be.eq(StatusCodes.INTERNAL_SERVER_ERROR);
});
});
+
+ describe('Transfers', () => {
+ it('should list transfers', async () => {
+ const transfer = BillingTransferFactory.build();
+ const transferID = await BillingStorage.saveTransfer(billingTestHelper.tenantContext.getTenant(), transfer);
+ transfer.id = transferID;
+
+ const transfersResponse = await billingTestHelper.userService.billingApi.readTransfers({});
+ expect(transfersResponse.status).to.be.eq(StatusCodes.OK);
+ expect(transfersResponse.data.result).to.containSubset([transfer]);
+ });
+ });
});
describe('Where basic user', () => {
@@ -1232,6 +1244,11 @@ describeif(isBillingProperlyConfigured)('Billing', () => {
const subAccountResponse = await billingTestHelper.userService.billingApi.sendSubAccountOnboarding('62978713f146ea8cb3bf8a95');
expect(subAccountResponse.status).to.be.eq(StatusCodes.FORBIDDEN);
});
+
+ it('should not be able to list transfers', async () => {
+ const transfersResponse = await billingTestHelper.userService.billingApi.readTransfers({});
+ expect(transfersResponse.status).to.be.eq(StatusCodes.FORBIDDEN);
+ });
});
describe('Storage', () => {

MODIFY
test/api/client/BillingApi.ts
test/api/client/BillingApi.ts
@@ -71,4 +71,8 @@ export default class BillingApi extends CrudApi {
public async sendSubAccountOnboarding(id: string) {
return super.patch({}, super.buildRestEndpointUrl(RESTServerRoute.REST_BILLING_SUB_ACCOUNT_ONBOARD, { id }));
}
+
+ public async readTransfers(params, paging = TestConstants.DEFAULT_PAGING, ordering = TestConstants.DEFAULT_ORDERING) {
+ return super.readAll(params, paging, ordering, super.buildRestEndpointUrl(RESTServerRoute.REST_BILLING_TRANSFERS));
+ }
}

feat(billing): added getTransfers endpoint