feat(backend): complete AI and business migration
This commit is contained in:
@@ -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ı.]`;
|
||||
}
|
||||
@@ -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.",
|
||||
);
|
||||
}
|
||||
@@ -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 },
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user