@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
@@ -6,6 +6,7 @@ import { resolveOrganizationScopeForRequest } from "@open-mercato/core/modules/d
6
6
  import { CrudHttpError, isCrudHttpError } from "@open-mercato/shared/lib/crud/errors";
7
7
  import { getCommandInterceptorHttpRejection } from "@open-mercato/shared/lib/commands/errors";
8
8
  import { readJsonSafe } from "@open-mercato/shared/lib/http/readJsonSafe";
9
+ import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
9
10
  import { runRouteMutationGuards } from "@open-mercato/shared/lib/crud/route-mutation-guard";
10
11
  import { withScopedPayload } from "@open-mercato/shared/lib/api/scoped";
11
12
  import { claimCreateSalesReturnSchema } from "../../data/validators.js";
@@ -28,22 +29,19 @@ const errorResponseSchema = z.object({
28
29
  const metadata = {
29
30
  POST: { requireAuth: true, requireFeatures: ["warranty_claims.claim.manage", "sales.returns.create"] }
30
31
  };
31
- function translateKey(key) {
32
- return key;
33
- }
34
32
  function toRecord(value) {
35
33
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
36
34
  }
37
- async function resolveActionContext(req) {
35
+ async function resolveActionContext(req, translate) {
38
36
  const container = await createRequestContainer();
39
37
  const auth = await getAuthFromRequest(req);
40
38
  if (!auth || !auth.tenantId) {
41
- throw new CrudHttpError(401, { error: "warranty_claims.errors.unauthorized" });
39
+ throw new CrudHttpError(401, { error: translate("warranty_claims.errors.unauthorized", "Unauthorized") });
42
40
  }
43
41
  const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req });
44
42
  const organizationId = scope?.selectedId ?? auth.orgId ?? null;
45
43
  if (!organizationId) {
46
- throw new CrudHttpError(400, { error: "warranty_claims.errors.organization_required" });
44
+ throw new CrudHttpError(400, { error: translate("warranty_claims.errors.organization_required", "Organization context is required.") });
47
45
  }
48
46
  return {
49
47
  ctx: {
@@ -56,7 +54,7 @@ async function resolveActionContext(req) {
56
54
  },
57
55
  tenantId: auth.tenantId,
58
56
  organizationId,
59
- translate: translateKey
57
+ translate
60
58
  };
61
59
  }
62
60
  function toSalesReturnRequest(payload) {
@@ -75,7 +73,7 @@ function toCommandInput(input, context) {
75
73
  async function runGuard(req, context, input) {
76
74
  const userId = context.ctx.auth?.sub;
77
75
  if (!userId) {
78
- throw new CrudHttpError(401, { error: "warranty_claims.errors.unauthorized" });
76
+ throw new CrudHttpError(401, { error: context.translate("warranty_claims.errors.unauthorized", "Unauthorized") });
79
77
  }
80
78
  return runRouteMutationGuards({
81
79
  container: context.ctx.container,
@@ -94,8 +92,9 @@ async function runGuard(req, context, input) {
94
92
  });
95
93
  }
96
94
  async function POST(req) {
95
+ const { translate } = await resolveTranslations();
97
96
  try {
98
- const context = await resolveActionContext(req);
97
+ const context = await resolveActionContext(req, translate);
99
98
  const requestInput = toSalesReturnRequest(toRecord(await readJsonSafe(req, {})));
100
99
  const guarded = await runGuard(req, context, requestInput);
101
100
  if (!guarded.ok) {
@@ -107,7 +106,7 @@ async function POST(req) {
107
106
  "warranty_claims.claim.create_sales_return",
108
107
  { input: toCommandInput(guardedInput, context), ctx: context.ctx }
109
108
  );
110
- if (!result) throw new CrudHttpError(400, { error: "warranty_claims.errors.save_failed" });
109
+ if (!result) throw new CrudHttpError(400, { error: translate("warranty_claims.errors.save_failed", "Failed to save warranty claim.") });
111
110
  await guarded.runAfterSuccess();
112
111
  return NextResponse.json({ salesReturnId: result.salesReturnId, skippedLineIds: result.skippedLineIds });
113
112
  } catch (err) {
@@ -117,10 +116,10 @@ async function POST(req) {
117
116
  return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status });
118
117
  }
119
118
  if (err instanceof z.ZodError) {
120
- return NextResponse.json({ error: "warranty_claims.errors.invalidInput" }, { status: 400 });
119
+ return NextResponse.json({ error: translate("warranty_claims.errors.invalidInput", "Invalid input") }, { status: 400 });
121
120
  }
122
121
  logger.error("warranty_claims.sales-return.post failed", { err });
123
- return NextResponse.json({ error: "warranty_claims.errors.save_failed" }, { status: 500 });
122
+ return NextResponse.json({ error: translate("warranty_claims.errors.save_failed", "Failed to save warranty claim.") }, { status: 500 });
124
123
  }
125
124
  }
126
125
  const openApi = {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/warranty_claims/api/sales-return/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { runRouteMutationGuards, type RouteMutationGuardResult } from '@open-mercato/shared/lib/crud/route-mutation-guard'\nimport { withScopedPayload } from '@open-mercato/shared/lib/api/scoped'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { claimCreateSalesReturnSchema, type ClaimCreateSalesReturnInput } from '../../data/validators'\nimport { WARRANTY_CLAIM_RESOURCE_KIND } from '../../commands/shared'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('warranty_claims')\n\ntype ActionRouteContext = {\n ctx: CommandRuntimeContext\n tenantId: string\n organizationId: string\n translate: (key: string, fallback?: string) => string\n}\n\ntype SalesReturnCommandResult = {\n claimId: string\n salesReturnId: string\n salesReturnUpdatedAt: string | null\n skippedLineIds: string[]\n}\n\nconst uuid = z.string().uuid()\nconst optimisticLockTokenSchema = z.string().datetime().nullable().optional()\n\nconst salesReturnRequestSchema = z\n .object({\n claimId: uuid,\n updatedAt: optimisticLockTokenSchema,\n })\n .strict()\n\ntype SalesReturnRequest = z.infer<typeof salesReturnRequestSchema>\n\nconst salesReturnResponseSchema = z.object({\n salesReturnId: z.string(),\n skippedLineIds: z.array(z.string()),\n})\n\nconst errorResponseSchema = z.object({\n error: z.string(),\n})\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['warranty_claims.claim.manage', 'sales.returns.create'] },\n}\n\nfunction translateKey(key: string): string {\n return key\n}\n\nfunction toRecord(value: unknown): Record<string, unknown> {\n return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : {}\n}\n\nasync function resolveActionContext(req: Request): Promise<ActionRouteContext> {\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) {\n throw new CrudHttpError(401, { error: 'warranty_claims.errors.unauthorized' })\n }\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const organizationId = scope?.selectedId ?? auth.orgId ?? null\n if (!organizationId) {\n throw new CrudHttpError(400, { error: 'warranty_claims.errors.organization_required' })\n }\n return {\n ctx: {\n container,\n auth,\n organizationScope: scope,\n selectedOrganizationId: organizationId,\n organizationIds: scope?.filterIds ?? (auth.orgId ? [auth.orgId] : null),\n request: req,\n },\n tenantId: auth.tenantId,\n organizationId,\n translate: translateKey,\n }\n}\n\nfunction toSalesReturnRequest(payload: Record<string, unknown>): SalesReturnRequest {\n return salesReturnRequestSchema.parse({\n claimId: payload.claimId ?? payload.id,\n updatedAt: payload.updatedAt,\n })\n}\n\nfunction toCommandInput(input: SalesReturnRequest, context: ActionRouteContext): ClaimCreateSalesReturnInput {\n const scopedPayload = withScopedPayload({\n id: input.claimId,\n updatedAt: input.updatedAt,\n }, context.ctx, context.translate)\n return claimCreateSalesReturnSchema.parse(scopedPayload)\n}\n\nasync function runGuard(\n req: Request,\n context: ActionRouteContext,\n input: SalesReturnRequest,\n): Promise<RouteMutationGuardResult> {\n const userId = context.ctx.auth?.sub\n if (!userId) {\n throw new CrudHttpError(401, { error: 'warranty_claims.errors.unauthorized' })\n }\n return runRouteMutationGuards({\n container: context.ctx.container,\n req,\n auth: {\n userId,\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n },\n input: {\n resourceKind: WARRANTY_CLAIM_RESOURCE_KIND,\n resourceId: input.claimId,\n operation: 'custom',\n mutationPayload: { ...input },\n },\n })\n}\n\nexport async function POST(req: Request) {\n try {\n const context = await resolveActionContext(req)\n const requestInput = toSalesReturnRequest(toRecord(await readJsonSafe(req, {})))\n const guarded = await runGuard(req, context, requestInput)\n if (!guarded.ok) {\n return guarded.response\n }\n const guardedInput = guarded.modifiedPayload ? toSalesReturnRequest(guarded.modifiedPayload) : requestInput\n const commandBus = context.ctx.container.resolve('commandBus') as CommandBus\n const { result } = await commandBus.execute<ClaimCreateSalesReturnInput, SalesReturnCommandResult>(\n 'warranty_claims.claim.create_sales_return',\n { input: toCommandInput(guardedInput, context), ctx: context.ctx },\n )\n if (!result) throw new CrudHttpError(400, { error: 'warranty_claims.errors.save_failed' })\n await guarded.runAfterSuccess()\n return NextResponse.json({ salesReturnId: result.salesReturnId, skippedLineIds: result.skippedLineIds })\n } catch (err) {\n if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })\n const interceptorRejection = getCommandInterceptorHttpRejection(err)\n if (interceptorRejection) {\n return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status })\n }\n if (err instanceof z.ZodError) {\n return NextResponse.json({ error: 'warranty_claims.errors.invalidInput' }, { status: 400 })\n }\n logger.error('warranty_claims.sales-return.post failed', { err })\n return NextResponse.json({ error: 'warranty_claims.errors.save_failed' }, { status: 500 })\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Warranty Claims',\n summary: 'Create a sales return from an approved warranty claim',\n methods: {\n POST: {\n summary: 'Create a sales return document from the claim\\'s eligible lines and link it to the claim',\n requestBody: {\n contentType: 'application/json',\n schema: salesReturnRequestSchema,\n description: 'Use claimId to identify the warranty claim. The id field is also accepted as an alias for claimId.',\n },\n responses: [\n {\n status: 200,\n description: 'Sales return created and linked; lines without an order-line reference are reported as skipped',\n schema: salesReturnResponseSchema,\n },\n { status: 400, description: 'Invalid request or claim is not eligible for sales-return creation', schema: errorResponseSchema },\n { status: 401, description: 'Authentication required', schema: errorResponseSchema },\n { status: 403, description: 'Insufficient permissions', schema: errorResponseSchema },\n { status: 409, description: 'Optimistic lock conflict', schema: errorResponseSchema },\n ],\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { runRouteMutationGuards, type RouteMutationGuardResult } from '@open-mercato/shared/lib/crud/route-mutation-guard'\nimport { withScopedPayload } from '@open-mercato/shared/lib/api/scoped'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { claimCreateSalesReturnSchema, type ClaimCreateSalesReturnInput } from '../../data/validators'\nimport { WARRANTY_CLAIM_RESOURCE_KIND } from '../../commands/shared'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('warranty_claims')\n\ntype ActionRouteContext = {\n ctx: CommandRuntimeContext\n tenantId: string\n organizationId: string\n translate: (key: string, fallback?: string) => string\n}\n\ntype SalesReturnCommandResult = {\n claimId: string\n salesReturnId: string\n salesReturnUpdatedAt: string | null\n skippedLineIds: string[]\n}\n\nconst uuid = z.string().uuid()\nconst optimisticLockTokenSchema = z.string().datetime().nullable().optional()\n\nconst salesReturnRequestSchema = z\n .object({\n claimId: uuid,\n updatedAt: optimisticLockTokenSchema,\n })\n .strict()\n\ntype SalesReturnRequest = z.infer<typeof salesReturnRequestSchema>\n\nconst salesReturnResponseSchema = z.object({\n salesReturnId: z.string(),\n skippedLineIds: z.array(z.string()),\n})\n\nconst errorResponseSchema = z.object({\n error: z.string(),\n})\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['warranty_claims.claim.manage', 'sales.returns.create'] },\n}\n\nfunction toRecord(value: unknown): Record<string, unknown> {\n return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : {}\n}\n\nasync function resolveActionContext(\n req: Request,\n translate: ActionRouteContext['translate'],\n): Promise<ActionRouteContext> {\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) {\n throw new CrudHttpError(401, { error: translate('warranty_claims.errors.unauthorized', 'Unauthorized') })\n }\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const organizationId = scope?.selectedId ?? auth.orgId ?? null\n if (!organizationId) {\n throw new CrudHttpError(400, { error: translate('warranty_claims.errors.organization_required', 'Organization context is required.') })\n }\n return {\n ctx: {\n container,\n auth,\n organizationScope: scope,\n selectedOrganizationId: organizationId,\n organizationIds: scope?.filterIds ?? (auth.orgId ? [auth.orgId] : null),\n request: req,\n },\n tenantId: auth.tenantId,\n organizationId,\n translate,\n }\n}\n\nfunction toSalesReturnRequest(payload: Record<string, unknown>): SalesReturnRequest {\n return salesReturnRequestSchema.parse({\n claimId: payload.claimId ?? payload.id,\n updatedAt: payload.updatedAt,\n })\n}\n\nfunction toCommandInput(input: SalesReturnRequest, context: ActionRouteContext): ClaimCreateSalesReturnInput {\n const scopedPayload = withScopedPayload({\n id: input.claimId,\n updatedAt: input.updatedAt,\n }, context.ctx, context.translate)\n return claimCreateSalesReturnSchema.parse(scopedPayload)\n}\n\nasync function runGuard(\n req: Request,\n context: ActionRouteContext,\n input: SalesReturnRequest,\n): Promise<RouteMutationGuardResult> {\n const userId = context.ctx.auth?.sub\n if (!userId) {\n throw new CrudHttpError(401, { error: context.translate('warranty_claims.errors.unauthorized', 'Unauthorized') })\n }\n return runRouteMutationGuards({\n container: context.ctx.container,\n req,\n auth: {\n userId,\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n },\n input: {\n resourceKind: WARRANTY_CLAIM_RESOURCE_KIND,\n resourceId: input.claimId,\n operation: 'custom',\n mutationPayload: { ...input },\n },\n })\n}\n\nexport async function POST(req: Request) {\n const { translate } = await resolveTranslations()\n try {\n const context = await resolveActionContext(req, translate)\n const requestInput = toSalesReturnRequest(toRecord(await readJsonSafe(req, {})))\n const guarded = await runGuard(req, context, requestInput)\n if (!guarded.ok) {\n return guarded.response\n }\n const guardedInput = guarded.modifiedPayload ? toSalesReturnRequest(guarded.modifiedPayload) : requestInput\n const commandBus = context.ctx.container.resolve('commandBus') as CommandBus\n const { result } = await commandBus.execute<ClaimCreateSalesReturnInput, SalesReturnCommandResult>(\n 'warranty_claims.claim.create_sales_return',\n { input: toCommandInput(guardedInput, context), ctx: context.ctx },\n )\n if (!result) throw new CrudHttpError(400, { error: translate('warranty_claims.errors.save_failed', 'Failed to save warranty claim.') })\n await guarded.runAfterSuccess()\n return NextResponse.json({ salesReturnId: result.salesReturnId, skippedLineIds: result.skippedLineIds })\n } catch (err) {\n if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })\n const interceptorRejection = getCommandInterceptorHttpRejection(err)\n if (interceptorRejection) {\n return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status })\n }\n if (err instanceof z.ZodError) {\n return NextResponse.json({ error: translate('warranty_claims.errors.invalidInput', 'Invalid input') }, { status: 400 })\n }\n logger.error('warranty_claims.sales-return.post failed', { err })\n return NextResponse.json({ error: translate('warranty_claims.errors.save_failed', 'Failed to save warranty claim.') }, { status: 500 })\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Warranty Claims',\n summary: 'Create a sales return from an approved warranty claim',\n methods: {\n POST: {\n summary: 'Create a sales return document from the claim\\'s eligible lines and link it to the claim',\n requestBody: {\n contentType: 'application/json',\n schema: salesReturnRequestSchema,\n description: 'Use claimId to identify the warranty claim. The id field is also accepted as an alias for claimId.',\n },\n responses: [\n {\n status: 200,\n description: 'Sales return created and linked; lines without an order-line reference are reported as skipped',\n schema: salesReturnResponseSchema,\n },\n { status: 400, description: 'Invalid request or claim is not eligible for sales-return creation', schema: errorResponseSchema },\n { status: 401, description: 'Authentication required', schema: errorResponseSchema },\n { status: 403, description: 'Insufficient permissions', schema: errorResponseSchema },\n { status: 409, description: 'Optimistic lock conflict', schema: errorResponseSchema },\n ],\n },\n },\n}\n"],
