import "server-only"; import { Types } from "mongoose"; import { hasHouseholdRole } from "@/features/households/roles"; import { resolveMembership } from "@/features/households/service"; import { tr } from "@/i18n/tr"; import { fail, ok, type ActionResult } from "@/lib/action-result"; import { writeAudit } from "@/lib/audit"; import { toObjectId } from "@/lib/object-id"; import { StoreModel } from "@/models/store"; import type { MealCardProvider } from "@/shared/accounts"; import type { StoreForm } from "@/shared/schemas/store"; import { foldTr } from "@/shared/search"; import { effortLevel, effortScore, sortStores, type EffortLevel, type StoreKind, } from "@/shared/stores"; /** * Mağazalar (Bölüm 6.11). Hane bazlı, hane içinde ortak. Okuma: viewer; yazma: member. * Zahmet skoru okuma sırasında hesaplanır (saf motor: shared/stores). */ const t = tr.stores; type StoreLean = { _id: Types.ObjectId; name: string; kind: StoreKind; branch: string; distanceM: number | null; carryDifficulty: number; deliveryFeeMinor: number | null; freeDeliveryMinMinor: number | null; mealCardAccepted: boolean; mealCardProviders: MealCardProvider[]; note: string; archivedAt: Date | null; }; export type StoreView = { id: string; name: string; kind: StoreKind; branch: string; /** "Migros · Kadıköy". */ label: string; distanceM: number | null; carryDifficulty: number; deliveryFeeMinor: number | null; freeDeliveryMinMinor: number | null; mealCardAccepted: boolean; mealCardProviders: MealCardProvider[]; note: string; archived: boolean; effort: number; effortLevel: EffortLevel; }; export const storeLabel = (s: { name: string; branch: string }) => s.branch ? `${s.name} · ${s.branch}` : s.name; function toView(s: StoreLean): StoreView { const effort = effortScore(s); return { id: String(s._id), name: s.name, kind: s.kind, branch: s.branch, label: storeLabel(s), distanceM: s.distanceM, carryDifficulty: s.carryDifficulty, deliveryFeeMinor: s.deliveryFeeMinor, freeDeliveryMinMinor: s.freeDeliveryMinMinor, mealCardAccepted: s.mealCardAccepted, mealCardProviders: s.mealCardProviders, note: s.note, archived: Boolean(s.archivedAt), effort, effortLevel: effortLevel(effort), }; } export async function listStores( userId: string, householdId: string, ): Promise<{ stores: StoreView[]; canWrite: boolean } | null> { const m = await resolveMembership(userId, householdId, "viewer"); if (!m) return null; const rows = await StoreModel.find({ householdId: m.householdId }).lean(); return { stores: sortStores(rows.map(toView), "name"), canWrite: hasHouseholdRole(m.role, "member"), }; } /** Aynı ad + şube (Türkçe katlanmış) aktif mağazalarda tektir. */ async function isDuplicate( householdId: Types.ObjectId, input: Pick, except?: Types.ObjectId, ) { const rows = await StoreModel.find({ householdId, nameKey: foldTr(input.name), archivedAt: null, ...(except ? { _id: { $ne: except } } : {}), }) .select({ branch: 1 }) .lean<{ branch: string }[]>(); return rows.some((r) => foldTr(r.branch) === foldTr(input.branch)); } const fields = (input: StoreForm) => ({ ...input, nameKey: foldTr(input.name) }); export async function createStore( actorId: string, householdId: string, input: StoreForm, ): Promise> { const m = await resolveMembership(actorId, householdId, "member"); if (!m) return fail(tr.common.unauthorized); if (await isDuplicate(m.householdId, input)) return fail(t.validation.duplicate, { name: t.validation.duplicate }); const actor = new Types.ObjectId(actorId); const doc = await StoreModel.create({ householdId: m.householdId, ...fields(input), createdBy: actor, updatedBy: actor, }); return ok({ id: String(doc._id) }); } export async function updateStore( actorId: string, householdId: string, storeId: string, input: StoreForm, ): Promise { const m = await resolveMembership(actorId, householdId, "member"); if (!m) return fail(tr.common.unauthorized); const id = toObjectId(storeId); const current = id ? await StoreModel.findOne({ _id: id, householdId: m.householdId }).lean() : null; if (!current) return fail(tr.common.notFound); if (current.archivedAt) return fail(t.validation.archived); if (await isDuplicate(m.householdId, input, current._id)) return fail(t.validation.duplicate, { name: t.validation.duplicate }); await StoreModel.updateOne( { _id: current._id, householdId: m.householdId }, { $set: { ...fields(input), updatedBy: new Types.ObjectId(actorId) } }, ); return ok(undefined); } export async function setStoreArchived( actorId: string, householdId: string, storeId: string, archived: boolean, ): Promise { const m = await resolveMembership(actorId, householdId, "member"); if (!m) return fail(tr.common.unauthorized); const id = toObjectId(storeId); const current = id ? await StoreModel.findOne({ _id: id, householdId: m.householdId }).lean() : null; if (!current) return fail(tr.common.notFound); if (!archived && (await isDuplicate(m.householdId, current, current._id))) return fail(t.validation.duplicate); const actor = new Types.ObjectId(actorId); await StoreModel.updateOne( { _id: current._id, householdId: m.householdId }, { $set: { archivedAt: archived ? new Date() : null, updatedBy: actor } }, ); await writeAudit({ action: archived ? "store.archive" : "store.unarchive", actorId: actor, householdId: m.householdId, targetType: "store", targetId: storeId, diff: { name: storeLabel(current) }, }); return ok(undefined); }