@open-mercato/core 0.6.8-develop.7054.1.3c75040822 → 0.6.8-develop.7057.1.61440fc3bc

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -580,9 +580,6 @@ const createStatementCommand: CommandHandler<ScopedStatementCreateInput, Stateme
580
580
  if (parsed.status !== undefined && parsed.status !== 'draft') {
581
581
  throw new CrudHttpError(400, { error: 'eudr.errors.invalidTransition' })
582
582
  }
583
- if (parsed.referenceIssuedAt !== undefined) {
584
- throw new CrudHttpError(400, { error: 'eudr.errors.referenceIssuedAtImmutable' })
585
- }
586
583
 
587
584
  const entityManager = (ctx.container.resolve('em') as EntityManager).fork()
588
585
  let record!: EudrDueDiligenceStatement
@@ -46,6 +46,11 @@ export const GEOJSON_TYPES = ['Feature', 'FeatureCollection', 'Point', 'Polygon'
46
46
 
47
47
  const uuid = () => z.string().uuid()
48
48
  const geoJsonSizeLimit = 1_048_576
49
+ // Two conventions for server-managed fields on these entities: fields listed below
50
+ // are never advertised as create inputs, so a client sending a real value is rejected
51
+ // (`rejectServerComputedFields` tolerates a null/undefined no-op echo). A field that
52
+ // WAS advertised in the create schema — statements' `referenceIssuedAt` — is instead
53
+ // dropped from the create schema, so it is stripped rather than rejected (#5508).
49
54
  const serverComputedSubmissionFields = ['completenessScore', 'missingFields'] as const
50
55
  const serverComputedPlotFields = ['validationWarnings', 'computedArea'] as const
51
56
  const serverComputedRiskAssessmentFields = ['countryRisks', 'overallTier', 'isSimplified', 'assessedByName'] as const
@@ -64,12 +69,16 @@ const orderSnapshotSchema = z.object({
64
69
  orderNumber: z.string().max(255).optional().nullable(),
65
70
  })
66
71
 
72
+ // A whole-object POST echoes server-computed fields back with their no-op value
73
+ // (a fresh draft's `submittedAt` is null), so tolerate a null/undefined echo and
74
+ // reject only a meaningful attempt to set server-owned state (#5508).
67
75
  function rejectServerComputedFields(fields: readonly string[]): z.ZodType<unknown> {
68
76
  return z.unknown().superRefine((input, context) => {
69
77
  if (input === null || typeof input !== 'object' || Array.isArray(input)) return
70
78
  const record = input as Record<string, unknown>
71
79
  for (const field of fields) {
72
80
  if (!(field in record)) continue
81
+ if (record[field] === null || record[field] === undefined) continue
73
82
  context.addIssue({
74
83
  code: z.ZodIssueCode.custom,
75
84
  path: [field],
@@ -333,7 +342,13 @@ const statementBaseSchema = z.object({
333
342
  notes: z.string().max(5000).optional().nullable(),
334
343
  })
335
344
 
336
- export const statementCreateSchema = rejectServerComputedFields(serverComputedStatementFields).pipe(statementBaseSchema)
345
+ // referenceIssuedAt is server-managed (only set at the submitted->available
346
+ // transition) and is never a create input, so it is dropped from the create
347
+ // schema. A whole-object POST that echoes it (null or a value) is accepted and
348
+ // the field ignored, instead of a self-contradictory 400 (#5508).
349
+ export const statementCreateSchema = rejectServerComputedFields(serverComputedStatementFields).pipe(
350
+ statementBaseSchema.omit({ referenceIssuedAt: true }),
351
+ )
337
352
 
338
353
  export const statementUpdateSchema = rejectServerComputedFields(serverComputedStatementFields).pipe(
339
354
  z
@@ -40,12 +40,12 @@ export function parseRequestedIds(raw: string): string[] {
40
40
  }
41
41
 
42
42
  export async function GET(req: Request) {
43
+ const { translate } = await resolveTranslations()
43
44
  try {
44
45
  const url = new URL(req.url)
45
46
  const parsedQuery = querySchema.parse(Object.fromEntries(url.searchParams))
46
47
  const container = await createRequestContainer()
47
48
  const auth = await getAuthFromRequest(req)
48
- const { translate } = await resolveTranslations()
49
49
  if (!auth || !auth.tenantId) {
50
50
  throw new CrudHttpError(401, { error: translate('warranty_claims.errors.unauthorized', 'Unauthorized') })
51
51
  }
@@ -75,10 +75,10 @@ export async function GET(req: Request) {
75
75
  return NextResponse.json(error.body, { status: error.status })
76
76
  }
77
77
  if (error instanceof z.ZodError) {
78
- return NextResponse.json({ error: 'warranty_claims.errors.invalidInput' }, { status: 400 })
78
+ return NextResponse.json({ error: translate('warranty_claims.errors.invalidInput', 'Invalid input') }, { status: 400 })
79
79
  }
80
80
  logger.error('[warranty_claims] Failed to resolve assignee display names', { error })
81
- return NextResponse.json({ error: 'warranty_claims.errors.load_failed' }, { status: 500 })
81
+ return NextResponse.json({ error: translate('warranty_claims.errors.load_failed', 'Failed to load warranty claim data.') }, { status: 500 })
82
82
  }
83
83
  }
84
84
 
@@ -153,12 +153,13 @@ function serializePortalClaim(claim: WarrantyClaim, lines: WarrantyClaimLine[])
153
153
  }
154
154
 
155
155
  async function resolvePortalContext(req: Request): Promise<PortalContext | Response> {
156
+ const { translate } = await resolveTranslations()
156
157
  const auth = await getCustomerAuthFromRequest(req)
157
158
  if (!auth) {
158
- return NextResponse.json({ ok: false, error: 'warranty_claims.errors.unauthorized' }, { status: 401 })
159
+ return NextResponse.json({ ok: false, error: translate('warranty_claims.errors.unauthorized', 'Unauthorized') }, { status: 401 })
159
160
  }
160
161
  if (!auth.customerEntityId) {
161
- return NextResponse.json({ ok: false, error: 'warranty_claims.errors.customerAccountNotLinked' }, { status: 403 })
162
+ return NextResponse.json({ ok: false, error: translate('warranty_claims.errors.customerAccountNotLinked', 'Customer account is not linked to a customer record') }, { status: 403 })
162
163
  }
163
164
  const container = await createRequestContainer()
164
165
  const em = container.resolve('em') as EntityManager
@@ -264,6 +265,7 @@ async function validatePortalOrderOwnership(
264
265
  context: PortalContext,
265
266
  input: PortalIntakeInput,
266
267
  ): Promise<OrderValidationResult | Response> {
268
+ const { translate } = await resolveTranslations()
267
269
  const orderLineIds = Array.from(new Set(input.lines.flatMap((line) => line.orderLineId ? [line.orderLineId] : [])))
268
270
  const orderLinesById = await loadOwnedOrderLines(context, orderLineIds)
269
271
  const orderIds = new Set<string>()
@@ -273,7 +275,7 @@ async function validatePortalOrderOwnership(
273
275
  let resolvedOrder = input.orderId ? (ordersById.get(input.orderId) ?? null) : null
274
276
  if (input.orderId) {
275
277
  if (!resolvedOrder) {
276
- return NextResponse.json({ ok: false, error: 'warranty_claims.errors.orderNotOwned' }, { status: 404 })
278
+ return NextResponse.json({ ok: false, error: translate('warranty_claims.errors.orderNotOwned', 'Order not found.') }, { status: 404 })
277
279
  }
278
280
  }
279
281
 
@@ -282,10 +284,9 @@ async function validatePortalOrderOwnership(
282
284
  const resolvedLine = orderLinesById.get(line.orderLineId)
283
285
  const lineOrder = resolvedLine ? ordersById.get(resolvedLine.orderId) : null
284
286
  if (!resolvedLine || !lineOrder) {
285
- return NextResponse.json({ ok: false, error: 'warranty_claims.errors.orderLineMismatch' }, { status: 404 })
287
+ return NextResponse.json({ ok: false, error: translate('warranty_claims.errors.orderLineMismatch', 'Order line does not belong to the selected order') }, { status: 404 })
286
288
  }
287
289
  if (resolvedOrder && lineOrder.id !== resolvedOrder.id) {
288
- const { translate } = await resolveTranslations()
289
290
  return NextResponse.json(
290
291
  { ok: false, error: translate('warranty_claims.errors.orderLineMismatch', 'Order line does not belong to the selected order') },
291
292
  { status: 400 },
@@ -337,7 +338,8 @@ export async function GET(req: Request) {
337
338
  serialNumber: url.searchParams.get('serialNumber') ?? undefined,
338
339
  })
339
340
  if (!query.success) {
340
- return NextResponse.json({ ok: false, error: 'warranty_claims.errors.invalidInput' }, { status: 400 })
341
+ const { translate } = await resolveTranslations()
342
+ return NextResponse.json({ ok: false, error: translate('warranty_claims.errors.invalidInput', 'Invalid input') }, { status: 400 })
341
343
  }
342
344
  const { page, pageSize, search, status, stateGroup, serialNumber } = query.data
343
345
  const where = buildPortalOwnedClaimWhere(context)
@@ -415,15 +417,16 @@ export async function POST(req: Request) {
415
417
  const contextOrResponse = await resolvePortalContext(req)
416
418
  if (contextOrResponse instanceof Response) return contextOrResponse
417
419
  const context = contextOrResponse
420
+ const { translate } = await resolveTranslations()
418
421
  let body: unknown
419
422
  try {
420
423
  body = await req.json()
421
424
  } catch {
422
- return NextResponse.json({ ok: false, error: 'warranty_claims.errors.invalidInput' }, { status: 400 })
425
+ return NextResponse.json({ ok: false, error: translate('warranty_claims.errors.invalidInput', 'Invalid input') }, { status: 400 })
423
426
  }
424
427
  const parsed = portalIntakeInputSchema.safeParse(body)
425
428
  if (!parsed.success) {
426
- return NextResponse.json({ ok: false, error: 'warranty_claims.errors.invalidInput' }, { status: 400 })
429
+ return NextResponse.json({ ok: false, error: translate('warranty_claims.errors.invalidInput', 'Invalid input') }, { status: 400 })
427
430
  }
428
431
  const ownership = await validatePortalOrderOwnership(context, parsed.data)
429
432
  if (ownership instanceof Response) return ownership
@@ -487,7 +490,7 @@ export async function POST(req: Request) {
487
490
  )
488
491
  const claimId = createResult.result?.claimId
489
492
  if (typeof claimId !== 'string') {
490
- return NextResponse.json({ ok: false, error: 'warranty_claims.errors.save_failed' }, { status: 500 })
493
+ return NextResponse.json({ ok: false, error: translate('warranty_claims.errors.save_failed', 'Failed to save warranty claim.') }, { status: 500 })
491
494
  }
492
495
  try {
493
496
  await commandBus.execute<{ id: string; actorCustomerId: string }, { claimId: string }>(