Files
helios-evcs/apps/csms/src/routes/tariff.ts
Timothy Yin f7ee298060 feat(charge-points): add pricing mode for charge points with validation
feat(pricing): implement tariff management with peak, valley, and flat pricing
feat(api): add tariff API for fetching and updating pricing configurations
feat(tariff-schema): create database schema for tariff configuration
feat(pricing-page): create UI for displaying and managing pricing tiers
fix(sidebar): update sidebar to include pricing settings link
2026-03-12 17:23:06 +08:00

99 lines
2.9 KiB
TypeScript

import { Hono } from "hono";
import { eq } from "drizzle-orm";
import { useDrizzle } from "@/lib/db.js";
import { tariff } from "@/db/schema.js";
import type { HonoEnv } from "@/types/hono.ts";
import type { PriceTier, TariffSlot } from "@/db/tariff-schema.ts";
const app = new Hono<HonoEnv>();
/** GET /api/tariff — 返回当前生效的电价配置(任何已登录用户) */
app.get("/", async (c) => {
const db = useDrizzle();
const [row] = await db.select().from(tariff).where(eq(tariff.isActive, true)).limit(1);
if (!row) return c.json(null);
return c.json(rowToPayload(row));
});
/** PUT /api/tariff — 更新/初始化电价配置(仅管理员) */
app.put("/", async (c) => {
const currentUser = c.get("user");
if (currentUser?.role !== "admin") {
return c.json({ error: "Forbidden" }, 403);
}
type TierPricing = { electricityPrice: number; serviceFee: number };
type TariffBody = {
slots: TariffSlot[];
prices: Record<PriceTier, TierPricing>;
};
let body: TariffBody;
try {
body = await c.req.json<TariffBody>();
} catch {
return c.json({ error: "Invalid JSON" }, 400);
}
const { slots, prices } = body;
if (!Array.isArray(slots) || !prices) {
return c.json({ error: "Missing slots or prices" }, 400);
}
const db = useDrizzle();
// 停用旧配置
await db.update(tariff).set({ isActive: false });
const [row] = await db
.insert(tariff)
.values({
id: crypto.randomUUID(),
slots,
peakElectricityPrice: fenFromCny(prices.peak?.electricityPrice),
peakServiceFee: fenFromCny(prices.peak?.serviceFee),
valleyElectricityPrice: fenFromCny(prices.valley?.electricityPrice),
valleyServiceFee: fenFromCny(prices.valley?.serviceFee),
flatElectricityPrice: fenFromCny(prices.flat?.electricityPrice),
flatServiceFee: fenFromCny(prices.flat?.serviceFee),
isActive: true,
})
.returning();
return c.json(rowToPayload(row));
});
// ── helpers ────────────────────────────────────────────────────────────────
function fenFromCny(cny: number | undefined): number {
if (typeof cny !== "number" || isNaN(cny)) return 0;
return Math.round(cny * 100);
}
function rowToPayload(row: typeof tariff.$inferSelect) {
return {
id: row.id,
slots: row.slots as TariffSlot[],
prices: {
peak: {
electricityPrice: row.peakElectricityPrice / 100,
serviceFee: row.peakServiceFee / 100,
},
valley: {
electricityPrice: row.valleyElectricityPrice / 100,
serviceFee: row.valleyServiceFee / 100,
},
flat: {
electricityPrice: row.flatElectricityPrice / 100,
serviceFee: row.flatServiceFee / 100,
},
},
createdAt: row.createdAt,
updatedAt: row.updatedAt,
};
}
export default app;