Display sessions w/ no invoice in error + Link to new invoice
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 canLinkTransactionToInvoice(loggedUser: UserToken): boolean {
+ return Authorizations.canPerformAction(loggedUser, Entity.INVOICES, Action.LINK_INVOICE);
+ }
+
public static canCheckConnectionAsset(loggedUser: UserToken): boolean {
return Authorizations.canPerformAction(loggedUser, Entity.ASSET, Action.CHECK_CONNECTION);
}

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.LINK_INVOICE], attributes: ['*'] },
{ resource: Entity.ASSET, action: [Action.CREATE, Action.READ, Action.UPDATE, Action.DELETE, Action.CHECK_CONNECTION], attributes: ['*'] },
{ resource: Entity.ASSETS, action: Action.LIST, attributes: ['*'] },
{ resource: Entity.SETTINGS, action: Action.LIST, attributes: ['*'] },

MODIFY
src/server/rest/CentralRestServerService.ts
src/server/rest/CentralRestServerService.ts
@@ -81,6 +81,7 @@ class RequestMapper {
[ServerAction.BILLING_FORCE_SYNCHRONIZE_USER]: BillingService.handleForceSynchronizeUser.bind(this),
[ServerAction.BILLING_SYNCHRONIZE_INVOICES]: BillingService.handleSynchronizeInvoices.bind(this),
[ServerAction.BILLING_FORCE_SYNCHRONIZE_USER_INVOICES]: BillingService.handleForceSynchronizeUserInvoices.bind(this),
+ [ServerAction.BILLING_LINK_TRANSACTION_TO_INVOICE]: BillingService.handleLinkTransactionToInvoice.bind(this),
[ServerAction.OCPI_ENPOINT_CREATE]: OCPIEndpointService.handleCreateOcpiEndpoint.bind(this),
[ServerAction.OCPI_ENPOINT_PING]: OCPIEndpointService.handlePingOcpiEndpoint.bind(this),
[ServerAction.OCPI_ENPOINT_TRIGGER_JOBS]: OCPIEndpointService.handleTriggerJobsEndpoint.bind(this),

MODIFY
src/server/rest/service/BillingService.ts
src/server/rest/service/BillingService.ts
@@ -14,8 +14,10 @@ import Logging from '../../../utils/Logging';
import { ServerAction } from '../../../types/Server';
import TenantComponents from '../../../types/TenantComponents';
import TenantStorage from '../../../storage/mongodb/TenantStorage';
+import TransactionStorage from '../../../storage/mongodb/TransactionStorage';
import User from '../../../types/User';
import UserStorage from '../../../storage/mongodb/UserStorage';
+import Utils from '../../../utils/Utils';
import UtilsService from './UtilsService';
const MODULE_NAME = 'BillingService';
@@ -330,4 +332,50 @@ export default class BillingService {
res.json(Object.assign(synchronizeAction, Constants.REST_RESPONSE_SUCCESS));
next();
}
+
+ public static async handleLinkTransactionToInvoice(action: ServerAction, req: Request, res: Response, next: NextFunction): Promise<void> {
+ if (!Authorizations.canLinkTransactionToInvoice(req.user)) {
+ throw new AppAuthError({
+ errorCode: HTTPAuthError.ERROR,
+ user: req.user,
+ entity: Entity.USER, action: Action.LINK_INVOICE,
+ module: MODULE_NAME, method: 'handleLinkInvoiceToTransaction',
+ });
+ }
+ // Check if component is active
+ UtilsService.assertComponentIsActiveFromToken(req.user, TenantComponents.BILLING,
+ Action.LINK_INVOICE, Entity.BILLING, MODULE_NAME, 'handleLinkInvoiceToTransaction');
+ 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: 'handleLinkInvoiceToTransaction',
+ action: action,
+ user: req.user
+ });
+ }
+ const filteredRequest = BillingSecurity.filterLinkTransactionToInvoiceRequest(req.body);
+ // Get the Transaction
+ const transaction = await TransactionStorage.getTransaction(req.user.tenantID, Utils.convertToInt(filteredRequest.transactionID));
+ UtilsService.assertObjectExists(action, transaction, `Transaction '${filteredRequest.transactionID}' does not exist`,
+ MODULE_NAME, 'handleLinkInvoiceToTransaction', req.user);
+
+ // Create an invoice for the transaction
+ const billingDataStop = await billingImpl.stopTransaction(transaction);
+ // Update transaction
+ transaction.billingData = {
+ status: billingDataStop.status,
+ invoiceID: billingDataStop.invoiceID,
+ invoiceStatus: billingDataStop.invoiceStatus,
+ invoiceItem: billingDataStop.invoiceItem,
+ lastUpdate: new Date()
+ };
+ await TransactionStorage.saveTransaction(req.user.tenantID, transaction);
+ // Ok
+ res.json(Object.assign(Constants.REST_RESPONSE_SUCCESS));
+ next();
+ }
}

