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:
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user