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
This commit is contained in:
98
apps/csms/src/routes/tariff.ts
Normal file
98
apps/csms/src/routes/tariff.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user