feat(db): mesh data model — meshes, members, invites, audit log

- pgSchema "mesh" with 4 tables isolating the peer mesh domain
- Enums: visibility, transport, tier, role
- audit_log is metadata-only (E2E encryption enforced at broker/client)
- Cascade on mesh delete, soft-delete via archivedAt/revokedAt

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-04-04 21:19:32 +01:00
commit d3163a5bff
1384 changed files with 314925 additions and 0 deletions

View File

@@ -0,0 +1,181 @@
import { describe, it, expect } from "vitest";
import { updateCreditsSchema, getTransactionsSchema } from "../../src/schema/admin";
describe("updateCreditsSchema", () => {
describe("action field", () => {
it("should accept 'set' action", () => {
const result = updateCreditsSchema.safeParse({
action: "set",
amount: 100,
});
expect(result.success).toBe(true);
});
it("should accept 'add' action", () => {
const result = updateCreditsSchema.safeParse({
action: "add",
amount: 100,
});
expect(result.success).toBe(true);
});
it("should accept 'deduct' action", () => {
const result = updateCreditsSchema.safeParse({
action: "deduct",
amount: 100,
});
expect(result.success).toBe(true);
});
it("should reject invalid action", () => {
const result = updateCreditsSchema.safeParse({
action: "invalid",
amount: 100,
});
expect(result.success).toBe(false);
});
});
describe("amount field", () => {
it("should accept positive integers", () => {
const result = updateCreditsSchema.safeParse({
action: "add",
amount: 500,
});
expect(result.success).toBe(true);
});
it("should reject zero", () => {
const result = updateCreditsSchema.safeParse({
action: "add",
amount: 0,
});
expect(result.success).toBe(false);
});
it("should reject negative numbers", () => {
const result = updateCreditsSchema.safeParse({
action: "add",
amount: -100,
});
expect(result.success).toBe(false);
});
it("should reject non-integers", () => {
const result = updateCreditsSchema.safeParse({
action: "add",
amount: 10.5,
});
expect(result.success).toBe(false);
});
});
describe("reason field", () => {
it("should accept optional reason", () => {
const result = updateCreditsSchema.safeParse({
action: "add",
amount: 100,
reason: "Promotional credit",
});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.reason).toBe("Promotional credit");
}
});
it("should accept missing reason", () => {
const result = updateCreditsSchema.safeParse({
action: "add",
amount: 100,
});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.reason).toBeUndefined();
}
});
it("should reject reason over 500 characters", () => {
const result = updateCreditsSchema.safeParse({
action: "add",
amount: 100,
reason: "a".repeat(501),
});
expect(result.success).toBe(false);
});
});
});
describe("getTransactionsSchema", () => {
it("should require customerId", () => {
const result = getTransactionsSchema.safeParse({
page: 1,
perPage: 20,
});
expect(result.success).toBe(false);
});
it("should accept valid input with defaults", () => {
const result = getTransactionsSchema.safeParse({
customerId: "cust-123",
});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.page).toBe(1);
expect(result.data.perPage).toBe(20);
}
});
it("should accept pagination parameters", () => {
const result = getTransactionsSchema.safeParse({
customerId: "cust-123",
page: 3,
perPage: 50,
});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.page).toBe(3);
expect(result.data.perPage).toBe(50);
}
});
it("should reject perPage over 100", () => {
const result = getTransactionsSchema.safeParse({
customerId: "cust-123",
perPage: 150,
});
expect(result.success).toBe(false);
});
describe("type filter", () => {
const validTypes = [
"signup",
"purchase",
"usage",
"admin_grant",
"admin_deduct",
"refund",
"promo",
"referral",
"expiry",
] as const;
validTypes.forEach((type) => {
it(`should accept type '${type}'`, () => {
const result = getTransactionsSchema.safeParse({
customerId: "cust-123",
type,
});
expect(result.success).toBe(true);
});
});
it("should reject invalid type", () => {
const result = getTransactionsSchema.safeParse({
customerId: "cust-123",
type: "invalid",
});
expect(result.success).toBe(false);
});
});
});

View File

