Store invoice pdf in DB
MODIFY
src/authorization/Authorizations.ts
src/authorization/Authorizations.ts
@@ -589,6 +589,10 @@ export default class Authorizations {
return Authorizations.canPerformAction(loggedUser, Entity.INVOICES, Action.SYNCHRONIZE_INVOICES);
}
+ public static canDownloadInvoiceBilling(loggedUser: UserToken): boolean {
+ return Authorizations.canPerformAction(loggedUser, Entity.INVOICES, Action.DOWNLOAD);
+ }
+
public static isSuperAdmin(user: UserToken | User): boolean {
return user.role === UserRole.SUPER_ADMIN;
}

MODIFY
src/authorization/AuthorizationsDefinition.ts
src/authorization/AuthorizationsDefinition.ts
@@ -62,7 +62,7 @@ const AUTHORIZATION_DEFINITION: AuthorizationDefinition = {
action: [Action.CHECK_CONNECTION, Action.SYNCHRONIZE_USER, Action.SYNCHRONIZE_USERS]
},
{ resource: Entity.TAXES, action: [Action.LIST], attributes: ['*'] },
- { resource: Entity.INVOICES, action: [Action.LIST, Action.SYNCHRONIZE_INVOICES], attributes: ['*'] },
+ { resource: Entity.INVOICES, action: [Action.LIST, Action.SYNCHRONIZE_INVOICES, Action.DOWNLOAD], attributes: ['*'] },
{ resource: Entity.ASSET, action: [Action.CREATE, Action.READ, Action.UPDATE, Action.DELETE], attributes: ['*'] },
{ resource: Entity.ASSETS, action: Action.LIST, attributes: ['*'] },
{ resource: Entity.SETTINGS, action: Action.LIST, attributes: ['*'] },
@@ -108,7 +108,7 @@ const AUTHORIZATION_DEFINITION: AuthorizationDefinition = {
resource: Entity.COMPANY, action: Action.READ, attributes: ['*'],
condition: { Fn: 'LIST_CONTAINS', args: { 'companies': '$.company' } }
},
- { resource: Entity.INVOICES, action: [Action.LIST, Action.SYNCHRONIZE_INVOICES], attributes: ['*'] },
+ { resource: Entity.INVOICES, action: [Action.LIST, Action.SYNCHRONIZE_INVOICES, Action.DOWNLOAD], attributes: ['*'] },
{ resource: Entity.INVOICE, action: [Action.DOWNLOAD], attributes: ['*'] },
{ resource: Entity.SITES, action: Action.LIST, attributes: ['*'] },
{

MODIFY
src/integration/billing/BillingIntegration.ts
src/integration/billing/BillingIntegration.ts
-import { BillingDataTransactionStart, BillingDataTransactionStop, BillingDataTransactionUpdate, BillingInvoice, BillingInvoiceItem, BillingTax, BillingUser, BillingUserSynchronizeAction } from '../../types/Billing';
+import { BillingDataTransactionStart, BillingDataTransactionStop, BillingDataTransactionUpdate, BillingInvoice, BillingInvoiceItem, BillingInvoicePdf, BillingTax, BillingUser, BillingUserSynchronizeAction } from '../../types/Billing';
import User, { UserStatus } from '../../types/User';
import { ActionsResponse } from '../../types/GlobalType';
import BackendError from '../../exception/BackendError';
@@ -429,7 +429,13 @@ export default abstract class BillingIntegration<T extends BillingSetting> {
userInInvoice = await UserStorage.getUserByBillingID(tenantID, invoiceBilling.customerID);
}
invoiceBilling.userID = userInInvoice ? userInInvoice.id : null;
+ invoiceBilling.id = await BillingStorage.saveInvoice(tenantID, invoiceBilling);
+ const invoicePdf = await this.downloadInvoicePdf(invoiceBilling);
+ if (invoicePdf) {
+ await BillingStorage.saveInvoicePdf(tenantID, invoicePdf);
+ invoiceBilling.downloadable = true;
await BillingStorage.saveInvoice(tenantID, invoiceBilling);
+ }
Logging.logDebug({
tenantID: tenantID,
user: user,
@@ -562,4 +568,6 @@ export default abstract class BillingIntegration<T extends BillingSetting> {
async abstract createInvoice(user: BillingUser, invoiceItem: BillingInvoiceItem, idempotencyKey?: string | number): Promise<{ invoice: BillingInvoice; invoiceItem: BillingInvoiceItem }>;
async abstract sendInvoiceToUser(invoice: BillingInvoice): Promise<BillingInvoice>;
+
+ async abstract downloadInvoicePdf(invoice: BillingInvoice): Promise<BillingInvoicePdf>;
}

MODIFY
src/integration/billing/stripe/StripeBillingIntegration.ts
src/integration/billing/stripe/StripeBillingIntegration.ts
-import { BillingDataTransactionStart, BillingDataTransactionStop, BillingDataTransactionUpdate, BillingInvoice, BillingInvoiceItem, BillingInvoiceStatus, BillingMethod, BillingStatus, BillingTax, BillingUser } from '../../../types/Billing';
+import { BillingDataTransactionStart, BillingDataTransactionStop, BillingDataTransactionUpdate, BillingInvoice, BillingInvoiceItem, BillingInvoicePdf, BillingInvoiceStatus, BillingMethod, BillingStatus, BillingTax, BillingUser } from '../../../types/Billing';
import Stripe, { IResourceObject } from 'stripe';
import BackendError from '../../../exception/BackendError';
@@ -173,7 +173,8 @@ export default class StripeBillingIntegration extends BillingIntegration<StripeB
status: stripeInvoice.status as BillingInvoiceStatus,
currency: stripeInvoice.currency,
createdOn: new Date(stripeInvoice.created * 1000),
- nbrOfItems: stripeInvoice.lines.total_count
+ nbrOfItems: stripeInvoice.lines.total_count,
+ downloadUrl: stripeInvoice.invoice_pdf
} as BillingInvoice;
}
return null;
@@ -385,35 +386,46 @@ export default class StripeBillingIntegration extends BillingIntegration<StripeB
public async sendInvoiceToUser(invoice: BillingInvoice): Promise<BillingInvoice> {
await this.checkConnection();
- let pdfUrl = '';
// Send invoice to user
try {
const stripeInvoice = await this.stripe.invoices.sendInvoice(invoice.invoiceID);
invoice.status = stripeInvoice.status as BillingInvoiceStatus;
+ invoice.downloadUrl = stripeInvoice.invoice_pdf;
invoice.id = await BillingStorage.saveInvoice(this.tenantID, invoice);
- pdfUrl = stripeInvoice.invoice_pdf;
} catch (error) {
throw new BackendError({
source: Constants.CENTRAL_SERVER,
action: ServerAction.BILLING_SEND_INVOICE,
- module: MODULE_NAME, method: 'sendInvoice',
+ module: MODULE_NAME, method: 'sendInvoiceToUser',
message: 'Failed to send invoice',
detailedMessages: { error: error.message, stack: error.stack }
});
}
// Save generated pdf
- if (pdfUrl !== '') {
+ if (invoice.downloadUrl && invoice.downloadUrl !== '') {
+ // Download PDF file
+ const invoicePdf = await this.downloadInvoicePdf(invoice);
+ // Save PDF file into database
try {
- const response = await axios.get(pdfUrl, { responseType: 'arraybuffer' });
- const base64Image = Buffer.from(response.data).toString('base64');
- const encodedImage = 'data:' + response.headers['content-type'] + ';base64,' + base64Image;
- invoice.encodedPdf = encodedImage;
- await BillingStorage.saveInvoicePdf(this.tenantID, invoice.id, invoice.encodedPdf);
+ await BillingStorage.saveInvoicePdf(this.tenantID, invoicePdf);
+ } catch (error) {
+ throw new BackendError({
+ source: Constants.CENTRAL_SERVER,
+ action: ServerAction.BILLING_DOWNLOAD_INVOICE,
+ module: MODULE_NAME, method: 'sendInvoiceToUser',
+ message: 'Failed to save invoice PDF in database',
+ detailedMessages: { error: error.message, stack: error.stack }
+ });
+ }
+ // Update related invoice as downloadable
+ invoice.downloadable = true;
+ try {
+ await BillingStorage.saveInvoice(this.tenantID, invoice);
} catch (error) {
throw new BackendError({
source: Constants.CENTRAL_SERVER,
action: ServerAction.BILLING_CREATE_INVOICE,
- module: MODULE_NAME, method: 'createInvoice',
+ module: MODULE_NAME, method: 'sendInvoiceToUser',
message: 'Failed to save invoice PDF in database',
detailedMessages: { error: error.message, stack: error.stack }
});
@@ -422,6 +434,15 @@ export default class StripeBillingIntegration extends BillingIntegration<StripeB
return invoice;
}
+ public async downloadInvoicePdf(invoice: BillingInvoice): Promise<BillingInvoicePdf> {
+ if (invoice.downloadUrl && invoice.downloadUrl !== '') {
+ const response = await axios.get(invoice.downloadUrl, { responseType: 'arraybuffer' });
+ const base64Image = Buffer.from(response.data).toString('base64');
+ const encodedPdf = 'data:' + response.headers['content-type'] + ';base64,' + base64Image;
+ return { id: invoice.id, encodedPdf: encodedPdf } as BillingInvoicePdf;
+ }
+ }
+
public async startTransaction(transaction: Transaction): Promise<BillingDataTransactionStart> {
try {
// Check Stripe

MODIFY
src/server/rest/CentralRestServerService.ts
src/server/rest/CentralRestServerService.ts
@@ -178,6 +178,7 @@ class RequestMapper {
[ServerAction.CHECK_BILLING_CONNECTION]: BillingService.handleCheckBillingConnection.bind(this),
[ServerAction.BILLING_TAXES]: BillingService.handleGetBillingTaxes.bind(this),
[ServerAction.BILLING_USER_INVOICES]: BillingService.handleGetUserInvoices.bind(this),
+ [ServerAction.BILLING_DOWNLOAD_INVOICE]: BillingService.handleDownloadInvoice.bind(this),
[ServerAction.OCPI_ENDPOINTS]: OCPIEndpointService.handleGetOcpiEndpoints.bind(this),
[ServerAction.OCPI_ENDPOINT]: OCPIEndpointService.handleGetOcpiEndpoint.bind(this),
[ServerAction.INTEGRATION_CONNECTIONS]: ConnectionService.handleGetConnections.bind(this),

MODIFY
src/server/rest/service/BillingService.ts
src/server/rest/service/BillingService.ts
@@ -17,6 +17,7 @@ import TenantStorage from '../../../storage/mongodb/TenantStorage';
import User from '../../../types/User';
import UserStorage from '../../../storage/mongodb/UserStorage';
import UtilsService from './UtilsService';
+import fs from 'fs';
const MODULE_NAME = 'BillingService';
@@ -330,4 +331,59 @@ export default class BillingService {
res.json(Object.assign(synchronizeAction, Constants.REST_RESPONSE_SUCCESS));
next();
}
+
+ public static async handleDownloadInvoice(action: ServerAction, req: Request, res: Response, next: NextFunction): Promise<void> {
+ if (!Authorizations.canDownloadInvoiceBilling(req.user)) {
+ throw new AppAuthError({
+ errorCode: HTTPAuthError.ERROR,
+ user: req.user,
+ entity: Entity.INVOICE, action: Action.DOWNLOAD,
+ module: MODULE_NAME, method: 'handleDownloadInvoice',
+ });
+ }
+ // Check if component is active
+ UtilsService.assertComponentIsActiveFromToken(req.user, TenantComponents.BILLING,
+ Action.DOWNLOAD, Entity.BILLING, MODULE_NAME, 'handleDownloadInvoice');
+ const tenant = await TenantStorage.getTenant(req.user.tenantID);
+ const billingImpl = await BillingFactory.getBillingImpl(tenant.id);
+ if (!billingImpl) {
+ throw new AppError({
+ source: Constants.CENTRAL_SERVER,
+ errorCode: HTTPError.GENERAL_ERROR,
+ message: 'Billing service is not configured',
+ module: MODULE_NAME, method: 'handleDownloadInvoice',
+ action: action,
+ user: req.user
+ });
+ }
+ const filteredRequest = BillingSecurity.filterDownloadInvoiceRequest(req.query);
+ // Get the Invoice
+ const invoice = await BillingStorage.getInvoiceByBillingInvoiceID(req.user.tenantID, filteredRequest.invoiceID);
+ const invoicepdf = await BillingStorage.getInvoicePdf(req.user.tenantID, invoice.id);
+ UtilsService.assertObjectExists(action, invoicepdf, `Invoice '${filteredRequest.invoiceID}' does not exist`,
+ MODULE_NAME, 'handleDownloadInvoice', req.user);
+
+ if (invoicepdf && invoicepdf.encodedPdf) {
+ const base64RawData = invoicepdf.encodedPdf.split(';base64,').pop();
+ const filename = 'invoice_' + invoice.invoiceID + '.pdf';
+ fs.writeFile(filename, base64RawData, { encoding: 'base64' }, (err) => {
+ if (err) {
+ console.log(err);
+ throw err;
+ }
+ res.download(filename, (err2) => {
+ if (err2) {
+ console.log(err2);
+ throw err2;
+ }
+ fs.unlink(filename, (err3) => {
+ if (err3) {
+ console.log(err3);
+ throw err3;
+ }
+ });
+ });
+ });
+ }
+ }
}

MODIFY
src/server/rest/service/security/BillingSecurity.ts
src/server/rest/service/security/BillingSecurity.ts
import { BillingInvoice, BillingTax } from '../../../../types/Billing';
-import { HttpForceSynchronizeUserInvoicesRequest, HttpSynchronizeUserRequest } from '../../../../types/requests/HttpUserRequest';
+import { HttpDownloadInvoiceRequest, HttpForceSynchronizeUserInvoicesRequest, HttpSynchronizeUserRequest } from '../../../../types/requests/HttpUserRequest';
import Authorizations from '../../../../authorization/Authorizations';
import { DataResult } from '../../../../types/DataResult';
@@ -71,6 +71,7 @@ export default class BillingSecurity {
filteredInvoice.createdOn = invoice.createdOn;
filteredInvoice.currency = invoice.currency;
filteredInvoice.customerID = invoice.customerID;
+ filteredInvoice.downloadable = invoice.downloadable;
}
return filteredInvoice;
}
@@ -113,4 +114,10 @@ export default class BillingSecurity {
userID: sanitize(request.userID)
};
}
+
+ static filterDownloadInvoiceRequest(request: any): HttpDownloadInvoiceRequest {
+ return {
+ invoiceID: sanitize(request.invoiceID)
+ };
+ }
}

MODIFY
src/storage/mongodb/BillingStorage.ts
src/storage/mongodb/BillingStorage.ts
@@ -164,7 +164,9 @@ export default class BillingStorage {
status: invoiceToSave.status,
currency: invoiceToSave.currency,
createdOn: Utils.convertToDate(invoiceToSave.createdOn),
- nbrOfItems: Utils.convertToInt(invoiceToSave.nbrOfItems)
+ nbrOfItems: Utils.convertToInt(invoiceToSave.nbrOfItems),
+ downloadable: Utils.convertToBoolean(invoiceToSave.downloadable),
+ downloadUrl: invoiceToSave.downloadUrl
};
// Modify and return the modified document
await global.database.getCollection<BillingInvoice>(tenantId, 'invoices').findOneAndReplace(
@@ -177,14 +179,14 @@ export default class BillingStorage {
return invoiceMDB._id.toHexString();
}
- public static async saveInvoicePdf(tenantId: string, invoiceId: string, encodedPdf: string): Promise<BillingInvoicePdf> {
+ public static async saveInvoicePdf(tenantId: string, invoicePdf: BillingInvoicePdf): Promise<BillingInvoicePdf> {
// Debug
const uniqueTimerID = Logging.traceStart(MODULE_NAME, 'saveInvoicePdf');
// Build Request
// Properties to save
const invoiceFileMDB: any = {
- _id: invoiceId ? Utils.convertToObjectID(invoiceId) : new ObjectID(),
- encodedPdf: encodedPdf
+ _id: invoicePdf.id ? Utils.convertToObjectID(invoicePdf.id) : new ObjectID(),
+ encodedPdf: invoicePdf.encodedPdf
};
// Modify and return the modified document
await global.database.getCollection<BillingInvoicePdf>(tenantId, 'invoicespdf').findOneAndReplace(
@@ -197,6 +199,21 @@ export default class BillingStorage {
return invoiceFileMDB._id.toHexString();
}
+ public static async getInvoicePdf(tenantID: string, id: string): Promise<BillingInvoicePdf> {
+ // Debug
+ const uniqueTimerID = Logging.traceStart(MODULE_NAME, 'getInvoicePdf');
+ // Check Tenant
+ await Utils.checkTenant(tenantID);
+ // Read DB
+ const invoicePdfMDB: { _id: string; encodedPdf: string } = await global.database.getCollection(tenantID, 'invoicespdf')
+ .findOne({ _id: Utils.convertToObjectID(id) });
+ // Debug
+ Logging.traceEnd(MODULE_NAME, 'getInvoicePdf', uniqueTimerID, { id });
+ return {
+ id: id, encodedPdf: (invoicePdfMDB ? invoicePdfMDB.encodedPdf : null)
+ };
+ }
+
public static async deleteInvoice(tenantID: string, id: string): Promise<void> {
// Debug
const uniqueTimerID = Logging.traceStart(MODULE_NAME, 'deleteInvoice');

MODIFY
src/types/Authorization.ts
src/types/Authorization.ts
@@ -109,6 +109,7 @@ export enum Action {
SYNCHRONIZE_USERS = 'SynchronizeUsers',
SYNCHRONIZE_USER = 'SynchronizeUser',
SYNCHRONIZE_INVOICES = 'SynchronizeInvoices',
+ DOWNLOAD_INVOICE = 'DownloadInvoice',
CHECK_CONNECTION = 'CheckConnection',
PING = 'Ping',
GENERATE_LOCAL_TOKEN = 'GenerateLocalToken',

MODIFY
src/types/Billing.ts
src/types/Billing.ts
@@ -71,7 +71,8 @@ export interface BillingInvoice {
customerID?: string;
createdOn?: Date;
nbrOfItems?: number;
- encodedPdf?: string;
+ downloadable?: boolean
+ downloadUrl?: string;
}
export interface BillingInvoiceItem {

MODIFY
src/types/Server.ts
src/types/Server.ts
@@ -340,6 +340,7 @@ export enum ServerAction {
BILLING_USER_INVOICES = 'BillingUserInvoices',
BILLING_SYNCHRONIZE_INVOICES = 'BillingSynchronizeInvoices',
BILLING_FORCE_SYNCHRONIZE_USER_INVOICES = 'BillingForceSynchronizeUserInvoices',
+ BILLING_DOWNLOAD_INVOICE = 'BillingDownloadInvoice',
MONGO_DB = 'MongoDB',

MODIFY
src/types/requests/HttpUserRequest.ts
src/types/requests/HttpUserRequest.ts
@@ -14,6 +14,10 @@ export interface HttpForceSynchronizeUserInvoicesRequest {
userID?: string;
}
+export interface HttpDownloadInvoiceRequest {
+ invoiceID?: string;
+}
+
export interface HttpUserMobileTokenRequest {
id: string;
mobileToken: string;

Store invoice pdf in DB