216 lines
7.0 KiB
TypeScript
216 lines
7.0 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import Database from "better-sqlite3";
|
|
import { drizzle } from "drizzle-orm/better-sqlite3";
|
|
import * as schema from "../server/db/schema";
|
|
import { buildChatContext, buildFinanceAnalysisContext, buildProjectRiskContext } from "../server/ai/context";
|
|
import type { DomainActor } from "../server/domain/actor";
|
|
import { DomainError } from "../server/domain/errors";
|
|
import { DomainService } from "../server/services/domain";
|
|
|
|
const databasePath = process.argv[2];
|
|
assert.ok(databasePath, "Database path is required");
|
|
|
|
const sqlite = new Database(databasePath);
|
|
sqlite.pragma("foreign_keys = ON");
|
|
const db = drizzle({ client: sqlite, schema });
|
|
let generatedId = 0;
|
|
const service = new DomainService(db, () => `phase7-generated-${++generatedId}`);
|
|
const owner: DomainActor = {
|
|
authUserId: "phase7-owner",
|
|
role: "freelancer",
|
|
clientId: null,
|
|
disabled: false,
|
|
};
|
|
const otherOwner: DomainActor = {
|
|
authUserId: "phase7-other-owner",
|
|
role: "freelancer",
|
|
clientId: null,
|
|
disabled: false,
|
|
};
|
|
const clientActor: DomainActor = {
|
|
authUserId: "phase7-client-user",
|
|
role: "client",
|
|
clientId: "phase7-client",
|
|
disabled: false,
|
|
};
|
|
|
|
try {
|
|
for (const actor of [owner, otherOwner, clientActor]) {
|
|
db.insert(schema.user).values({
|
|
id: actor.authUserId,
|
|
name: actor.authUserId,
|
|
email: `${actor.authUserId}@example.com`,
|
|
emailVerified: true,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
}).run();
|
|
}
|
|
|
|
service.createClient(owner, {
|
|
id: "phase7-client",
|
|
name: "Visible Client",
|
|
email: "visible@example.com",
|
|
});
|
|
service.createClient(otherOwner, {
|
|
id: "phase7-foreign-client",
|
|
name: "Foreign Secret Client",
|
|
});
|
|
service.createProject(owner, {
|
|
id: "phase7-project",
|
|
clientId: "phase7-client",
|
|
name: "Visible Project",
|
|
status: "active",
|
|
budgetAmountMinor: 250_000,
|
|
currency: "TRY",
|
|
});
|
|
service.createProject(otherOwner, {
|
|
id: "phase7-foreign-project",
|
|
clientId: "phase7-foreign-client",
|
|
name: "Foreign Secret Project",
|
|
status: "active",
|
|
});
|
|
service.createTask(owner, {
|
|
id: "phase7-task",
|
|
clientId: "phase7-client",
|
|
projectId: "phase7-project",
|
|
title: "Visible Task",
|
|
status: "done",
|
|
});
|
|
service.createFinanceTransaction(owner, {
|
|
id: "phase7-finance",
|
|
type: "income",
|
|
amountMinor: 12_345,
|
|
currency: "TRY",
|
|
transactionDate: "2026-07-17",
|
|
paymentStatus: "paid",
|
|
});
|
|
service.createFinanceTransaction(otherOwner, {
|
|
id: "phase7-foreign-finance",
|
|
type: "income",
|
|
amountMinor: 999_999,
|
|
currency: "TRY",
|
|
transactionDate: "2026-07-17",
|
|
paymentStatus: "paid",
|
|
description: "Foreign Secret Finance",
|
|
});
|
|
service.saveJournalEntry(owner, {
|
|
id: "phase7-journal",
|
|
entryDate: "2026-07-17",
|
|
moodScore: 4,
|
|
note: "Visible journal note",
|
|
});
|
|
|
|
const chatContext = buildChatContext(service, owner, new Date("2026-07-17T12:00:00.000Z"));
|
|
assert.match(chatContext, /Visible Project/);
|
|
assert.match(chatContext, /Visible Task/);
|
|
assert.match(chatContext, /Visible journal note/);
|
|
assert.doesNotMatch(chatContext, /Foreign Secret/);
|
|
|
|
const financeContext = buildFinanceAnalysisContext(
|
|
service,
|
|
owner,
|
|
new Date("2026-07-17T12:00:00.000Z"),
|
|
);
|
|
assert.equal(financeContext.hasData, true);
|
|
assert.match(financeContext.text, /123\.45/);
|
|
assert.doesNotMatch(financeContext.text, /9999\.99|Foreign Secret/);
|
|
assert.match(buildProjectRiskContext(service, owner, "phase7-project"), /Visible Client/);
|
|
assertDomainError(
|
|
() => buildProjectRiskContext(service, owner, "phase7-foreign-project"),
|
|
"NOT_FOUND",
|
|
);
|
|
|
|
service.createChatSession(owner, { id: "phase7-chat", title: "Owner chat" });
|
|
service.addChatMessage(owner, {
|
|
id: "phase7-message",
|
|
sessionId: "phase7-chat",
|
|
role: "user",
|
|
content: "Owner question",
|
|
});
|
|
assert.equal(service.listChatSessions(owner).length, 1);
|
|
assert.equal(service.listChatMessages(owner, "phase7-chat")[0]?.content, "Owner question");
|
|
assertDomainError(() => service.getChatSession(otherOwner, "phase7-chat"), "NOT_FOUND");
|
|
assertDomainError(() => service.listChatMessages(otherOwner, "phase7-chat"), "NOT_FOUND");
|
|
assertDomainError(() => service.deleteChatSession(otherOwner, "phase7-chat"), "NOT_FOUND");
|
|
service.deleteChatSession(owner, "phase7-chat");
|
|
const remainingMessages = sqlite
|
|
.prepare("select count(*) as value from chat_messages where session_id = ?")
|
|
.get("phase7-chat") as { value: number };
|
|
assert.equal(
|
|
remainingMessages.value,
|
|
0,
|
|
"Chat session deletion must cascade to messages",
|
|
);
|
|
|
|
service.createProposal(owner, {
|
|
id: "phase7-proposal",
|
|
clientId: "phase7-client",
|
|
projectId: "phase7-project",
|
|
title: "Proposal",
|
|
amountMinor: 100_00,
|
|
});
|
|
service.createContract(owner, {
|
|
id: "phase7-contract",
|
|
proposalId: "phase7-proposal",
|
|
clientId: "phase7-client",
|
|
title: "Contract",
|
|
});
|
|
service.createInvoice(owner, {
|
|
id: "phase7-invoice",
|
|
clientId: "phase7-client",
|
|
projectId: "phase7-project",
|
|
invoiceNumber: "P7-001",
|
|
amountMinor: 100_00,
|
|
issueDate: "2026-07-17",
|
|
});
|
|
service.createSubscription(owner, {
|
|
id: "phase7-subscription",
|
|
name: "Hosting",
|
|
amountMinor: 500_00,
|
|
});
|
|
|
|
assert.equal(service.listProposals(owner).length, 1);
|
|
assert.equal(service.updateProposal(owner, "phase7-proposal", { status: "sent" }).status, "sent");
|
|
assert.equal(service.listContracts(owner).length, 1);
|
|
assert.equal(service.updateContract(owner, "phase7-contract", { status: "active" }).status, "active");
|
|
assert.equal(service.listInvoices(owner).length, 1);
|
|
assert.equal(service.updateInvoice(owner, "phase7-invoice", { status: "paid" }).status, "paid");
|
|
assert.equal(service.listSubscriptions(owner).length, 1);
|
|
assert.equal(
|
|
service.updateSubscription(owner, "phase7-subscription", { status: "cancelled" }).status,
|
|
"cancelled",
|
|
);
|
|
|
|
for (const run of [
|
|
() => service.getProposal(otherOwner, "phase7-proposal"),
|
|
() => service.updateContract(otherOwner, "phase7-contract", { status: "active" }),
|
|
() => service.deleteInvoice(otherOwner, "phase7-invoice"),
|
|
() => service.getSubscription(otherOwner, "phase7-subscription"),
|
|
]) {
|
|
assertDomainError(run, "NOT_FOUND");
|
|
}
|
|
assertDomainError(() => service.listProposals(clientActor), "FORBIDDEN");
|
|
|
|
service.deleteContract(owner, "phase7-contract");
|
|
service.deleteProposal(owner, "phase7-proposal");
|
|
service.deleteInvoice(owner, "phase7-invoice");
|
|
service.deleteSubscription(owner, "phase7-subscription");
|
|
assert.deepEqual(
|
|
[
|
|
service.listContracts(owner).length,
|
|
service.listProposals(owner).length,
|
|
service.listInvoices(owner).length,
|
|
service.listSubscriptions(owner).length,
|
|
],
|
|
[0, 0, 0, 0],
|
|
);
|
|
|
|
console.log("Phase 7 domain smoke passed: owner-scoped chat, AI context and business CRUD verified.");
|
|
} finally {
|
|
sqlite.close();
|
|
}
|
|
|
|
function assertDomainError(run: () => unknown, code: DomainError["code"]) {
|
|
assert.throws(run, (error) => error instanceof DomainError && error.code === code);
|
|
}
|