@@ -0,0 +1,91 @@
import { describe, it, expect } from "vitest";
import {
Credits,
hasEnoughCredits,
getCreditsLevel,
getCreditsProgress,
} from "@turbostarter/ai/credits/utils";
describe("Credits constants", () => {
it("should have default balance of 100", () => {
expect(Credits.BALANCE).toBe(100);
});
it("should have FREE cost of 0", () => {
expect(Credits.COST.FREE).toBe(0);
});
it("should have DEFAULT cost of 5", () => {
expect(Credits.COST.DEFAULT).toBe(5);
});
it("should have HIGH cost of 10", () => {
expect(Credits.COST.HIGH).toBe(10);
});
});
describe("hasEnoughCredits", () => {
it("should return true when credits are sufficient", () => {
expect(hasEnoughCredits(100, 50)).toBe(true);
expect(hasEnoughCredits(50, 50)).toBe(true);
expect(hasEnoughCredits(100, 0)).toBe(true);
});
it("should return false when credits are insufficient", () => {
expect(hasEnoughCredits(50, 100)).toBe(false);
expect(hasEnoughCredits(0, 1)).toBe(false);
expect(hasEnoughCredits(99, 100)).toBe(false);
});
it("should handle edge cases", () => {
expect(hasEnoughCredits(0, 0)).toBe(true);
expect(hasEnoughCredits(1, 1)).toBe(true);
});
});
describe("getCreditsLevel", () => {
it("should return 'high' when credits > 50%", () => {
expect(getCreditsLevel(60, 100)).toBe("high");
expect(getCreditsLevel(100, 100)).toBe("high");
expect(getCreditsLevel(51, 100)).toBe("high");
});
it("should return 'medium' when credits between 15% and 50%", () => {
expect(getCreditsLevel(50, 100)).toBe("medium");
expect(getCreditsLevel(16, 100)).toBe("medium");
expect(getCreditsLevel(30, 100)).toBe("medium");
});
it("should return 'low' when credits <= 15%", () => {
expect(getCreditsLevel(15, 100)).toBe("low");
expect(getCreditsLevel(10, 100)).toBe("low");
expect(getCreditsLevel(0, 100)).toBe("low");
expect(getCreditsLevel(1, 100)).toBe("low");
});
it("should use default max of 100 when not specified", () => {
expect(getCreditsLevel(60)).toBe("high");
expect(getCreditsLevel(30)).toBe("medium");
expect(getCreditsLevel(10)).toBe("low");
});
});
describe("getCreditsProgress", () => {
it("should calculate correct progress ratio", () => {
expect(getCreditsProgress(50, 100)).toBe(0.5);
expect(getCreditsProgress(25, 100)).toBe(0.25);
expect(getCreditsProgress(100, 100)).toBe(1);
expect(getCreditsProgress(0, 100)).toBe(0);
});
it("should use default max of 100", () => {
expect(getCreditsProgress(50)).toBe(0.5);
expect(getCreditsProgress(100)).toBe(1);
});
it("should handle custom max values", () => {
expect(getCreditsProgress(500, 1000)).toBe(0.5);
expect(getCreditsProgress(250, 500)).toBe(0.5);
});
});

View File

