@open-mercato/core 0.7.1-develop.7131.1.20abf9e885 → 0.7.1-develop.7136.1.0f76137a1d

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.
Files changed (73) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/helpers/integration/communicationChannelsFixtures.js.map +2 -2
  3. package/dist/modules/communication_channels/api/post/test-seed/route.js +40 -2
  4. package/dist/modules/communication_channels/api/post/test-seed/route.js.map +2 -2
  5. package/dist/modules/communication_channels/lib/access-control.js +6 -1
  6. package/dist/modules/communication_channels/lib/access-control.js.map +2 -2
  7. package/dist/modules/communication_channels/lib/system-user.js +8 -3
  8. package/dist/modules/communication_channels/lib/system-user.js.map +2 -2
  9. package/dist/modules/warranty_claims/api/ai/assess/route.js +20 -18
  10. package/dist/modules/warranty_claims/api/ai/assess/route.js.map +2 -2
  11. package/dist/modules/warranty_claims/api/credit-memo/route.js +11 -12
  12. package/dist/modules/warranty_claims/api/credit-memo/route.js.map +2 -2
  13. package/dist/modules/warranty_claims/api/events/route.js +1 -1
  14. package/dist/modules/warranty_claims/api/events/route.js.map +2 -2
  15. package/dist/modules/warranty_claims/api/portal/attachments/route.js +24 -27
  16. package/dist/modules/warranty_claims/api/portal/attachments/route.js.map +2 -2
  17. package/dist/modules/warranty_claims/api/portal/claims/[id]/route.js +8 -10
  18. package/dist/modules/warranty_claims/api/portal/claims/[id]/route.js.map +2 -2
  19. package/dist/modules/warranty_claims/api/portal/claims/[id]/shared.js +4 -9
  20. package/dist/modules/warranty_claims/api/portal/claims/[id]/shared.js.map +2 -2
  21. package/dist/modules/warranty_claims/api/portal/claims/[id]/submit/route.js +3 -3
  22. package/dist/modules/warranty_claims/api/portal/claims/[id]/submit/route.js.map +2 -2
  23. package/dist/modules/warranty_claims/api/portal/claims/[id]/withdraw/route.js +3 -3
  24. package/dist/modules/warranty_claims/api/portal/claims/[id]/withdraw/route.js.map +2 -2
  25. package/dist/modules/warranty_claims/api/portal/claims/route.js +4 -9
  26. package/dist/modules/warranty_claims/api/portal/claims/route.js.map +2 -2
  27. package/dist/modules/warranty_claims/api/portal/events/route.js +12 -13
  28. package/dist/modules/warranty_claims/api/portal/events/route.js.map +2 -2
  29. package/dist/modules/warranty_claims/api/receiving/route.js +14 -15
  30. package/dist/modules/warranty_claims/api/receiving/route.js.map +2 -2
  31. package/dist/modules/warranty_claims/api/registrations/route.js +9 -4
  32. package/dist/modules/warranty_claims/api/registrations/route.js.map +2 -2
  33. package/dist/modules/warranty_claims/api/replacement-order/route.js +11 -12
  34. package/dist/modules/warranty_claims/api/replacement-order/route.js.map +2 -2
  35. package/dist/modules/warranty_claims/api/return-label/route.js +20 -19
  36. package/dist/modules/warranty_claims/api/return-label/route.js.map +2 -2
  37. package/dist/modules/warranty_claims/api/risk/route.js +2 -1
  38. package/dist/modules/warranty_claims/api/risk/route.js.map +2 -2
  39. package/dist/modules/warranty_claims/api/sales-return/route.js +11 -12
  40. package/dist/modules/warranty_claims/api/sales-return/route.js.map +2 -2
  41. package/dist/modules/warranty_claims/api/troubleshooting-guides/route.js +22 -1
  42. package/dist/modules/warranty_claims/api/troubleshooting-guides/route.js.map +2 -2
  43. package/dist/modules/warranty_claims/api/vendor-recovery-suggestions/route.js +2 -1
  44. package/dist/modules/warranty_claims/api/vendor-recovery-suggestions/route.js.map +2 -2
  45. package/dist/modules/warranty_claims/commands/shared.js +2 -2
  46. package/dist/modules/warranty_claims/commands/shared.js.map +2 -2
  47. package/dist/modules/warranty_claims/lib/portalAuthGuard.js +31 -0
  48. package/dist/modules/warranty_claims/lib/portalAuthGuard.js.map +7 -0
  49. package/package.json +7 -7
  50. package/src/helpers/integration/communicationChannelsFixtures.ts +13 -0
  51. package/src/modules/communication_channels/api/post/test-seed/route.ts +47 -2
  52. package/src/modules/communication_channels/lib/access-control.ts +28 -0
  53. package/src/modules/communication_channels/lib/system-user.ts +40 -22
  54. package/src/modules/warranty_claims/api/ai/assess/route.ts +23 -17
  55. package/src/modules/warranty_claims/api/credit-memo/route.ts +14 -13
  56. package/src/modules/warranty_claims/api/events/route.ts +1 -1
  57. package/src/modules/warranty_claims/api/portal/attachments/route.ts +29 -27
  58. package/src/modules/warranty_claims/api/portal/claims/[id]/route.ts +9 -10
  59. package/src/modules/warranty_claims/api/portal/claims/[id]/shared.ts +5 -9
  60. package/src/modules/warranty_claims/api/portal/claims/[id]/submit/route.ts +3 -3
  61. package/src/modules/warranty_claims/api/portal/claims/[id]/withdraw/route.ts +3 -3
  62. package/src/modules/warranty_claims/api/portal/claims/route.ts +5 -9
  63. package/src/modules/warranty_claims/api/portal/events/route.ts +13 -13
  64. package/src/modules/warranty_claims/api/receiving/route.ts +17 -15
  65. package/src/modules/warranty_claims/api/registrations/route.ts +9 -3
  66. package/src/modules/warranty_claims/api/replacement-order/route.ts +14 -13
  67. package/src/modules/warranty_claims/api/return-label/route.ts +21 -17
  68. package/src/modules/warranty_claims/api/risk/route.ts +3 -1
  69. package/src/modules/warranty_claims/api/sales-return/route.ts +14 -13
  70. package/src/modules/warranty_claims/api/troubleshooting-guides/route.ts +23 -1
  71. package/src/modules/warranty_claims/api/vendor-recovery-suggestions/route.ts +3 -1
  72. package/src/modules/warranty_claims/commands/shared.ts +7 -1
  73. package/src/modules/warranty_claims/lib/portalAuthGuard.ts +30 -0
