Restore from Gitea ZIP snapshot (12.08.2026) after full instance reinstall
Git history was lost when the previous Gitea instance was wiped and reinstalled due to an unresolved corruption bug — this commit is the last known-good file content, exported before the reinstall. Prior commit history is not recoverable through this path.
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
import { eq, desc } from "drizzle-orm";
|
||||
import { db } from "@/lib/db/client";
|
||||
import { wallets, coinTransactions } from "@/lib/db/schema";
|
||||
|
||||
export type CoinReason = "signup_bonus" | "manual_save" | "purchase";
|
||||
|
||||
export interface WalletDTO {
|
||||
userId: string;
|
||||
balance: number;
|
||||
}
|
||||
|
||||
export interface CoinTransactionDTO {
|
||||
id: string;
|
||||
amount: number;
|
||||
reason: CoinReason;
|
||||
relatedGameId: string | null;
|
||||
createdAt: number;
|
||||
}
|
||||
|
||||
export async function getOrCreateWallet(userId: string): Promise<WalletDTO> {
|
||||
const existing = db.select().from(wallets).where(eq(wallets.userId, userId)).get();
|
||||
if (existing) return existing;
|
||||
|
||||
db.insert(wallets).values({ userId, balance: 0 }).onConflictDoNothing().run();
|
||||
const created = db.select().from(wallets).where(eq(wallets.userId, userId)).get();
|
||||
return created!;
|
||||
}
|
||||
|
||||
export async function getBalance(userId: string): Promise<number> {
|
||||
const wallet = await getOrCreateWallet(userId);
|
||||
return wallet.balance;
|
||||
}
|
||||
|
||||
export async function listRecentTransactions(userId: string, limit = 20): Promise<CoinTransactionDTO[]> {
|
||||
const rows = await db.query.coinTransactions.findMany({
|
||||
where: eq(coinTransactions.userId, userId),
|
||||
orderBy: desc(coinTransactions.createdAt),
|
||||
limit,
|
||||
});
|
||||
return rows.map((row) => ({
|
||||
id: row.id,
|
||||
amount: row.amount,
|
||||
reason: row.reason,
|
||||
relatedGameId: row.relatedGameId,
|
||||
createdAt: row.createdAt.getTime(),
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Credits coins and logs the ledger entry atomically. The better-sqlite3
|
||||
* driver's db.transaction() callback must stay fully synchronous — no
|
||||
* await inside — or the transaction commits before the awaited work
|
||||
* finishes; every query here uses the sync .get()/.run() methods.
|
||||
*/
|
||||
export async function grantCoins(
|
||||
userId: string,
|
||||
amount: number,
|
||||
reason: CoinReason,
|
||||
relatedGameId?: string,
|
||||
): Promise<void> {
|
||||
await getOrCreateWallet(userId);
|
||||
|
||||
db.transaction((tx) => {
|
||||
const wallet = tx.select().from(wallets).where(eq(wallets.userId, userId)).get();
|
||||
const balance = wallet?.balance ?? 0;
|
||||
tx.update(wallets)
|
||||
.set({ balance: balance + amount, updatedAt: new Date() })
|
||||
.where(eq(wallets.userId, userId))
|
||||
.run();
|
||||
tx.insert(coinTransactions)
|
||||
.values({ userId, amount, reason, relatedGameId: relatedGameId ?? null })
|
||||
.run();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Debits coins and logs the ledger entry atomically — returns false without
|
||||
* writing anything if the balance is insufficient. This is the one place
|
||||
* correctness really matters: the balance check and the debit happen inside
|
||||
* the same transaction, so two concurrent spends can't both pass the check
|
||||
* against a stale balance.
|
||||
*/
|
||||
export async function spendCoins(
|
||||
userId: string,
|
||||
amount: number,
|
||||
reason: CoinReason,
|
||||
relatedGameId?: string,
|
||||
): Promise<boolean> {
|
||||
await getOrCreateWallet(userId);
|
||||
|
||||
return db.transaction((tx) => {
|
||||
const wallet = tx.select().from(wallets).where(eq(wallets.userId, userId)).get();
|
||||
const balance = wallet?.balance ?? 0;
|
||||
if (balance < amount) return false;
|
||||
|
||||
tx.update(wallets)
|
||||
.set({ balance: balance - amount, updatedAt: new Date() })
|
||||
.where(eq(wallets.userId, userId))
|
||||
.run();
|
||||
tx.insert(coinTransactions)
|
||||
.values({ userId, amount: -amount, reason, relatedGameId: relatedGameId ?? null })
|
||||
.run();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user