Files
fluxent-web/lib/actions/transaction.ts
T
SerinaNya c33857ffac feat(ledger): add bookkeeping workbench and transaction views
- Add transactions_dirty table and zero-tolerance cleansing action

- Implement bookkeeping workbench (/bookkeeping) with searchable Combobox channel selector and card suffix / account identifier

- Implement transaction ledger (/transactions) with date groupings, multi-currency metrics, and filters

- Update AGENTS.md guidelines for Base UI Select and Combobox bindings
2026-09-06 23:23:29 +08:00

120 lines
3.1 KiB
TypeScript

"use server"
import { and, desc, eq, isNull } from "drizzle-orm"
import { revalidatePath } from "next/cache"
import { auth } from "@/lib/auth"
import { db } from "@/lib/db"
import { channels, transactions, type Transaction } from "@/lib/db/schema"
async function requireUser() {
const session = await auth()
if (!session?.user?.id) {
throw new Error("请先登录")
}
return session.user.id
}
export type TransactionWithChannelDetails = Transaction & {
channelDetails: {
id: string
cardBrand: string | null
channelType: string
displayName: string
region: string | null
cardNumberSuffix: string | null
}[]
}
export async function getOfficialTransactionsAction(): Promise<{
success: boolean
data?: TransactionWithChannelDetails[]
error?: string
}> {
try {
const userId = await requireUser()
const officialTxns = await db
.select()
.from(transactions)
.where(
and(eq(transactions.userId, userId), isNull(transactions.deletedAt))
)
.orderBy(desc(transactions.txnDate), desc(transactions.createdAt))
const userChannels = await db
.select()
.from(channels)
.where(and(eq(channels.userId, userId), isNull(channels.deletedAt)))
const channelMap = new Map(userChannels.map((c) => [c.id, c]))
const result: TransactionWithChannelDetails[] = officialTxns.map((t) => {
const chDetails: TransactionWithChannelDetails["channelDetails"] = []
if (Array.isArray(t.refChannels)) {
for (const chId of t.refChannels) {
const ch = channelMap.get(chId)
if (ch) {
const name = ch.desc || ch.issuerName || ch.platform || "渠道"
chDetails.push({
id: ch.id,
cardBrand: ch.cardBrand,
channelType: ch.channelType,
displayName: name,
region: ch.region,
cardNumberSuffix: ch.cardNumberSuffix,
})
}
}
}
return {
...t,
channelDetails: chDetails,
}
})
return { success: true, data: result }
} catch (err) {
console.error("getOfficialTransactionsAction error:", err)
return {
success: false,
error: err instanceof Error ? err.message : "获取交易记录失败",
}
}
}
export async function deleteOfficialTransactionAction(
id: string
): Promise<{ success: boolean; error?: string }> {
try {
const userId = await requireUser()
const [deleted] = await db
.update(transactions)
.set({
deletedAt: new Date(),
updatedAt: new Date(),
})
.where(
and(
eq(transactions.id, id),
eq(transactions.userId, userId),
isNull(transactions.deletedAt)
)
)
.returning()
if (!deleted) {
return { success: false, error: "交易不存在或已删除" }
}
revalidatePath("/transactions")
revalidatePath("/")
return { success: true }
} catch (err) {
console.error("deleteOfficialTransactionAction error:", err)
return {
success: false,
error: err instanceof Error ? err.message : "删除交易失败",
}
}
}