@@ -22,16 +22,16 @@ export async function POST(req: Request, ctx: PortalClaimActionRouteContext) {
22
22
  const contextOrResponse = await resolvePortalActionContext(req)
23
23
  if (contextOrResponse instanceof Response) return contextOrResponse
24
24
  const context = contextOrResponse
25
+ const { translate } = await resolveTranslations()
25
26
  const claimId = await resolvePortalClaimId(ctx)
26
27
  if (!claimId) {
27
- return NextResponse.json({ ok: false, error: 'warranty_claims.errors.notFound' }, { status: 404 })
28
+ return NextResponse.json({ ok: false, error: translate('warranty_claims.errors.notFound', 'Claim not found.') }, { status: 404 })
28
29
  }
29
30
  const claim = await loadOwnedClaim(context, claimId)
30
31
  if (!claim) {
31
- return NextResponse.json({ ok: false, error: 'warranty_claims.errors.notFound' }, { status: 404 })
32
+ return NextResponse.json({ ok: false, error: translate('warranty_claims.errors.notFound', 'Claim not found.') }, { status: 404 })
32
33
  }
33
34
  if (claim.status !== 'draft') {
34
- const { translate } = await resolveTranslations()
35
35
  return NextResponse.json(
36
36
  { ok: false, error: translate('warranty_claims.errors.invalidTransition', 'That status change is not allowed.') },
37
37
  { status: 400 },
@@ -24,16 +24,16 @@ export async function POST(req: Request, ctx: PortalClaimActionRouteContext) {
24
24
  const contextOrResponse = await resolvePortalActionContext(req)
25
25
  if (contextOrResponse instanceof Response) return contextOrResponse
26
26
  const context = contextOrResponse
27
+ const { translate } = await resolveTranslations()
27
28
  const claimId = await resolvePortalClaimId(ctx)
28
29
  if (!claimId) {
29
- return NextResponse.json({ ok: false, error: 'warranty_claims.errors.notFound' }, { status: 404 })
30
+ return NextResponse.json({ ok: false, error: translate('warranty_claims.errors.notFound', 'Claim not found.') }, { status: 404 })
30
31
  }
31
32
  const claim = await loadOwnedClaim(context, claimId)
32
33
  if (!claim) {
33
- return NextResponse.json({ ok: false, error: 'warranty_claims.errors.notFound' }, { status: 404 })
34
+ return NextResponse.json({ ok: false, error: translate('warranty_claims.errors.notFound', 'Claim not found.') }, { status: 404 })
34
35
  }
35
36
  if (!PORTAL_WITHDRAWABLE_STATUSES.has(claim.status)) {
36
- const { translate } = await resolveTranslations()
37
37
  return NextResponse.json(
38
38
  { ok: false, error: translate('warranty_claims.errors.invalidTransition', 'That status change is not allowed.') },
39
39
  { status: 400 },
@@ -11,7 +11,8 @@ import { runRouteMutationGuards, type RouteMutationGuardResult } from '@open-mer
11
11
  import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
12
12
  import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
13
13
  import { createLogger } from '@open-mercato/shared/lib/logger'
14
- import { getCustomerAuthFromRequest, type CustomerAuthContext } from '@open-mercato/core/modules/customer_accounts/lib/customerAuth'
14
+ import type { CustomerAuthContext } from '@open-mercato/core/modules/customer_accounts/lib/customerAuth'
15
+ import { resolveLinkedCustomerAuth } from '../../../lib/portalAuthGuard'
15
16
  import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
16
17
  import { WarrantyClaim, WarrantyClaimLine } from '../../../data/entities'
17
18
  import {
@@ -154,14 +155,9 @@ function serializePortalClaim(claim: WarrantyClaim, lines: WarrantyClaimLine[])
154
155
  }
155
156
 
156
157
  async function resolvePortalContext(req: Request): Promise<PortalContext | Response> {
157
- const { translate } = await resolveTranslations()
158
- const auth = await getCustomerAuthFromRequest(req)
159
- if (!auth) {
160
- return NextResponse.json({ ok: false, error: translate('warranty_claims.errors.unauthorized', 'Unauthorized') }, { status: 401 })
161
- }
162
- if (!auth.customerEntityId) {
163
- return NextResponse.json({ ok: false, error: translate('warranty_claims.errors.customerAccountNotLinked', 'Customer account is not linked to a customer record') }, { status: 403 })
164
- }
158
+ const authOrResponse = await resolveLinkedCustomerAuth(req)
159
+ if (authOrResponse instanceof Response) return authOrResponse
160
+ const auth = authOrResponse
165
161
  const container = await createRequestContainer()
166
162
  const em = container.resolve('em') as EntityManager
167
163
  const commandAuth: NonNullable<AuthContext> = {
@@ -7,13 +7,15 @@ import type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib
7
7
  import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
8
8
  import { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'
9
9
  import { runRouteMutationGuards, type RouteMutationGuardResult } from '@open-mercato/shared/lib/crud/route-mutation-guard'
10
+ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
10
11
  import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
11
12
  import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
12
- import { getCustomerAuthFromRequest, type CustomerAuthContext } from '@open-mercato/core/modules/customer_accounts/lib/customerAuth'
13
+ import type { CustomerAuthContext } from '@open-mercato/core/modules/customer_accounts/lib/customerAuth'
13
14
  import { WarrantyClaim, WarrantyClaimEvent } from '../../../data/entities'
14
15
  import type { CommentClaimInput } from '../../../data/validators'
15
16
  import { WARRANTY_CLAIM_RESOURCE_KIND } from '../../../commands/shared'
16
17
  import { loadPortalOwnedClaim } from '../../../lib/portalClaimAccess'
18
+ import { resolveLinkedCustomerAuth } from '../../../lib/portalAuthGuard'
17
19
 
18
20
  export const metadata = {
19
21
  GET: { requireAuth: false },
@@ -71,13 +73,9 @@ function serializeEvent(event: WarrantyClaimEvent) {
71
73
  }
72
74
 
73
75
  async function resolvePortalContext(req: Request): Promise<PortalContext | Response> {
74
- const auth = await getCustomerAuthFromRequest(req)
75
- if (!auth) {
76
- return NextResponse.json({ ok: false, error: 'warranty_claims.errors.unauthorized' }, { status: 401 })
77
- }
78
- if (!auth.customerEntityId) {
79
- return NextResponse.json({ ok: false, error: 'warranty_claims.errors.customerAccountNotLinked' }, { status: 403 })
80
- }
76
+ const authOrResponse = await resolveLinkedCustomerAuth(req)
77
+ if (authOrResponse instanceof Response) return authOrResponse
78
+ const auth = authOrResponse
81
79
  const container = await createRequestContainer()
82
80
  const em = container.resolve('em') as EntityManager
83
81
  const commandAuth: NonNullable<AuthContext> = {
@@ -146,14 +144,15 @@ export async function GET(req: Request) {
146
144
  const contextOrResponse = await resolvePortalContext(req)
147
145
  if (contextOrResponse instanceof Response) return contextOrResponse
148
146
  const context = contextOrResponse
147
+ const { translate } = await resolveTranslations()
149
148
  const url = new URL(req.url)
150
149
  const parsed = eventQuerySchema.safeParse({ claimId: url.searchParams.get('claimId') ?? undefined })
151
150
  if (!parsed.success) {
152
- return NextResponse.json({ ok: false, error: 'warranty_claims.errors.invalidInput' }, { status: 400 })
151
+ return NextResponse.json({ ok: false, error: translate('warranty_claims.errors.invalidInput', 'Invalid input') }, { status: 400 })
153
152
  }
154
153
  const claim = await loadOwnedClaim(context, parsed.data.claimId)
155
154
  if (!claim) {
156
- return NextResponse.json({ ok: false, error: 'warranty_claims.errors.notFound' }, { status: 404 })
155
+ return NextResponse.json({ ok: false, error: translate('warranty_claims.errors.notFound', 'Claim not found.') }, { status: 404 })
157
156
  }
158
157
  const events = await findWithDecryption(
159
158
  context.em,
@@ -169,19 +168,20 @@ export async function POST(req: Request) {
169
168
  const contextOrResponse = await resolvePortalContext(req)
170
169
  if (contextOrResponse instanceof Response) return contextOrResponse
171
170
  const context = contextOrResponse
171
+ const { translate } = await resolveTranslations()
172
172
  let body: unknown
173
173
  try {
174
174
  body = await req.json()
175
175
  } catch {
176
- return NextResponse.json({ ok: false, error: 'warranty_claims.errors.invalidInput' }, { status: 400 })
176
+ return NextResponse.json({ ok: false, error: translate('warranty_claims.errors.invalidInput', 'Invalid input') }, { status: 400 })
177
177
  }
178
178
  const parsed = portalCommentSchema.safeParse(body)
179
179
  if (!parsed.success) {
180
- return NextResponse.json({ ok: false, error: 'warranty_claims.errors.invalidInput' }, { status: 400 })
180
+ return NextResponse.json({ ok: false, error: translate('warranty_claims.errors.invalidInput', 'Invalid input') }, { status: 400 })
181
181
  }
182
182
  const claim = await loadOwnedClaim(context, parsed.data.claimId)
183
183
  if (!claim) {
184
- return NextResponse.json({ ok: false, error: 'warranty_claims.errors.notFound' }, { status: 404 })
184
+ return NextResponse.json({ ok: false, error: translate('warranty_claims.errors.notFound', 'Claim not found.') }, { status: 404 })
185
185
  }
186
186
  const input: CommentClaimInput = {
187
187
  claimId: claim.id,
@@ -7,6 +7,7 @@ import type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib
7
7
  import { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
8
8
  import { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'
9
9
  import { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'
10
+ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
10
11
  import { runRouteMutationGuards, type RouteMutationGuardResult } from '@open-mercato/shared/lib/crud/route-mutation-guard'
11
12
  import { withScopedPayload } from '@open-mercato/shared/lib/api/scoped'
12
13
  import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
@@ -63,20 +64,19 @@ function toRecord(value: unknown): Record<string, unknown> {
63
64
  return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : {}
64
65
  }
65
66
 
66
- function translateKey(key: string): string {
67
- return key
68
- }
69
-
70
- async function resolveActionContext(req: Request): Promise<ActionRouteContext> {
67
+ async function resolveActionContext(
68
+ req: Request,
69
+ translate: ActionRouteContext['translate'],
70
+ ): Promise<ActionRouteContext> {
71
71
  const container = await createRequestContainer()
72
72
  const auth = await getAuthFromRequest(req)
73
73
  if (!auth || !auth.tenantId) {
74
- throw new CrudHttpError(401, { error: 'warranty_claims.errors.unauthorized' })
74
+ throw new CrudHttpError(401, { error: translate('warranty_claims.errors.unauthorized', 'Unauthorized') })
75
75
  }
76
76
  const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })
77
77
  const organizationId = scope?.selectedId ?? auth.orgId ?? null
78
78
  if (!organizationId) {
79
- throw new CrudHttpError(400, { error: 'warranty_claims.errors.organization_required' })
79
+ throw new CrudHttpError(400, { error: translate('warranty_claims.errors.organization_required', 'Organization context is required.') })
80
80
  }
81
81
  return {
82
82
  ctx: {
@@ -89,7 +89,7 @@ async function resolveActionContext(req: Request): Promise<ActionRouteContext> {
89
89
  },
90
90
  tenantId: auth.tenantId,
91
91
  organizationId,
92
- translate: translateKey,
92
+ translate,
93
93
  }
94
94
  }
95
95
 
@@ -126,7 +126,7 @@ async function runGuard(
126
126
  ): Promise<RouteMutationGuardResult> {
127
127
  const userId = context.ctx.auth?.sub
128
128
  if (!userId) {
129
- throw new CrudHttpError(401, { error: 'warranty_claims.errors.unauthorized' })
129
+ throw new CrudHttpError(401, { error: context.translate('warranty_claims.errors.unauthorized', 'Unauthorized') })
130
130
  }
131
131
  return runRouteMutationGuards({
132
132
  container: context.ctx.container,
@@ -149,26 +149,28 @@ async function executeReceivingAction(
149
149
  commandBus: CommandBus,
150
150
  action: ReceivingAction,
151
151
  ctx: CommandRuntimeContext,
152
+ translate: ActionRouteContext['translate'],
152
153
  ): Promise<{ lineId: string; claimId: string }> {
153
154
  if (action.kind === 'release') {
154
155
  const { result } = await commandBus.execute<ClaimLineReleaseQuarantineInput, { lineId: string; claimId: string }>(
155
156
  'warranty_claims.claim_line.release_quarantine',
156
157
  { input: action.input, ctx },
157
158
  )
158
- if (!result) throw new CrudHttpError(400, { error: 'warranty_claims.errors.save_failed' })
159
+ if (!result) throw new CrudHttpError(400, { error: translate('warranty_claims.errors.save_failed', 'Failed to save warranty claim.') })
159
160
  return result
160
161
  }
161
162
  const { result } = await commandBus.execute<ClaimLineReceiveInput, { lineId: string; claimId: string }>(
162
163
  'warranty_claims.claim_line.receive',
163
164
  { input: action.input, ctx },
164
165
  )
165
- if (!result) throw new CrudHttpError(400, { error: 'warranty_claims.errors.save_failed' })
166
+ if (!result) throw new CrudHttpError(400, { error: translate('warranty_claims.errors.save_failed', 'Failed to save warranty claim.') })
166
167
  return result
167
168
  }
168
169
 
169
170
  export async function POST(req: Request) {
171
+ const { translate } = await resolveTranslations()
170
172
  try {
171
- const context = await resolveActionContext(req)
173
+ const context = await resolveActionContext(req, translate)
172
174
  const payload = toRecord(await readJsonSafe(req, {}))
173
175
  const action = toReceivingAction(payload, context)
174
176
  const guarded = await runGuard(req, context, action)
@@ -178,7 +180,7 @@ export async function POST(req: Request) {
178
180
  const commandAction = guarded.modifiedPayload ? toGuardedAction(action.kind, guarded.modifiedPayload) : action
179
181
 
180
182
  const commandBus = context.ctx.container.resolve('commandBus') as CommandBus
181
- const result = await executeReceivingAction(commandBus, commandAction, context.ctx)
183
+ const result = await executeReceivingAction(commandBus, commandAction, context.ctx, translate)
182
184
 
183
185
  await guarded.runAfterSuccess()
184
186
 
@@ -190,10 +192,10 @@ export async function POST(req: Request) {
190
192
  return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status })
191
193
  }
192
194
  if (err instanceof z.ZodError) {
193
- return NextResponse.json({ error: 'warranty_claims.errors.invalidInput' }, { status: 400 })
195
+ return NextResponse.json({ error: translate('warranty_claims.errors.invalidInput', 'Invalid input') }, { status: 400 })
194
196
  }
195
197
  logger.error('warranty_claims.receiving.post failed', { err })
196
- return NextResponse.json({ error: 'warranty_claims.errors.save_failed' }, { status: 500 })
198
+ return NextResponse.json({ error: translate('warranty_claims.errors.save_failed', 'Failed to save warranty claim.') }, { status: 500 })
197
199
  }
198
200
  }
199
201
 
@@ -5,6 +5,7 @@ import { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'
5
5
  import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
6
6
  import { buildIlikeTerm } from '@open-mercato/shared/lib/db/buildIlikeTerm'
7
7
  import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
8
+ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
8
9
  import { E } from '#generated/entities.ids.generated'
9
10
  import * as F from '#generated/entities/warranty_claim_registration'
10
11
  import { WarrantyClaimRegistration } from '../../data/entities'
@@ -92,6 +93,7 @@ async function assertRegistrationSerialUnique(
92
93
  scope: { organizationId: string; tenantId: string },
93
94
  serialNumber: string | null | undefined,
94
95
  excludeId: string | null,
96
+ translate: (key: string, fallback?: string) => string,
95
97
  ): Promise<void> {
96
98
  if (!serialNumber) return
97
99
  const where: FilterQuery<WarrantyClaimRegistration> = {
@@ -103,7 +105,9 @@ async function assertRegistrationSerialUnique(
103
105
  if (excludeId) where.id = { $ne: excludeId }
104
106
  const existing = await em.count(WarrantyClaimRegistration, where)
105
107
  if (existing > 0) {
106
- throw new CrudHttpError(400, { error: 'warranty_claims.errors.duplicateSerial' })
108
+ throw new CrudHttpError(400, {
109
+ error: translate('warranty_claims.errors.duplicateSerial', 'A registration with this serial number already exists.'),
110
+ })
107
111
  }
108
112
  }
109
113
 
@@ -320,8 +324,9 @@ const crud = makeCrudRoute<RawRegistrationInput, RawRegistrationInput, Registrat
320
324
  const result = registrationCreateSchema.safeParse({ ...input, ...scope })
321
325
  if (!result.success) return
322
326
  const em = (ctx.container.resolve('em') as EntityManager).fork()
327
+ const { translate } = await resolveTranslations()
323
328
  await assertOrderBelongsToCustomer(em, scope, result.data.orderId ?? null, result.data.customerId ?? null)
324
- await assertRegistrationSerialUnique(em, scope, result.data.serialNumber ?? null, null)
329
+ await assertRegistrationSerialUnique(em, scope, result.data.serialNumber ?? null, null, translate)
325
330
  },
326
331
  beforeUpdate: async (input, ctx) => {
327
332
  const scope = scopeFromContext(ctx)
@@ -346,7 +351,8 @@ const crud = makeCrudRoute<RawRegistrationInput, RawRegistrationInput, Registrat
346
351
  const effectiveOrderId = hasOwn(parsed, 'orderId') ? (parsed.orderId ?? null) : (existing.orderId ?? null)
347
352
  await assertOrderBelongsToCustomer(em, scope, effectiveOrderId, effectiveCustomerId)
348
353
  if (hasOwn(parsed, 'serialNumber') && (parsed.serialNumber ?? null) !== (existing.serialNumber ?? null)) {
349
- await assertRegistrationSerialUnique(em, scope, parsed.serialNumber ?? null, existing.id)
354
+ const { translate } = await resolveTranslations()
355
+ await assertRegistrationSerialUnique(em, scope, parsed.serialNumber ?? null, existing.id, translate)
350
356
  }
351
357
  },
352
358
  afterCreate: async (entity) => {
@@ -7,6 +7,7 @@ import type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib
7
7
  import { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
8
8
  import { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'
9
9
  import { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'
10
+ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
10
11
  import { runRouteMutationGuards, type RouteMutationGuardResult } from '@open-mercato/shared/lib/crud/route-mutation-guard'
11
12
  import { withScopedPayload } from '@open-mercato/shared/lib/api/scoped'
12
13
  import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
@@ -57,24 +58,23 @@ export const metadata = {
57
58
  POST: { requireAuth: true, requireFeatures: ['warranty_claims.claim.manage', 'sales.orders.manage'] },
58
59
  }
59
60
 
60
- function translateKey(key: string): string {
61
- return key
62
- }
63
-
64
61
  function toRecord(value: unknown): Record<string, unknown> {
65
62
  return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : {}
66
63
  }
67
64
 
68
- async function resolveActionContext(req: Request): Promise<ActionRouteContext> {
65
+ async function resolveActionContext(
66
+ req: Request,
67
+ translate: ActionRouteContext['translate'],
68
+ ): Promise<ActionRouteContext> {
69
69
  const container = await createRequestContainer()
70
70
  const auth = await getAuthFromRequest(req)
71
71
  if (!auth || !auth.tenantId) {
72
- throw new CrudHttpError(401, { error: 'warranty_claims.errors.unauthorized' })
72
+ throw new CrudHttpError(401, { error: translate('warranty_claims.errors.unauthorized', 'Unauthorized') })
73
73
  }
74
74
  const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })
75
75
  const organizationId = scope?.selectedId ?? auth.orgId ?? null
76
76
  if (!organizationId) {
77
- throw new CrudHttpError(400, { error: 'warranty_claims.errors.organization_required' })
77
+ throw new CrudHttpError(400, { error: translate('warranty_claims.errors.organization_required', 'Organization context is required.') })
78
78
  }
79
79
  return {
80
80
  ctx: {
@@ -87,7 +87,7 @@ async function resolveActionContext(req: Request): Promise<ActionRouteContext> {
87
87
  },
88
88
  tenantId: auth.tenantId,
89
89
  organizationId,
90
- translate: translateKey,
90
+ translate,
91
91
  }
92
92
  }
93
93
 
@@ -114,7 +114,7 @@ async function runGuard(
114
114
  ): Promise<RouteMutationGuardResult> {
115
115
  const userId = context.ctx.auth?.sub
116
116
  if (!userId) {
117
- throw new CrudHttpError(401, { error: 'warranty_claims.errors.unauthorized' })
117
+ throw new CrudHttpError(401, { error: context.translate('warranty_claims.errors.unauthorized', 'Unauthorized') })
118
118
  }
119
119
  return runRouteMutationGuards({
120
120
  container: context.ctx.container,
@@ -134,8 +134,9 @@ async function runGuard(
134
134
  }
135
135
 
136
136
  export async function POST(req: Request) {
137
+ const { translate } = await resolveTranslations()
137
138
  try {
138
- const context = await resolveActionContext(req)
139
+ const context = await resolveActionContext(req, translate)
139
140
  const requestInput = toReplacementOrderRequest(toRecord(await readJsonSafe(req, {})))
140
141
  const guarded = await runGuard(req, context, requestInput)
141
142
  if (!guarded.ok) {
@@ -149,7 +150,7 @@ export async function POST(req: Request) {
149
150
  'warranty_claims.claim.create_replacement_order',
150
151
  { input: toCommandInput(guardedInput, context), ctx: context.ctx },
151
152
  )
152
- if (!result) throw new CrudHttpError(400, { error: 'warranty_claims.errors.save_failed' })
153
+ if (!result) throw new CrudHttpError(400, { error: translate('warranty_claims.errors.save_failed', 'Failed to save warranty claim.') })
153
154
  await guarded.runAfterSuccess()
154
155
  return NextResponse.json({
155
156
  replacementOrderId: result.replacementOrderId,
@@ -162,10 +163,10 @@ export async function POST(req: Request) {
162
163
  return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status })
163
164
  }
164
165
  if (err instanceof z.ZodError) {
165
- return NextResponse.json({ error: 'warranty_claims.errors.invalidInput' }, { status: 400 })
166
+ return NextResponse.json({ error: translate('warranty_claims.errors.invalidInput', 'Invalid input') }, { status: 400 })
166
167
  }
167
168
  logger.error('warranty_claims.replacement-order.post failed', { err })
168
- return NextResponse.json({ error: 'warranty_claims.errors.save_failed' }, { status: 500 })
169
+ return NextResponse.json({ error: translate('warranty_claims.errors.save_failed', 'Failed to save warranty claim.') }, { status: 500 })
169
170
  }
170
171
  }
171
172
 
@@ -8,6 +8,7 @@ import type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib
8
8
  import { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
9
9
  import { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'
10
10
  import { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'
11
+ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
11
12
  import { runRouteMutationGuards, type RouteMutationGuardResult } from '@open-mercato/shared/lib/crud/route-mutation-guard'
12
13
  import { withScopedPayload } from '@open-mercato/shared/lib/api/scoped'
13
14
  import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
@@ -70,24 +71,23 @@ export const metadata = {
70
71
  POST: { requireAuth: true, requireFeatures: ['warranty_claims.claim.manage'] },
71
72
  }
72
73
 
73
- function translateKey(key: string): string {
74
- return key
75
- }
76
-
77
74
  function toRecord(value: unknown): Record<string, unknown> {
78
75
  return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : {}
79
76
  }
80
77
 
81
- async function resolveActionContext(req: Request): Promise<ActionRouteContext> {
78
+ async function resolveActionContext(
79
+ req: Request,
80
+ translate: ActionRouteContext['translate'],
81
+ ): Promise<ActionRouteContext> {
82
82
  const container = await createRequestContainer()
83
83
  const auth = await getAuthFromRequest(req)
84
84
  if (!auth || !auth.tenantId) {
85
- throw new CrudHttpError(401, { error: 'warranty_claims.errors.unauthorized' })
85
+ throw new CrudHttpError(401, { error: translate('warranty_claims.errors.unauthorized', 'Unauthorized') })
86
86
  }
87
87
  const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })
88
88
  const organizationId = scope?.selectedId ?? auth.orgId ?? null
89
89
  if (!organizationId) {
90
- throw new CrudHttpError(400, { error: 'warranty_claims.errors.organization_required' })
90
+ throw new CrudHttpError(400, { error: translate('warranty_claims.errors.organization_required', 'Organization context is required.') })
91
91
  }
92
92
  return {
93
93
  ctx: {
@@ -100,7 +100,7 @@ async function resolveActionContext(req: Request): Promise<ActionRouteContext> {
100
100
  },
101
101
  tenantId: auth.tenantId,
102
102
  organizationId,
103
- translate: translateKey,
103
+ translate,
104
104
  }
105
105
  }
106
106
 
@@ -143,7 +143,7 @@ async function runGuard(
143
143
  ): Promise<RouteMutationGuardResult> {
144
144
  const userId = context.ctx.auth?.sub
145
145
  if (!userId) {
146
- throw new CrudHttpError(401, { error: 'warranty_claims.errors.unauthorized' })
146
+ throw new CrudHttpError(401, { error: context.translate('warranty_claims.errors.unauthorized', 'Unauthorized') })
147
147
  }
148
148
  return runRouteMutationGuards({
149
149
  container: context.ctx.container,
@@ -168,7 +168,7 @@ async function loadClaimAndLines(
168
168
  ): Promise<{ claim: WarrantyClaim; lines: WarrantyClaimLine[] }> {
169
169
  const scope = { tenantId: context.tenantId, organizationId: context.organizationId }
170
170
  const em = (context.ctx.container.resolve('em') as EntityManager).fork()
171
- const claim = await requireScopedClaim(em, claimId, scope)
171
+ const claim = await requireScopedClaim(em, claimId, scope, {}, context.translate('warranty_claims.errors.notFound', 'Claim not found.'))
172
172
  const lines = await findWithDecryption(
173
173
  em,
174
174
  WarrantyClaimLine,
@@ -179,9 +179,9 @@ async function loadClaimAndLines(
179
179
  return { claim, lines }
180
180
  }
181
181
 
182
- function assertGeneratableStatus(claim: WarrantyClaim): void {
182
+ function assertGeneratableStatus(claim: WarrantyClaim, translate: ActionRouteContext['translate']): void {
183
183
  if (claim.status !== 'approved' && claim.status !== 'awaiting_return') {
184
- throw new CrudHttpError(400, { error: 'warranty_claims.errors.returnLabelInvalidStatus' })
184
+ throw new CrudHttpError(400, { error: translate('warranty_claims.errors.returnLabelInvalidStatus', 'Return labels can only be created for approved claims that are awaiting return.') })
185
185
  }
186
186
  }
187
187
 
@@ -189,12 +189,13 @@ async function executeSetReturnLabel(
189
189
  commandBus: CommandBus,
190
190
  input: ClaimSetReturnLabelInput,
191
191
  ctx: CommandRuntimeContext,
192
+ translate: ActionRouteContext['translate'],
192
193
  ): Promise<ReturnLabelCreatedResponse> {
193
194
  const { result } = await commandBus.execute<ClaimSetReturnLabelInput, ReturnLabelCommandResult>(
194
195
  'warranty_claims.claim.set_return_label',
195
196
  { input, ctx },
196
197
  )
197
- if (!result) throw new CrudHttpError(400, { error: 'warranty_claims.errors.save_failed' })
198
+ if (!result) throw new CrudHttpError(400, { error: translate('warranty_claims.errors.save_failed', 'Failed to save warranty claim.') })
198
199
  return {
199
200
  status: 'created',
200
201
  labelUrl: result.labelUrl,
@@ -211,8 +212,9 @@ export function firstTranslatableIssueKey(err: z.ZodError): string {
211
212
  }
212
213
 
213
214
  export async function POST(req: Request) {
215
+ const { translate } = await resolveTranslations()
214
216
  try {
215
- const context = await resolveActionContext(req)
217
+ const context = await resolveActionContext(req, translate)
216
218
  const requestInput = toReturnLabelRequest(toRecord(await readJsonSafe(req, {})))
217
219
  const guarded = await runGuard(req, context, requestInput)
218
220
  if (!guarded.ok) {
@@ -226,13 +228,14 @@ export async function POST(req: Request) {
226
228
  commandBus,
227
229
  toSetReturnLabelInput(guardedInput, context),
228
230
  context.ctx,
231
+ translate,
229
232
  )
230
233
  await guarded.runAfterSuccess()
231
234
  return NextResponse.json(response)
232
235
  }
233
236
 
234
237
  const { claim, lines } = await loadClaimAndLines(context, guardedInput.claimId)
235
- assertGeneratableStatus(claim)
238
+ assertGeneratableStatus(claim, translate)
236
239
  await enforceWarrantyClaimOptimisticLock(
237
240
  context.ctx,
238
241
  claim,
@@ -260,6 +263,7 @@ export async function POST(req: Request) {
260
263
  carrier: result.carrier,
261
264
  }, context),
262
265
  context.ctx,
266
+ translate,
263
267
  )
264
268
  await guarded.runAfterSuccess()
265
269
  return NextResponse.json(response)
@@ -270,10 +274,10 @@ export async function POST(req: Request) {
270
274
  return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status })
271
275
  }
272
276
  if (err instanceof z.ZodError) {
273
- return NextResponse.json({ error: firstTranslatableIssueKey(err) }, { status: 400 })
277
+ return NextResponse.json({ error: translate(firstTranslatableIssueKey(err), 'Invalid input') }, { status: 400 })
274
278
  }
275
279
  logger.error('warranty_claims.return-label.post failed', { err })
276
- return NextResponse.json({ error: 'warranty_claims.errors.save_failed' }, { status: 500 })
280
+ return NextResponse.json({ error: translate('warranty_claims.errors.save_failed', 'Failed to save warranty claim.') }, { status: 500 })
277
281
  }
278
282
  }
279
283
 
@@ -39,6 +39,7 @@ type RiskRouteContext = {
39
39
  tenantId: string
40
40
  organizationId: string
41
41
  scope: WarrantyClaimScope
42
+ translate: (key: string, fallback?: string) => string
42
43
  em: EntityManager
43
44
  }
44
45
 
@@ -63,6 +64,7 @@ async function resolveRiskContext(req: Request): Promise<RiskRouteContext> {
63
64
  tenantId: auth.tenantId,
64
65
  organizationId,
65
66
  scope: { tenantId: auth.tenantId, organizationId },
67
+ translate,
66
68
  em,
67
69
  }
68
70
  }
@@ -72,7 +74,7 @@ export async function GET(req: Request) {
72
74
  const context = await resolveRiskContext(req)
73
75
  const url = new URL(req.url)
74
76
  const query = querySchema.parse(Object.fromEntries(url.searchParams))
75
- const claim = await requireScopedClaim(context.em, query.claimId, context.scope)
77
+ const claim = await requireScopedClaim(context.em, query.claimId, context.scope, {}, context.translate('warranty_claims.errors.notFound', 'Claim not found.'))
76
78
  const lines = await findWithDecryption(
77
79
  context.em,
78
80
  WarrantyClaimLine,
@@ -7,6 +7,7 @@ import type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib
7
7
  import { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
8
8
  import { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'
9
9
  import { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'
10
+ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
10
11
  import { runRouteMutationGuards, type RouteMutationGuardResult } from '@open-mercato/shared/lib/crud/route-mutation-guard'
11
12
  import { withScopedPayload } from '@open-mercato/shared/lib/api/scoped'
12
13
  import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
@@ -55,24 +56,23 @@ export const metadata = {
55
56
  POST: { requireAuth: true, requireFeatures: ['warranty_claims.claim.manage', 'sales.returns.create'] },
56
57
  }
57
58
 
58
- function translateKey(key: string): string {
59
- return key
60
- }
61
-
62
59
  function toRecord(value: unknown): Record<string, unknown> {
63
60
  return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : {}
64
61
  }
65
62
 
66
- async function resolveActionContext(req: Request): Promise<ActionRouteContext> {
63
+ async function resolveActionContext(
64
+ req: Request,
65
+ translate: ActionRouteContext['translate'],
66
+ ): Promise<ActionRouteContext> {
67
67
  const container = await createRequestContainer()
68
68
  const auth = await getAuthFromRequest(req)
69
69
  if (!auth || !auth.tenantId) {
70
- throw new CrudHttpError(401, { error: 'warranty_claims.errors.unauthorized' })
70
+ throw new CrudHttpError(401, { error: translate('warranty_claims.errors.unauthorized', 'Unauthorized') })
71
71
  }
72
72
  const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })
73
73
  const organizationId = scope?.selectedId ?? auth.orgId ?? null
74
74
  if (!organizationId) {
75
- throw new CrudHttpError(400, { error: 'warranty_claims.errors.organization_required' })
75
+ throw new CrudHttpError(400, { error: translate('warranty_claims.errors.organization_required', 'Organization context is required.') })
76
76
  }
77
77
  return {
78
78
  ctx: {
@@ -85,7 +85,7 @@ async function resolveActionContext(req: Request): Promise<ActionRouteContext> {
85
85
  },
86
86
  tenantId: auth.tenantId,
87
87
  organizationId,
88
- translate: translateKey,
88
+ translate,
89
89
  }
90
90
  }
91
91
 
@@ -111,7 +111,7 @@ async function runGuard(
111
111
  ): Promise<RouteMutationGuardResult> {
112
112
  const userId = context.ctx.auth?.sub
113
113
  if (!userId) {
114
- throw new CrudHttpError(401, { error: 'warranty_claims.errors.unauthorized' })
114
+ throw new CrudHttpError(401, { error: context.translate('warranty_claims.errors.unauthorized', 'Unauthorized') })
115
115
  }
116
116
  return runRouteMutationGuards({
117
117
  container: context.ctx.container,
@@ -131,8 +131,9 @@ async function runGuard(
131
131
  }
132
132
 
133
133
  export async function POST(req: Request) {
134
+ const { translate } = await resolveTranslations()
134
135
  try {
135
- const context = await resolveActionContext(req)
136
+ const context = await resolveActionContext(req, translate)
136
137
  const requestInput = toSalesReturnRequest(toRecord(await readJsonSafe(req, {})))
137
138
  const guarded = await runGuard(req, context, requestInput)
138
139
  if (!guarded.ok) {
@@ -144,7 +145,7 @@ export async function POST(req: Request) {
144
145
  'warranty_claims.claim.create_sales_return',
145
146
  { input: toCommandInput(guardedInput, context), ctx: context.ctx },
146
147
  )
147
- if (!result) throw new CrudHttpError(400, { error: 'warranty_claims.errors.save_failed' })
148
+ if (!result) throw new CrudHttpError(400, { error: translate('warranty_claims.errors.save_failed', 'Failed to save warranty claim.') })
148
149
  await guarded.runAfterSuccess()
149
150
  return NextResponse.json({ salesReturnId: result.salesReturnId, skippedLineIds: result.skippedLineIds })
150
151
  } catch (err) {
@@ -154,10 +155,10 @@ export async function POST(req: Request) {
154
155
  return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status })
155
156
  }
156
157
  if (err instanceof z.ZodError) {
157
- return NextResponse.json({ error: 'warranty_claims.errors.invalidInput' }, { status: 400 })
158
+ return NextResponse.json({ error: translate('warranty_claims.errors.invalidInput', 'Invalid input') }, { status: 400 })
158
159
  }
159
160
  logger.error('warranty_claims.sales-return.post failed', { err })
160
- return NextResponse.json({ error: 'warranty_claims.errors.save_failed' }, { status: 500 })
161
+ return NextResponse.json({ error: translate('warranty_claims.errors.save_failed', 'Failed to save warranty claim.') }, { status: 500 })
161
162
  }
162
163
  }
163
164