UNPKG

@citrineos/data

Version:

The OCPP data module which includes all persistence layer implementation.

557 lines 25.7 kB
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