feat(i18n): localize assignment validation messages

This commit is contained in:
2026-06-15 01:15:24 +02:00
parent bfea2b77ab
commit 349559f4e0
12 changed files with 446 additions and 67 deletions
+42 -26
View File
@@ -1,12 +1,15 @@
"use server" "use server"
import { revalidatePath } from "next/cache" import { revalidatePath } from "next/cache"
import { flattenError } from "zod"
import { localizeAssignmentFieldErrors } from "@/actions/assignment.messages"
import { getI18n } from "@/i18n/server"
import { import {
assignmentSchema, buildCreateAssignmentSchema,
buildUpdateAssignmentSchema,
type CreateAssignmentFormType, type CreateAssignmentFormType,
type ReturnAssignmentFormType, type ReturnAssignmentFormType,
type UpdateAssignmentFormType, type UpdateAssignmentFormType,
updateAssignmentSchema,
} from "@/schemas/assignment.schema" } from "@/schemas/assignment.schema"
import { getAuthenticatedUserId } from "@/services/auth.service" import { getAuthenticatedUserId } from "@/services/auth.service"
import { import {
@@ -16,52 +19,60 @@ import {
} from "@/use-cases/assignment.use-cases" } from "@/use-cases/assignment.use-cases"
export async function createAssignment(formData: CreateAssignmentFormType) { export async function createAssignment(formData: CreateAssignmentFormType) {
const createdBy = await getAuthenticatedUserId() const { dictionary } = await getI18n()
const copy = dictionary.inventory.assignments
const validatedFields = assignmentSchema.safeParse({ const validatedFields = buildCreateAssignmentSchema(copy.schema).safeParse(
...formData, formData,
createdBy, )
})
if (!validatedFields.success) { if (!validatedFields.success) {
return { return {
success: false, errors: flattenError(validatedFields.error).fieldErrors,
errors: validatedFields.error.flatten().fieldErrors,
} }
} }
try { try {
const createdBy = await getAuthenticatedUserId()
const result = await createAssignmentUseCase({ const result = await createAssignmentUseCase({
...validatedFields.data, ...validatedFields.data,
actorId: createdBy, actorId: createdBy,
}) })
if (!result.success) { if (!result.success) {
return result return {
...result,
errors: localizeAssignmentFieldErrors(result.errors, copy.actions),
}
} }
revalidatePath("/assignments") revalidatePath("/assignments")
return { return {
success: true, success: true as const,
message: "Assignment created successfully", message: copy.actions.createSuccess,
} }
} catch (error) { } catch (error) {
console.error("Database error:", error) console.error("Database error:", error)
return { return {
success: false, success: false as const,
errors: { error: ["Error creating assignment"] }, message: copy.actions.createFailure,
} }
} }
} }
export async function updateAssignment(formData: UpdateAssignmentFormType) { export async function updateAssignment(formData: UpdateAssignmentFormType) {
const validatedFields = updateAssignmentSchema.safeParse(formData) const { dictionary } = await getI18n()
const copy = dictionary.inventory.assignments
const validatedFields = buildUpdateAssignmentSchema(copy.schema).safeParse(
formData,
)
if (!validatedFields.success) { if (!validatedFields.success) {
return { return {
success: false, errors: flattenError(validatedFields.error).fieldErrors,
errors: validatedFields.error.flatten().fieldErrors,
} }
} }
@@ -74,37 +85,42 @@ export async function updateAssignment(formData: UpdateAssignmentFormType) {
}) })
if (!result.success) { if (!result.success) {
return result return {
...result,
errors: localizeAssignmentFieldErrors(result.errors, copy.actions),
}
} }
revalidatePath("/assignments") revalidatePath("/assignments")
return { return {
success: true, success: true as const,
message: "Assignment updated successfully", message: copy.actions.updateSuccess,
} }
} catch (error) { } catch (error) {
console.error("Database error:", error) console.error("Database error:", error)
return { return {
success: false, success: false as const,
errors: { error: ["Error updating assignment"] }, message: copy.actions.updateFailure,
} }
} }
} }
export async function returnAssignment(formData: ReturnAssignmentFormType) { export async function returnAssignment(formData: ReturnAssignmentFormType) {
const { id } = formData const { dictionary } = await getI18n()
const copy = dictionary.inventory.assignments
const userId = await getAuthenticatedUserId() const userId = await getAuthenticatedUserId()
const result = await returnAssignmentUseCase({ const result = await returnAssignmentUseCase({
id, id: formData.id,
actorId: userId, actorId: userId,
}) })
if (!result.success) { if (!result.success) {
return { return {
...result, ...result,
message: "Error returning assignment", errors: localizeAssignmentFieldErrors(result.errors, copy.actions),
message: copy.actions.returnFailure,
} }
} }
@@ -112,6 +128,6 @@ export async function returnAssignment(formData: ReturnAssignmentFormType) {
return { return {
success: true as const, success: true as const,
message: "Assignment returned successfully", message: copy.actions.returnSuccess,
} }
} }
+44
View File
@@ -0,0 +1,44 @@
import type { Dictionary } from "@/i18n/dictionaries"
type AssignmentActionCopy = Dictionary["inventory"]["assignments"]["actions"]
type FieldErrors = Record<string, string[]>
const assignmentErrorMessageKeys = {
"Item not found": "itemNotFound",
"Item does not have enough stock": "itemInsufficientStock",
"Asset not found": "assetNotFound",
"Asset does not belong to item": "assetItemMismatch",
"Assignment not found": "notFound",
"Assignment already returned": "assignmentAlreadyReturned",
"Invalid assignment data": "invalidData",
} as const satisfies Record<string, keyof AssignmentActionCopy>
function isAssignmentErrorMessage(
message: string,
): message is keyof typeof assignmentErrorMessageKeys {
return message in assignmentErrorMessageKeys
}
function localizeAssignmentMessage(
message: string,
copy: AssignmentActionCopy,
): string {
if (!isAssignmentErrorMessage(message)) return message
return copy[assignmentErrorMessageKeys[message]]
}
export function localizeAssignmentFieldErrors(
errors: FieldErrors | undefined,
copy: AssignmentActionCopy,
): FieldErrors | undefined {
if (!errors) return undefined
return Object.fromEntries(
Object.entries(errors).map(([field, messages]) => [
field,
messages.map((message) => localizeAssignmentMessage(message, copy)),
]),
)
}
@@ -42,6 +42,7 @@ export default async function EditAssignmentPage({
assets={assets} assets={assets}
initialData={assignment as UpdateAssignmentFormType} initialData={assignment as UpdateAssignmentFormType}
formCopy={copy.form} formCopy={copy.form}
schemaCopy={copy.schema}
submitButtonCopy={dictionary.common.submitButton} submitButtonCopy={dictionary.common.submitButton}
/> />
</div> </div>
@@ -2,6 +2,7 @@
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod"
import { useRouter } from "next/navigation" import { useRouter } from "next/navigation"
import { useMemo } from "react"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { toast } from "sonner" import { toast } from "sonner"
import { updateAssignment } from "@/actions/assignment.actions" import { updateAssignment } from "@/actions/assignment.actions"
@@ -11,12 +12,13 @@ import {
} from "@/components/forms/submitButton" } from "@/components/forms/submitButton"
import type { Dictionary } from "@/i18n/dictionaries" import type { Dictionary } from "@/i18n/dictionaries"
import { import {
buildUpdateAssignmentSchema,
type UpdateAssignmentFormType, type UpdateAssignmentFormType,
updateAssignmentSchema,
} from "@/schemas/assignment.schema" } from "@/schemas/assignment.schema"
import type { Asset, Item, Recipient } from "@/types" import type { Asset, Item, Recipient } from "@/types"
type AssignmentFormCopy = Dictionary["inventory"]["assignments"]["form"] type AssignmentFormCopy = Dictionary["inventory"]["assignments"]["form"]
type AssignmentSchemaCopy = Dictionary["inventory"]["assignments"]["schema"]
interface Props { interface Props {
recipients: Recipient[] recipients: Recipient[]
@@ -24,6 +26,7 @@ interface Props {
assets: Asset[] assets: Asset[]
initialData: UpdateAssignmentFormType initialData: UpdateAssignmentFormType
formCopy: AssignmentFormCopy formCopy: AssignmentFormCopy
schemaCopy: AssignmentSchemaCopy
submitButtonCopy: SubmitButtonCopy submitButtonCopy: SubmitButtonCopy
} }
@@ -33,17 +36,23 @@ export default function EditAssignmentForm({
assets, assets,
initialData, initialData,
formCopy, formCopy,
schemaCopy,
submitButtonCopy, submitButtonCopy,
}: Props) { }: Props) {
const router = useRouter() const router = useRouter()
const schema = useMemo(
() => buildUpdateAssignmentSchema(schemaCopy),
[schemaCopy],
)
const { const {
register, register,
handleSubmit, handleSubmit,
formState: { errors, isSubmitting, isSubmitSuccessful }, formState: { errors, isSubmitting, isSubmitSuccessful },
watch, watch,
} = useForm<UpdateAssignmentFormType>({ } = useForm<UpdateAssignmentFormType>({
resolver: zodResolver(updateAssignmentSchema), resolver: zodResolver(schema),
defaultValues: { defaultValues: {
...initialData, ...initialData,
id: initialData.id || undefined, id: initialData.id || undefined,
@@ -12,18 +12,20 @@ import {
} from "@/components/forms/submitButton" } from "@/components/forms/submitButton"
import type { Dictionary } from "@/i18n/dictionaries" import type { Dictionary } from "@/i18n/dictionaries"
import { import {
buildCreateAssignmentSchema,
type CreateAssignmentFormType, type CreateAssignmentFormType,
createAssignmentSchema,
} from "@/schemas/assignment.schema" } from "@/schemas/assignment.schema"
import type { Asset, Item, Recipient } from "@/types" import type { Asset, Item, Recipient } from "@/types"
type AssignmentFormCopy = Dictionary["inventory"]["assignments"]["form"] type AssignmentFormCopy = Dictionary["inventory"]["assignments"]["form"]
type AssignmentSchemaCopy = Dictionary["inventory"]["assignments"]["schema"]
interface Props { interface Props {
recipients: Recipient[] recipients: Recipient[]
items: Item[] items: Item[]
assets: Asset[] assets: Asset[]
formCopy: AssignmentFormCopy formCopy: AssignmentFormCopy
schemaCopy: AssignmentSchemaCopy
submitButtonCopy: SubmitButtonCopy submitButtonCopy: SubmitButtonCopy
} }
@@ -32,17 +34,23 @@ export default function CreateAssignmentForm({
items, items,
assets, assets,
formCopy, formCopy,
schemaCopy,
submitButtonCopy, submitButtonCopy,
}: Props) { }: Props) {
const router = useRouter() const router = useRouter()
const schema = useMemo(
() => buildCreateAssignmentSchema(schemaCopy),
[schemaCopy],
)
const { const {
register, register,
handleSubmit, handleSubmit,
formState: { errors, isSubmitting, isSubmitSuccessful }, formState: { errors, isSubmitting, isSubmitSuccessful },
watch, watch,
} = useForm<CreateAssignmentFormType>({ } = useForm<CreateAssignmentFormType>({
resolver: zodResolver(createAssignmentSchema), resolver: zodResolver(schema),
mode: "onSubmit", mode: "onSubmit",
}) })
@@ -22,6 +22,7 @@ export default async function NewAssignmentPage() {
items={items} items={items}
assets={assets} assets={assets}
formCopy={copy.form} formCopy={copy.form}
schemaCopy={copy.schema}
submitButtonCopy={dictionary.common.submitButton} submitButtonCopy={dictionary.common.submitButton}
/> />
</div> </div>
+23
View File
@@ -294,6 +294,29 @@ export const en = {
fallback: { fallback: {
missingValue: "N/A", missingValue: "N/A",
}, },
actions: {
createSuccess: "Assignment created successfully",
createFailure: "Error creating assignment",
updateSuccess: "Assignment updated successfully",
updateFailure: "Error updating assignment",
returnSuccess: "Assignment returned successfully",
returnFailure: "Error returning assignment",
notFound: "Assignment not found",
itemNotFound: "Item not found",
itemInsufficientStock: "Item does not have enough stock",
assetNotFound: "Asset not found",
assetItemMismatch: "Asset does not belong to item",
assignmentAlreadyReturned: "Assignment already returned",
invalidData: "Invalid assignment data",
genericFailure: "Error processing assignment",
},
schema: {
recipientRequired: "Recipient is required",
itemIdRequired: "Item is required",
quantityMinOne: "Quantity must be at least 1",
assetIdRequired: "Asset ID is required when item ID is provided",
idRequired: "Assignment ID is required",
},
}, },
recipients: { recipients: {
list: { list: {
+24
View File
@@ -298,6 +298,30 @@ export const es = {
fallback: { fallback: {
missingValue: "No disponible", missingValue: "No disponible",
}, },
actions: {
createSuccess: "Asignación creada correctamente",
createFailure: "Error al crear la asignación",
updateSuccess: "Asignación actualizada correctamente",
updateFailure: "Error al actualizar la asignación",
returnSuccess: "Asignación devuelta correctamente",
returnFailure: "Error al devolver la asignación",
notFound: "Asignación no encontrada",
itemNotFound: "Artículo no encontrado",
itemInsufficientStock: "El artículo no tiene stock suficiente",
assetNotFound: "Activo no encontrado",
assetItemMismatch: "El activo no pertenece al artículo",
assignmentAlreadyReturned: "La asignación ya fue devuelta",
invalidData: "Datos de asignación inválidos",
genericFailure: "Error al procesar la asignación",
},
schema: {
recipientRequired: "El destinatario es obligatorio",
itemIdRequired: "El artículo es obligatorio",
quantityMinOne: "La cantidad debe ser al menos 1",
assetIdRequired:
"El activo es obligatorio cuando se especifica el artículo",
idRequired: "El ID de asignación es obligatorio",
},
}, },
recipients: { recipients: {
list: { list: {
+72 -37
View File
@@ -1,52 +1,87 @@
import { z } from "zod" import { z } from "zod"
export const assignmentSchema = z.object({ import type { Dictionary } from "@/i18n/dictionaries"
id: z.string().optional(),
quantity: z.coerce.number().int().nonnegative().min(1, {
error: "Quantity is required",
}),
notes: z.string().optional(),
itemId: z
.string()
.min(1, {
error: "Item is required",
})
.optional(),
assetId: z.string().optional(),
recipientId: z.string().min(1, {
error: "Recipient is required",
}),
assignmentDate: z.date().optional(),
returnDate: z.date().optional(),
})
export const createAssignmentSchema = assignmentSchema.omit({ export type AssignmentSchemaCopy =
id: true, Dictionary["inventory"]["assignments"]["schema"]
returnDate: true,
}) const defaultAssignmentSchemaCopy: AssignmentSchemaCopy = {
recipientRequired: "Recipient is required",
itemIdRequired: "Item is required",
quantityMinOne: "Quantity must be at least 1",
assetIdRequired: "Asset ID is required when item ID is provided",
idRequired: "Assignment ID is required",
}
function buildAssignmentBaseSchema(copy: AssignmentSchemaCopy) {
return z.object({
id: z.string().optional(),
quantity: z.coerce.number().int().nonnegative().min(1, {
error: copy.quantityMinOne,
}),
notes: z.string().optional(),
itemId: z
.string()
.min(1, {
error: copy.itemIdRequired,
})
.optional(),
assetId: z.string().optional(),
recipientId: z.string().min(1, {
error: copy.recipientRequired,
}),
assignmentDate: z.date().optional(),
returnDate: z.date().optional(),
})
}
export const assignmentSchema = buildAssignmentBaseSchema(
defaultAssignmentSchemaCopy,
)
export function buildCreateAssignmentSchema(copy: AssignmentSchemaCopy) {
return buildAssignmentBaseSchema(copy).omit({
id: true,
returnDate: true,
})
}
export const createAssignmentSchema = buildCreateAssignmentSchema(
defaultAssignmentSchemaCopy,
)
export type CreateAssignmentFormType = z.input<typeof createAssignmentSchema> export type CreateAssignmentFormType = z.input<typeof createAssignmentSchema>
export type CreateAssignmentData = z.output<typeof createAssignmentSchema> export type CreateAssignmentData = z.output<typeof createAssignmentSchema>
export const updateAssignmentSchema = assignmentSchema export function buildUpdateAssignmentSchema(copy: AssignmentSchemaCopy) {
.omit({ return buildAssignmentBaseSchema(copy)
returnDate: true, .omit({
}) returnDate: true,
.superRefine((data, ctx) => { })
if (data.itemId && !data.assetId) { .extend({
ctx.addIssue({ id: z.string().min(1, {
code: "custom", error: copy.idRequired,
message: "Asset ID is required when item ID is provided", }),
path: ["assetId"], })
}) .superRefine((data, ctx) => {
} if (data.itemId && !data.assetId) {
}) ctx.addIssue({
code: "custom",
message: copy.assetIdRequired,
path: ["assetId"],
})
}
})
}
export const updateAssignmentSchema = buildUpdateAssignmentSchema(
defaultAssignmentSchemaCopy,
)
export type UpdateAssignmentFormType = z.input<typeof updateAssignmentSchema> export type UpdateAssignmentFormType = z.input<typeof updateAssignmentSchema>
export type UpdateAssignmentData = z.output<typeof updateAssignmentSchema> export type UpdateAssignmentData = z.output<typeof updateAssignmentSchema>
export const returnAssignmentSchema = z.object({ export const returnAssignmentSchema = z.object({
id: z.string().min(1, { id: z.string().min(1, {
error: "Assignment ID is required", error: defaultAssignmentSchemaCopy.idRequired,
}), }),
}) })
export type ReturnAssignmentFormType = z.infer<typeof returnAssignmentSchema> export type ReturnAssignmentFormType = z.infer<typeof returnAssignmentSchema>
@@ -0,0 +1,73 @@
import { describe, expect, it } from "vitest"
import { localizeAssignmentFieldErrors } from "@/actions/assignment.messages"
const actionCopy = {
createSuccess: "Asignación creada correctamente",
createFailure: "Error al crear la asignación",
updateSuccess: "Asignación actualizada correctamente",
updateFailure: "Error al actualizar la asignación",
returnSuccess: "Asignación devuelta correctamente",
returnFailure: "Error al devolver la asignación",
notFound: "Asignación no encontrada",
itemNotFound: "Artículo no encontrado",
itemInsufficientStock: "El artículo no tiene stock suficiente",
assetNotFound: "Activo no encontrado",
assetItemMismatch: "El activo no pertenece al artículo",
assignmentAlreadyReturned: "La asignación ya fue devuelta",
invalidData: "Datos de asignación inválidos",
genericFailure: "Error al procesar la asignación",
}
describe("assignment action message localization", () => {
it("localizes known assignment field errors from use-case output", () => {
expect(
localizeAssignmentFieldErrors(
{
itemId: ["Item not found"],
quantity: ["Item does not have enough stock"],
assetId: ["Asset not found"],
id: ["Assignment not found", "Assignment already returned"],
},
actionCopy,
),
).toEqual({
itemId: [actionCopy.itemNotFound],
quantity: [actionCopy.itemInsufficientStock],
assetId: [actionCopy.assetNotFound],
id: [actionCopy.notFound, actionCopy.assignmentAlreadyReturned],
})
})
it("localizes asset-item mismatch and generic data errors", () => {
expect(
localizeAssignmentFieldErrors(
{
assetId: ["Asset does not belong to item"],
error: ["Invalid assignment data"],
},
actionCopy,
),
).toEqual({
assetId: [actionCopy.assetItemMismatch],
error: [actionCopy.invalidData],
})
})
it("keeps unknown messages unchanged", () => {
expect(
localizeAssignmentFieldErrors(
{ error: ["Unexpected assignment issue"] },
actionCopy,
),
).toEqual({ error: ["Unexpected assignment issue"] })
})
it("returns undefined when no errors are provided", () => {
expect(localizeAssignmentFieldErrors(undefined, actionCopy)).toBeUndefined()
})
it("returns undefined when errors object is empty", () => {
expect(localizeAssignmentFieldErrors({}, actionCopy)).toEqual({})
})
})
+47
View File
@@ -448,6 +448,29 @@ describe("i18n dictionaries", () => {
fallback: { fallback: {
missingValue: "N/A", missingValue: "N/A",
}, },
actions: {
createSuccess: "Assignment created successfully",
createFailure: "Error creating assignment",
updateSuccess: "Assignment updated successfully",
updateFailure: "Error updating assignment",
returnSuccess: "Assignment returned successfully",
returnFailure: "Error returning assignment",
notFound: "Assignment not found",
itemNotFound: "Item not found",
itemInsufficientStock: "Item does not have enough stock",
assetNotFound: "Asset not found",
assetItemMismatch: "Asset does not belong to item",
assignmentAlreadyReturned: "Assignment already returned",
invalidData: "Invalid assignment data",
genericFailure: "Error processing assignment",
},
schema: {
recipientRequired: "Recipient is required",
itemIdRequired: "Item is required",
quantityMinOne: "Quantity must be at least 1",
assetIdRequired: "Asset ID is required when item ID is provided",
idRequired: "Assignment ID is required",
},
}) })
expect(getDictionary("es").inventory.assignments).toEqual({ expect(getDictionary("es").inventory.assignments).toEqual({
@@ -489,6 +512,30 @@ describe("i18n dictionaries", () => {
fallback: { fallback: {
missingValue: "No disponible", missingValue: "No disponible",
}, },
actions: {
createSuccess: "Asignación creada correctamente",
createFailure: "Error al crear la asignación",
updateSuccess: "Asignación actualizada correctamente",
updateFailure: "Error al actualizar la asignación",
returnSuccess: "Asignación devuelta correctamente",
returnFailure: "Error al devolver la asignación",
notFound: "Asignación no encontrada",
itemNotFound: "Artículo no encontrado",
itemInsufficientStock: "El artículo no tiene stock suficiente",
assetNotFound: "Activo no encontrado",
assetItemMismatch: "El activo no pertenece al artículo",
assignmentAlreadyReturned: "La asignación ya fue devuelta",
invalidData: "Datos de asignación inválidos",
genericFailure: "Error al procesar la asignación",
},
schema: {
recipientRequired: "El destinatario es obligatorio",
itemIdRequired: "El artículo es obligatorio",
quantityMinOne: "La cantidad debe ser al menos 1",
assetIdRequired:
"El activo es obligatorio cuando se especifica el artículo",
idRequired: "El ID de asignación es obligatorio",
},
}) })
}) })
@@ -0,0 +1,98 @@
import { describe, expect, it } from "vitest"
import {
buildCreateAssignmentSchema,
buildUpdateAssignmentSchema,
} from "@/schemas/assignment.schema"
const schemaCopy = {
recipientRequired: "El destinatario es obligatorio",
itemIdRequired: "El artículo es obligatorio",
quantityMinOne: "La cantidad debe ser al menos 1",
assetIdRequired: "El activo es obligatorio cuando se especifica el artículo",
idRequired: "El ID de asignación es obligatorio",
}
describe("assignment schema localization", () => {
it("uses localized create validation messages for missing required fields", () => {
const result = buildCreateAssignmentSchema(schemaCopy).safeParse({
recipientId: "",
quantity: 0,
})
expect(result.success).toBe(false)
if (!result.success) {
const errors = result.error.flatten().fieldErrors
expect(errors.recipientId).toContain(schemaCopy.recipientRequired)
expect(errors.quantity).toContain(schemaCopy.quantityMinOne)
}
})
it("uses localized update validation messages for missing identifier and invalid item-asset combination", () => {
const result = buildUpdateAssignmentSchema(schemaCopy).safeParse({
id: "",
itemId: "item-1",
recipientId: "recipient-1",
quantity: 1,
assetId: "",
})
expect(result.success).toBe(false)
if (!result.success) {
const errors = result.error.flatten().fieldErrors
expect(errors.id).toContain(schemaCopy.idRequired)
expect(errors.assetId).toContain(schemaCopy.assetIdRequired)
}
})
it("preserves valid create and update payloads without errors", () => {
const createResult = buildCreateAssignmentSchema(schemaCopy).safeParse({
recipientId: "recipient-1",
itemId: "item-1",
quantity: 2,
})
expect(createResult.success).toBe(true)
if (createResult.success) {
expect(createResult.data.recipientId).toBe("recipient-1")
expect(createResult.data.quantity).toBe(2)
}
const updateResult = buildUpdateAssignmentSchema(schemaCopy).safeParse({
id: "assignment-1",
recipientId: "recipient-1",
itemId: "item-1",
assetId: "asset-1",
quantity: 1,
})
expect(updateResult.success).toBe(true)
if (updateResult.success) {
expect(updateResult.data.id).toBe("assignment-1")
}
})
it("keeps optional assignment fields optional in create", () => {
const result = buildCreateAssignmentSchema(schemaCopy).safeParse({
recipientId: "recipient-1",
quantity: 1,
})
expect(result.success).toBe(true)
})
it("allows update without itemId when no assetId is required", () => {
const result = buildUpdateAssignmentSchema(schemaCopy).safeParse({
id: "assignment-1",
recipientId: "recipient-1",
quantity: 1,
})
expect(result.success).toBe(true)
if (result.success) {
expect(result.data.id).toBe("assignment-1")
}
})
})