@citrineos/data
Version:
The OCPP data module which includes all persistence layer implementation.
557 lines • 25.7 kB
JavaScript
import { ChargingStationSequenceTypeEnum, CrudRepository, MeterValueUtils, OCPP1_6, OCPP2_0_1, } from '@citrineos/base';
import { Op } from 'sequelize';
import { Sequelize } from 'sequelize-typescript';
import { Logger } from 'tslog';
import { OCPP2_0_1_Mapper } from '../index.js';
import { AuthorizationMapper, MeterValueMapper } from '../mapper/2.0.1/index.js';
import { Authorization, ChargingStation, Connector, Evse, EvseType, MeterValue, StartTransaction, StopTransaction, Tariff, Transaction, TransactionEvent, } from '../model/index.js';
import { SequelizeRepository } from './Base.js';
import { SequelizeChargingStationSequenceRepository } from './ChargingStationSequence.js';
export class SequelizeTransactionEventRepository extends SequelizeRepository {
transaction;
evse;
station;
meterValue;
startTransaction;
stopTransaction;
connector;
chargingStationSequence;
constructor(config, logger, namespace = TransactionEvent.MODEL_NAME, sequelizeInstance, transaction, station, evse, meterValue, startTransaction, stopTransaction, connector, chargingStationSequence) {
super(config, namespace, logger, sequelizeInstance);
this.transaction = transaction
? transaction
: new SequelizeRepository(config, Transaction.MODEL_NAME, logger, sequelizeInstance);
this.evse = evse
? evse
: new SequelizeRepository(config, Evse.MODEL_NAME, logger, sequelizeInstance);
this.station = station
? station
: new SequelizeRepository(config, ChargingStation.MODEL_NAME, logger, sequelizeInstance);
this.meterValue = meterValue
? meterValue
: new SequelizeRepository(config, MeterValue.MODEL_NAME, logger, sequelizeInstance);
this.startTransaction = startTransaction
? startTransaction
: new SequelizeRepository(config, StartTransaction.MODEL_NAME, logger, sequelizeInstance);
this.stopTransaction = stopTransaction
? stopTransaction
: new SequelizeRepository(config, StopTransaction.MODEL_NAME, logger, sequelizeInstance);
this.connector = connector
? connector
: new SequelizeRepository(config, Connector.MODEL_NAME, logger, sequelizeInstance);
this.chargingStationSequence =
chargingStationSequence || new SequelizeChargingStationSequenceRepository(config, logger);
}
/**
* @param value TransactionEventRequest received from charging station. Will be used to create TransactionEvent,
* MeterValues, and either create or update Transaction. IdTokens (and associated AdditionalInfo) and EVSEs are
* assumed to already exist and will not be created as part of this call.
*
* @param stationId StationId of charging station which sent TransactionEventRequest.
*
* @returns Saved TransactionEvent
*/
async createOrUpdateTransactionByTransactionEventAndStationId(tenantId, value, stationId) {
return await this.s.transaction(async (sequelizeTransaction) => {
let finalTransaction;
let created = false;
const existingTransaction = await this.transaction.readOnlyOneByQuery(tenantId, {
where: {
stationId,
transactionId: value.transactionInfo.transactionId,
},
transaction: sequelizeTransaction,
});
if (existingTransaction) {
let evseId = existingTransaction.evseId;
if (!evseId && value.evse) {
const [evse] = await this.evse.readOrCreateByQuery(tenantId, {
where: {
tenantId,
stationId,
evseTypeId: value.evse.id,
},
});
evseId = evse.id;
}
let connectorId = existingTransaction.connectorId;
let tariffId = existingTransaction.tariffId;
if (!connectorId && value.evse?.connectorId) {
const [evse] = await this.evse.readOrCreateByQuery(tenantId, {
where: {
tenantId,
stationId,
evseTypeId: value.evse.id,
},
});
const [connector] = await this.connector.readOrCreateByQuery(tenantId, {
where: {
tenantId,
stationId,
evseId: evse.id,
evseTypeConnectorId: value.evse.connectorId,
},
include: [Tariff],
});
connectorId = connector.id;
tariffId = connector.tariffs?.[0]?.id;
}
let authorizationId = existingTransaction.authorizationId;
if (!authorizationId && value.idToken) {
// Find Authorization by IdToken
const authorization = await Authorization.findOne({
where: {
tenantId,
idToken: value.idToken.idToken,
idTokenType: OCPP2_0_1_Mapper.AuthorizationMapper.fromIdTokenEnumType(value.idToken.type),
},
transaction: sequelizeTransaction,
});
if (authorization) {
authorizationId = authorization.id;
}
else {
this.logger.warn(`Authorization with idToken ${value.idToken.idToken} : ${value.idToken.type} does not exist. Transaction ${existingTransaction.transactionId} will not be associated with an authorization.`);
}
}
finalTransaction = await existingTransaction.update({
isActive: value.eventType !== OCPP2_0_1.TransactionEventEnumType.Ended,
endTime: value.eventType === OCPP2_0_1.TransactionEventEnumType.Ended
? value.timestamp
: undefined,
...value.transactionInfo,
authorizationId,
evseId,
connectorId,
tariffId,
}, {
transaction: sequelizeTransaction,
});
}
else {
const newTransaction = Transaction.build({
tenantId,
stationId,
isActive: value.eventType !== OCPP2_0_1.TransactionEventEnumType.Ended,
startTime: value.eventType === OCPP2_0_1.TransactionEventEnumType.Started
? value.timestamp
: undefined,
...value.transactionInfo,
});
if (value.evse) {
const [evse] = await this.evse.readOrCreateByQuery(tenantId, {
where: {
tenantId,
stationId,
evseTypeId: value.evse.id,
},
});
newTransaction.set('evseId', evse.id);
if (value.evse?.connectorId) {
const [connector] = await this.connector.readOrCreateByQuery(tenantId, {
where: {
tenantId,
stationId,
evseId: evse.id,
evseTypeConnectorId: value.evse.connectorId,
},
include: [Tariff],
});
newTransaction.set('connectorId', connector.id);
newTransaction.set('tariffId', connector.tariffs?.[0]?.id);
}
}
if (value.idToken) {
// Find Authorization by IdToken
const authorization = await Authorization.findOne({
where: {
tenantId,
idToken: value.idToken.idToken,
idTokenType: OCPP2_0_1_Mapper.AuthorizationMapper.fromIdTokenEnumType(value.idToken.type),
},
transaction: sequelizeTransaction,
});
if (authorization) {
newTransaction.set('authorizationId', authorization.id);
}
else {
this.logger.warn(`Authorization with idToken ${value.idToken.idToken} : ${value.idToken.type} does not exist. Transaction ${newTransaction.transactionId} will not be associated with an authorization.`);
}
}
const chargingStation = await this.station.readByKey(tenantId, stationId);
if (!chargingStation) {
this.logger.error(`Charging station with stationId ${stationId} does not exist.`);
}
else {
if (chargingStation.locationId) {
newTransaction.set('locationId', chargingStation.locationId);
}
else {
this.logger.warn(`Charging station with stationId ${stationId} does not have a locationId. Transaction ${newTransaction.transactionId} will not be associated with a location, which may prevent it from being sent to upstream partners.`);
}
}
finalTransaction = await newTransaction.save({ transaction: sequelizeTransaction });
created = true;
}
const transactionDatabaseId = finalTransaction.id;
let event = TransactionEvent.build({
tenantId,
stationId,
transactionDatabaseId,
...value,
});
if (value.idToken && value.idToken.type !== OCPP2_0_1.IdTokenEnumType.NoAuthorization) {
const authorization = await Authorization.findOne({
where: {
tenantId,
idToken: value.idToken.idToken,
idTokenType: AuthorizationMapper.fromIdTokenEnumType(value.idToken.type),
},
transaction: sequelizeTransaction,
});
if (!authorization) {
this.logger.warn(`Authorization not found for ${value.idToken.idToken}:${value.idToken.type}`);
}
else {
event.idTokenValue = authorization.idToken;
event.idTokenType = authorization.idTokenType ? authorization.idTokenType : undefined;
}
}
event = await event.save({ transaction: sequelizeTransaction });
if (value.meterValue && value.meterValue.length > 0) {
await Promise.all(value.meterValue.map(async (meterValue) => {
const savedMeterValue = await MeterValue.create({
tenantId,
transactionEventId: event.id,
transactionDatabaseId: transactionDatabaseId,
transactionId: finalTransaction.transactionId,
tariffId: finalTransaction.tariffId,
...meterValue,
}, { transaction: sequelizeTransaction });
this.meterValue.emit('created', [savedMeterValue]);
}));
}
await event.reload({ include: [MeterValue], transaction: sequelizeTransaction });
this.emit('created', [event]);
const allMeterValues = await this.meterValue.readAllByQuery(tenantId, {
where: {
transactionDatabaseId,
},
transaction: sequelizeTransaction,
});
const meterValueTypes = allMeterValues.map((meterValue) => MeterValueMapper.toMeterValueType(meterValue));
await finalTransaction.update({ totalKwh: MeterValueUtils.getTotalKwh(meterValueTypes) }, { transaction: sequelizeTransaction });
await finalTransaction.reload({
include: [
{
model: TransactionEvent,
as: Transaction.TRANSACTION_EVENTS_ALIAS,
include: [EvseType],
},
MeterValue,
],
transaction: sequelizeTransaction,
});
this.transaction.emit(created ? 'created' : 'updated', [finalTransaction]);
return finalTransaction;
});
}
async readAllByStationIdAndTransactionId(tenantId, stationId, transactionId) {
return await super
.readAllByQuery(tenantId, {
where: { stationId },
include: [{ model: Transaction, where: { transactionId } }, MeterValue, Evse],
})
.then((transactionEvents) => {
transactionEvents?.forEach((transactionEvent) => (transactionEvent.transaction = undefined));
return transactionEvents;
});
}
async readTransactionByStationIdAndTransactionId(tenantId, stationId, transactionId) {
return await this.transaction.readOnlyOneByQuery(tenantId, {
where: { stationId, transactionId },
include: [MeterValue],
});
}
async readAllTransactionsByStationIdAndEvseAndChargingStates(tenantId, stationId, evse, chargingStates) {
const includeObj = evse
? [
{
model: Evse,
where: { evseTypeId: evse.id },
},
]
: [];
if (evse?.connectorId) {
includeObj.push({
model: Connector,
where: { evseTypeConnectorId: evse.connectorId },
});
}
return await this.transaction
.readAllByQuery(tenantId, {
where: {
stationId,
...(chargingStates ? { chargingState: { [Op.in]: chargingStates } } : {}),
},
include: includeObj,
})
.then((row) => row);
}
async readAllActiveTransactionsByAuthorizationId(tenantId, authorizationId) {
return await this.transaction.readAllByQuery(tenantId, {
where: { isActive: true, authorizationId },
});
}
async readAllMeterValuesByTransactionDataBaseId(tenantId, transactionDataBaseId) {
return this.meterValue
.readAllByQuery(tenantId, {
where: { transactionDatabaseId: transactionDataBaseId },
})
.then((row) => row);
}
async findByTransactionId(tenantId, transactionId) {
return this.transaction.readOnlyOneByQuery(tenantId, {
where: { transactionId },
include: [
{ model: TransactionEvent, as: Transaction.TRANSACTION_EVENTS_ALIAS, include: [EvseType] },
MeterValue,
],
});
}
async getTransactions(tenantId, dateFrom, dateTo, offset, limit) {
const queryOptions = {
where: {},
include: [
{ model: TransactionEvent, as: Transaction.TRANSACTION_EVENTS_ALIAS, include: [EvseType] },
MeterValue,
],
};
if (dateFrom) {
queryOptions.where.updatedAt = queryOptions.where.updatedAt || {};
queryOptions.where.updatedAt[Op.gte] = dateFrom;
}
if (dateTo) {
queryOptions.where.updatedAt = queryOptions.where.updatedAt || {};
queryOptions.where.updatedAt[Op.lt] = dateTo;
}
if (offset) {
queryOptions.offset = offset;
}
if (limit) {
queryOptions.limit = limit;
}
return this.transaction.readAllByQuery(tenantId, queryOptions);
}
async getTransactionsCount(tenantId, dateFrom, dateTo) {
const queryOptions = {
where: {},
};
if (dateFrom) {
queryOptions.where.updatedAt = queryOptions.where.updatedAt || {};
queryOptions.where.updatedAt[Op.gte] = dateFrom;
}
if (dateTo) {
queryOptions.where.updatedAt = queryOptions.where.updatedAt || {};
queryOptions.where.updatedAt[Op.lt] = dateTo;
}
return Transaction.count(queryOptions);
}
async readAllTransactionsByQuery(tenantId, query) {
return await this.transaction.readAllByQuery(tenantId, query);
}
async getEvseIdsWithActiveTransactionByStationId(tenantId, stationId) {
const activeTransactions = await this.transaction.readAllByQuery(tenantId, {
where: {
stationId: stationId,
isActive: true,
},
include: [Evse],
});
const evseIds = [];
activeTransactions.forEach((transaction) => {
const evseId = transaction.evse?.evseTypeId;
if (evseId) {
evseIds.push(evseId);
}
});
return evseIds;
}
async getActiveTransactionByStationIdAndEvseId(tenantId, stationId, evseId) {
return await this.transaction
.readAllByQuery(tenantId, {
where: {
stationId,
isActive: true,
},
include: [
{
model: TransactionEvent,
as: Transaction.TRANSACTION_EVENTS_ALIAS,
include: [EvseType],
},
MeterValue,
{ model: Evse, where: { evseTypeId: evseId }, required: true },
],
})
.then((transactions) => {
if (transactions.length > 1) {
transactions.sort((t1, t2) => t2.updatedAt.getTime() - t1.updatedAt.getTime());
}
return transactions[0];
});
}
async createMeterValue(tenantId, meterValue, transactionDatabaseId, transactionId, tariffId) {
await this.s.transaction(async (sequelizeTransaction) => {
const savedMeterValue = await MeterValue.create({
tenantId,
transactionDatabaseId: transactionDatabaseId,
transactionId,
tariffId,
...meterValue,
}, { transaction: sequelizeTransaction });
this.meterValue.emit('created', [savedMeterValue]);
});
}
async updateTransactionTotalCostById(tenantId, totalCost, id) {
await this.transaction.updateByKey(tenantId, { totalCost: totalCost }, id.toString());
}
async updateTransactionByMeterValues(tenantId, meterValues, stationId, transactionId) {
// Find existing transaction
const transaction = await this.readTransactionByStationIdAndTransactionId(tenantId, stationId, transactionId.toString());
if (!transaction) {
this.logger.error(`Transaction ${transactionId} on station ${stationId} does not exist.`);
return;
}
// Store meter values
await Promise.all(meterValues.map(async (meterValue) => {
meterValue.transactionDatabaseId = transaction.id;
meterValue.transactionId = transaction.transactionId;
meterValue.tariffId = transaction.tariffId;
await meterValue.save();
this.meterValue.emit('created', [meterValue]);
}));
// Update transaction total kWh
const allMeterValues = await this.meterValue.readAllByQuery(tenantId, {
where: {
transactionDatabaseId: transaction.id,
},
});
const meterValueTypes = allMeterValues.map((meterValue) => MeterValueMapper.toMeterValueType(meterValue));
await transaction.update({ totalKwh: MeterValueUtils.getTotalKwh(meterValueTypes) });
}
async createTransactionByStartTransaction(tenantId, request, stationId) {
return await this.s.transaction(async (sequelizeTransaction) => {
// Build StartTransaction event
let event = StartTransaction.build({
tenantId,
stationId,
...request,
});
// Associate Connector with StartTransaction
const connector = await this.connector.readOnlyOneByQuery(tenantId, {
where: {
connectorId: request.connectorId,
stationId,
},
include: [Tariff],
sequelizeTransaction,
});
if (!connector) {
this.logger.error(`Unable to find connector ${request.connectorId}.`);
throw new Error(`Unable to find connector ${request.connectorId}.`);
}
event.connectorDatabaseId = connector.id;
// Find Authorization by IdToken
const authorization = await Authorization.findOne({
where: {
tenantId,
idToken: request.idTag,
},
transaction: sequelizeTransaction,
});
if (!authorization) {
this.logger.warn(`Authorization with idToken ${request.idTag} does not exist.`);
}
// Generate transactionId
const transactionId = await this.chargingStationSequence.getNextSequenceValue(tenantId, stationId, ChargingStationSequenceTypeEnum.transactionId);
// Store transaction in db
let newTransaction = Transaction.build({
tenantId,
stationId,
evseId: connector.evseId,
connectorId: connector.id,
tariffId: connector.tariffs?.[0]?.id,
isActive: true,
transactionId: transactionId.toString(),
authorizationId: authorization ? authorization.id : null,
startTime: request.timestamp,
});
const chargingStation = await this.station.readByKey(tenantId, stationId);
if (chargingStation) {
if (chargingStation.locationId) {
newTransaction.set('locationId', chargingStation.locationId);
}
else {
this.logger.warn(`Charging station with stationId ${stationId} does not have a locationId. Transaction ${newTransaction.transactionId} will not be associated with a location, which may prevent it from being sent to upstream partners.`);
}
}
newTransaction = await newTransaction.save({ transaction: sequelizeTransaction });
// Store StartTransaction in db
event.transactionDatabaseId = newTransaction.id;
event = await event.save({ transaction: sequelizeTransaction });
this.startTransaction.emit('created', [event]);
// Return the new transaction with StartTransaction and IdToken
await newTransaction.reload({
include: [{ model: StartTransaction }],
transaction: sequelizeTransaction,
});
this.transaction.emit('created', [newTransaction]);
return newTransaction;
});
}
async createStopTransaction(tenantId, transactionDatabaseId, stationId, meterStop, timestamp, meterValues, reason) {
const transaction = await this.transaction.readOnlyOneByQuery(tenantId, {
where: { id: transactionDatabaseId },
include: [StartTransaction],
});
if (!transaction) {
this.logger.error(`Transaction with id ${transactionDatabaseId} not found.`);
throw new Error(`Transaction with id ${transactionDatabaseId} not found.`);
}
const stopTransaction = await StopTransaction.create({
tenantId,
stationId,
transactionDatabaseId,
meterStop,
timestamp: timestamp.toISOString(),
reason,
meterValues,
});
this.stopTransaction.emit('created', [stopTransaction]);
await transaction.update({
endTime: timestamp,
isActive: false,
});
if (meterValues.length > 0) {
await Promise.all(meterValues.map(async (meterValue) => {
meterValue.transactionDatabaseId = transactionDatabaseId;
meterValue.stopTransactionDatabaseId = stopTransaction.id;
await meterValue.save();
this.meterValue.emit('created', [meterValue]);
}));
}
return stopTransaction;
}
async updateTransactionByStationIdAndTransactionId(tenantId, transaction, transactionId, stationId) {
const transactions = await this.transaction.updateAllByQuery(tenantId, transaction, {
where: {
// unique constraint
transactionId,
stationId,
},
});
return transactions.length > 0 ? transactions[0] : undefined;
}
}
//# sourceMappingURL=TransactionEvent.js.map