@@ -0,0 +1,183 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
// Mock the database module before importing the mutation
vi.mock("@turbostarter/db/server", () => ({
db: {
transaction: vi.fn(),
},
}));
vi.mock("@turbostarter/shared/utils", () => ({
generateId: vi.fn(() => "test-transaction-id"),
HttpException: class HttpException extends Error {
constructor(public statusCode: number, public body?: { code: string; message?: string }) {
super(body?.message ?? "HttpException");
}
},
}));
import { db } from "@turbostarter/db/server";
import { updateCustomerCredits } from "../../src/modules/admin/customers/mutations";
describe("updateCustomerCredits", () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe("action: add", () => {
it("should add credits to customer balance", async () => {
const mockCustomer = { id: "cust-1", credits: 100 };
const mockTx = {
select: vi.fn().mockReturnThis(),
from: vi.fn().mockReturnThis(),
where: vi.fn().mockResolvedValue([mockCustomer]),
update: vi.fn().mockReturnThis(),
set: vi.fn().mockReturnThis(),
insert: vi.fn().mockReturnThis(),
values: vi.fn().mockResolvedValue(undefined),
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument, @typescript-eslint/unbound-method
vi.mocked(db.transaction).mockImplementation((callback) => callback(mockTx as any));
const result = await updateCustomerCredits(
"cust-1",
{ action: "add", amount: 50 },
"admin-1"
);
expect(result).toEqual({
previousBalance: 100,
newBalance: 150,
action: "add",
amount: 50,
});
expect(mockTx.insert).toHaveBeenCalled();
});
});
describe("action: deduct", () => {
it("should deduct credits from customer balance", async () => {
const mockCustomer = { id: "cust-1", credits: 100 };
const mockTx = {
select: vi.fn().mockReturnThis(),
from: vi.fn().mockReturnThis(),
where: vi.fn().mockResolvedValue([mockCustomer]),
update: vi.fn().mockReturnThis(),
set: vi.fn().mockReturnThis(),
insert: vi.fn().mockReturnThis(),
values: vi.fn().mockResolvedValue(undefined),
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument, @typescript-eslint/unbound-method
vi.mocked(db.transaction).mockImplementation((callback) => callback(mockTx as any));
const result = await updateCustomerCredits(
"cust-1",
{ action: "deduct", amount: 30 },
"admin-1"
);
expect(result).toEqual({
previousBalance: 100,
newBalance: 70,
action: "deduct",
amount: 30,
});
});
it("should throw error when deducting more than available", async () => {
const mockCustomer = { id: "cust-1", credits: 20 };
const mockTx = {
select: vi.fn().mockReturnThis(),
from: vi.fn().mockReturnThis(),
where: vi.fn().mockResolvedValue([mockCustomer]),
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument, @typescript-eslint/unbound-method
vi.mocked(db.transaction).mockImplementation((callback) => callback(mockTx as any));
await expect(
updateCustomerCredits("cust-1", { action: "deduct", amount: 50 }, "admin-1")
).rejects.toThrow();
});
});
describe("action: set", () => {
it("should set credits to exact amount (increase)", async () => {
const mockCustomer = { id: "cust-1", credits: 100 };
const mockTx = {
select: vi.fn().mockReturnThis(),
from: vi.fn().mockReturnThis(),
where: vi.fn().mockResolvedValue([mockCustomer]),
update: vi.fn().mockReturnThis(),
set: vi.fn().mockReturnThis(),
insert: vi.fn().mockReturnThis(),
values: vi.fn().mockResolvedValue(undefined),
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument, @typescript-eslint/unbound-method
vi.mocked(db.transaction).mockImplementation((callback) => callback(mockTx as any));
const result = await updateCustomerCredits(
"cust-1",
{ action: "set", amount: 200 },
"admin-1"
);
expect(result).toEqual({
previousBalance: 100,
newBalance: 200,
action: "set",
amount: 200,
});
});
it("should set credits to exact amount (decrease)", async () => {
const mockCustomer = { id: "cust-1", credits: 100 };
const mockTx = {
select: vi.fn().mockReturnThis(),
from: vi.fn().mockReturnThis(),
where: vi.fn().mockResolvedValue([mockCustomer]),
update: vi.fn().mockReturnThis(),
set: vi.fn().mockReturnThis(),
insert: vi.fn().mockReturnThis(),
values: vi.fn().mockResolvedValue(undefined),
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument, @typescript-eslint/unbound-method
vi.mocked(db.transaction).mockImplementation((callback) => callback(mockTx as any));
const result = await updateCustomerCredits(
"cust-1",
{ action: "set", amount: 50 },
"admin-1"
);
expect(result).toEqual({
previousBalance: 100,
newBalance: 50,
action: "set",
amount: 50,
});
});
});
describe("error cases", () => {
it("should throw when customer not found", async () => {
const mockTx = {
select: vi.fn().mockReturnThis(),
from: vi.fn().mockReturnThis(),
where: vi.fn().mockResolvedValue([]),
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument, @typescript-eslint/unbound-method
vi.mocked(db.transaction).mockImplementation((callback) => callback(mockTx as any));
await expect(
updateCustomerCredits("nonexistent", { action: "add", amount: 100 }, "admin-1")
).rejects.toThrow();
});
});
});