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
103 lines
3.1 KiB
TypeScript
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 };
|
|
}
|