feat(dashboard): add transactions and users management pages with CRUD functionality
feat(auth): implement login page and authentication middleware feat(sidebar): create sidebar component with user info and navigation links feat(api): establish API client for interacting with backend services
This commit is contained in:
101
apps/csms/src/ocpp/actions/stop-transaction.ts
Normal file
101
apps/csms/src/ocpp/actions/stop-transaction.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { eq, sql } from "drizzle-orm";
|
||||
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: new Date(payload.timestamp),
|
||||
stopMeterValue: payload.meterStop,
|
||||
stopIdTag: payload.idTag ?? null,
|
||||
stopReason: (payload.reason as (typeof transaction.$inferSelect)["stopReason"]) ?? null,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.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: new Date() })
|
||||
.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: new Date(mv.timestamp),
|
||||
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: new Date() })
|
||||
.where(eq(transaction.id, tx.id));
|
||||
|
||||
if (feeFen > 0) {
|
||||
await db
|
||||
.update(idTag)
|
||||
.set({
|
||||
balance: sql`${idTag.balance} - ${feeFen}`,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.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 };
|
||||
}
|
||||
Reference in New Issue
Block a user