MODIFY
src/server/rest/service/TransactionService.ts
src/server/rest/service/TransactionService.ts
@@ -596,10 +596,15 @@ export default class TransactionService {
}
if (filteredRequest.ErrorType) {
filter.errorType = filteredRequest.ErrorType.split('|');
- } else if (Utils.isComponentActiveFromToken(req.user, TenantComponents.PRICING)) {
- filter.errorType = [TransactionInErrorType.LONG_INACTIVITY, TransactionInErrorType.NEGATIVE_ACTIVITY, TransactionInErrorType.NEGATIVE_DURATION, TransactionInErrorType.OVER_CONSUMPTION, TransactionInErrorType.INVALID_START_DATE, TransactionInErrorType.NO_CONSUMPTION, TransactionInErrorType.MISSING_PRICE, TransactionInErrorType.MISSING_USER];
} else {
- filter.errorType = [TransactionInErrorType.LONG_INACTIVITY, TransactionInErrorType.NEGATIVE_ACTIVITY, TransactionInErrorType.NEGATIVE_DURATION, TransactionInErrorType.OVER_CONSUMPTION, TransactionInErrorType.INVALID_START_DATE, TransactionInErrorType.NO_CONSUMPTION, TransactionInErrorType.MISSING_USER];
+ const types = [TransactionInErrorType.LONG_INACTIVITY, TransactionInErrorType.NEGATIVE_ACTIVITY, TransactionInErrorType.NEGATIVE_DURATION, TransactionInErrorType.OVER_CONSUMPTION, TransactionInErrorType.INVALID_START_DATE, TransactionInErrorType.NO_CONSUMPTION, TransactionInErrorType.MISSING_USER];
+ if (Utils.isComponentActiveFromToken(req.user, TenantComponents.PRICING)) {
+ types.push(TransactionInErrorType.MISSING_PRICE);
+ }
+ if (Utils.isComponentActiveFromToken(req.user, TenantComponents.BILLING)) {
+ types.push(TransactionInErrorType.NO_BILLING_DATA);
+ }
+ filter.errorType = types;
}
// Site Area
const transactions = await TransactionStorage.getTransactionsInError(req.user.tenantID,

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 { HttpForceSynchronizeUserInvoicesRequest, HttpLinkTransactionToInvoiceRequest, HttpSynchronizeUserRequest } from '../../../../types/requests/HttpUserRequest';
import Authorizations from '../../../../authorization/Authorizations';
import { DataResult } from '../../../../types/DataResult';
@@ -113,4 +113,10 @@ export default class BillingSecurity {
userID: sanitize(request.userID)
};
}
+
+ static filterLinkTransactionToInvoiceRequest(request: any): HttpLinkTransactionToInvoiceRequest {
+ return {
+ transactionID: sanitize(request.transactionID)
+ };
+ }
}

