feat(transfer): endpoint to trigger invoice creation
MODIFY
src/assets/server/rest/v1/docs/e-mobility-oas.json
src/assets/server/rest/v1/docs/e-mobility-oas.json
}
}
},
+ "/api/billing/transfers/{id}/send": {
+ "patch": {
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "description": "Send a Transfer invoice to the sub-account owner",
+ "parameters": [
+ {
+ "in": "path",
+ "name": "id",
+ "description": "Transfer ID",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "tags": [
+ "Billing"
+ ],
+ "responses": {
+ "200": {
+ "description": "Transfer sent"
+ },
+ "401": {
+ "$ref": "#/components/responses/UnauthorizedError"
+ },
+ "403": {
+ "$ref": "#/components/responses/ForbiddenError"
+ },
+ "500": {
+ "$ref": "#/components/responses/BackendError"
+ },
+ "550": {
+ "$ref": "#/components/responses/ResourceDoesNotExists"
+ }
+ }
+ }
+ },
"/api/invoices": {
"get": {
"security": [

MODIFY
src/assets/server/rest/v1/schemas/billing/billing-transfer-finalize.json
src/assets/server/rest/v1/schemas/billing/billing-transfer-finalize.json
{
- "$id": "billing-transfers-finalize",
+ "$id": "billing-transfer-finalize",
"title": "Finalize transfer Request",
"type": "object",
"properties": {

ADD
src/assets/server/rest/v1/schemas/billing/billing-transfer-send.json
+{
+ "$id": "billing-transfer-send",
+ "title": "Send transfer Request",
+ "type": "object",
+ "properties": {
+ "ID": {
+ "$ref": "common#/definitions/id"
+ }
+ },
+ "required": [
+ "ID"
+ ]
+}

MODIFY
src/authorization/AuthorizationsDefinition.ts
src/authorization/AuthorizationsDefinition.ts
@@ -485,7 +485,7 @@ export const AUTHORIZATION_DEFINITION: AuthorizationDefinition = {
},
{
resource: Entity.BILLING_TRANSFER,
- action: [Action.LIST, Action.BILLING_FINALIZE_TRANSFER],
+ action: [Action.LIST, Action.BILLING_FINALIZE_TRANSFER, Action.BILLING_SEND_TRANSFER],
attributes: [
'id', 'status', 'createdOn', 'sessions', 'totalAmount', 'transferAmount', 'accountID', 'transferExternalID',
'account.businessOwnerID', 'account.accountExternalID', 'businessOwner.name', 'businessOwner.firstName',

MODIFY
src/server/rest/v1/router/api/BillingRouter.ts
src/server/rest/v1/router/api/BillingRouter.ts
@@ -49,6 +49,7 @@ export default class BillingRouter {
this.buildRouteBillingSendSubAccountOnboarding();
this.buildRouteBillingGetTransfers();
this.buildRouteBillingFinalizeTransfer();
+ this.buildRouteBillingSendTransfer();
return this.router;
}
@@ -186,4 +187,11 @@ export default class BillingRouter {
void RouterUtils.handleRestServerAction(BillingService.handleFinalizeTransfer.bind(this), ServerAction.BILLING_TRANSFER_FINALIZE, req, res, next);
});
}
+
+ private buildRouteBillingSendTransfer(): void {
+ this.router.patch(`/${RESTServerRoute.REST_BILLING_TRANSFER_SEND}`, (req: Request, res: Response, next: NextFunction) => {
+ req.params.ID = req.params.id;
+ void RouterUtils.handleRestServerAction(BillingService.handleSendTransferInvoice.bind(this), ServerAction.BILLING_TRANSFER_SEND, req, res, next);
+ });
+ }
}

MODIFY
src/server/rest/v1/service/BillingService.ts
src/server/rest/v1/service/BillingService.ts
@@ -666,13 +666,50 @@ export default class BillingService {
}
transfer.status = BillingTransferStatus.FINALIZED;
await BillingStorage.saveTransfer(req.tenant, transfer);
- // Generate the invoice
+ res.json(Constants.REST_RESPONSE_SUCCESS);
+ next();
+ }
+
+ public static async handleSendTransferInvoice(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_TRANSFER, Entity.BILLING_TRANSFER, MODULE_NAME, 'handleSendTransferInvoice');
+ const filteredRequest = BillingValidatorRest.getInstance().validateBillingTransferFinalizeReq(req.params);
+ // Check authorization
+ await AuthorizationService.checkAndGetBillingTransfersAuthorizations(req.tenant, req.user, Action.BILLING_SEND_TRANSFER);
+ // Get the billing implementation
+ 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: 'handleSendTransferInvoice',
+ action: action,
+ user: req.user
+ });
+ }
+ // Get the transfer
+ const transfer = await BillingStorage.getTransferByID(req.tenant, filteredRequest.ID);
+ UtilsService.assertObjectExists(action, transfer, `Transfer ID '${filteredRequest.ID}' does not exist`, MODULE_NAME, 'handleSendTransferInvoice', req.user);
+ // Check if the transfer is in draft status
+ if (transfer.status !== BillingTransferStatus.FINALIZED) {
+ throw new AppError({
+ errorCode: HTTPError.GENERAL_ERROR,
+ message: 'Transfer finalization aborted - current status should be FINALIZED',
+ module: MODULE_NAME, method: 'handleSendTransferInvoice',
+ action: action,
+ user: req.user
+ });
+ }
+ // Get the targeted sub account
const subAccount = await BillingStorage.getSubAccountByID(req.tenant, transfer.accountID);
- UtilsService.assertObjectExists(action, subAccount, `Sub account ID '${transfer.accountID}' does not exist`, MODULE_NAME, 'handleFinalizeTransfer', req.user);
+ UtilsService.assertObjectExists(action, subAccount, `Sub account ID '${transfer.accountID}' does not exist`, MODULE_NAME, 'handleSendTransferInvoice', req.user);
+ // Get the sub account owner
const user = await UserStorage.getUser(req.tenant, subAccount.businessOwnerID);
- UtilsService.assertObjectExists(action, user, `User ID '${transfer.accountID}' does not exist`, MODULE_NAME, 'handleFinalizeTransfer', req.user);
+ UtilsService.assertObjectExists(action, user, `User ID '${transfer.accountID}' does not exist`, MODULE_NAME, 'handleSendTransferInvoice', req.user);
+ // Synchronize owner and generate invoice
user.billingData = (await billingImpl.forceSynchronizeUser(user)).billingData;
- const invoice = await billingImpl.generateTransferInvoice(transfer, user);
+ await billingImpl.generateTransferInvoice(transfer, user);
res.json(Constants.REST_RESPONSE_SUCCESS);
next();
}

MODIFY
src/server/rest/v1/validator/BillingValidatorRest.ts
src/server/rest/v1/validator/BillingValidatorRest.ts
-import { HttpBillingInvoiceRequest, HttpBillingInvoicesRequest, HttpBillingSubAccountActivateRequest, HttpBillingSubAccountCreateRequest, HttpBillingSubAccountGetRequest, HttpBillingSubAccountsGetRequest, HttpBillingTransferFinalizeRequest, HttpBillingTransfersGetRequest, HttpDeletePaymentMethod, HttpPaymentMethods, HttpSetupPaymentMethod } from '../../../../types/requests/HttpBillingRequest';
+import { HttpBillingInvoiceRequest, HttpBillingInvoicesRequest, HttpBillingSubAccountActivateRequest, HttpBillingSubAccountCreateRequest, HttpBillingSubAccountGetRequest, HttpBillingSubAccountsGetRequest, HttpBillingTransferFinalizeRequest, HttpBillingTransferSendRequest, HttpBillingTransfersGetRequest, HttpDeletePaymentMethod, HttpPaymentMethods, HttpSetupPaymentMethod } from '../../../../types/requests/HttpBillingRequest';
import { BillingSettings } from '../../../../types/Setting';
-import HttpByIDRequest from '../../../../types/requests/HttpByIDRequest';
import Schema from '../../../../types/validator/Schema';
import SchemaValidator from '../../../../validator/SchemaValidator';
import fs from 'fs';
@@ -21,6 +20,7 @@ export default class BillingValidatorRest extends SchemaValidator {
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 billingTransferFinalize: Schema = JSON.parse(fs.readFileSync(`${global.appRoot}/assets/server/rest/v1/schemas/billing/billing-transfer-finalize.json`, 'utf8'));
+ private billingTransferSend: Schema = JSON.parse(fs.readFileSync(`${global.appRoot}/assets/server/rest/v1/schemas/billing/billing-transfer-send.json`, 'utf8'));
private constructor() {
super('BillingValidatorRest');
@@ -80,4 +80,8 @@ export default class BillingValidatorRest extends SchemaValidator {
public validateBillingTransferFinalizeReq(data: Record<string, unknown>): HttpBillingTransferFinalizeRequest {
return this.validate(this.billingTransferFinalize, data);
}
+
+ public validateBillingTransferSendReq(data: Record<string, unknown>): HttpBillingTransferSendRequest {
+ return this.validate(this.billingTransferSend, data);
+ }
}

MODIFY
src/types/Authorization.ts
src/types/Authorization.ts
@@ -141,6 +141,7 @@ export enum Action {
BILLING_ACTIVATE_SUB_ACCOUNT = 'BillingSubAccountActivate',
BILLING_ONBOARD_SUB_ACCOUNT = 'BillingSubAccountOnboard',
BILLING_FINALIZE_TRANSFER = 'BillingFinalizeTransfer',
+ BILLING_SEND_TRANSFER = 'BillingSendTransfer',
CHECK_CONNECTION = 'CheckConnection',
CLEAR_BILLING_TEST_DATA = 'ClearBillingTestData',
RETRIEVE_CONSUMPTION = 'RetrieveConsumption',

MODIFY
src/types/Server.ts
src/types/Server.ts
@@ -460,6 +460,7 @@ export enum ServerAction {
BILLING_SUB_ACCOUNTS = 'BillingSubAccounts',
BILLING_TRANSFERS = 'BillingTransfers',
BILLING_TRANSFER_FINALIZE = 'BillingTransferFinalize',
+ BILLING_TRANSFER_SEND = 'BillingTransferSend',
PRICING = 'Pricing',
PRICING_DEFINITION = 'PricingDefinition',
@@ -701,6 +702,7 @@ export enum RESTServerRoute {
REST_BILLING_TRANSFERS = 'billing/transfers',
REST_BILLING_TRANSFER_FINALIZE = 'billing/transfers/:id/finalize',
+ REST_BILLING_TRANSFER_SEND = 'billing/transfers/:id/send',
// PRICING URLs for CRUD operations
REST_PRICING_DEFINITIONS = 'pricing-definitions',

MODIFY
src/types/requests/HttpBillingRequest.ts
src/types/requests/HttpBillingRequest.ts
@@ -67,3 +67,7 @@ export interface HttpBillingTransfersGetRequest extends HttpDatabaseRequest {
export interface HttpBillingTransferFinalizeRequest extends HttpByIDRequest {
ID: string;
}
+
+export interface HttpBillingTransferSendRequest extends HttpByIDRequest {
+ ID: string;
+}

MODIFY
test/api/BillingTest.ts
test/api/BillingTest.ts
@@ -1260,6 +1260,9 @@ describeif(isBillingProperlyConfigured)('Billing', () => {
transfer.id = await BillingStorage.saveTransfer(billingTestHelper.tenantContext.getTenant(), transfer);
const finalizeResponse = await billingTestHelper.userService.billingApi.finalizeTransfer(transfer.id);
expect(finalizeResponse.status).to.be.eq(StatusCodes.OK);
+
+ const sendResponse = await billingTestHelper.userService.billingApi.sendTransfer(transfer.id);
+ expect(sendResponse.status).to.be.eq(StatusCodes.OK);
});
});
});
@@ -1320,6 +1323,11 @@ describeif(isBillingProperlyConfigured)('Billing', () => {
const finalizeResponse = await billingTestHelper.userService.billingApi.finalizeTransfer('5ce249a1a39ae1c056c389bd'); // inexistent transfer
expect(finalizeResponse.status).to.be.eq(StatusCodes.FORBIDDEN);
});
+
+ it('should not be able to send a transfer invoice', async () => {
+ const sendResponse = await billingTestHelper.userService.billingApi.sendTransfer('5ce249a1a39ae1c056c389bd'); // inexistent transfer
+ expect(sendResponse.status).to.be.eq(StatusCodes.FORBIDDEN);
+ });
});
});

MODIFY
test/api/StripeTestHelper.ts
test/api/StripeTestHelper.ts
@@ -455,8 +455,4 @@ export default class StripeTestHelper {
public async createSubAccount(): Promise<BillingAccount> {
return await this.billingImpl.createSubAccount();
}
-
- public async generateTransferInvoice(billingTransfer: BillingTransfer, user: User): Promise<BillingInvoice> {
- return await this.billingImpl.generateTransferInvoice(billingTransfer, user);
- }
}

MODIFY
test/api/client/BillingApi.ts
test/api/client/BillingApi.ts
@@ -79,4 +79,8 @@ export default class BillingApi extends CrudApi {
public async finalizeTransfer(id) {
return super.patch({}, super.buildRestEndpointUrl(RESTServerRoute.REST_BILLING_TRANSFER_FINALIZE, { id }));
}
+
+ public async sendTransfer(id) {
+ return super.patch({}, super.buildRestEndpointUrl(RESTServerRoute.REST_BILLING_TRANSFER_SEND, { id }));
+ }
}

feat(transfer): endpoint to trigger invoice creation