2e2e33233f
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
137 lines
5.3 KiB
TypeScript
137 lines
5.3 KiB
TypeScript
"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 { reconcileSchema } from "@/shared/schemas/reconciliation";
|
||
import { transactionFormSchema } from "@/shared/schemas/transaction";
|
||
import {
|
||
createTransaction,
|
||
getTransaction,
|
||
getTransactionFormOptions,
|
||
searchPayees,
|
||
setTransactionDeleted,
|
||
updateTransaction,
|
||
type PayeeSuggestion,
|
||
type TransactionDetail,
|
||
type TransactionFormOptions,
|
||
} from "./service";
|
||
import { previewReconciliation, reconcileAccount, type ReconcilePreview } from "./statement";
|
||
|
||
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);
|
||
}
|
||
|
||
/** Mutabakat önizlemesi: ekstre bakiyesi ile uygulama bakiyesi arasındaki fark. */
|
||
export async function previewReconciliationAction(
|
||
accountId: unknown,
|
||
input: unknown,
|
||
): Promise<ActionResult<ReconcilePreview>> {
|
||
const ctx = await getHouseholdActionContext("member");
|
||
if (!ctx) return fail(tr.common.unauthorized);
|
||
const id = idSchema.safeParse(accountId);
|
||
if (!id.success) return fail(tr.common.notFound);
|
||
const parsed = reconcileSchema.safeParse(input);
|
||
if (!parsed.success) return fail(tr.adminUsers.errors.checkFields, fieldErrors(parsed.error));
|
||
return previewReconciliation(ctx.user.id, ctx.household.id, id.data, parsed.data);
|
||
}
|
||
|
||
export async function reconcileAccountAction(
|
||
accountId: unknown,
|
||
input: unknown,
|
||
): Promise<ActionResult<{ adjustmentId: string | null; markedCount: number }>> {
|
||
const ctx = await getHouseholdActionContext("member");
|
||
if (!ctx) return fail(tr.common.unauthorized);
|
||
const id = idSchema.safeParse(accountId);
|
||
if (!id.success) return fail(tr.common.notFound);
|
||
const parsed = reconcileSchema.safeParse(input);
|
||
if (!parsed.success) return fail(tr.adminUsers.errors.checkFields, fieldErrors(parsed.error));
|
||
const result = await reconcileAccount(ctx.user.id, ctx.household.id, id.data, parsed.data);
|
||
if (result.ok) revalidate();
|
||
return result;
|
||
}
|