Fix billing unit test
MODIFY
src/integration/billing/BillingIntegration.ts
src/integration/billing/BillingIntegration.ts
-import { BillingDataTransactionStart, BillingDataTransactionStop, BillingDataTransactionUpdate, BillingInvoice, BillingInvoiceDocument, BillingInvoiceItem, BillingTax, BillingUser, BillingUserSynchronizeAction } from '../../types/Billing';
+import { BillingDataTransactionStart, BillingDataTransactionStop, BillingDataTransactionUpdate, BillingInvoice, BillingInvoiceDocument, BillingInvoiceItem, BillingInvoiceStatus, BillingTax, BillingUser, BillingUserSynchronizeAction } from '../../types/Billing';
import User, { UserStatus } from '../../types/User';
import BackendError from '../../exception/BackendError';
import { BillingSetting } from '../../types/Setting';
@@ -345,10 +345,31 @@ export default abstract class BillingIntegration<T extends BillingSetting> {
return actionsDone;
}
- public async sendInvoiceToUser(invoice: BillingInvoice): Promise<BillingInvoice> {
- // Save generated pdf
- if (invoice.downloadable) {
+ public async sendInvoiceToUser(tenantID: string, invoice: BillingInvoice): Promise<BillingInvoice> {
// Send link to the user using our notification framework (link to the front-end + download)
+ try {
+ invoice.downloadUrl = await this.finalizeInvoice(invoice);
+ invoice.status = BillingInvoiceStatus.OPEN;
+ invoice.downloadable = true;
+ await BillingStorage.saveInvoice(tenantID, invoice);
+ const invoicedocument = await this.downloadInvoiceDocument(invoice);
+ await BillingStorage.saveInvoiceDocument(tenantID, invoicedocument);
+ Logging.logDebug({
+ message: 'Sent invoice to user',
+ action: ServerAction.BILLING_SEND_INVOICE,
+ method: 'sendInvoiceToUser',
+ module: 'Billing',
+ tenantID: tenantID
+ });
+ } catch (error) {
+ Logging.logError({
+ tenantID: tenantID,
+ source: Constants.CENTRAL_SERVER,
+ action: ServerAction.BILLING_SEND_INVOICE,
+ module: MODULE_NAME, method: 'sendInvoiceToUser',
+ message: 'Unable to send invoice to user',
+ detailedMessages: { error: error.message, stack: error.stack }
+ });
}
return invoice;
}
@@ -375,7 +396,7 @@ export default abstract class BillingIntegration<T extends BillingSetting> {
});
}
// Check Charging Station
- if (transaction.billingData) {
+ if (transaction.billingData.invoiceID) {
throw new BackendError({
message: 'Transaction has already billing data',
source: Constants.CENTRAL_SERVER,
@@ -506,4 +527,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 downloadInvoiceDocument(invoice: BillingInvoice): Promise<BillingInvoiceDocument>;
+
+ async abstract finalizeInvoice(invoice: BillingInvoice): Promise<string>;
}

MODIFY
src/integration/billing/stripe/StripeBillingIntegration.ts
src/integration/billing/stripe/StripeBillingIntegration.ts
@@ -367,6 +367,22 @@ export default class StripeBillingIntegration extends BillingIntegration<StripeB
}
}
+ public async finalizeInvoice(invoice: BillingInvoice): Promise<string> {
+ await this.checkConnection();
+ try {
+ const stripeInvoice = await this.stripe.invoices.finalizeInvoice(invoice.invoiceID);
+ return stripeInvoice.invoice_pdf;
+ } catch (error) {
+ throw new BackendError({
+ message: 'Failed to finalize invoice',
+ source: Constants.CENTRAL_SERVER,
+ module: MODULE_NAME,
+ method: 'finalizeInvoice',
+ action: ServerAction.BILLING_SEND_INVOICE
+ });
+ }
+ }
+
public async startTransaction(transaction: Transaction): Promise<BillingDataTransactionStart> {
// Check Stripe
await this.checkConnection();
@@ -442,12 +458,12 @@ export default class StripeBillingIntegration extends BillingIntegration<StripeB
};
// Get the draft invoice
const draftInvoices = await BillingStorage.getInvoices(this.tenantID,
- { invoiceStatus: [BillingInvoiceStatus.DRAFT] }, Constants.DB_PARAMS_SINGLE_RECORD);
+ { invoiceStatus: [BillingInvoiceStatus.DRAFT], userIDs: [transaction.userID] }, Constants.DB_PARAMS_SINGLE_RECORD);
// Set
invoice.invoice = draftInvoices.count > 0 ? draftInvoices.result[0] : null;
if (invoice.invoice) {
// Append a new invoice item
- invoice.invoiceItem = await this.createInvoiceItem(billingUser, invoice.invoice.id, {
+ invoice.invoiceItem = await this.createInvoiceItem(billingUser, invoice.invoice.invoiceID, {
description: description,
amount: Math.round(transaction.stop.roundedPrice * 100)
}, transaction.id);

MODIFY
test/api/BillingTest.ts
test/api/BillingTest.ts
@@ -452,14 +452,15 @@ describe('Billing Service', function() {
await TestData.setBillingSystemValidCredentials(testData);
});
- it('should create an invoice after a transaction', async () => {
- let response = await testData.userService.billingApi.readAll({}, TestConstants.DEFAULT_PAGING, TestConstants.DEFAULT_ORDERING, '/client/api/BillingUserInvoices');
- const invoicesBefore = response.data.count;
+ it('should add an item to the existing invoice after a transaction', async () => {
await testData.userService.billingApi.forceSynchronizeUser({ id: testData.userContext.id });
+ let response = await testData.userService.billingApi.readAll({ Status: BillingInvoiceStatus.DRAFT, UserID: [testData.userContext.id] }, TestConstants.DEFAULT_PAGING, [{ field: 'createdOn', direction: 'desc' }], '/client/api/BillingUserInvoices');
+ const itemsBefore: number = response.data.result[0].nbrOfItems;
await generateTransaction(testData.userContext, testData.chargingStationContext);
- response = await testData.userService.billingApi.readAll({}, TestConstants.DEFAULT_PAGING, TestConstants.DEFAULT_ORDERING, '/client/api/BillingUserInvoices');
- const invoicesAfter = response.data.count;
- expect(invoicesAfter).to.be.eq(invoicesBefore + 1);
+ await testData.userService.billingApi.synchronizeInvoices({});
+ response = await testData.userService.billingApi.readAll({ Status: BillingInvoiceStatus.DRAFT, UserID: [testData.userContext.id] }, TestConstants.DEFAULT_PAGING, [{ field: 'createdOn', direction: 'desc' }], '/client/api/BillingUserInvoices');
+ const itemsAfter = response.data.result[0].nbrOfItems;
+ expect(itemsAfter).to.be.eq(itemsBefore + 1);
});
it('should synchronize 1 invoice after a transaction', async () => {
@@ -488,6 +489,12 @@ describe('Billing Service', function() {
describe('Where basic user', () => {
before(async () => {
+ testData.userContext = testData.tenantContext.getUserContext(ContextDefinition.USER_CONTEXTS.DEFAULT_ADMIN);
+ testData.userService = new CentralServerService(
+ testData.tenantContext.getTenant().subdomain,
+ testData.userContext
+ );
+ await TestData.setBillingSystemValidCredentials(testData);
testData.userContext = testData.tenantContext.getUserContext(ContextDefinition.USER_CONTEXTS.BASIC_USER);
assert(testData.userContext, 'User context cannot be null');
if (testData.userContext === testData.centralUserContext) {
@@ -499,12 +506,9 @@ describe('Billing Service', function() {
testData.userContext
);
}
- await TestData.setBillingSystemValidCredentials(testData);
});
it('should create an invoice after a transaction', async () => {
- let response = await testData.userService.billingApi.readAll({}, TestConstants.DEFAULT_PAGING, TestConstants.DEFAULT_ORDERING, '/client/api/BillingUserInvoices');
- const invoicesBefore = response.data.count;
const adminUser = testData.tenantContext.getUserContext(ContextDefinition.USER_CONTEXTS.DEFAULT_ADMIN);
const basicUser = testData.tenantContext.getUserContext(ContextDefinition.USER_CONTEXTS.BASIC_USER);
// Connect as Admin to Force synchronize basic user
@@ -520,10 +524,14 @@ describe('Billing Service', function() {
testData.tenantContext.getTenant().subdomain,
testData.userContext
);
+ await testData.userService.billingApi.synchronizeInvoices({});
+ let response = await testData.userService.billingApi.readAll({ Status: BillingInvoiceStatus.DRAFT }, TestConstants.DEFAULT_PAGING, [{ field: 'createdOn', direction: 'desc' }], '/client/api/BillingUserInvoices');
+ const itemsBefore: number = response.data.result[0].nbrOfItems;
await generateTransaction(testData.userContext, testData.chargingStationContext);
- response = await testData.userService.billingApi.readAll({}, TestConstants.DEFAULT_PAGING, TestConstants.DEFAULT_ORDERING, '/client/api/BillingUserInvoices');
- const invoicesAfter = response.data.count;
- expect(invoicesAfter).to.be.eq(invoicesBefore + 1);
+ await testData.userService.billingApi.synchronizeInvoices({});
+ response = await testData.userService.billingApi.readAll({ Status: BillingInvoiceStatus.DRAFT }, TestConstants.DEFAULT_PAGING, [{ field: 'createdOn', direction: 'desc' }], '/client/api/BillingUserInvoices');
+ const itemsAfter = response.data.result[0].nbrOfItems;
+ expect(itemsAfter).to.be.eq(itemsBefore + 1);
});
});

MODIFY
test/api/context/BillingContext.ts
test/api/context/BillingContext.ts
@@ -71,9 +71,9 @@ export default class BillingContext {
await billingImpl.createInvoice(basicBillingUser, { description: 'TestBasic' + (i + 1), amount: 100 });
}
let invoice = await billingImpl.createInvoice(adminBillingUser, { description: 'TestAdmin3', amount: 100 });
- await billingImpl.sendInvoiceToUser(invoice.invoice);
+ await billingImpl.sendInvoiceToUser(this.tenantContext.getTenant().id, invoice.invoice);
invoice = await billingImpl.createInvoice(basicBillingUser, { description: 'TestBasic3', amount: 100 });
- await billingImpl.sendInvoiceToUser(invoice.invoice);
+ await billingImpl.sendInvoiceToUser(this.tenantContext.getTenant().id, invoice.invoice);
}
private async saveBillingSettings(stripeSettings) {

Fix billing unit test