5
+ "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AACnC,SAAS,0CAA0C;AAEnD,SAAS,eAAe,uBAAuB;AAC/C,SAAS,0CAA0C;AACnD,SAAS,oBAAoB;AAC7B,SAAS,2BAA2B;AACpC,SAAS,8BAA6D;AACtE,SAAS,yBAAyB;AAElC,SAAS,oCAAsE;AAC/E,SAAS,oCAAoC;AAC7C,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,iBAAiB;AAgB7C,MAAM,OAAO,EAAE,OAAO,EAAE,KAAK;AAC7B,MAAM,4BAA4B,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS;AAE5E,MAAM,2BAA2B,EAC9B,OAAO;AAAA,EACN,SAAS;AAAA,EACT,WAAW;AACb,CAAC,EACA,OAAO;AAIV,MAAM,4BAA4B,EAAE,OAAO;AAAA,EACzC,eAAe,EAAE,OAAO;AAAA,EACxB,gBAAgB,EAAE,MAAM,EAAE,OAAO,CAAC;AACpC,CAAC;AAED,MAAM,sBAAsB,EAAE,OAAO;AAAA,EACnC,OAAO,EAAE,OAAO;AAClB,CAAC;AAEM,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,gCAAgC,sBAAsB,EAAE;AACvG;AAEA,SAAS,SAAS,OAAyC;AACzD,SAAO,SAAS,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK,IAAI,QAAmC,CAAC;AAC3G;AAEA,eAAe,qBACb,KACA,WAC6B;AAC7B,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,QAAQ,CAAC,KAAK,UAAU;AAC3B,UAAM,IAAI,cAAc,KAAK,EAAE,OAAO,UAAU,uCAAuC,cAAc,EAAE,CAAC;AAAA,EAC1G;AACA,QAAM,QAAQ,MAAM,mCAAmC,EAAE,WAAW,MAAM,SAAS,IAAI,CAAC;AACxF,QAAM,iBAAiB,OAAO,cAAc,KAAK,SAAS;AAC1D,MAAI,CAAC,gBAAgB;AACnB,UAAM,IAAI,cAAc,KAAK,EAAE,OAAO,UAAU,gDAAgD,mCAAmC,EAAE,CAAC;AAAA,EACxI;AACA,SAAO;AAAA,IACL,KAAK;AAAA,MACH;AAAA,MACA;AAAA,MACA,mBAAmB;AAAA,MACnB,wBAAwB;AAAA,MACxB,iBAAiB,OAAO,cAAc,KAAK,QAAQ,CAAC,KAAK,KAAK,IAAI;AAAA,MAClE,SAAS;AAAA,IACX;AAAA,IACA,UAAU,KAAK;AAAA,IACf;AAAA,IACA;AAAA,EACF;AACF;AAEA,SAAS,qBAAqB,SAAsD;AAClF,SAAO,yBAAyB,MAAM;AAAA,IACpC,SAAS,QAAQ,WAAW,QAAQ;AAAA,IACpC,WAAW,QAAQ;AAAA,EACrB,CAAC;AACH;AAEA,SAAS,eAAe,OAA2B,SAA0D;AAC3G,QAAM,gBAAgB,kBAAkB;AAAA,IACtC,IAAI,MAAM;AAAA,IACV,WAAW,MAAM;AAAA,EACnB,GAAG,QAAQ,KAAK,QAAQ,SAAS;AACjC,SAAO,6BAA6B,MAAM,aAAa;AACzD;AAEA,eAAe,SACb,KACA,SACA,OACmC;AACnC,QAAM,SAAS,QAAQ,IAAI,MAAM;AACjC,MAAI,CAAC,QAAQ;AACX,UAAM,IAAI,cAAc,KAAK,EAAE,OAAO,QAAQ,UAAU,uCAAuC,cAAc,EAAE,CAAC;AAAA,EAClH;AACA,SAAO,uBAAuB;AAAA,IAC5B,WAAW,QAAQ,IAAI;AAAA,IACvB;AAAA,IACA,MAAM;AAAA,MACJ;AAAA,MACA,UAAU,QAAQ;AAAA,MAClB,gBAAgB,QAAQ;AAAA,IAC1B;AAAA,IACA,OAAO;AAAA,MACL,cAAc;AAAA,MACd,YAAY,MAAM;AAAA,MAClB,WAAW;AAAA,MACX,iBAAiB,EAAE,GAAG,MAAM;AAAA,IAC9B;AAAA,EACF,CAAC;AACH;AAEA,eAAsB,KAAK,KAAc;AACvC,QAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,MAAI;AACF,UAAM,UAAU,MAAM,qBAAqB,KAAK,SAAS;AACzD,UAAM,eAAe,qBAAqB,SAAS,MAAM,aAAa,KAAK,CAAC,CAAC,CAAC,CAAC;AAC/E,UAAM,UAAU,MAAM,SAAS,KAAK,SAAS,YAAY;AACzD,QAAI,CAAC,QAAQ,IAAI;AACf,aAAO,QAAQ;AAAA,IACjB;AACA,UAAM,eAAe,QAAQ,kBAAkB,qBAAqB,QAAQ,eAAe,IAAI;AAC/F,UAAM,aAAa,QAAQ,IAAI,UAAU,QAAQ,YAAY;AAC7D,UAAM,EAAE,OAAO,IAAI,MAAM,WAAW;AAAA,MAClC;AAAA,MACA,EAAE,OAAO,eAAe,cAAc,OAAO,GAAG,KAAK,QAAQ,IAAI;AAAA,IACnE;AACA,QAAI,CAAC,OAAQ,OAAM,IAAI,cAAc,KAAK,EAAE,OAAO,UAAU,sCAAsC,gCAAgC,EAAE,CAAC;AACtI,UAAM,QAAQ,gBAAgB;AAC9B,WAAO,aAAa,KAAK,EAAE,eAAe,OAAO,eAAe,gBAAgB,OAAO,eAAe,CAAC;AAAA,EACzG,SAAS,KAAK;AACZ,QAAI,gBAAgB,GAAG,EAAG,QAAO,aAAa,KAAK,IAAI,MAAM,EAAE,QAAQ,IAAI,OAAO,CAAC;AACnF,UAAM,uBAAuB,mCAAmC,GAAG;AACnE,QAAI,sBAAsB;AACxB,aAAO,aAAa,KAAK,qBAAqB,MAAM,EAAE,QAAQ,qBAAqB,OAAO,CAAC;AAAA,IAC7F;AACA,QAAI,eAAe,EAAE,UAAU;AAC7B,aAAO,aAAa,KAAK,EAAE,OAAO,UAAU,uCAAuC,eAAe,EAAE,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IACxH;AACA,WAAO,MAAM,4CAA4C,EAAE,IAAI,CAAC;AAChE,WAAO,aAAa,KAAK,EAAE,OAAO,UAAU,sCAAsC,gCAAgC,EAAE,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACxI;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,aAAa;AAAA,QACX,aAAa;AAAA,QACb,QAAQ;AAAA,QACR,aAAa;AAAA,MACf;AAAA,MACA,WAAW;AAAA,QACT;AAAA,UACE,QAAQ;AAAA,UACR,aAAa;AAAA,UACb,QAAQ;AAAA,QACV;AAAA,QACA,EAAE,QAAQ,KAAK,aAAa,sEAAsE,QAAQ,oBAAoB;AAAA,QAC9H,EAAE,QAAQ,KAAK,aAAa,2BAA2B,QAAQ,oBAAoB;AAAA,QACnF,EAAE,QAAQ,KAAK,aAAa,4BAA4B,QAAQ,oBAAoB;AAAA,QACpF,EAAE,QAAQ,KAAK,aAAa,4BAA4B,QAAQ,oBAAoB;AAAA,MACtF;AAAA,IACF;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { makeCrudRoute } from "@open-mercato/shared/lib/crud/factory";
3
3
  import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