MODIFY
src/storage/mongodb/TransactionStorage.ts
src/storage/mongodb/TransactionStorage.ts
@@ -733,7 +733,7 @@ export default class TransactionStorage {
params: {
search?: string; issuer?: boolean; userIDs?: string[]; chargeBoxIDs?: string[];
siteAreaIDs?: string[]; siteIDs?: string[]; startDateTime?: Date; endDateTime?: Date; withChargeBoxes?: boolean;
- errorType?: (TransactionInErrorType.LONG_INACTIVITY | TransactionInErrorType.NEGATIVE_ACTIVITY | TransactionInErrorType.NEGATIVE_DURATION | TransactionInErrorType.OVER_CONSUMPTION | TransactionInErrorType.INVALID_START_DATE | TransactionInErrorType.NO_CONSUMPTION | TransactionInErrorType.MISSING_USER | TransactionInErrorType.MISSING_PRICE)[];
+ errorType?: (TransactionInErrorType.LONG_INACTIVITY | TransactionInErrorType.NEGATIVE_ACTIVITY | TransactionInErrorType.NEGATIVE_DURATION | TransactionInErrorType.OVER_CONSUMPTION | TransactionInErrorType.INVALID_START_DATE | TransactionInErrorType.NO_CONSUMPTION | TransactionInErrorType.MISSING_USER | TransactionInErrorType.MISSING_PRICE | TransactionInErrorType.NO_BILLING_DATA)[];
},
dbParams: DbParams, projectFields?: string[]): Promise<DataResult<TransactionInError>> {
// Debug
@@ -1221,6 +1221,13 @@ export default class TransactionStorage {
},
{ $addFields: { 'errorCode': TransactionInErrorType.MISSING_USER } }
];
+ case TransactionInErrorType.NO_BILLING_DATA:
+ return [
+ { $match: { 'billingData': { $exists: false } } },
+ { $match: { 'billingData.invoiceID': { $exists: false } } },
+ { $addFields: { 'errorCode': TransactionInErrorType.NO_BILLING_DATA } }
+ ];
+ break;
default:
return [];
}

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',
+ LINK_INVOICE = 'LinkInvoice',
CHECK_CONNECTION = 'CheckConnection',
PING = 'Ping',
GENERATE_LOCAL_TOKEN = 'GenerateLocalToken',

MODIFY
src/types/InError.ts
src/types/InError.ts
@@ -35,7 +35,8 @@ export enum TransactionInErrorType {
NEGATIVE_DURATION = 'negative_duration',
INVALID_START_DATE = 'incorrect_starting_date',
MISSING_PRICE = 'missing_price',
- MISSING_USER = 'missing_user'
+ MISSING_USER = 'missing_user',
+ NO_BILLING_DATA = 'no_billing_data'
}
export enum ChargingStationInErrorType {

MODIFY
src/types/Server.ts
src/types/Server.ts
@@ -338,9 +338,10 @@ export enum ServerAction {
BILLING_FORCE_SYNCHRONIZE_USER = 'BillingForceSynchronizeUser',
CHECK_BILLING_CONNECTION = 'CheckBillingConnection',
BILLING_TAXES = 'BillingTaxes',
- BILLING_USER_INVOICES = 'BillingUserInvoices',
+ BILLING_USER_INVOICES = 'UserInvoices',
BILLING_SYNCHRONIZE_INVOICES = 'BillingSynchronizeInvoices',
BILLING_FORCE_SYNCHRONIZE_USER_INVOICES = 'BillingForceSynchronizeUserInvoices',
+ BILLING_LINK_TRANSACTION_TO_INVOICE = 'BillingLinkTransactionToInvoice',
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 HttpLinkTransactionToInvoiceRequest {
+ transactionID?: string;
+}
+
export interface HttpUserMobileTokenRequest {
id: string;
mobileToken: string;

Display sessions w/ no invoice in error + Link to new invoice