Files
helios-evcs/apps/csms/src/ocpp/actions/stop-transaction.ts
Timothy Yin 02a361488b feat(api): add stats chart endpoint for admin access with time series data
feat(dayjs): integrate dayjs for date handling and formatting across the application
refactor(routes): update date handling in id-tags, transactions, users, and dashboard routes to use dayjs
style(globals): improve CSS variable definitions for better readability and consistency
deps: add dayjs as a dependency for date manipulation
2026-03-11 21:34:21 +08:00

103 lines
3.1 KiB
TypeScript

import { eq, sql } from "drizzle-orm";
import dayjs from "dayjs";
import { useDrizzle } from "@/lib/db.js";
import { chargePoint, connector, idTag, meterValue, transaction } from "@/db/schema.js";
import type {
StopTransactionRequest,
StopTransactionResponse,
OcppConnectionContext,
} from "../types.ts";
import { resolveIdTagInfo } from "./authorize.ts";
export async function handleStopTransaction(
payload: StopTransactionRequest,
_ctx: OcppConnectionContext,
): Promise<StopTransactionResponse> {
const db = useDrizzle();
// Update the transaction record
const [tx] = await db
.update(transaction)
.set({
stopTimestamp: dayjs(payload.timestamp).toDate(),
stopMeterValue: payload.meterStop,
stopIdTag: payload.idTag ?? null,
stopReason: (payload.reason as (typeof transaction.$inferSelect)["stopReason"]) ?? null,
updatedAt: dayjs().toDate(),
})
.where(eq(transaction.id, payload.transactionId))
.returning();
if (!tx) {
console.warn(`[OCPP] StopTransaction: transaction ${payload.transactionId} not found`);
return {};
}
// Set connector back to Available
await db
.update(connector)
.set({ status: "Available", updatedAt: dayjs().toDate() })
.where(eq(connector.id, tx.connectorId));
// Store embedded meter values (transactionData)
if (payload.transactionData?.length) {
const records = payload.transactionData.flatMap((mv) =>
mv.sampledValue?.length
? [
{
id: crypto.randomUUID(),
transactionId: tx.id,
connectorId: tx.connectorId,
chargePointId: tx.chargePointId,
connectorNumber: tx.connectorNumber,
timestamp: dayjs(mv.timestamp).toDate(),
sampledValues:
mv.sampledValue as unknown as (typeof meterValue.$inferInsert)["sampledValues"],
},
]
: [],
);
if (records.length) {
await db.insert(meterValue).values(records);
}
}
const energyWh = payload.meterStop - tx.startMeterValue;
// Deduct balance from the idTag based on actual energy consumed
const [cp] = await db
.select({ feePerKwh: chargePoint.feePerKwh })
.from(chargePoint)
.where(eq(chargePoint.id, tx.chargePointId))
.limit(1);
const feeFen =
cp && cp.feePerKwh > 0 && energyWh > 0 ? Math.ceil((energyWh * cp.feePerKwh) / 1000) : 0;
// Always record the charge amount (0 if free)
await db
.update(transaction)
.set({ chargeAmount: feeFen, updatedAt: dayjs().toDate() })
.where(eq(transaction.id, tx.id));
if (feeFen > 0) {
await db
.update(idTag)
.set({
balance: sql`${idTag.balance} - ${feeFen}`,
updatedAt: dayjs().toDate(),
})
.where(eq(idTag.idTag, tx.idTag));
}
console.log(
`[OCPP] StopTransaction txId=${payload.transactionId} ` +
`reason=${payload.reason ?? "none"} energyWh=${energyWh} feeFen=${feeFen}`,
);
// Resolve idTagInfo for the stop tag if provided (no balance check — charging already occurred)
const idTagInfo = payload.idTag ? await resolveIdTagInfo(payload.idTag, false) : undefined;
return { idTagInfo };
}