feat(web): add public commission request entrypoint

This commit is contained in:
2026-02-12 21:35:34 +01:00
parent dc0a41a5ae
commit 1fddb6d858
12 changed files with 441 additions and 9 deletions

View File

@@ -29,6 +29,26 @@ export const createCommissionInputSchema = z.object({
dueAt: z.date().nullable().optional(),
})
export const createPublicCommissionRequestInputSchema = z
.object({
customerName: z.string().min(1).max(180),
customerEmail: z.string().email().max(320),
customerPhone: z.string().max(80).nullable().optional(),
customerInstagram: z.string().max(120).nullable().optional(),
title: z.string().min(1).max(180),
description: z.string().max(4000).nullable().optional(),
budgetMin: z.number().nonnegative().nullable().optional(),
budgetMax: z.number().nonnegative().nullable().optional(),
})
.refine(
(value) =>
value.budgetMin == null || value.budgetMax == null || value.budgetMax >= value.budgetMin,
{
message: "budgetMax must be greater than or equal to budgetMin.",
path: ["budgetMax"],
},
)
export const updateCommissionStatusInputSchema = z.object({
id: z.string().uuid(),
status: commissionStatusSchema,
@@ -37,4 +57,7 @@ export const updateCommissionStatusInputSchema = z.object({
export type CommissionStatus = z.infer<typeof commissionStatusSchema>
export type CreateCustomerInput = z.infer<typeof createCustomerInputSchema>
export type CreateCommissionInput = z.infer<typeof createCommissionInputSchema>
export type CreatePublicCommissionRequestInput = z.infer<
typeof createPublicCommissionRequestInputSchema
>
export type UpdateCommissionStatusInput = z.infer<typeof updateCommissionStatusInputSchema>

View File

@@ -2,9 +2,12 @@ import { beforeEach, describe, expect, it, vi } from "vitest"
const { mockDb } = vi.hoisted(() => ({
mockDb: {
$transaction: vi.fn(),
customer: {
create: vi.fn(),
findFirst: vi.fn(),
findMany: vi.fn(),
update: vi.fn(),
},
commission: {
create: vi.fn(),
@@ -18,17 +21,29 @@ vi.mock("./client", () => ({
db: mockDb,
}))
import { createCommission, createCustomer, updateCommissionStatus } from "./commissions"
import {
createCommission,
createCustomer,
createPublicCommissionRequest,
updateCommissionStatus,
} from "./commissions"
describe("commissions service", () => {
beforeEach(() => {
for (const value of Object.values(mockDb)) {
if (typeof value === "function") {
value.mockReset()
continue
}
for (const fn of Object.values(value)) {
if (typeof fn === "function") {
fn.mockReset()
}
}
}
mockDb.$transaction.mockImplementation(async (callback) => callback(mockDb))
})
it("creates customer and commission payloads", async () => {
@@ -51,6 +66,37 @@ describe("commissions service", () => {
expect(mockDb.commission.create).toHaveBeenCalledTimes(1)
})
it("creates a public commission request with customer upsert behavior", async () => {
mockDb.customer.findFirst.mockResolvedValue({
id: "customer-existing",
phone: null,
instagram: null,
})
mockDb.customer.update.mockResolvedValue({
id: "customer-existing",
})
mockDb.commission.create.mockResolvedValue({
id: "commission-2",
})
await createPublicCommissionRequest({
customerName: "Grace Hopper",
customerEmail: "GRACE@EXAMPLE.COM",
customerPhone: "12345",
title: "Landscape commission",
description: "Oil painting request",
budgetMin: 500,
budgetMax: 900,
})
expect(mockDb.customer.findFirst).toHaveBeenCalledWith({
where: { email: "grace@example.com" },
orderBy: { updatedAt: "desc" },
})
expect(mockDb.customer.update).toHaveBeenCalledTimes(1)
expect(mockDb.commission.create).toHaveBeenCalledTimes(1)
})
it("updates commission status", async () => {
mockDb.commission.update.mockResolvedValue({ id: "commission-1", status: "done" })

View File

@@ -2,6 +2,7 @@ import {
commissionStatusSchema,
createCommissionInputSchema,
createCustomerInputSchema,
createPublicCommissionRequestInputSchema,
updateCommissionStatusInputSchema,
} from "@cms/content"
@@ -56,6 +57,63 @@ export async function createCommission(input: unknown) {
})
}
export async function createPublicCommissionRequest(input: unknown) {
const payload = createPublicCommissionRequestInputSchema.parse(input)
const normalizedEmail = payload.customerEmail.trim().toLowerCase()
return db.$transaction(async (tx) => {
const existingCustomer = await tx.customer.findFirst({
where: {
email: normalizedEmail,
},
orderBy: {
updatedAt: "desc",
},
})
const customer = existingCustomer
? await tx.customer.update({
where: { id: existingCustomer.id },
data: {
name: payload.customerName,
phone: payload.customerPhone ?? existingCustomer.phone,
instagram: payload.customerInstagram ?? existingCustomer.instagram,
isRecurring: true,
},
})
: await tx.customer.create({
data: {
name: payload.customerName,
email: normalizedEmail,
phone: payload.customerPhone,
instagram: payload.customerInstagram,
isRecurring: false,
},
})
return tx.commission.create({
data: {
title: payload.title,
description: payload.description,
status: "new",
customerId: customer.id,
budgetMin: payload.budgetMin,
budgetMax: payload.budgetMax,
},
include: {
customer: {
select: {
id: true,
name: true,
email: true,
isRecurring: true,
},
},
},
})
})
}
export async function updateCommissionStatus(input: unknown) {
const payload = updateCommissionStatusInputSchema.parse(input)

View File

@@ -11,6 +11,7 @@ export {
commissionKanbanOrder,
createCommission,
createCustomer,
createPublicCommissionRequest,
listCommissions,
listCustomers,
updateCommissionStatus,