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 { 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 }; }