feat(commissions): add customer records and kanban workflow baseline
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "Customer" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"email" TEXT,
|
||||
"phone" TEXT,
|
||||
"instagram" TEXT,
|
||||
"notes" TEXT,
|
||||
"isRecurring" BOOLEAN NOT NULL DEFAULT false,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Customer_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Commission" (
|
||||
"id" TEXT NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"status" TEXT NOT NULL,
|
||||
"customerId" TEXT,
|
||||
"assignedUserId" TEXT,
|
||||
"budgetMin" DOUBLE PRECISION,
|
||||
"budgetMax" DOUBLE PRECISION,
|
||||
"dueAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Commission_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Customer_email_idx" ON "Customer"("email");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Customer_isRecurring_idx" ON "Customer"("isRecurring");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Commission_status_idx" ON "Commission"("status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Commission_customerId_idx" ON "Commission"("customerId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Commission_assignedUserId_idx" ON "Commission"("assignedUserId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Commission" ADD CONSTRAINT "Commission_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "Customer"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Commission" ADD CONSTRAINT "Commission_assignedUserId_fkey" FOREIGN KEY ("assignedUserId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
@@ -34,6 +34,7 @@ model User {
|
||||
isProtected Boolean @default(false)
|
||||
sessions Session[]
|
||||
accounts Account[]
|
||||
commissions Commission[] @relation("CommissionAssignee")
|
||||
|
||||
@@unique([email])
|
||||
@@index([role])
|
||||
@@ -302,3 +303,39 @@ model NavigationItem {
|
||||
@@index([parentId])
|
||||
@@unique([menuId, parentId, sortOrder, label])
|
||||
}
|
||||
|
||||
model Customer {
|
||||
id String @id @default(uuid())
|
||||
name String
|
||||
email String?
|
||||
phone String?
|
||||
instagram String?
|
||||
notes String?
|
||||
isRecurring Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
commissions Commission[]
|
||||
|
||||
@@index([email])
|
||||
@@index([isRecurring])
|
||||
}
|
||||
|
||||
model Commission {
|
||||
id String @id @default(uuid())
|
||||
title String
|
||||
description String?
|
||||
status String
|
||||
customerId String?
|
||||
assignedUserId String?
|
||||
budgetMin Float?
|
||||
budgetMax Float?
|
||||
dueAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
customer Customer? @relation(fields: [customerId], references: [id], onDelete: SetNull)
|
||||
assignedUser User? @relation("CommissionAssignee", fields: [assignedUserId], references: [id], onDelete: SetNull)
|
||||
|
||||
@@index([status])
|
||||
@@index([customerId])
|
||||
@@index([assignedUserId])
|
||||
}
|
||||
|
||||
@@ -158,6 +158,54 @@ async function main() {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const existingCustomer = await db.customer.findFirst({
|
||||
where: {
|
||||
email: "collector@example.com",
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
})
|
||||
|
||||
const seededCustomer = existingCustomer
|
||||
? await db.customer.update({
|
||||
where: {
|
||||
id: existingCustomer.id,
|
||||
},
|
||||
data: {
|
||||
name: "Collector One",
|
||||
phone: "+1-555-0101",
|
||||
isRecurring: true,
|
||||
notes: "Interested in recurring portrait commissions.",
|
||||
},
|
||||
})
|
||||
: await db.customer.create({
|
||||
data: {
|
||||
name: "Collector One",
|
||||
email: "collector@example.com",
|
||||
phone: "+1-555-0101",
|
||||
isRecurring: true,
|
||||
notes: "Interested in recurring portrait commissions.",
|
||||
},
|
||||
})
|
||||
|
||||
await db.commission.upsert({
|
||||
where: {
|
||||
id: "11111111-1111-1111-1111-111111111111",
|
||||
},
|
||||
update: {},
|
||||
create: {
|
||||
id: "11111111-1111-1111-1111-111111111111",
|
||||
title: "Portrait Commission Baseline",
|
||||
description: "Initial seeded commission request for MVP1 board validation.",
|
||||
status: "new",
|
||||
customerId: seededCustomer.id,
|
||||
budgetMin: 400,
|
||||
budgetMax: 900,
|
||||
dueAt: new Date(Date.now() + 14 * 24 * 60 * 60 * 1000),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
main()
|
||||
|
||||
64
packages/db/src/commissions.test.ts
Normal file
64
packages/db/src/commissions.test.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest"
|
||||
|
||||
const { mockDb } = vi.hoisted(() => ({
|
||||
mockDb: {
|
||||
customer: {
|
||||
create: vi.fn(),
|
||||
findMany: vi.fn(),
|
||||
},
|
||||
commission: {
|
||||
create: vi.fn(),
|
||||
findMany: vi.fn(),
|
||||
update: vi.fn(),
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock("./client", () => ({
|
||||
db: mockDb,
|
||||
}))
|
||||
|
||||
import { createCommission, createCustomer, updateCommissionStatus } from "./commissions"
|
||||
|
||||
describe("commissions service", () => {
|
||||
beforeEach(() => {
|
||||
for (const value of Object.values(mockDb)) {
|
||||
for (const fn of Object.values(value)) {
|
||||
if (typeof fn === "function") {
|
||||
fn.mockReset()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it("creates customer and commission payloads", async () => {
|
||||
mockDb.customer.create.mockResolvedValue({ id: "customer-1" })
|
||||
mockDb.commission.create.mockResolvedValue({ id: "commission-1" })
|
||||
|
||||
await createCustomer({
|
||||
name: "Ada Lovelace",
|
||||
email: "ada@example.com",
|
||||
isRecurring: true,
|
||||
})
|
||||
|
||||
await createCommission({
|
||||
title: "Portrait Request",
|
||||
status: "new",
|
||||
customerId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
})
|
||||
|
||||
expect(mockDb.customer.create).toHaveBeenCalledTimes(1)
|
||||
expect(mockDb.commission.create).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("updates commission status", async () => {
|
||||
mockDb.commission.update.mockResolvedValue({ id: "commission-1", status: "done" })
|
||||
|
||||
await updateCommissionStatus({
|
||||
id: "550e8400-e29b-41d4-a716-446655440001",
|
||||
status: "done",
|
||||
})
|
||||
|
||||
expect(mockDb.commission.update).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
66
packages/db/src/commissions.ts
Normal file
66
packages/db/src/commissions.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import {
|
||||
commissionStatusSchema,
|
||||
createCommissionInputSchema,
|
||||
createCustomerInputSchema,
|
||||
updateCommissionStatusInputSchema,
|
||||
} from "@cms/content"
|
||||
|
||||
import { db } from "./client"
|
||||
|
||||
export const commissionKanbanOrder = commissionStatusSchema.options
|
||||
|
||||
export async function listCustomers(limit = 200) {
|
||||
return db.customer.findMany({
|
||||
orderBy: [{ updatedAt: "desc" }],
|
||||
take: limit,
|
||||
})
|
||||
}
|
||||
|
||||
export async function createCustomer(input: unknown) {
|
||||
const payload = createCustomerInputSchema.parse(input)
|
||||
|
||||
return db.customer.create({
|
||||
data: payload,
|
||||
})
|
||||
}
|
||||
|
||||
export async function listCommissions(limit = 300) {
|
||||
return db.commission.findMany({
|
||||
orderBy: [{ updatedAt: "desc" }],
|
||||
take: limit,
|
||||
include: {
|
||||
customer: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
isRecurring: true,
|
||||
},
|
||||
},
|
||||
assignedUser: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
username: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export async function createCommission(input: unknown) {
|
||||
const payload = createCommissionInputSchema.parse(input)
|
||||
|
||||
return db.commission.create({
|
||||
data: payload,
|
||||
})
|
||||
}
|
||||
|
||||
export async function updateCommissionStatus(input: unknown) {
|
||||
const payload = updateCommissionStatusInputSchema.parse(input)
|
||||
|
||||
return db.commission.update({
|
||||
where: { id: payload.id },
|
||||
data: { status: payload.status },
|
||||
})
|
||||
}
|
||||
@@ -1,4 +1,12 @@
|
||||
export { db } from "./client"
|
||||
export {
|
||||
commissionKanbanOrder,
|
||||
createCommission,
|
||||
createCustomer,
|
||||
listCommissions,
|
||||
listCustomers,
|
||||
updateCommissionStatus,
|
||||
} from "./commissions"
|
||||
export {
|
||||
attachArtworkRendition,
|
||||
createAlbum,
|
||||
|
||||
Reference in New Issue
Block a user