feat(backend): complete AI and business migration

This commit is contained in:
poyrazavsever
2026-07-17 09:12:45 +03:00
parent 42f165b27e
commit 64a5f96872
23 changed files with 1286 additions and 409 deletions
+152
View File
@@ -0,0 +1,152 @@
import type { DomainActor } from "../domain/actor";
import { DomainError } from "../domain/errors";
import type { DomainService } from "../services/domain";
const MAX_CONTEXT_CHARS = 16_000;
export function buildChatContext(
service: DomainService,
actor: DomainActor,
now = new Date(),
): string {
const since = daysAgo(now, 30);
const tasks = service.listTasks(actor).slice(0, 20);
const projects = service.listProjects(actor).slice(0, 12);
const clients = new Map(service.listClients(actor).map((client) => [client.id, client.name]));
const finance = service
.listFinanceTransactions(actor)
.filter((item) => item.transactionDate >= since)
.slice(0, 20);
const journal = service
.listJournalEntries(actor)
.filter((item) => item.entryDate >= since)
.slice(0, 14);
return capContext([
section("Görevler", tasks.map((task) => ({
title: task.title,
status: task.status,
priority: task.priority,
dueAt: task.dueAt?.toISOString() ?? null,
}))),
section("Projeler", projects.map((project) => ({
name: project.name,
client: project.clientId ? clients.get(project.clientId) ?? null : null,
status: project.status,
progress: project.progress,
dueDate: project.dueDate,
}))),
section("Son 30 gün finans", finance.map((item) => ({
type: item.type,
amount: minorToMajor(item.amountMinor),
currency: item.currency,
category: item.category,
paymentStatus: item.paymentStatus,
date: item.transactionDate,
}))),
section("Son günlük kayıtlar", journal.map((entry) => ({
date: entry.entryDate,
mood: entry.moodScore,
energy: entry.energyScore,
workSatisfaction: entry.workSatisfactionScore,
note: entry.note,
}))),
].join("\n\n"));
}
export function buildFinanceAnalysisContext(
service: DomainService,
actor: DomainActor,
now = new Date(),
): { hasData: boolean; text: string } {
const since = daysAgo(now, 30);
const transactions = service
.listFinanceTransactions(actor)
.filter((item) => item.transactionDate >= since)
.slice(0, 200);
const totals = new Map<string, { incomeMinor: number; expenseMinor: number }>();
for (const transaction of transactions) {
const current = totals.get(transaction.currency) ?? { incomeMinor: 0, expenseMinor: 0 };
if (transaction.type === "income") current.incomeMinor += transaction.amountMinor;
else current.expenseMinor += transaction.amountMinor;
totals.set(transaction.currency, current);
}
return {
hasData: transactions.length > 0,
text: capContext([
"Kullanıcının son 30 günlük finansal durumu:",
...Array.from(totals, ([currency, value]) =>
`- ${currency}: gelir ${minorToMajor(value.incomeMinor)}, gider ${minorToMajor(value.expenseMinor)}, net ${minorToMajor(value.incomeMinor - value.expenseMinor)}`,
),
`- İşlem sayısı: ${transactions.length}`,
"İşlemler:",
...transactions.map((item) =>
`- ${item.transactionDate} | ${item.type === "income" ? "Gelir" : "Gider"} | ${clean(item.category) || "Kategorisiz"} | ${minorToMajor(item.amountMinor)} ${item.currency}`,
),
].join("\n")),
};
}
export function buildProjectRiskContext(
service: DomainService,
actor: DomainActor,
projectId?: string,
): string {
const projects = service.listProjects(actor);
const clients = new Map(service.listClients(actor).map((client) => [client.id, client.name]));
if (projectId) {
const project = service.getProject(actor, projectId);
const tasks = service.listTasks(actor, project.id);
const completed = tasks.filter((task) => task.status === "done").length;
return capContext([
`Proje adı: ${clean(project.name)}`,
`Müşteri: ${project.clientId ? clean(clients.get(project.clientId) ?? "Bilinmiyor") : "Yok"}`,
`Durum: ${project.status}`,
`Bütçe: ${project.budgetAmountMinor == null ? "Bilinmiyor" : minorToMajor(project.budgetAmountMinor)} ${project.currency}`,
`İlerleme: %${project.progress}`,
`Başlangıç: ${project.startDate ?? "Bilinmiyor"}`,
`Bitiş: ${project.dueDate ?? "Bilinmiyor"}`,
`Görevler: ${tasks.length} adet (${completed} tamamlandı)`,
].join("\n"));
}
const activeProjects = projects.filter((project) => project.status === "active").slice(0, 50);
if (activeProjects.length === 0) {
throw new DomainError("NOT_FOUND", "Aktif proje bulunamadı.");
}
return capContext([
"Aktif projeler:",
...activeProjects.map((project) =>
`- ${clean(project.name)} | İlerleme: %${project.progress} | Bitiş: ${project.dueDate ?? "Yok"}`,
),
].join("\n"));
}
function section(title: string, rows: unknown[]): string {
if (rows.length === 0) return `${title}: kayıt yok.`;
return `${title}:\n${rows.map((row) => `- ${JSON.stringify(row)}`).join("\n")}`;
}
function daysAgo(now: Date, days: number): string {
const date = new Date(now);
date.setUTCDate(date.getUTCDate() - days);
return date.toISOString().slice(0, 10);
}
function minorToMajor(value: number): string {
return (value / 100).toFixed(2);
}
function clean(value: string | null | undefined): string {
return (value ?? "").replace(/\s+/g, " ").trim();
}
function capContext(value: string): string {
return value.length <= MAX_CONTEXT_CHARS
? value
: `${value.slice(0, MAX_CONTEXT_CHARS)}\n[Bağlam boyut sınırı nedeniyle kısaltıldı.]`;
}
+80
View File
@@ -0,0 +1,80 @@
import "server-only";
import { createGoogleGenerativeAI } from "@ai-sdk/google";
import { createGroq } from "@ai-sdk/groq";
import { createOpenAI } from "@ai-sdk/openai";
import type { LanguageModel } from "ai";
import { getServerConfig } from "../config";
import type { DomainActor } from "../domain/actor";
import { DomainError } from "../domain/errors";
import { getAiRuntimeSettings } from "../settings/ai";
const defaultModels = {
gemini: "gemini-1.5-pro-latest",
groq: "llama-3.1-8b-instant",
openai: "gpt-4o",
ollama: "llama3.2",
} as const;
export type AiRuntime = {
model: LanguageModel;
provider: keyof typeof defaultModels;
modelName: string;
timeout: number;
};
export function getAiRuntime(actor: DomainActor): AiRuntime {
const settings = getAiRuntimeSettings(actor);
const modelName = settings.model ?? defaultModels[settings.provider];
const config = getServerConfig();
if (settings.provider !== "ollama" && !settings.apiKey) {
throw new DomainError(
"VALIDATION_ERROR",
"Ayarlardan bir AI sağlayıcısı ve API anahtarı seçmelisiniz.",
);
}
let model: LanguageModel;
switch (settings.provider) {
case "gemini":
model = createGoogleGenerativeAI({ apiKey: settings.apiKey ?? "" })(modelName);
break;
case "groq":
model = createGroq({ apiKey: settings.apiKey ?? "" })(modelName);
break;
case "ollama":
model = createOpenAI({
apiKey: "ollama",
baseURL: config.ollamaBaseUrl,
})(modelName);
break;
default:
model = createOpenAI({ apiKey: settings.apiKey ?? "" })(modelName);
}
return {
model,
provider: settings.provider,
modelName,
timeout: config.aiRequestTimeoutMs,
};
}
export function normalizeAiError(error: unknown): DomainError {
if (error instanceof DomainError) return error;
const name = error instanceof Error ? error.name : "";
if (name === "AbortError" || name === "TimeoutError") {
return new DomainError(
"UPSTREAM_TIMEOUT",
"AI sağlayıcısı zamanında yanıt vermedi. Lütfen tekrar deneyin.",
);
}
console.error("AI provider request failed", error);
return new DomainError(
"UPSTREAM_ERROR",
"AI sağlayıcısına ulaşılamadı. Sağlayıcı ayarlarını kontrol edip tekrar deneyin.",
);
}
+12
View File
@@ -0,0 +1,12 @@
import "server-only";
import { NextResponse } from "next/server";
import { normalizeAiError } from "./provider";
export function aiJsonError(error: unknown): NextResponse {
const normalized = normalizeAiError(error);
return NextResponse.json(
{ error: normalized.message, code: normalized.code },
{ status: normalized.status },
);
}
+6
View File
@@ -14,6 +14,8 @@ const envSchema = z.object({
TRUSTED_ORIGINS: z.string().trim().optional(),
DATA_DIR: z.string().trim().optional(),
DATABASE_PATH: z.string().trim().optional(),
OLLAMA_BASE_URL: z.string().url().optional(),
AI_REQUEST_TIMEOUT_MS: z.coerce.number().int().min(1_000).max(120_000).optional(),
});
export type ServerConfig = {
@@ -27,6 +29,8 @@ export type ServerConfig = {
trustedOrigins: string[];
secureCookies: boolean;
betterAuthSecret?: string;
ollamaBaseUrl: string;
aiRequestTimeoutMs: number;
};
let cachedConfig: ServerConfig | undefined;
@@ -76,6 +80,8 @@ export function getServerConfig(): ServerConfig {
trustedOrigins,
secureCookies,
betterAuthSecret,
ollamaBaseUrl: parsed.OLLAMA_BASE_URL ?? "http://127.0.0.1:11434/v1",
aiRequestTimeoutMs: parsed.AI_REQUEST_TIMEOUT_MS ?? 30_000,
};
return cachedConfig;
+5 -1
View File
@@ -4,7 +4,9 @@ export type DomainErrorCode =
| "FORBIDDEN"
| "NOT_FOUND"
| "CONFLICT"
| "INVARIANT_VIOLATION";
| "INVARIANT_VIOLATION"
| "UPSTREAM_ERROR"
| "UPSTREAM_TIMEOUT";
const statusByCode: Record<DomainErrorCode, number> = {
VALIDATION_ERROR: 400,
@@ -13,6 +15,8 @@ const statusByCode: Record<DomainErrorCode, number> = {
NOT_FOUND: 404,
CONFLICT: 409,
INVARIANT_VIOLATION: 422,
UPSTREAM_ERROR: 502,
UPSTREAM_TIMEOUT: 504,
};
export class DomainError extends Error {
+4
View File
@@ -245,6 +245,7 @@ export const proposalCreateSchema = z.object({
status: z.enum(proposalStatuses).default("draft"),
validUntil: z.date().nullable().optional(),
});
export const proposalUpdateSchema = proposalCreateSchema.omit({ id: true }).partial();
export const contractCreateSchema = z.object({
id: resourceIdSchema.optional(),
proposalId: optionalId,
@@ -254,6 +255,7 @@ export const contractCreateSchema = z.object({
status: z.enum(contractStatuses).default("draft"),
signedAt: z.date().nullable().optional(),
});
export const contractUpdateSchema = contractCreateSchema.omit({ id: true }).partial();
export const invoiceCreateSchema = z.object({
id: resourceIdSchema.optional(),
clientId: optionalId,
@@ -267,6 +269,7 @@ export const invoiceCreateSchema = z.object({
dueDate: optionalDate,
paidAt: z.date().nullable().optional(),
});
export const invoiceUpdateSchema = invoiceCreateSchema.omit({ id: true }).partial();
export const subscriptionCreateSchema = z.object({
id: resourceIdSchema.optional(),
name: z.string().trim().min(1).max(300),
@@ -277,6 +280,7 @@ export const subscriptionCreateSchema = z.object({
status: z.enum(subscriptionStatuses).default("active"),
category: optionalText(160),
});
export const subscriptionUpdateSchema = subscriptionCreateSchema.omit({ id: true }).partial();
export function parseDomainInput<TSchema extends z.ZodType>(
schema: TSchema,
+16
View File
@@ -167,15 +167,31 @@ export function createDomainRepositories(db: DomainDatabase) {
listSessions: (scope: OwnerScope) => db.select().from(chatSessions).where(eq(chatSessions.ownerUserId, scope.ownerUserId)).orderBy(desc(chatSessions.updatedAt)).all(),
getSession: (scope: OwnerScope, id: string) => db.select().from(chatSessions).where(and(eq(chatSessions.id, id), eq(chatSessions.ownerUserId, scope.ownerUserId))).get(),
createSession: (scope: OwnerScope, value: Omit<typeof chatSessions.$inferInsert, "ownerUserId">) => db.insert(chatSessions).values({ ...value, ownerUserId: scope.ownerUserId }).returning().get(),
removeSession: (scope: OwnerScope, id: string) => db.delete(chatSessions).where(and(eq(chatSessions.id, id), eq(chatSessions.ownerUserId, scope.ownerUserId))).returning().get(),
listMessages: (scope: OwnerScope, sessionId: string) => db.select({ message: chatMessages }).from(chatMessages).innerJoin(chatSessions, eq(chatMessages.sessionId, chatSessions.id)).where(and(eq(chatMessages.sessionId, sessionId), eq(chatSessions.ownerUserId, scope.ownerUserId))).orderBy(asc(chatMessages.createdAt)).all().map(({ message }) => message),
createMessage: (value: typeof chatMessages.$inferInsert) => db.insert(chatMessages).values(value).returning().get(),
},
business: {
listProposals: (scope: OwnerScope) => db.select().from(proposals).where(eq(proposals.ownerUserId, scope.ownerUserId)).orderBy(desc(proposals.createdAt)).all(),
getProposal: (scope: OwnerScope, id: string) => db.select().from(proposals).where(and(eq(proposals.id, id), eq(proposals.ownerUserId, scope.ownerUserId))).get(),
createProposal: (scope: OwnerScope, value: Omit<typeof proposals.$inferInsert, "ownerUserId">) => db.insert(proposals).values({ ...value, ownerUserId: scope.ownerUserId }).returning().get(),
updateProposal: (scope: OwnerScope, id: string, value: Partial<typeof proposals.$inferInsert>) => db.update(proposals).set({ ...value, updatedAt: new Date() }).where(and(eq(proposals.id, id), eq(proposals.ownerUserId, scope.ownerUserId))).returning().get(),
removeProposal: (scope: OwnerScope, id: string) => db.delete(proposals).where(and(eq(proposals.id, id), eq(proposals.ownerUserId, scope.ownerUserId))).returning().get(),
listContracts: (scope: OwnerScope) => db.select().from(contracts).where(eq(contracts.ownerUserId, scope.ownerUserId)).orderBy(desc(contracts.createdAt)).all(),
getContract: (scope: OwnerScope, id: string) => db.select().from(contracts).where(and(eq(contracts.id, id), eq(contracts.ownerUserId, scope.ownerUserId))).get(),
createContract: (scope: OwnerScope, value: Omit<typeof contracts.$inferInsert, "ownerUserId">) => db.insert(contracts).values({ ...value, ownerUserId: scope.ownerUserId }).returning().get(),
updateContract: (scope: OwnerScope, id: string, value: Partial<typeof contracts.$inferInsert>) => db.update(contracts).set({ ...value, updatedAt: new Date() }).where(and(eq(contracts.id, id), eq(contracts.ownerUserId, scope.ownerUserId))).returning().get(),
removeContract: (scope: OwnerScope, id: string) => db.delete(contracts).where(and(eq(contracts.id, id), eq(contracts.ownerUserId, scope.ownerUserId))).returning().get(),
listInvoices: (scope: OwnerScope) => db.select().from(invoices).where(eq(invoices.ownerUserId, scope.ownerUserId)).orderBy(desc(invoices.createdAt)).all(),
getInvoice: (scope: OwnerScope, id: string) => db.select().from(invoices).where(and(eq(invoices.id, id), eq(invoices.ownerUserId, scope.ownerUserId))).get(),
createInvoice: (scope: OwnerScope, value: Omit<typeof invoices.$inferInsert, "ownerUserId">) => db.insert(invoices).values({ ...value, ownerUserId: scope.ownerUserId }).returning().get(),
updateInvoice: (scope: OwnerScope, id: string, value: Partial<typeof invoices.$inferInsert>) => db.update(invoices).set({ ...value, updatedAt: new Date() }).where(and(eq(invoices.id, id), eq(invoices.ownerUserId, scope.ownerUserId))).returning().get(),
removeInvoice: (scope: OwnerScope, id: string) => db.delete(invoices).where(and(eq(invoices.id, id), eq(invoices.ownerUserId, scope.ownerUserId))).returning().get(),
listSubscriptions: (scope: OwnerScope) => db.select().from(subscriptions).where(eq(subscriptions.ownerUserId, scope.ownerUserId)).orderBy(desc(subscriptions.createdAt)).all(),
getSubscription: (scope: OwnerScope, id: string) => db.select().from(subscriptions).where(and(eq(subscriptions.id, id), eq(subscriptions.ownerUserId, scope.ownerUserId))).get(),
createSubscription: (scope: OwnerScope, value: Omit<typeof subscriptions.$inferInsert, "ownerUserId">) => db.insert(subscriptions).values({ ...value, ownerUserId: scope.ownerUserId }).returning().get(),
updateSubscription: (scope: OwnerScope, id: string, value: Partial<typeof subscriptions.$inferInsert>) => db.update(subscriptions).set({ ...value, updatedAt: new Date() }).where(and(eq(subscriptions.id, id), eq(subscriptions.ownerUserId, scope.ownerUserId))).returning().get(),
removeSubscription: (scope: OwnerScope, id: string) => db.delete(subscriptions).where(and(eq(subscriptions.id, id), eq(subscriptions.ownerUserId, scope.ownerUserId))).returning().get(),
},
analytics: {
summary: (scope: OwnerScope) => db.select({
+140 -4
View File
@@ -17,9 +17,11 @@ import {
clientCreateSchema,
clientUpdateSchema,
contractCreateSchema,
contractUpdateSchema,
financeTransactionCreateSchema,
financeTransactionUpdateSchema,
invoiceCreateSchema,
invoiceUpdateSchema,
journalEntrySchema,
parseDomainInput,
planningSectionCreateSchema,
@@ -27,9 +29,11 @@ import {
projectCreateSchema,
projectUpdateSchema,
proposalCreateSchema,
proposalUpdateSchema,
revisionCreateSchema,
revisionStatusSchema,
subscriptionCreateSchema,
subscriptionUpdateSchema,
taskCreateSchema,
taskUpdateSchema,
calendarEventUpdateSchema,
@@ -355,6 +359,26 @@ export class DomainService {
return this.repositories.chat.createSession(scope, { ...value, id: value.id ?? this.id() });
}
listChatSessions(actor: DomainActor) {
return this.repositories.chat.listSessions(requireOwnerScope(actor));
}
getChatSession(actor: DomainActor, sessionId: string) {
return this.repositories.chat.getSession(requireOwnerScope(actor), sessionId)
?? this.throwNotFound("Sohbet");
}
listChatMessages(actor: DomainActor, sessionId: string) {
const scope = requireOwnerScope(actor);
if (!this.repositories.chat.getSession(scope, sessionId)) throw notFound("Sohbet");
return this.repositories.chat.listMessages(scope, sessionId);
}
deleteChatSession(actor: DomainActor, sessionId: string) {
return this.repositories.chat.removeSession(requireOwnerScope(actor), sessionId)
?? this.throwNotFound("Sohbet");
}
addChatMessage(actor: DomainActor, input: unknown) {
const scope = requireOwnerScope(actor);
const value = parseDomainInput(chatMessageCreateSchema, input);
@@ -467,16 +491,61 @@ export class DomainService {
return this.repositories.business.createProposal(scope, { ...value, id: value.id ?? this.id() });
}
listProposals(actor: DomainActor) {
return this.repositories.business.listProposals(requireOwnerScope(actor));
}
getProposal(actor: DomainActor, proposalId: string) {
return this.repositories.business.getProposal(requireOwnerScope(actor), proposalId)
?? this.throwNotFound("Teklif");
}
updateProposal(actor: DomainActor, proposalId: string, input: unknown) {
const scope = requireOwnerScope(actor);
const current = this.repositories.business.getProposal(scope, proposalId)
?? this.throwNotFound("Teklif");
const value = parseDomainInput(proposalUpdateSchema, input);
this.assertTaskRelations(scope, { ...current, ...value });
return this.repositories.business.updateProposal(scope, proposalId, value)
?? this.throwNotFound("Teklif");
}
deleteProposal(actor: DomainActor, proposalId: string) {
return this.repositories.business.removeProposal(requireOwnerScope(actor), proposalId)
?? this.throwNotFound("Teklif");
}
createContract(actor: DomainActor, input: unknown) {
const scope = requireOwnerScope(actor);
const value = parseDomainInput(contractCreateSchema, input);
if (value.clientId) this.requireOwnedClient(scope, value.clientId);
if (value.proposalId && !this.repositories.business.getProposal(scope, value.proposalId)) {
throw notFound("Teklif");
}
this.assertContractRelations(scope, value);
return this.repositories.business.createContract(scope, { ...value, id: value.id ?? this.id() });
}
listContracts(actor: DomainActor) {
return this.repositories.business.listContracts(requireOwnerScope(actor));
}
getContract(actor: DomainActor, contractId: string) {
return this.repositories.business.getContract(requireOwnerScope(actor), contractId)
?? this.throwNotFound("Sözleşme");
}
updateContract(actor: DomainActor, contractId: string, input: unknown) {
const scope = requireOwnerScope(actor);
const current = this.repositories.business.getContract(scope, contractId)
?? this.throwNotFound("Sözleşme");
const value = parseDomainInput(contractUpdateSchema, input);
this.assertContractRelations(scope, { ...current, ...value });
return this.repositories.business.updateContract(scope, contractId, value)
?? this.throwNotFound("Sözleşme");
}
deleteContract(actor: DomainActor, contractId: string) {
return this.repositories.business.removeContract(requireOwnerScope(actor), contractId)
?? this.throwNotFound("Sözleşme");
}
createInvoice(actor: DomainActor, input: unknown) {
const scope = requireOwnerScope(actor);
const value = parseDomainInput(invoiceCreateSchema, input);
@@ -484,12 +553,60 @@ export class DomainService {
return this.repositories.business.createInvoice(scope, { ...value, id: value.id ?? this.id() });
}
listInvoices(actor: DomainActor) {
return this.repositories.business.listInvoices(requireOwnerScope(actor));
}
getInvoice(actor: DomainActor, invoiceId: string) {
return this.repositories.business.getInvoice(requireOwnerScope(actor), invoiceId)
?? this.throwNotFound("Fatura");
}
updateInvoice(actor: DomainActor, invoiceId: string, input: unknown) {
const scope = requireOwnerScope(actor);
const current = this.repositories.business.getInvoice(scope, invoiceId)
?? this.throwNotFound("Fatura");
const value = parseDomainInput(invoiceUpdateSchema, input);
this.assertTaskRelations(scope, { ...current, ...value });
return this.repositories.business.updateInvoice(scope, invoiceId, value)
?? this.throwNotFound("Fatura");
}
deleteInvoice(actor: DomainActor, invoiceId: string) {
return this.repositories.business.removeInvoice(requireOwnerScope(actor), invoiceId)
?? this.throwNotFound("Fatura");
}
createSubscription(actor: DomainActor, input: unknown) {
const scope = requireOwnerScope(actor);
const value = parseDomainInput(subscriptionCreateSchema, input);
return this.repositories.business.createSubscription(scope, { ...value, id: value.id ?? this.id() });
}
listSubscriptions(actor: DomainActor) {
return this.repositories.business.listSubscriptions(requireOwnerScope(actor));
}
getSubscription(actor: DomainActor, subscriptionId: string) {
return this.repositories.business.getSubscription(requireOwnerScope(actor), subscriptionId)
?? this.throwNotFound("Abonelik");
}
updateSubscription(actor: DomainActor, subscriptionId: string, input: unknown) {
const scope = requireOwnerScope(actor);
const value = parseDomainInput(subscriptionUpdateSchema, input);
if (!this.repositories.business.getSubscription(scope, subscriptionId)) {
throw notFound("Abonelik");
}
return this.repositories.business.updateSubscription(scope, subscriptionId, value)
?? this.throwNotFound("Abonelik");
}
deleteSubscription(actor: DomainActor, subscriptionId: string) {
return this.repositories.business.removeSubscription(requireOwnerScope(actor), subscriptionId)
?? this.throwNotFound("Abonelik");
}
private requireOwnedClient(scope: OwnerScope, clientId: string) {
return this.repositories.clients.get(scope, clientId) ?? this.throwNotFound("Müşteri");
}
@@ -533,6 +650,25 @@ export class DomainService {
}
}
private assertContractRelations(scope: OwnerScope, value: {
clientId?: string | null;
proposalId?: string | null;
}) {
const client = value.clientId ? this.requireOwnedClient(scope, value.clientId) : null;
const proposal = value.proposalId
? this.repositories.business.getProposal(scope, value.proposalId) ?? this.throwNotFound("Teklif")
: null;
if (proposal?.clientId && client?.id && proposal.clientId !== client.id) {
throw new DomainError("INVARIANT_VIOLATION", "Teklif ve sözleşme müşterisi uyuşmuyor.");
}
if (proposal?.clientId && !client) {
throw new DomainError(
"INVARIANT_VIOLATION",
"Müşterili tekliften üretilen sözleşme müşteri kimliğini içermelidir.",
);
}
}
private recalculateProjectProgress(scope: OwnerScope, projectId: string) {
const project = this.repositories.projects.get(scope, projectId);
if (!project || project.progressType !== "auto") return;