feat(transactions): işlem servisi, bakiye hesaplama, satıcı kuralları ve izolasyon

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-05 03:23:30 +03:00
parent d18c8a0d76
commit 1208e2d1eb
9 changed files with 1394 additions and 11 deletions
+105
View File
@@ -0,0 +1,105 @@
"use server";
import { revalidatePath } from "next/cache";
import { z } from "zod";
import { tr } from "@/i18n/tr";
import { fail, ok, type ActionResult } from "@/lib/action-result";
import { getHouseholdActionContext } from "@/lib/dal";
import { transactionFormSchema } from "@/shared/schemas/transaction";
import {
createTransaction,
getTransaction,
getTransactionFormOptions,
searchPayees,
setTransactionDeleted,
updateTransaction,
type PayeeSuggestion,
type TransactionDetail,
type TransactionFormOptions,
} from "./service";
const idSchema = z.string().regex(/^[a-f0-9]{24}$/i);
/** zod hatalarını alan bazlı mesajlara çevirir ("splits.1.amount" gibi yollar korunur). */
function fieldErrors(error: z.ZodError) {
const out: Record<string, string> = {};
for (const issue of error.issues) {
const key = issue.path.join(".");
if (key && !out[key]) out[key] = issue.message;
}
return out;
}
function revalidate() {
// Bakiye, liste ve (ileride) dashboard aynı veriden beslenir.
revalidatePath("/", "layout");
}
export async function createTransactionAction(
input: unknown,
): Promise<ActionResult<{ id: string }>> {
const ctx = await getHouseholdActionContext("member");
if (!ctx) return fail(tr.common.unauthorized);
const parsed = transactionFormSchema.safeParse(input);
if (!parsed.success) return fail(tr.adminUsers.errors.checkFields, fieldErrors(parsed.error));
const result = await createTransaction(ctx.user.id, ctx.household.id, parsed.data);
if (result.ok) revalidate();
return result;
}
export async function updateTransactionAction(
transactionId: unknown,
input: unknown,
): Promise<ActionResult> {
const ctx = await getHouseholdActionContext("member");
if (!ctx) return fail(tr.common.unauthorized);
const id = idSchema.safeParse(transactionId);
if (!id.success) return fail(tr.common.notFound);
const parsed = transactionFormSchema.safeParse(input);
if (!parsed.success) return fail(tr.adminUsers.errors.checkFields, fieldErrors(parsed.error));
const result = await updateTransaction(ctx.user.id, ctx.household.id, id.data, parsed.data);
if (result.ok) revalidate();
return result;
}
export async function setTransactionDeletedAction(
transactionId: unknown,
deleted: unknown,
): Promise<ActionResult> {
const ctx = await getHouseholdActionContext("member");
if (!ctx) return fail(tr.common.unauthorized);
const id = idSchema.safeParse(transactionId);
const d = z.boolean().safeParse(deleted);
if (!id.success || !d.success) return fail(tr.common.genericError);
const result = await setTransactionDeleted(ctx.user.id, ctx.household.id, id.data, d.data);
if (result.ok) revalidate();
return result;
}
/** Form açılırken: yazılabilir hesaplar ve kategoriler (hızlı ekle her ekrandan çağırır). */
export async function getTransactionFormOptionsAction(): Promise<
ActionResult<TransactionFormOptions>
> {
const ctx = await getHouseholdActionContext("member");
if (!ctx) return fail(tr.common.unauthorized);
const options = await getTransactionFormOptions(ctx.user.id, ctx.household.id);
return options ? ok(options) : fail(tr.common.unauthorized);
}
export async function getTransactionAction(
transactionId: unknown,
): Promise<ActionResult<TransactionDetail>> {
const ctx = await getHouseholdActionContext("viewer");
if (!ctx) return fail(tr.common.unauthorized);
const id = idSchema.safeParse(transactionId);
if (!id.success) return fail(tr.common.notFound);
const detail = await getTransaction(ctx.user.id, ctx.household.id, id.data);
return detail ? ok(detail) : fail(tr.common.notFound);
}
export async function searchPayeesAction(query: unknown): Promise<PayeeSuggestion[]> {
const ctx = await getHouseholdActionContext("member");
const q = z.string().max(80).safeParse(query);
if (!ctx || !q.success) return [];
return searchPayees(ctx.user.id, ctx.household.id, q.data);
}