Download invoice transfer
MODIFY
src/authorization/AuthorizationsDefinition.ts
src/authorization/AuthorizationsDefinition.ts
@@ -510,12 +510,12 @@ export const AUTHORIZATION_DEFINITION: AuthorizationDefinition = {
},
{
resource: Entity.BILLING_TRANSFER,
- action: [Action.LIST, Action.READ, Action.BILLING_FINALIZE_TRANSFER, Action.BILLING_SEND_TRANSFER],
+ action: [Action.LIST, Action.READ, Action.BILLING_FINALIZE_TRANSFER, Action.BILLING_SEND_TRANSFER, Action.DOWNLOAD],
attributes: [
'id', 'status', 'createdOn', 'sessionCounter', 'collectedFunds', 'collectedFlatFees', 'collectedFees', 'totalConsumptionWh', 'totalDurationSecs',
'transferAmount', 'accountID', 'transferExternalID',
'account.businessOwnerID', 'account.accountExternalID', 'businessOwner.name', 'businessOwner.firstName',
- 'platformFeeData.feeAmount', 'platformFeeData.feeTaxAmount', 'currency', 'invoice.documentNumber', 'invoice.totalAmount'
+ 'platformFeeData.feeAmount', 'platformFeeData.feeTaxAmount', 'currency', 'invoice.documentNumber','invoice.invoiceID','invoice.userID', 'invoice.totalAmount'
]
},
{ resource: Entity.TAX, action: [Action.LIST] },
@@ -850,6 +850,16 @@ export const AUTHORIZATION_DEFINITION: AuthorizationDefinition = {
}
}
},
+ {
+ resource: Entity.BILLING_TRANSFER, action: Action.DOWNLOAD,
+ condition: {
+ Fn: 'custom:dynamicAuthorizations',
+ args: {
+ asserts: [],
+ filters: ['OwnUser']
+ }
+ }
+ },
{
resource: Entity.PAYMENT_METHOD, action: Action.LIST,
condition: {

MODIFY
src/integration/billing/BillingIntegration.ts
src/integration/billing/BillingIntegration.ts
@@ -696,6 +696,8 @@ export default abstract class BillingIntegration {
public abstract downloadInvoiceDocument(invoice: BillingInvoice): Promise<Buffer>;
+ public abstract downloadTransferDocument(transfer: BillingTransfer): Promise<Buffer>;
+
public abstract chargeInvoice(invoice: BillingInvoice): Promise<BillingInvoice>;
public abstract consumeBillingEvent(req: Request): Promise<boolean>;

MODIFY
src/integration/billing/stripe/StripeBillingIntegration.ts
src/integration/billing/stripe/StripeBillingIntegration.ts
@@ -378,6 +378,19 @@ export default class StripeBillingIntegration extends BillingIntegration {
}
}
+ public async downloadTransferDocument(transfer: BillingTransfer): Promise<Buffer> {
+ await this.checkConnection();
+ // Get fresh data because persisted url expires after 30 days
+ const stripeTransferInvoice = await this.getStripeInvoice(transfer.invoice.invoiceID);
+ const downloadUrl = stripeTransferInvoice.invoice_pdf;
+ // Get document
+ const response = await this.axiosInstance.get(downloadUrl, {
+ responseType: 'arraybuffer'
+ });
+ // Convert
+ return Buffer.from(response.data);
+ }
+
// eslint-disable-next-line @typescript-eslint/require-await
public async consumeBillingEvent(req: Request): Promise<boolean> {
let event: { data, type: string };

MODIFY
src/server/rest/RestServerService.ts
src/server/rest/RestServerService.ts
@@ -7,8 +7,8 @@ import ChargingStationService from './v1/service/ChargingStationService';
import CompanyService from './v1/service/CompanyService';
import ConnectionService from './v1/service/ConnectionService';
import Constants from '../../utils/Constants';
-import Logging from '../../utils/Logging';
import LogService from './v1/service/LogService';
+import Logging from '../../utils/Logging';
import NotificationService from './v1/service/NotificationService';
import OCPIEndpointService from './v1/service/OCPIEndpointService';
import OICPEndpointService from './v1/service/OICPEndpointService';
@@ -148,6 +148,7 @@ class RequestMapper {
[ServerAction.BILLING_TAXES]: BillingService.handleGetBillingTaxes.bind(this),
[ServerAction.BILLING_INVOICES]: BillingService.handleGetInvoices.bind(this),
[ServerAction.BILLING_DOWNLOAD_INVOICE]: BillingService.handleDownloadInvoice.bind(this),
+ [ServerAction.BILLING_DOWNLOAD_TRANSFER]: BillingService.handleDownloadTransfer.bind(this),
[ServerAction.BILLING_PAYMENT_METHODS]: BillingService.handleBillingGetPaymentMethods.bind(this),
[ServerAction.OCPI_ENDPOINTS]: OCPIEndpointService.handleGetOcpiEndpoints.bind(this),
[ServerAction.OCPI_ENDPOINT]: OCPIEndpointService.handleGetOcpiEndpoint.bind(this),

MODIFY
src/server/rest/v1/router/api/BillingRouter.ts
src/server/rest/v1/router/api/BillingRouter.ts
@@ -43,6 +43,7 @@ export default class BillingRouter {
// -----------------------------------
// ROUTES for SUB-ACCOUNTS
// -----------------------------------
+ this.buildRouteBillingTransferDownload();
this.buildRouteBillingCreateAccount();
this.buildRouteBillingGetAccounts();
this.buildRouteBillingGetAccount();
@@ -144,6 +145,13 @@ export default class BillingRouter {
});
}
+ private buildRouteBillingTransferDownload(): void {
+ this.router.get(`/${RESTServerRoute.REST_BILLING_DOWNLOAD_TRANSFER}`, (req: Request, res: Response, next: NextFunction) => {
+ req.query.ID = req.params.transferID;
+ void RouterUtils.handleRestServerAction(BillingService.handleDownloadTransfer.bind(this), ServerAction.BILLING_DOWNLOAD_TRANSFER, req, res, next);
+ });
+ }
+
private buildRouteBillingGetTaxes(): void {
this.router.get(`/${RESTServerRoute.REST_BILLING_TAXES}`, (req: Request, res: Response, next: NextFunction) => {
void RouterUtils.handleRestServerAction(BillingService.handleGetBillingTaxes.bind(this), ServerAction.BILLING_TAXES, req, res, next);

MODIFY
src/server/rest/v1/service/AuthorizationService.ts
src/server/rest/v1/service/AuthorizationService.ts
@@ -931,8 +931,9 @@ export default class AuthorizationService {
// eslint-disable-next-line @typescript-eslint/require-await
public static async addTransferAuthorizations(tenant: Tenant, userToken: UserToken, billingTransfer: BillingTransfer, authorizationFilter: AuthorizationFilter): Promise<void> {
billingTransfer.canRead = true; // Always true as it should be filtered upfront
- // billingTransfer.canDownload = await AuthorizationService.canPerformAuthorizationAction(
- // tenant, userToken, Entity.TRANSFER, Action.DOWNLOAD, authorizationFilter, billingTransfer.businessOwnerID ? { UserID: billingTransfer.businessOwnerID } : {}, billingTransfer);
+ billingTransfer.canDownload = await AuthorizationService.canPerformAuthorizationAction(
+ tenant, userToken, Entity.BILLING_TRANSFER, Action.DOWNLOAD, authorizationFilter,
+ billingTransfer.invoice ? { UserID: billingTransfer.invoice.userID } : {}, billingTransfer);
// Optimize data over the net
Utils.removeCanPropertiesWithFalseValue(billingTransfer);
}
@@ -956,6 +957,9 @@ export default class AuthorizationService {
billingAccounts.metadata = authorizationFilter.metadata;
billingAccounts.canListAccounts = await AuthorizationService.canPerformAuthorizationAction(
tenant, userToken, Entity.BILLING_ACCOUNT, Action.LIST, authorizationFilter);
+ for (const billingAcount of billingAccounts.result) {
+ await AuthorizationService.addTransferAuthorizations(tenant, userToken, billingAcount, authorizationFilter);
+ }
}
public static async checkAndGetPaymentMethodsAuthorizations(tenant: Tenant, userToken: UserToken,

MODIFY
src/server/rest/v1/service/BillingService.ts
src/server/rest/v1/service/BillingService.ts
@@ -386,6 +386,45 @@ export default class BillingService {
res.end(buffer, 'binary');
}
+ public static async handleDownloadTransfer(action: ServerAction, req: Request, res: Response, next: NextFunction): Promise<void> {
+ // Check if component is active
+ UtilsService.assertComponentIsActiveFromToken(req.user, TenantComponents.BILLING_PLATFORM,
+ Action.DOWNLOAD, Entity.BILLING_PLATFORM, MODULE_NAME, 'handleDownloadTransfer');
+ // Filter
+ const filteredRequest = BillingValidatorRest.getInstance().validateBillingTransferGetReq(req.query);
+ // Get the Invoice
+ const billingTransfer = await BillingStorage.getTransfer(req.tenant, filteredRequest.ID);
+ UtilsService.assertObjectExists(action, billingTransfer, `Transfer ID '${filteredRequest.ID}' does not exist`,
+ MODULE_NAME, 'handleDownloadTransfer', req.user);
+ // Check and get authorizations
+ await UtilsService.checkAndGetTransferAuthorization(req.tenant, req.user, filteredRequest.ID, Action.DOWNLOAD, action, null, {}, true);
+ // 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: 'handleDownloadTransfer',
+ action: action,
+ user: req.user
+ });
+ }
+ const buffer = await billingImpl.downloadTransferDocument(billingTransfer);
+ if (!billingTransfer.invoice.invoiceID || !buffer) {
+ throw new AppError({
+ errorCode: HTTPError.GENERAL_ERROR,
+ message: 'Transfer document not found',
+ module: MODULE_NAME, method: 'handleDownloadTransfer',
+ action: action,
+ user: req.user
+ });
+ }
+ const fileName = 'invoice_' + billingTransfer.invoice.invoiceID + '.pdf';
+ res.attachment(fileName);
+ res.setHeader('Content-Type', 'application/pdf');
+ res.end(buffer, 'binary');
+ }
+
public static async handleGetBillingSetting(action: ServerAction, req: Request, res: Response, next: NextFunction): Promise<void> {
// Check if component is active
UtilsService.assertComponentIsActiveFromToken(req.user, TenantComponents.BILLING,

MODIFY
src/types/Authorization.ts
src/types/Authorization.ts
@@ -292,6 +292,7 @@ export interface BillingAccountAuthorizationActions extends AuthorizationActions
export interface BillingTransferAuthorizationActions extends AuthorizationActions {
canTransfer?: boolean;
+ canDownload?: boolean;
}
export enum DynamicAuthorizationFilterName {

MODIFY
src/types/Server.ts
src/types/Server.ts
@@ -445,6 +445,7 @@ export enum ServerAction {
BILLING_INVOICE = 'BillingInvoice',
BILLING_PERFORM_OPERATIONS = 'BillingPeriodicOperations',
BILLING_DOWNLOAD_INVOICE = 'BillingDownloadInvoice',
+ BILLING_DOWNLOAD_TRANSFER = 'BillingDownloadTransfer',
BILLING_NEW_INVOICE = 'BillingNewInvoice',
BILLING_SETUP_PAYMENT_METHOD = 'BillingSetupPaymentMethod',
BILLING_PAYMENT_METHODS = 'BillingPaymentMethods',
@@ -709,6 +710,7 @@ export enum RESTServerRoute {
REST_BILLING_TRANSFER = 'billing/transfers/:id',
REST_BILLING_TRANSFER_FINALIZE = 'billing/transfers/:id/finalize',
REST_BILLING_TRANSFER_SEND = 'billing/transfers/:id/send',
+ REST_BILLING_DOWNLOAD_TRANSFER = 'tranfers/:transferID/download',
// PRICING URLs for CRUD operations
REST_PRICING_DEFINITIONS = 'pricing-definitions',

Download invoice transfer