4
+ import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
4
5
  import { parseBooleanFromUnknown } from "@open-mercato/shared/lib/boolean";
5
6
  import { buildIlikeTerm } from "@open-mercato/shared/lib/db/buildIlikeTerm";
6
7
  import { applyIdsFilter } from "../../lib/apiIdsFilter.js";
@@ -96,10 +97,22 @@ function toEntitySteps(value) {
96
97
  if (value === void 0 || value === null) return null;
97
98
  const parsed = parseGuideSteps(value);
98
99
  if (!parsed) {
99
- throw new CrudHttpError(400, { error: "warranty_claims.errors.invalidTroubleshootingSteps" });
100
+ throw new CrudHttpError(400, { error: "[internal] troubleshooting steps reached the entity mapper unvalidated" });
100
101
  }
101
102
  return { prompt: parsed.prompt, options: parsed.options };
102
103
  }
104
+ async function assertParsableGuideSteps(input) {
105
+ if (!hasOwn(input, "steps")) return;
106
+ const value = input.steps;
107
+ if (value === void 0 || value === null || parseGuideSteps(value)) return;
108
+ const { translate } = await resolveTranslations();
109
+ throw new CrudHttpError(400, {
110
+ error: translate(
111
+ "warranty_claims.errors.invalidTroubleshootingSteps",
112
+ "Troubleshooting steps must include a prompt and valid option branches."
113
+ )
114
+ });
115
+ }
103
116
  function toTroubleshootingGuideEntityData(input) {
104
117
  return {
105
118
  claimType: input.claimType ?? null,
@@ -199,6 +212,14 @@ const crud = makeCrudRoute({
199
212
  schema: rawBodySchema,
200
213
  mapToEntity: (input, ctx) => toTroubleshootingGuideEntityData(parseCreateInput(input, ctx))
201
214
  },
215
+ hooks: {
216
+ beforeCreate: async (input) => {
217
+ await assertParsableGuideSteps(input);
218
+ },
219
+ beforeUpdate: async (input) => {
220
+ await assertParsableGuideSteps(input);
221
+ }
222
+ },
202
223
  update: {
203
224
  schema: rawBodySchema,
204
225
  getId: (input) => {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/warranty_claims/api/troubleshooting-guides/route.ts"],
4
- "sourcesContent": ["import { z } from 'zod'\nimport type { CrudCtx } from '@open-mercato/shared/lib/crud/factory'\nimport { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { parseBooleanFromUnknown } from '@open-mercato/shared/lib/boolean'\nimport { buildIlikeTerm } from '@open-mercato/shared/lib/db/buildIlikeTerm'\nimport { applyIdsFilter } from '../../lib/apiIdsFilter'\nimport { E } from '#generated/entities.ids.generated'\nimport * as F from '#generated/entities/warranty_troubleshooting_guide'\nimport { WarrantyTroubleshootingGuide } from '../../data/entities'\nimport {\n claimTypeSchema,\n troubleshootingGuideCreateSchema,\n troubleshootingGuideUpdateSchema,\n troubleshootingGuideDeleteSchema,\n type TroubleshootingGuideCreateInput,\n type TroubleshootingGuideUpdateInput,\n} from '../../data/validators'\nimport { parseGuideSteps, type TroubleshootingNode } from '../../lib/troubleshooting'\nimport {\n createPagedListResponseSchema,\n createWarrantyClaimsCrudOpenApi,\n defaultOkResponseSchema,\n} from '../openapi'\n\nconst rawBodySchema = z.object({}).passthrough()\nconst uuid = z.string().uuid()\n\nconst idsSchema = z.preprocess((value) => {\n if (typeof value === 'string') {\n return value\n .split(',')\n .map((part) => part.trim())\n .filter(Boolean)\n }\n return value\n}, z.array(uuid).min(1).max(500).optional())\n\nconst optionalReasonCodeSchema = z.preprocess((value) => {\n if (typeof value !== 'string') return value\n const trimmed = value.trim()\n return trimmed.length ? trimmed : undefined\n}, z.string().max(120).optional())\n\nconst booleanQuerySchema = z.preprocess((value) => parseBooleanFromUnknown(value) ?? undefined, z.boolean().optional())\n\nconst listSchema = z\n .object({\n id: uuid.optional(),\n ids: idsSchema,\n claimType: claimTypeSchema.optional(),\n reasonCode: optionalReasonCodeSchema,\n isActive: booleanQuerySchema,\n search: z.string().trim().max(300).optional(),\n page: z.coerce.number().int().min(1).default(1),\n pageSize: z.coerce.number().int().min(1).max(100).default(20),\n sortField: z.enum(['title', 'claimType', 'reasonCode', 'updatedAt', 'createdAt']).optional(),\n sortDir: z.enum(['asc', 'desc']).optional(),\n })\n .passthrough()\n\ntype TroubleshootingGuideListQuery = z.infer<typeof listSchema>\ntype RawTroubleshootingGuideInput = z.infer<typeof rawBodySchema>\n\nconst routeMetadata = {\n GET: { requireAuth: true, requireFeatures: ['warranty_claims.troubleshooting.manage'] },\n POST: { requireAuth: true, requireFeatures: ['warranty_claims.troubleshooting.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['warranty_claims.troubleshooting.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['warranty_claims.troubleshooting.manage'] },\n}\n\nexport const metadata = routeMetadata\n\nconst baseListFields = [\n F.id,\n F.organization_id,\n F.tenant_id,\n F.title,\n F.claim_type,\n F.reason_code,\n F.is_active,\n F.created_at,\n F.updated_at,\n F.deleted_at,\n]\n\nconst detailFields = [...baseListFields, F.steps]\n\nfunction scopeFromContext(ctx: CrudCtx): { organizationId: string; tenantId: string } {\n const organizationId = ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null\n const tenantId = ctx.auth?.tenantId ?? null\n if (!organizationId || !tenantId) {\n throw new CrudHttpError(400, { error: '[internal] warranty troubleshooting guide scope is missing' })\n }\n return { organizationId, tenantId }\n}\n\nfunction parseCreateInput(input: RawTroubleshootingGuideInput, ctx: CrudCtx): TroubleshootingGuideCreateInput {\n return troubleshootingGuideCreateSchema.parse({\n ...input,\n ...scopeFromContext(ctx),\n })\n}\n\nfunction parseUpdateInput(input: RawTroubleshootingGuideInput, ctx: CrudCtx): TroubleshootingGuideUpdateInput {\n return troubleshootingGuideUpdateSchema.parse({\n ...input,\n ...scopeFromContext(ctx),\n })\n}\n\nfunction hasOwn(input: object, key: string): boolean {\n return Object.prototype.hasOwnProperty.call(input, key)\n}\n\nfunction toNullableText(value: string | null | undefined): string | null {\n const trimmed = value?.trim()\n return trimmed ? trimmed : null\n}\n\nfunction toEntitySteps(value: unknown): Record<string, unknown> | null {\n if (value === undefined || value === null) return null\n const parsed = parseGuideSteps(value)\n if (!parsed) {\n throw new CrudHttpError(400, { error: 'warranty_claims.errors.invalidTroubleshootingSteps' })\n }\n return { prompt: parsed.prompt, options: parsed.options }\n}\n\nfunction toTroubleshootingGuideEntityData(input: TroubleshootingGuideCreateInput): Record<string, unknown> {\n return {\n claimType: input.claimType ?? null,\n reasonCode: toNullableText(input.reasonCode),\n title: input.title,\n steps: toEntitySteps(input.steps),\n isActive: input.isActive !== false,\n }\n}\n\nfunction applyTroubleshootingGuideUpdate(\n entity: WarrantyTroubleshootingGuide,\n input: TroubleshootingGuideUpdateInput,\n): void {\n if (hasOwn(input, 'claimType')) entity.claimType = input.claimType ?? null\n if (hasOwn(input, 'reasonCode')) entity.reasonCode = toNullableText(input.reasonCode)\n if (hasOwn(input, 'title') && input.title) entity.title = input.title\n if (hasOwn(input, 'steps')) entity.steps = toEntitySteps(input.steps)\n if (hasOwn(input, 'isActive')) entity.isActive = input.isActive !== false\n}\n\nfunction toRecord(value: unknown): Record<string, unknown> {\n return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : {}\n}\n\nfunction readString(record: Record<string, unknown>, snakeKey: string, camelKey: string): string | null {\n const value = record[snakeKey] ?? record[camelKey]\n return typeof value === 'string' ? value : null\n}\n\nfunction readBool(record: Record<string, unknown>, snakeKey: string, camelKey: string): boolean {\n const value = record[snakeKey] ?? record[camelKey]\n return value === true\n}\n\nfunction toIso(value: unknown): string | null {\n if (value instanceof Date) return value.toISOString()\n if (typeof value === 'string') {\n const date = new Date(value)\n return Number.isNaN(date.getTime()) ? value : date.toISOString()\n }\n return null\n}\n\nfunction isDetailQuery(query: TroubleshootingGuideListQuery): boolean {\n if (query.id) return true\n return Array.isArray(query.ids) && query.ids.length === 1\n}\n\nfunction readSteps(record: Record<string, unknown>): TroubleshootingNode | null {\n if (!hasOwn(record, 'steps')) return null\n return parseGuideSteps(record.steps)\n}\n\nfunction transformTroubleshootingGuideItem(item: unknown): unknown {\n const record = toRecord(item)\n if (!Object.keys(record).length) return item\n const result: Record<string, unknown> = {\n id: readString(record, 'id', 'id'),\n organizationId: readString(record, 'organization_id', 'organizationId'),\n tenantId: readString(record, 'tenant_id', 'tenantId'),\n title: readString(record, 'title', 'title'),\n claimType: readString(record, 'claim_type', 'claimType'),\n reasonCode: readString(record, 'reason_code', 'reasonCode'),\n isActive: readBool(record, 'is_active', 'isActive'),\n createdAt: toIso(record.created_at ?? record.createdAt),\n updatedAt: toIso(record.updated_at ?? record.updatedAt),\n deletedAt: toIso(record.deleted_at ?? record.deletedAt),\n }\n if (hasOwn(record, 'steps')) result.steps = readSteps(record)\n return result\n}\n\nconst crud = makeCrudRoute<RawTroubleshootingGuideInput, RawTroubleshootingGuideInput, TroubleshootingGuideListQuery>({\n metadata: routeMetadata,\n orm: {\n entity: WarrantyTroubleshootingGuide,\n idField: 'id',\n orgField: 'organizationId',\n tenantField: 'tenantId',\n softDeleteField: 'deletedAt',\n },\n indexer: { entityType: E.warranty_claims.warranty_troubleshooting_guide },\n list: {\n schema: listSchema,\n entityId: E.warranty_claims.warranty_troubleshooting_guide,\n fields: (query) => (isDetailQuery(query) ? detailFields : baseListFields),\n sortFieldMap: {\n title: F.title,\n claimType: F.claim_type,\n reasonCode: F.reason_code,\n createdAt: F.created_at,\n updatedAt: F.updated_at,\n },\n buildFilters: async (query) => {\n const filters: Record<string, unknown> = {}\n applyIdsFilter(filters, query)\n if (query.claimType) filters[F.claim_type] = { $eq: query.claimType }\n if (query.reasonCode) filters[F.reason_code] = { $eq: query.reasonCode }\n if (query.isActive !== undefined) filters[F.is_active] = { $eq: query.isActive }\n if (query.search) {\n filters.$or = [{ [F.title]: { $ilike: buildIlikeTerm(query.search) } }]\n }\n return filters\n },\n transformItem: transformTroubleshootingGuideItem,\n },\n create: {\n schema: rawBodySchema,\n mapToEntity: (input, ctx) => toTroubleshootingGuideEntityData(parseCreateInput(input, ctx)),\n },\n update: {\n schema: rawBodySchema,\n getId: (input) => {\n const id = input.id\n return typeof id === 'string' ? id : ''\n },\n applyToEntity: (entity, input, ctx) => {\n applyTroubleshootingGuideUpdate(entity as WarrantyTroubleshootingGuide, parseUpdateInput(input, ctx))\n },\n response: () => ({ ok: true }),\n },\n del: { idFrom: 'query', softDelete: true, response: () => ({ ok: true }) },\n})\n\nexport const GET = crud.GET\nexport const POST = crud.POST\nexport const PUT = crud.PUT\nexport const DELETE = crud.DELETE\n\nconst troubleshootingStepsSchema = z.record(z.string(), z.unknown())\n\nconst troubleshootingGuideListItemSchema = z.object({\n id: z.string().uuid().nullable(),\n organizationId: z.string().uuid().nullable(),\n tenantId: z.string().uuid().nullable(),\n title: z.string().nullable(),\n claimType: claimTypeSchema.nullable(),\n reasonCode: z.string().nullable(),\n steps: troubleshootingStepsSchema.nullable().optional(),\n isActive: z.boolean(),\n createdAt: z.string().nullable(),\n updatedAt: z.string().nullable(),\n}).passthrough()\n\nexport const openApi = createWarrantyClaimsCrudOpenApi({\n resourceName: 'WarrantyTroubleshootingGuide',\n pluralName: 'WarrantyTroubleshootingGuides',\n querySchema: listSchema,\n listResponseSchema: createPagedListResponseSchema(troubleshootingGuideListItemSchema),\n create: {\n schema: troubleshootingGuideCreateSchema,\n responseSchema: z.object({ id: z.string().uuid() }),\n description: 'Creates a guided warranty troubleshooting decision tree.',\n },\n update: {\n schema: troubleshootingGuideUpdateSchema,\n responseSchema: defaultOkResponseSchema,\n description: 'Updates a guided warranty troubleshooting decision tree.',\n },\n del: {\n schema: troubleshootingGuideDeleteSchema,\n responseSchema: defaultOkResponseSchema,\n description: 'Soft-deletes a guided warranty troubleshooting decision tree.',\n },\n})\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { z } from 'zod'\nimport type { CrudCtx } from '@open-mercato/shared/lib/crud/factory'\nimport { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { parseBooleanFromUnknown } from '@open-mercato/shared/lib/boolean'\nimport { buildIlikeTerm } from '@open-mercato/shared/lib/db/buildIlikeTerm'\nimport { applyIdsFilter } from '../../lib/apiIdsFilter'\nimport { E } from '#generated/entities.ids.generated'\nimport * as F from '#generated/entities/warranty_troubleshooting_guide'\nimport { WarrantyTroubleshootingGuide } from '../../data/entities'\nimport {\n claimTypeSchema,\n troubleshootingGuideCreateSchema,\n troubleshootingGuideUpdateSchema,\n troubleshootingGuideDeleteSchema,\n type TroubleshootingGuideCreateInput,\n type TroubleshootingGuideUpdateInput,\n} from '../../data/validators'\nimport { parseGuideSteps, type TroubleshootingNode } from '../../lib/troubleshooting'\nimport {\n createPagedListResponseSchema,\n createWarrantyClaimsCrudOpenApi,\n defaultOkResponseSchema,\n} from '../openapi'\n\nconst rawBodySchema = z.object({}).passthrough()\nconst uuid = z.string().uuid()\n\nconst idsSchema = z.preprocess((value) => {\n if (typeof value === 'string') {\n return value\n .split(',')\n .map((part) => part.trim())\n .filter(Boolean)\n }\n return value\n}, z.array(uuid).min(1).max(500).optional())\n\nconst optionalReasonCodeSchema = z.preprocess((value) => {\n if (typeof value !== 'string') return value\n const trimmed = value.trim()\n return trimmed.length ? trimmed : undefined\n}, z.string().max(120).optional())\n\nconst booleanQuerySchema = z.preprocess((value) => parseBooleanFromUnknown(value) ?? undefined, z.boolean().optional())\n\nconst listSchema = z\n .object({\n id: uuid.optional(),\n ids: idsSchema,\n claimType: claimTypeSchema.optional(),\n reasonCode: optionalReasonCodeSchema,\n isActive: booleanQuerySchema,\n search: z.string().trim().max(300).optional(),\n page: z.coerce.number().int().min(1).default(1),\n pageSize: z.coerce.number().int().min(1).max(100).default(20),\n sortField: z.enum(['title', 'claimType', 'reasonCode', 'updatedAt', 'createdAt']).optional(),\n sortDir: z.enum(['asc', 'desc']).optional(),\n })\n .passthrough()\n\ntype TroubleshootingGuideListQuery = z.infer<typeof listSchema>\ntype RawTroubleshootingGuideInput = z.infer<typeof rawBodySchema>\n\nconst routeMetadata = {\n GET: { requireAuth: true, requireFeatures: ['warranty_claims.troubleshooting.manage'] },\n POST: { requireAuth: true, requireFeatures: ['warranty_claims.troubleshooting.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['warranty_claims.troubleshooting.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['warranty_claims.troubleshooting.manage'] },\n}\n\nexport const metadata = routeMetadata\n\nconst baseListFields = [\n F.id,\n F.organization_id,\n F.tenant_id,\n F.title,\n F.claim_type,\n F.reason_code,\n F.is_active,\n F.created_at,\n F.updated_at,\n F.deleted_at,\n]\n\nconst detailFields = [...baseListFields, F.steps]\n\nfunction scopeFromContext(ctx: CrudCtx): { organizationId: string; tenantId: string } {\n const organizationId = ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null\n const tenantId = ctx.auth?.tenantId ?? null\n if (!organizationId || !tenantId) {\n throw new CrudHttpError(400, { error: '[internal] warranty troubleshooting guide scope is missing' })\n }\n return { organizationId, tenantId }\n}\n\nfunction parseCreateInput(input: RawTroubleshootingGuideInput, ctx: CrudCtx): TroubleshootingGuideCreateInput {\n return troubleshootingGuideCreateSchema.parse({\n ...input,\n ...scopeFromContext(ctx),\n })\n}\n\nfunction parseUpdateInput(input: RawTroubleshootingGuideInput, ctx: CrudCtx): TroubleshootingGuideUpdateInput {\n return troubleshootingGuideUpdateSchema.parse({\n ...input,\n ...scopeFromContext(ctx),\n })\n}\n\nfunction hasOwn(input: object, key: string): boolean {\n return Object.prototype.hasOwnProperty.call(input, key)\n}\n\nfunction toNullableText(value: string | null | undefined): string | null {\n const trimmed = value?.trim()\n return trimmed ? trimmed : null\n}\n\nfunction toEntitySteps(value: unknown): Record<string, unknown> | null {\n if (value === undefined || value === null) return null\n const parsed = parseGuideSteps(value)\n if (!parsed) {\n throw new CrudHttpError(400, { error: '[internal] troubleshooting steps reached the entity mapper unvalidated' })\n }\n return { prompt: parsed.prompt, options: parsed.options }\n}\n\nasync function assertParsableGuideSteps(input: object): Promise<void> {\n if (!hasOwn(input, 'steps')) return\n const value = (input as { steps?: unknown }).steps\n if (value === undefined || value === null || parseGuideSteps(value)) return\n const { translate } = await resolveTranslations()\n throw new CrudHttpError(400, {\n error: translate(\n 'warranty_claims.errors.invalidTroubleshootingSteps',\n 'Troubleshooting steps must include a prompt and valid option branches.',\n ),\n })\n}\n\nfunction toTroubleshootingGuideEntityData(input: TroubleshootingGuideCreateInput): Record<string, unknown> {\n return {\n claimType: input.claimType ?? null,\n reasonCode: toNullableText(input.reasonCode),\n title: input.title,\n steps: toEntitySteps(input.steps),\n isActive: input.isActive !== false,\n }\n}\n\nfunction applyTroubleshootingGuideUpdate(\n entity: WarrantyTroubleshootingGuide,\n input: TroubleshootingGuideUpdateInput,\n): void {\n if (hasOwn(input, 'claimType')) entity.claimType = input.claimType ?? null\n if (hasOwn(input, 'reasonCode')) entity.reasonCode = toNullableText(input.reasonCode)\n if (hasOwn(input, 'title') && input.title) entity.title = input.title\n if (hasOwn(input, 'steps')) entity.steps = toEntitySteps(input.steps)\n if (hasOwn(input, 'isActive')) entity.isActive = input.isActive !== false\n}\n\nfunction toRecord(value: unknown): Record<string, unknown> {\n return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : {}\n}\n\nfunction readString(record: Record<string, unknown>, snakeKey: string, camelKey: string): string | null {\n const value = record[snakeKey] ?? record[camelKey]\n return typeof value === 'string' ? value : null\n}\n\nfunction readBool(record: Record<string, unknown>, snakeKey: string, camelKey: string): boolean {\n const value = record[snakeKey] ?? record[camelKey]\n return value === true\n}\n\nfunction toIso(value: unknown): string | null {\n if (value instanceof Date) return value.toISOString()\n if (typeof value === 'string') {\n const date = new Date(value)\n return Number.isNaN(date.getTime()) ? value : date.toISOString()\n }\n return null\n}\n\nfunction isDetailQuery(query: TroubleshootingGuideListQuery): boolean {\n if (query.id) return true\n return Array.isArray(query.ids) && query.ids.length === 1\n}\n\nfunction readSteps(record: Record<string, unknown>): TroubleshootingNode | null {\n if (!hasOwn(record, 'steps')) return null\n return parseGuideSteps(record.steps)\n}\n\nfunction transformTroubleshootingGuideItem(item: unknown): unknown {\n const record = toRecord(item)\n if (!Object.keys(record).length) return item\n const result: Record<string, unknown> = {\n id: readString(record, 'id', 'id'),\n organizationId: readString(record, 'organization_id', 'organizationId'),\n tenantId: readString(record, 'tenant_id', 'tenantId'),\n title: readString(record, 'title', 'title'),\n claimType: readString(record, 'claim_type', 'claimType'),\n reasonCode: readString(record, 'reason_code', 'reasonCode'),\n isActive: readBool(record, 'is_active', 'isActive'),\n createdAt: toIso(record.created_at ?? record.createdAt),\n updatedAt: toIso(record.updated_at ?? record.updatedAt),\n deletedAt: toIso(record.deleted_at ?? record.deletedAt),\n }\n if (hasOwn(record, 'steps')) result.steps = readSteps(record)\n return result\n}\n\nconst crud = makeCrudRoute<RawTroubleshootingGuideInput, RawTroubleshootingGuideInput, TroubleshootingGuideListQuery>({\n metadata: routeMetadata,\n orm: {\n entity: WarrantyTroubleshootingGuide,\n idField: 'id',\n orgField: 'organizationId',\n tenantField: 'tenantId',\n softDeleteField: 'deletedAt',\n },\n indexer: { entityType: E.warranty_claims.warranty_troubleshooting_guide },\n list: {\n schema: listSchema,\n entityId: E.warranty_claims.warranty_troubleshooting_guide,\n fields: (query) => (isDetailQuery(query) ? detailFields : baseListFields),\n sortFieldMap: {\n title: F.title,\n claimType: F.claim_type,\n reasonCode: F.reason_code,\n createdAt: F.created_at,\n updatedAt: F.updated_at,\n },\n buildFilters: async (query) => {\n const filters: Record<string, unknown> = {}\n applyIdsFilter(filters, query)\n if (query.claimType) filters[F.claim_type] = { $eq: query.claimType }\n if (query.reasonCode) filters[F.reason_code] = { $eq: query.reasonCode }\n if (query.isActive !== undefined) filters[F.is_active] = { $eq: query.isActive }\n if (query.search) {\n filters.$or = [{ [F.title]: { $ilike: buildIlikeTerm(query.search) } }]\n }\n return filters\n },\n transformItem: transformTroubleshootingGuideItem,\n },\n create: {\n schema: rawBodySchema,\n mapToEntity: (input, ctx) => toTroubleshootingGuideEntityData(parseCreateInput(input, ctx)),\n },\n hooks: {\n beforeCreate: async (input) => {\n await assertParsableGuideSteps(input)\n },\n beforeUpdate: async (input) => {\n await assertParsableGuideSteps(input)\n },\n },\n update: {\n schema: rawBodySchema,\n getId: (input) => {\n const id = input.id\n return typeof id === 'string' ? id : ''\n },\n applyToEntity: (entity, input, ctx) => {\n applyTroubleshootingGuideUpdate(entity as WarrantyTroubleshootingGuide, parseUpdateInput(input, ctx))\n },\n response: () => ({ ok: true }),\n },\n del: { idFrom: 'query', softDelete: true, response: () => ({ ok: true }) },\n})\n\nexport const GET = crud.GET\nexport const POST = crud.POST\nexport const PUT = crud.PUT\nexport const DELETE = crud.DELETE\n\nconst troubleshootingStepsSchema = z.record(z.string(), z.unknown())\n\nconst troubleshootingGuideListItemSchema = z.object({\n id: z.string().uuid().nullable(),\n organizationId: z.string().uuid().nullable(),\n tenantId: z.string().uuid().nullable(),\n title: z.string().nullable(),\n claimType: claimTypeSchema.nullable(),\n reasonCode: z.string().nullable(),\n steps: troubleshootingStepsSchema.nullable().optional(),\n isActive: z.boolean(),\n createdAt: z.string().nullable(),\n updatedAt: z.string().nullable(),\n}).passthrough()\n\nexport const openApi = createWarrantyClaimsCrudOpenApi({\n resourceName: 'WarrantyTroubleshootingGuide',\n pluralName: 'WarrantyTroubleshootingGuides',\n querySchema: listSchema,\n listResponseSchema: createPagedListResponseSchema(troubleshootingGuideListItemSchema),\n create: {\n schema: troubleshootingGuideCreateSchema,\n responseSchema: z.object({ id: z.string().uuid() }),\n description: 'Creates a guided warranty troubleshooting decision tree.',\n },\n update: {\n schema: troubleshootingGuideUpdateSchema,\n responseSchema: defaultOkResponseSchema,\n description: 'Updates a guided warranty troubleshooting decision tree.',\n },\n del: {\n schema: troubleshootingGuideDeleteSchema,\n responseSchema: defaultOkResponseSchema,\n description: 'Soft-deletes a guided warranty troubleshooting decision tree.',\n },\n})\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -62,6 +62,7 @@ async function resolveSuggestionsContext(req) {
62
62
  tenantId: auth.tenantId,
63
63
  organizationId,
64
64
  scope: { tenantId: auth.tenantId, organizationId },
65
+ translate,
65
66
  em
66
67
  };
67
68
  }
@@ -70,7 +71,7 @@ async function GET(req) {
70
71
  const context = await resolveSuggestionsContext(req);
71
72
  const url = new URL(req.url);
72
73
  const query = querySchema.parse(Object.fromEntries(url.searchParams));
73
- const claim = await requireScopedClaim(context.em, query.claimId, context.scope);
74
+ const claim = await requireScopedClaim(context.em, query.claimId, context.scope, {}, context.translate("warranty_claims.errors.notFound", "Claim not found."));
74
75
  if (claim.claimType !== "warranty" || claim.status !== "resolved" && claim.status !== "closed") {
75
76
  return NextResponse.json({ ok: true, result: { claimId: claim.id, suggestions: [] } });
76
77
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/warranty_claims/api/vendor-recovery-suggestions/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { WarrantyClaimLine, WarrantyVendorPolicy } from '../../data/entities'\nimport { requireScopedClaim, type WarrantyClaimScope } from '../../commands/shared'\nimport { findVendorRecoveryMatches } from '../../lib/vendorPolicyRecovery'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('warranty_claims')\n\nconst querySchema = z.object({\n claimId: z.string().uuid(),\n}).strict()\n\nconst suggestionSchema = z.object({\n lineId: z.string().uuid(),\n lineNo: z.number().int(),\n productName: z.string().nullable(),\n sku: z.string().nullable(),\n vendorName: z.string(),\n policyId: z.string().uuid(),\n recoveryRatePct: z.string().nullable(),\n causalFault: z.string().nullable(),\n estimatedRecovery: z.string().nullable(),\n})\n\nconst responseSchema = z.object({\n ok: z.literal(true),\n result: z.object({\n claimId: z.string().uuid(),\n suggestions: z.array(suggestionSchema),\n }),\n})\n\ntype SuggestionsRouteContext = {\n tenantId: string\n organizationId: string\n scope: WarrantyClaimScope\n em: EntityManager\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['warranty_claims.claim.manage'] },\n}\n\nfunction toNullableString(value: number | string | null | undefined): string | null {\n if (typeof value === 'number') return Number.isFinite(value) ? String(value) : null\n if (typeof value !== 'string') return null\n const trimmed = value.trim()\n return trimmed ? trimmed : null\n}\n\nfunction toSuggestionVendorName(lineVendorName: string | null | undefined, policyVendorName: string): string {\n const normalized = lineVendorName?.trim()\n return normalized || policyVendorName\n}\n\nasync function resolveSuggestionsContext(req: Request): Promise<SuggestionsRouteContext> {\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n const { translate } = await resolveTranslations()\n if (!auth || !auth.tenantId) {\n throw new CrudHttpError(401, { error: translate('warranty_claims.errors.unauthorized', 'Unauthorized') })\n }\n const organizationScope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const organizationId = organizationScope?.selectedId ?? auth.orgId ?? null\n if (!organizationId) {\n throw new CrudHttpError(400, { error: translate('warranty_claims.errors.organization_required', 'Organization context is required') })\n }\n const em = container.resolve('em') as EntityManager\n return {\n tenantId: auth.tenantId,\n organizationId,\n scope: { tenantId: auth.tenantId, organizationId },\n em,\n }\n}\n\nexport async function GET(req: Request) {\n try {\n const context = await resolveSuggestionsContext(req)\n const url = new URL(req.url)\n const query = querySchema.parse(Object.fromEntries(url.searchParams))\n const claim = await requireScopedClaim(context.em, query.claimId, context.scope)\n if (claim.claimType !== 'warranty' || (claim.status !== 'resolved' && claim.status !== 'closed')) {\n return NextResponse.json({ ok: true, result: { claimId: claim.id, suggestions: [] } })\n }\n\n const lines = await findWithDecryption(\n context.em,\n WarrantyClaimLine,\n {\n claim: claim.id,\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n lineStatus: 'resolved',\n vendorClaimLineId: null,\n deletedAt: null,\n },\n { orderBy: { lineNo: 'ASC' } },\n context.scope,\n )\n const policies = await findWithDecryption(\n context.em,\n WarrantyVendorPolicy,\n {\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n isActive: true,\n deletedAt: null,\n },\n { orderBy: { vendorName: 'ASC', updatedAt: 'DESC' } },\n context.scope,\n )\n const matches = findVendorRecoveryMatches({\n claim,\n lines,\n policies,\n requireWarrantyResolved: true,\n })\n const suggestions = matches.map((match) => ({\n lineId: match.line.id,\n lineNo: match.line.lineNo ?? 0,\n productName: match.line.productName ?? null,\n sku: match.line.sku ?? null,\n vendorName: toSuggestionVendorName(match.line.vendorName, match.policy.vendorName),\n policyId: match.policy.id,\n recoveryRatePct: toNullableString(match.policy.recoveryRatePct),\n causalFault: match.causalFault,\n estimatedRecovery: match.estimatedRecovery,\n }))\n return NextResponse.json({ ok: true, result: { claimId: claim.id, suggestions } })\n } catch (err) {\n if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })\n const { translate } = await resolveTranslations()\n if (err instanceof z.ZodError) {\n return NextResponse.json({ error: translate('warranty_claims.errors.invalidInput', 'Invalid input') }, { status: 400 })\n }\n logger.error('[internal] warranty_claims.vendor-recovery-suggestions.get failed', { err })\n return NextResponse.json({ error: translate('warranty_claims.errors.load_failed', 'Failed to load warranty claim data') }, { status: 500 })\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Warranty Claims',\n summary: 'List supplier recovery suggestions',\n methods: {\n GET: {\n summary: 'List resolved warranty claim lines eligible for supplier recovery',\n query: querySchema,\n responses: [\n {\n status: 200,\n description: 'Supplier recovery suggestions',\n schema: responseSchema,\n },\n ],\n },\n },\n}\n"],
5
- "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AACnC,SAAS,0CAA0C;AACnD,SAAS,eAAe,uBAAuB;AAC/C,SAAS,0BAA0B;AACnC,SAAS,2BAA2B;AAEpC,SAAS,mBAAmB,4BAA4B;AACxD,SAAS,0BAAmD;AAC5D,SAAS,iCAAiC;AAC1C,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,iBAAiB;AAE7C,MAAM,cAAc,EAAE,OAAO;AAAA,EAC3B,SAAS,EAAE,OAAO,EAAE,KAAK;AAC3B,CAAC,EAAE,OAAO;AAEV,MAAM,mBAAmB,EAAE,OAAO;AAAA,EAChC,QAAQ,EAAE,OAAO,EAAE,KAAK;AAAA,EACxB,QAAQ,EAAE,OAAO,EAAE,IAAI;AAAA,EACvB,aAAa,EAAE,OAAO,EAAE,SAAS;AAAA,EACjC,KAAK,EAAE,OAAO,EAAE,SAAS;AAAA,EACzB,YAAY,EAAE,OAAO;AAAA,EACrB,UAAU,EAAE,OAAO,EAAE,KAAK;AAAA,EAC1B,iBAAiB,EAAE,OAAO,EAAE,SAAS;AAAA,EACrC,aAAa,EAAE,OAAO,EAAE,SAAS;AAAA,EACjC,mBAAmB,EAAE,OAAO,EAAE,SAAS;AACzC,CAAC;AAED,MAAM,iBAAiB,EAAE,OAAO;AAAA,EAC9B,IAAI,EAAE,QAAQ,IAAI;AAAA,EAClB,QAAQ,EAAE,OAAO;AAAA,IACf,SAAS,EAAE,OAAO,EAAE,KAAK;AAAA,IACzB,aAAa,EAAE,MAAM,gBAAgB;AAAA,EACvC,CAAC;AACH,CAAC;AASM,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,8BAA8B,EAAE;AAC9E;AAEA,SAAS,iBAAiB,OAA0D;AAClF,MAAI,OAAO,UAAU,SAAU,QAAO,OAAO,SAAS,KAAK,IAAI,OAAO,KAAK,IAAI;AAC/E,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,QAAM,UAAU,MAAM,KAAK;AAC3B,SAAO,UAAU,UAAU;AAC7B;AAEA,SAAS,uBAAuB,gBAA2C,kBAAkC;AAC3G,QAAM,aAAa,gBAAgB,KAAK;AACxC,SAAO,cAAc;AACvB;AAEA,eAAe,0BAA0B,KAAgD;AACvF,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,QAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,MAAI,CAAC,QAAQ,CAAC,KAAK,UAAU;AAC3B,UAAM,IAAI,cAAc,KAAK,EAAE,OAAO,UAAU,uCAAuC,cAAc,EAAE,CAAC;AAAA,EAC1G;AACA,QAAM,oBAAoB,MAAM,mCAAmC,EAAE,WAAW,MAAM,SAAS,IAAI,CAAC;AACpG,QAAM,iBAAiB,mBAAmB,cAAc,KAAK,SAAS;AACtE,MAAI,CAAC,gBAAgB;AACnB,UAAM,IAAI,cAAc,KAAK,EAAE,OAAO,UAAU,gDAAgD,kCAAkC,EAAE,CAAC;AAAA,EACvI;AACA,QAAM,KAAK,UAAU,QAAQ,IAAI;AACjC,SAAO;AAAA,IACL,UAAU,KAAK;AAAA,IACf;AAAA,IACA,OAAO,EAAE,UAAU,KAAK,UAAU,eAAe;AAAA,IACjD;AAAA,EACF;AACF;AAEA,eAAsB,IAAI,KAAc;AACtC,MAAI;AACF,UAAM,UAAU,MAAM,0BAA0B,GAAG;AACnD,UAAM,MAAM,IAAI,IAAI,IAAI,GAAG;AAC3B,UAAM,QAAQ,YAAY,MAAM,OAAO,YAAY,IAAI,YAAY,CAAC;AACpE,UAAM,QAAQ,MAAM,mBAAmB,QAAQ,IAAI,MAAM,SAAS,QAAQ,KAAK;AAC/E,QAAI,MAAM,cAAc,cAAe,MAAM,WAAW,cAAc,MAAM,WAAW,UAAW;AAChG,aAAO,aAAa,KAAK,EAAE,IAAI,MAAM,QAAQ,EAAE,SAAS,MAAM,IAAI,aAAa,CAAC,EAAE,EAAE,CAAC;AAAA,IACvF;AAEA,UAAM,QAAQ,MAAM;AAAA,MAClB,QAAQ;AAAA,MACR;AAAA,MACA;AAAA,QACE,OAAO,MAAM;AAAA,QACb,UAAU,QAAQ;AAAA,QAClB,gBAAgB,QAAQ;AAAA,QACxB,YAAY;AAAA,QACZ,mBAAmB;AAAA,QACnB,WAAW;AAAA,MACb;AAAA,MACA,EAAE,SAAS,EAAE,QAAQ,MAAM,EAAE;AAAA,MAC7B,QAAQ;AAAA,IACV;AACA,UAAM,WAAW,MAAM;AAAA,MACrB,QAAQ;AAAA,MACR;AAAA,MACA;AAAA,QACE,UAAU,QAAQ;AAAA,QAClB,gBAAgB,QAAQ;AAAA,QACxB,UAAU;AAAA,QACV,WAAW;AAAA,MACb;AAAA,MACA,EAAE,SAAS,EAAE,YAAY,OAAO,WAAW,OAAO,EAAE;AAAA,MACpD,QAAQ;AAAA,IACV;AACA,UAAM,UAAU,0BAA0B;AAAA,MACxC;AAAA,MACA;AAAA,MACA;AAAA,MACA,yBAAyB;AAAA,IAC3B,CAAC;AACD,UAAM,cAAc,QAAQ,IAAI,CAAC,WAAW;AAAA,MAC1C,QAAQ,MAAM,KAAK;AAAA,MACnB,QAAQ,MAAM,KAAK,UAAU;AAAA,MAC7B,aAAa,MAAM,KAAK,eAAe;AAAA,MACvC,KAAK,MAAM,KAAK,OAAO;AAAA,MACvB,YAAY,uBAAuB,MAAM,KAAK,YAAY,MAAM,OAAO,UAAU;AAAA,MACjF,UAAU,MAAM,OAAO;AAAA,MACvB,iBAAiB,iBAAiB,MAAM,OAAO,eAAe;AAAA,MAC9D,aAAa,MAAM;AAAA,MACnB,mBAAmB,MAAM;AAAA,IAC3B,EAAE;AACF,WAAO,aAAa,KAAK,EAAE,IAAI,MAAM,QAAQ,EAAE,SAAS,MAAM,IAAI,YAAY,EAAE,CAAC;AAAA,EACnF,SAAS,KAAK;AACZ,QAAI,gBAAgB,GAAG,EAAG,QAAO,aAAa,KAAK,IAAI,MAAM,EAAE,QAAQ,IAAI,OAAO,CAAC;AACnF,UAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,QAAI,eAAe,EAAE,UAAU;AAC7B,aAAO,aAAa,KAAK,EAAE,OAAO,UAAU,uCAAuC,eAAe,EAAE,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IACxH;AACA,WAAO,MAAM,qEAAqE,EAAE,IAAI,CAAC;AACzF,WAAO,aAAa,KAAK,EAAE,OAAO,UAAU,sCAAsC,oCAAoC,EAAE,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC5I;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,KAAK;AAAA,MACH,SAAS;AAAA,MACT,OAAO;AAAA,MACP,WAAW;AAAA,QACT;AAAA,UACE,QAAQ;AAAA,UACR,aAAa;AAAA,UACb,QAAQ;AAAA,QACV;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { WarrantyClaimLine, WarrantyVendorPolicy } from '../../data/entities'\nimport { requireScopedClaim, type WarrantyClaimScope } from '../../commands/shared'\nimport { findVendorRecoveryMatches } from '../../lib/vendorPolicyRecovery'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('warranty_claims')\n\nconst querySchema = z.object({\n claimId: z.string().uuid(),\n}).strict()\n\nconst suggestionSchema = z.object({\n lineId: z.string().uuid(),\n lineNo: z.number().int(),\n productName: z.string().nullable(),\n sku: z.string().nullable(),\n vendorName: z.string(),\n policyId: z.string().uuid(),\n recoveryRatePct: z.string().nullable(),\n causalFault: z.string().nullable(),\n estimatedRecovery: z.string().nullable(),\n})\n\nconst responseSchema = z.object({\n ok: z.literal(true),\n result: z.object({\n claimId: z.string().uuid(),\n suggestions: z.array(suggestionSchema),\n }),\n})\n\ntype SuggestionsRouteContext = {\n tenantId: string\n organizationId: string\n scope: WarrantyClaimScope\n translate: (key: string, fallback?: string) => string\n em: EntityManager\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['warranty_claims.claim.manage'] },\n}\n\nfunction toNullableString(value: number | string | null | undefined): string | null {\n if (typeof value === 'number') return Number.isFinite(value) ? String(value) : null\n if (typeof value !== 'string') return null\n const trimmed = value.trim()\n return trimmed ? trimmed : null\n}\n\nfunction toSuggestionVendorName(lineVendorName: string | null | undefined, policyVendorName: string): string {\n const normalized = lineVendorName?.trim()\n return normalized || policyVendorName\n}\n\nasync function resolveSuggestionsContext(req: Request): Promise<SuggestionsRouteContext> {\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n const { translate } = await resolveTranslations()\n if (!auth || !auth.tenantId) {\n throw new CrudHttpError(401, { error: translate('warranty_claims.errors.unauthorized', 'Unauthorized') })\n }\n const organizationScope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const organizationId = organizationScope?.selectedId ?? auth.orgId ?? null\n if (!organizationId) {\n throw new CrudHttpError(400, { error: translate('warranty_claims.errors.organization_required', 'Organization context is required') })\n }\n const em = container.resolve('em') as EntityManager\n return {\n tenantId: auth.tenantId,\n organizationId,\n scope: { tenantId: auth.tenantId, organizationId },\n translate,\n em,\n }\n}\n\nexport async function GET(req: Request) {\n try {\n const context = await resolveSuggestionsContext(req)\n const url = new URL(req.url)\n const query = querySchema.parse(Object.fromEntries(url.searchParams))\n const claim = await requireScopedClaim(context.em, query.claimId, context.scope, {}, context.translate('warranty_claims.errors.notFound', 'Claim not found.'))\n if (claim.claimType !== 'warranty' || (claim.status !== 'resolved' && claim.status !== 'closed')) {\n return NextResponse.json({ ok: true, result: { claimId: claim.id, suggestions: [] } })\n }\n\n const lines = await findWithDecryption(\n context.em,\n WarrantyClaimLine,\n {\n claim: claim.id,\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n lineStatus: 'resolved',\n vendorClaimLineId: null,\n deletedAt: null,\n },\n { orderBy: { lineNo: 'ASC' } },\n context.scope,\n )\n const policies = await findWithDecryption(\n context.em,\n WarrantyVendorPolicy,\n {\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n isActive: true,\n deletedAt: null,\n },\n { orderBy: { vendorName: 'ASC', updatedAt: 'DESC' } },\n context.scope,\n )\n const matches = findVendorRecoveryMatches({\n claim,\n lines,\n policies,\n requireWarrantyResolved: true,\n })\n const suggestions = matches.map((match) => ({\n lineId: match.line.id,\n lineNo: match.line.lineNo ?? 0,\n productName: match.line.productName ?? null,\n sku: match.line.sku ?? null,\n vendorName: toSuggestionVendorName(match.line.vendorName, match.policy.vendorName),\n policyId: match.policy.id,\n recoveryRatePct: toNullableString(match.policy.recoveryRatePct),\n causalFault: match.causalFault,\n estimatedRecovery: match.estimatedRecovery,\n }))\n return NextResponse.json({ ok: true, result: { claimId: claim.id, suggestions } })\n } catch (err) {\n if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })\n const { translate } = await resolveTranslations()\n if (err instanceof z.ZodError) {\n return NextResponse.json({ error: translate('warranty_claims.errors.invalidInput', 'Invalid input') }, { status: 400 })\n }\n logger.error('[internal] warranty_claims.vendor-recovery-suggestions.get failed', { err })\n return NextResponse.json({ error: translate('warranty_claims.errors.load_failed', 'Failed to load warranty claim data') }, { status: 500 })\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Warranty Claims',\n summary: 'List supplier recovery suggestions',\n methods: {\n GET: {\n summary: 'List resolved warranty claim lines eligible for supplier recovery',\n query: querySchema,\n responses: [\n {\n status: 200,\n description: 'Supplier recovery suggestions',\n schema: responseSchema,\n },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -35,9 +35,9 @@ async function loadScopedClaim(em, id, scope, options = {}) {
35
35
  scope
36
36
  );
37
37
  }
38
- async function requireScopedClaim(em, id, scope, options = {}) {
38
+ async function requireScopedClaim(em, id, scope, options = {}, notFoundMessage = "warranty_claims.errors.notFound") {
39
39
  const claim = await loadScopedClaim(em, id, scope, options);
40
- return assertFound(claim, "warranty_claims.errors.notFound");
40
+ return assertFound(claim, notFoundMessage);
41
41
  }
42
42
  function appendClaimEvent(em, claim, kind, input = {}) {
43
43
  const event = em.create(WarrantyClaimEvent, {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/warranty_claims/commands/shared.ts"],
4
- "sourcesContent": ["import { randomUUID } from 'crypto'\nimport type { FindOneOptions } from '@mikro-orm/core'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { assertFound } from '@open-mercato/shared/lib/crud/errors'\nimport { invalidateCrudCache } from '@open-mercato/shared/lib/crud/cache'\nimport { enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'\nimport { emitCrudSideEffects, flushCrudSideEffects } from '@open-mercato/shared/lib/commands/helpers'\nimport type { DataEngine } from '@open-mercato/shared/lib/data/engine'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n WarrantyClaim,\n WarrantyClaimEvent,\n WarrantyClaimLine,\n} from '../data/entities'\nimport type {\n WarrantyClaimEventKind,\n WarrantyClaimEventVisibility,\n} from '../data/validators'\nimport { computeHeaderRollups } from '../lib/stateMachine'\n\nexport { assertFound } from '@open-mercato/shared/lib/crud/errors'\nexport { ensureOrganizationScope, ensureSameScope, ensureTenantScope } from '@open-mercato/shared/lib/commands/scope'\nexport { extractUndoPayload } from '@open-mercato/shared/lib/commands/undo'\n\nexport const WARRANTY_CLAIM_RESOURCE_KIND = 'warranty_claims.claim'\nexport const WARRANTY_CLAIM_LINE_RESOURCE_KIND = 'warranty_claims.claim_line'\n\nexport type WarrantyClaimScope = {\n organizationId: string\n tenantId: string\n}\n\nexport type VersionedRecord = {\n id: string\n updatedAt?: Date | string | null\n}\n\nexport type ResolverContext = {\n resolve: <T = unknown>(name: string) => T\n}\n\nexport type AppendClaimEventInput = {\n visibility?: WarrantyClaimEventVisibility\n body?: string | null\n payload?: Record<string, unknown> | null\n actorUserId?: string | null\n actorCustomerId?: string | null\n}\n\nexport async function enforceWarrantyClaimOptimisticLock(\n ctx: CommandRuntimeContext,\n record: VersionedRecord | null | undefined,\n resourceKind = WARRANTY_CLAIM_RESOURCE_KIND,\n expected?: string | Date | null,\n): Promise<void> {\n if (!record) return\n await enforceCommandOptimisticLockWithGuards(ctx.container, {\n resourceKind,\n resourceId: record.id,\n current: record.updatedAt ?? null,\n expected,\n request: ctx.request ?? null,\n })\n}\n\nexport async function loadScopedClaim(\n em: EntityManager,\n id: string,\n scope: WarrantyClaimScope,\n options: FindOneOptions<WarrantyClaim> = {},\n): Promise<WarrantyClaim | null> {\n return findOneWithDecryption(\n em,\n WarrantyClaim,\n { id, tenantId: scope.tenantId, organizationId: scope.organizationId, deletedAt: null },\n options,\n scope,\n )\n}\n\nexport async function requireScopedClaim(\n em: EntityManager,\n id: string,\n scope: WarrantyClaimScope,\n options: FindOneOptions<WarrantyClaim> = {},\n): Promise<WarrantyClaim> {\n const claim = await loadScopedClaim(em, id, scope, options)\n return assertFound(claim, 'warranty_claims.errors.notFound')\n}\n\nexport function appendClaimEvent(\n em: EntityManager,\n claim: WarrantyClaim,\n kind: WarrantyClaimEventKind,\n input: AppendClaimEventInput = {},\n): WarrantyClaimEvent {\n const event = em.create(WarrantyClaimEvent, {\n id: randomUUID(),\n claim,\n organizationId: claim.organizationId,\n tenantId: claim.tenantId,\n kind,\n visibility: input.visibility ?? 'internal',\n body: input.body ?? null,\n payload: input.payload ?? null,\n actorUserId: input.actorUserId ?? null,\n actorCustomerId: input.actorCustomerId ?? null,\n createdAt: new Date(),\n })\n em.persist(event)\n return event\n}\n\nexport async function reconcileVendorRecoverySourceClaim(\n ctx: ResolverContext,\n input: { claimId: string; tenantId: string; organizationId: string },\n): Promise<void> {\n const em = (ctx.resolve('em') as EntityManager).fork()\n const scope = { tenantId: input.tenantId, organizationId: input.organizationId }\n const recoveryClaim = await findOneWithDecryption(\n em,\n WarrantyClaim,\n { id: input.claimId, tenantId: scope.tenantId, organizationId: scope.organizationId, deletedAt: null },\n {},\n scope,\n )\n if (!recoveryClaim || recoveryClaim.claimType !== 'vendor_recovery' || !recoveryClaim.sourceClaimId) return\n\n const sourceClaim = await findOneWithDecryption(\n em,\n WarrantyClaim,\n { id: recoveryClaim.sourceClaimId, tenantId: scope.tenantId, organizationId: scope.organizationId, deletedAt: null },\n {},\n scope,\n )\n if (!sourceClaim) return\n\n const resolvedChildren = await findWithDecryption(\n em,\n WarrantyClaim,\n {\n sourceClaimId: sourceClaim.id,\n claimType: 'vendor_recovery',\n status: { $in: ['resolved', 'closed'] },\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n deletedAt: null,\n },\n {},\n scope,\n )\n const childLines = resolvedChildren.length\n ? await findWithDecryption(\n em,\n WarrantyClaimLine,\n {\n claim: { $in: resolvedChildren.map((child) => child.id) },\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n deletedAt: null,\n },\n {},\n scope,\n )\n : []\n const recoveredTotal = computeHeaderRollups(childLines).totalApprovedAmount\n await withAtomicFlush(\n em,\n [\n () => {\n sourceClaim.totalRecoveredAmount = String(recoveredTotal)\n sourceClaim.updatedAt = new Date()\n },\n ],\n { transaction: true, label: 'warranty_claims.vendor_recovery.reconciliation' },\n )\n\n const dataEngine = ctx.resolve<DataEngine>('dataEngine')\n await emitCrudSideEffects({\n dataEngine,\n action: 'updated',\n entity: sourceClaim,\n identifiers: {\n id: sourceClaim.id,\n organizationId: sourceClaim.organizationId,\n tenantId: sourceClaim.tenantId,\n },\n indexer: { entityType: 'warranty_claims:warranty_claim' },\n events: {\n module: 'warranty_claims',\n entity: 'claim',\n persistent: true,\n buildPayload: () => ({\n id: sourceClaim.id,\n organizationId: sourceClaim.organizationId,\n tenantId: sourceClaim.tenantId,\n claimType: sourceClaim.claimType,\n status: sourceClaim.status,\n }),\n },\n })\n await flushCrudSideEffects(dataEngine)\n await invalidateCrudCache(\n ctx as unknown as Parameters<typeof invalidateCrudCache>[0],\n 'warranty_claims.claim',\n { id: sourceClaim.id, organizationId: sourceClaim.organizationId, tenantId: sourceClaim.tenantId },\n input.tenantId,\n 'warranty_claims.vendor_recovery.reconciliation',\n )\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { randomUUID } from 'crypto'\nimport type { FindOneOptions } from '@mikro-orm/core'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { assertFound } from '@open-mercato/shared/lib/crud/errors'\nimport { invalidateCrudCache } from '@open-mercato/shared/lib/crud/cache'\nimport { enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'\nimport { emitCrudSideEffects, flushCrudSideEffects } from '@open-mercato/shared/lib/commands/helpers'\nimport type { DataEngine } from '@open-mercato/shared/lib/data/engine'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n WarrantyClaim,\n WarrantyClaimEvent,\n WarrantyClaimLine,\n} from '../data/entities'\nimport type {\n WarrantyClaimEventKind,\n WarrantyClaimEventVisibility,\n} from '../data/validators'\nimport { computeHeaderRollups } from '../lib/stateMachine'\n\nexport { assertFound } from '@open-mercato/shared/lib/crud/errors'\nexport { ensureOrganizationScope, ensureSameScope, ensureTenantScope } from '@open-mercato/shared/lib/commands/scope'\nexport { extractUndoPayload } from '@open-mercato/shared/lib/commands/undo'\n\nexport const WARRANTY_CLAIM_RESOURCE_KIND = 'warranty_claims.claim'\nexport const WARRANTY_CLAIM_LINE_RESOURCE_KIND = 'warranty_claims.claim_line'\n\nexport type WarrantyClaimScope = {\n organizationId: string\n tenantId: string\n}\n\nexport type VersionedRecord = {\n id: string\n updatedAt?: Date | string | null\n}\n\nexport type ResolverContext = {\n resolve: <T = unknown>(name: string) => T\n}\n\nexport type AppendClaimEventInput = {\n visibility?: WarrantyClaimEventVisibility\n body?: string | null\n payload?: Record<string, unknown> | null\n actorUserId?: string | null\n actorCustomerId?: string | null\n}\n\nexport async function enforceWarrantyClaimOptimisticLock(\n ctx: CommandRuntimeContext,\n record: VersionedRecord | null | undefined,\n resourceKind = WARRANTY_CLAIM_RESOURCE_KIND,\n expected?: string | Date | null,\n): Promise<void> {\n if (!record) return\n await enforceCommandOptimisticLockWithGuards(ctx.container, {\n resourceKind,\n resourceId: record.id,\n current: record.updatedAt ?? null,\n expected,\n request: ctx.request ?? null,\n })\n}\n\nexport async function loadScopedClaim(\n em: EntityManager,\n id: string,\n scope: WarrantyClaimScope,\n options: FindOneOptions<WarrantyClaim> = {},\n): Promise<WarrantyClaim | null> {\n return findOneWithDecryption(\n em,\n WarrantyClaim,\n { id, tenantId: scope.tenantId, organizationId: scope.organizationId, deletedAt: null },\n options,\n scope,\n )\n}\n\n/**\n * `notFoundMessage` is passed to `assertFound` verbatim, so request-scoped callers\n * must hand in an already-translated sentence; the key default only suits the\n * command surface, which runs without a request locale.\n */\nexport async function requireScopedClaim(\n em: EntityManager,\n id: string,\n scope: WarrantyClaimScope,\n options: FindOneOptions<WarrantyClaim> = {},\n notFoundMessage = 'warranty_claims.errors.notFound',\n): Promise<WarrantyClaim> {\n const claim = await loadScopedClaim(em, id, scope, options)\n return assertFound(claim, notFoundMessage)\n}\n\nexport function appendClaimEvent(\n em: EntityManager,\n claim: WarrantyClaim,\n kind: WarrantyClaimEventKind,\n input: AppendClaimEventInput = {},\n): WarrantyClaimEvent {\n const event = em.create(WarrantyClaimEvent, {\n id: randomUUID(),\n claim,\n organizationId: claim.organizationId,\n tenantId: claim.tenantId,\n kind,\n visibility: input.visibility ?? 'internal',\n body: input.body ?? null,\n payload: input.payload ?? null,\n actorUserId: input.actorUserId ?? null,\n actorCustomerId: input.actorCustomerId ?? null,\n createdAt: new Date(),\n })\n em.persist(event)\n return event\n}\n\nexport async function reconcileVendorRecoverySourceClaim(\n ctx: ResolverContext,\n input: { claimId: string; tenantId: string; organizationId: string },\n): Promise<void> {\n const em = (ctx.resolve('em') as EntityManager).fork()\n const scope = { tenantId: input.tenantId, organizationId: input.organizationId }\n const recoveryClaim = await findOneWithDecryption(\n em,\n WarrantyClaim,\n { id: input.claimId, tenantId: scope.tenantId, organizationId: scope.organizationId, deletedAt: null },\n {},\n scope,\n )\n if (!recoveryClaim || recoveryClaim.claimType !== 'vendor_recovery' || !recoveryClaim.sourceClaimId) return\n\n const sourceClaim = await findOneWithDecryption(\n em,\n WarrantyClaim,\n { id: recoveryClaim.sourceClaimId, tenantId: scope.tenantId, organizationId: scope.organizationId, deletedAt: null },\n {},\n scope,\n )\n if (!sourceClaim) return\n\n const resolvedChildren = await findWithDecryption(\n em,\n WarrantyClaim,\n {\n sourceClaimId: sourceClaim.id,\n claimType: 'vendor_recovery',\n status: { $in: ['resolved', 'closed'] },\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n deletedAt: null,\n },\n {},\n scope,\n )\n const childLines = resolvedChildren.length\n ? await findWithDecryption(\n em,\n WarrantyClaimLine,\n {\n claim: { $in: resolvedChildren.map((child) => child.id) },\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n deletedAt: null,\n },\n {},\n scope,\n )\n : []\n const recoveredTotal = computeHeaderRollups(childLines).totalApprovedAmount\n await withAtomicFlush(\n em,\n [\n () => {\n sourceClaim.totalRecoveredAmount = String(recoveredTotal)\n sourceClaim.updatedAt = new Date()\n },\n ],\n { transaction: true, label: 'warranty_claims.vendor_recovery.reconciliation' },\n )\n\n const dataEngine = ctx.resolve<DataEngine>('dataEngine')\n await emitCrudSideEffects({\n dataEngine,\n action: 'updated',\n entity: sourceClaim,\n identifiers: {\n id: sourceClaim.id,\n organizationId: sourceClaim.organizationId,\n tenantId: sourceClaim.tenantId,\n },\n indexer: { entityType: 'warranty_claims:warranty_claim' },\n events: {\n module: 'warranty_claims',\n entity: 'claim',\n persistent: true,\n buildPayload: () => ({\n id: sourceClaim.id,\n organizationId: sourceClaim.organizationId,\n tenantId: sourceClaim.tenantId,\n claimType: sourceClaim.claimType,\n status: sourceClaim.status,\n }),\n },\n })\n await flushCrudSideEffects(dataEngine)\n await invalidateCrudCache(\n ctx as unknown as Parameters<typeof invalidateCrudCache>[0],\n 'warranty_claims.claim',\n { id: sourceClaim.id, organizationId: sourceClaim.organizationId, tenantId: sourceClaim.tenantId },\n input.tenantId,\n 'warranty_claims.vendor_recovery.reconciliation',\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["assertFound"]
7
7
  }
@@ -0,0 +1,31 @@
1
+ import { NextResponse } from "next/server";
2
+ import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
3
+ import { getCustomerAuthFromRequest } from "@open-mercato/core/modules/customer_accounts/lib/customerAuth";
4
+ async function resolveLinkedCustomerAuth(req) {
5
+ const auth = await getCustomerAuthFromRequest(req);
6
+ if (!auth) {
7
+ const { translate } = await resolveTranslations();
8
+ return NextResponse.json(
9
+ { ok: false, error: translate("warranty_claims.errors.unauthorized", "Unauthorized") },
10
+ { status: 401 }
11
+ );
12
+ }
13
+ if (!auth.customerEntityId) {
14
+ const { translate } = await resolveTranslations();
15
+ return NextResponse.json(
16
+ {
17
+ ok: false,
18
+ error: translate(
19
+ "warranty_claims.errors.customerAccountNotLinked",
20
+ "Customer account is not linked to a customer record"
21
+ )
22
+ },
23
+ { status: 403 }
24
+ );
25
+ }
26
+ return auth;
27
+ }
28
+ export {
29
+ resolveLinkedCustomerAuth
30
+ };
31
+ //# sourceMappingURL=portalAuthGuard.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/warranty_claims/lib/portalAuthGuard.ts"],
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { getCustomerAuthFromRequest, type CustomerAuthContext } from '@open-mercato/core/modules/customer_accounts/lib/customerAuth'\n\nexport type LinkedCustomerAuthContext = CustomerAuthContext & { customerEntityId: string }\n\nexport async function resolveLinkedCustomerAuth(req: Request): Promise<LinkedCustomerAuthContext | Response> {\n const auth = await getCustomerAuthFromRequest(req)\n if (!auth) {\n const { translate } = await resolveTranslations()\n return NextResponse.json(\n { ok: false, error: translate('warranty_claims.errors.unauthorized', 'Unauthorized') },\n { status: 401 },\n )\n }\n if (!auth.customerEntityId) {\n const { translate } = await resolveTranslations()\n return NextResponse.json(\n {\n ok: false,\n error: translate(\n 'warranty_claims.errors.customerAccountNotLinked',\n 'Customer account is not linked to a customer record',\n ),\n },\n { status: 403 },\n )\n }\n return auth as LinkedCustomerAuthContext\n}\n"],
5
+ "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,2BAA2B;AACpC,SAAS,kCAA4D;AAIrE,eAAsB,0BAA0B,KAA6D;AAC3G,QAAM,OAAO,MAAM,2BAA2B,GAAG;AACjD,MAAI,CAAC,MAAM;AACT,UAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,WAAO,aAAa;AAAA,MAClB,EAAE,IAAI,OAAO,OAAO,UAAU,uCAAuC,cAAc,EAAE;AAAA,MACrF,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AACA,MAAI,CAAC,KAAK,kBAAkB;AAC1B,UAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,WAAO,aAAa;AAAA,MAClB;AAAA,QACE,IAAI;AAAA,QACJ,OAAO;AAAA,UACL;AAAA,UACA;AAAA,QACF;AAAA,MACF;AAAA,MACA,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AACA,SAAO;AACT;",
6
+ "names": []
7
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.7.1-develop.7131.1.20abf9e885",
3
+ "version": "0.7.1-develop.7136.1.0f76137a1d",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -252,16 +252,16 @@
252
252
  "zod": "^4.4.3"
253
253
  },
254
254
  "peerDependencies": {
255
- "@open-mercato/ai-assistant": "0.7.1-develop.7131.1.20abf9e885",
256
- "@open-mercato/shared": "0.7.1-develop.7131.1.20abf9e885",
257
- "@open-mercato/ui": "0.7.1-develop.7131.1.20abf9e885",
255
+ "@open-mercato/ai-assistant": "0.7.1-develop.7136.1.0f76137a1d",
256
+ "@open-mercato/shared": "0.7.1-develop.7136.1.0f76137a1d",
257
+ "@open-mercato/ui": "0.7.1-develop.7136.1.0f76137a1d",
258
258
  "react": "^19.0.0",
259
259
  "react-dom": "^19.0.0"
260
260
  },
261
261
  "devDependencies": {
262
- "@open-mercato/ai-assistant": "0.7.1-develop.7131.1.20abf9e885",
263
- "@open-mercato/shared": "0.7.1-develop.7131.1.20abf9e885",
264
- "@open-mercato/ui": "0.7.1-develop.7131.1.20abf9e885",
262
+ "@open-mercato/ai-assistant": "0.7.1-develop.7136.1.0f76137a1d",
263
+ "@open-mercato/shared": "0.7.1-develop.7136.1.0f76137a1d",
264
+ "@open-mercato/ui": "0.7.1-develop.7136.1.0f76137a1d",
265
265
  "@testing-library/dom": "^10.4.1",
266
266
  "@testing-library/jest-dom": "^7.0.0",
267
267
  "@testing-library/react": "^16.3.1",
@@ -53,6 +53,19 @@ export async function seedConnectedChannel(
53
53
  * cannot prove it, because it can only ever supply email-shaped data.
54
54
  */
55
55
  providerFlavor?: 'email' | 'chat';
56
+ /**
57
+ * Relabel the connected stub channel's `provider_key`. Provider packages own
58
+ * routes that filter on their own key, and a real channel for those providers
59
+ * cannot be connected in CI (the Discord adapter validates its bot token
60
+ * against the live API), so without this a provider-scoped listing can only
61
+ * be asserted against an empty result — the assertion that stayed green
62
+ * through #5602.
63
+ *
64
+ * The row is relabelled, nothing is registered: do not drive an outbound send
65
+ * through a relabelled channel, because delivery would resolve the real
66
+ * provider's adapter.
67
+ */
68
+ labelAsProviderKey?: string;
56
69
  } = {},
57
70
  ): Promise<string> {
58
71
  const response = await apiRequest(request, 'POST', TEST_SEED_PATH, {
@@ -43,7 +43,9 @@ import {
43
43
  * (delegates to the real connect-credential command so the channel persists
44
44
  * credentials + lands in `status='connected'`). Enables the outbound
45
45
  * compose → deliver → `.sent` chain to complete in CI. `providerFlavor: 'chat'`
46
- * connects the non-email stub instead, for tests about sender identity.
46
+ * connects the non-email stub instead, for tests about sender identity, and
47
+ * `labelAsProviderKey` relabels the connected row so a provider-scoped
48
+ * listing has something to list without a live credential probe.
47
49
  * - `ingest-inbound`: run the REAL `ingest_inbound_message` command over an
48
50
  * adapter-normalized chat frame, so the platform compose path — and every
49
51
  * validation rule on it — actually executes. Use this for anything that
@@ -88,6 +90,27 @@ const connectChannelSchema = z.object({
88
90
  * without inventing one (#4975).
89
91
  */
90
92
  providerFlavor: z.enum(['email', 'chat']).optional(),
93
+ /**
94
+ * TEST-ONLY: rewrite the connected stub channel's `provider_key` to this value.
95
+ *
96
+ * Routes that a provider package owns filter on their own `provider_key`, and
97
+ * a real channel for those providers cannot be connected in CI — the Discord
98
+ * adapter's `validateCredentials` performs a live API call against a bot token
99
+ * no test environment has. Without this, a provider-scoped listing has nothing
100
+ * to list and can only be asserted against an empty result, which is exactly
101
+ * the assertion that would have stayed green through #5602.
102
+ *
103
+ * It relabels a row; it registers nothing. The stub adapter is still the only
104
+ * adapter present, so the channel remains network-free — but no test should
105
+ * drive an outbound send through a relabelled channel, because delivery would
106
+ * then resolve the real provider's adapter.
107
+ */
108
+ labelAsProviderKey: z
109
+ .string()
110
+ .min(1)
111
+ .max(64)
112
+ .regex(/^[a-z0-9_-]+$/)
113
+ .optional(),
91
114
  })
92
115
 
93
116
  /**
@@ -224,8 +247,30 @@ export async function POST(req: Request): Promise<Response> {
224
247
  { status: 500 },
225
248
  )
226
249
  }
250
+ if (body.labelAsProviderKey && body.labelAsProviderKey !== input.providerKey) {
251
+ const seedEm = (container.resolve('em') as EntityManager).fork()
252
+ const connected = await findOneWithDecryption(
253
+ seedEm,
254
+ CommunicationChannel,
255
+ { id: result.channelId, tenantId },
256
+ undefined,
257
+ { tenantId, organizationId },
258
+ )
259
+ if (!connected) {
260
+ return NextResponse.json(
261
+ { error: '[internal] test-seed could not reload the channel it just connected' },
262
+ { status: 500 },
263
+ )
264
+ }
265
+ connected.providerKey = body.labelAsProviderKey
266
+ await seedEm.flush()
267
+ }
227
268
  return NextResponse.json(
228
- { channelId: result.channelId, externalIdentifier: result.externalIdentifier },
269
+ {
270
+ channelId: result.channelId,
271
+ externalIdentifier: result.externalIdentifier,
272
+ providerKey: body.labelAsProviderKey ?? input.providerKey,
273
+ },
229
274
  { status: 201 },
230
275
  )
231
276
  }