@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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/helpers/integration/communicationChannelsFixtures.js.map +2 -2
- package/dist/modules/communication_channels/api/post/test-seed/route.js +40 -2
- package/dist/modules/communication_channels/api/post/test-seed/route.js.map +2 -2
- package/dist/modules/communication_channels/lib/access-control.js +6 -1
- package/dist/modules/communication_channels/lib/access-control.js.map +2 -2
- package/dist/modules/communication_channels/lib/system-user.js +8 -3
- package/dist/modules/communication_channels/lib/system-user.js.map +2 -2
- package/dist/modules/warranty_claims/api/ai/assess/route.js +20 -18
- package/dist/modules/warranty_claims/api/ai/assess/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/credit-memo/route.js +11 -12
- package/dist/modules/warranty_claims/api/credit-memo/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/events/route.js +1 -1
- package/dist/modules/warranty_claims/api/events/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/portal/attachments/route.js +24 -27
- package/dist/modules/warranty_claims/api/portal/attachments/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/portal/claims/[id]/route.js +8 -10
- package/dist/modules/warranty_claims/api/portal/claims/[id]/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/portal/claims/[id]/shared.js +4 -9
- package/dist/modules/warranty_claims/api/portal/claims/[id]/shared.js.map +2 -2
- package/dist/modules/warranty_claims/api/portal/claims/[id]/submit/route.js +3 -3
- package/dist/modules/warranty_claims/api/portal/claims/[id]/submit/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/portal/claims/[id]/withdraw/route.js +3 -3
- package/dist/modules/warranty_claims/api/portal/claims/[id]/withdraw/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/portal/claims/route.js +4 -9
- package/dist/modules/warranty_claims/api/portal/claims/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/portal/events/route.js +12 -13
- package/dist/modules/warranty_claims/api/portal/events/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/receiving/route.js +14 -15
- package/dist/modules/warranty_claims/api/receiving/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/registrations/route.js +9 -4
- package/dist/modules/warranty_claims/api/registrations/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/replacement-order/route.js +11 -12
- package/dist/modules/warranty_claims/api/replacement-order/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/return-label/route.js +20 -19
- package/dist/modules/warranty_claims/api/return-label/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/risk/route.js +2 -1
- package/dist/modules/warranty_claims/api/risk/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/sales-return/route.js +11 -12
- package/dist/modules/warranty_claims/api/sales-return/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/troubleshooting-guides/route.js +22 -1
- package/dist/modules/warranty_claims/api/troubleshooting-guides/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/vendor-recovery-suggestions/route.js +2 -1
- package/dist/modules/warranty_claims/api/vendor-recovery-suggestions/route.js.map +2 -2
- package/dist/modules/warranty_claims/commands/shared.js +2 -2
- package/dist/modules/warranty_claims/commands/shared.js.map +2 -2
- package/dist/modules/warranty_claims/lib/portalAuthGuard.js +31 -0
- package/dist/modules/warranty_claims/lib/portalAuthGuard.js.map +7 -0
- package/package.json +7 -7
- package/src/helpers/integration/communicationChannelsFixtures.ts +13 -0
- package/src/modules/communication_channels/api/post/test-seed/route.ts +47 -2
- package/src/modules/communication_channels/lib/access-control.ts +28 -0
- package/src/modules/communication_channels/lib/system-user.ts +40 -22
- package/src/modules/warranty_claims/api/ai/assess/route.ts +23 -17
- package/src/modules/warranty_claims/api/credit-memo/route.ts +14 -13
- package/src/modules/warranty_claims/api/events/route.ts +1 -1
- package/src/modules/warranty_claims/api/portal/attachments/route.ts +29 -27
- package/src/modules/warranty_claims/api/portal/claims/[id]/route.ts +9 -10
- package/src/modules/warranty_claims/api/portal/claims/[id]/shared.ts +5 -9
- package/src/modules/warranty_claims/api/portal/claims/[id]/submit/route.ts +3 -3
- package/src/modules/warranty_claims/api/portal/claims/[id]/withdraw/route.ts +3 -3
- package/src/modules/warranty_claims/api/portal/claims/route.ts +5 -9
- package/src/modules/warranty_claims/api/portal/events/route.ts +13 -13
- package/src/modules/warranty_claims/api/receiving/route.ts +17 -15
- package/src/modules/warranty_claims/api/registrations/route.ts +9 -3
- package/src/modules/warranty_claims/api/replacement-order/route.ts +14 -13
- package/src/modules/warranty_claims/api/return-label/route.ts +21 -17
- package/src/modules/warranty_claims/api/risk/route.ts +3 -1
- package/src/modules/warranty_claims/api/sales-return/route.ts +14 -13
- package/src/modules/warranty_claims/api/troubleshooting-guides/route.ts +23 -1
- package/src/modules/warranty_claims/api/vendor-recovery-suggestions/route.ts +3 -1
- package/src/modules/warranty_claims/commands/shared.ts +7 -1
- package/src/modules/warranty_claims/lib/portalAuthGuard.ts +30 -0
|
@@ -59,6 +59,34 @@ export function channelOrgScopeWhereFromFilter(
|
|
|
59
59
|
return { $or: [{ organizationId: { $in: organizationIds } }, { organizationId: null }] }
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Ownership fragment for LISTING channels — the SQL-layer twin of
|
|
64
|
+
* {@link assertCanAccessChannel}.
|
|
65
|
+
*
|
|
66
|
+
* Routes have historically hardcoded one of the two halves of the rule: the
|
|
67
|
+
* admin channel list narrows to `user_id IS NULL`, the profile list narrows to
|
|
68
|
+
* `user_id = <caller>`. A provider-scoped listing needs both, because which half
|
|
69
|
+
* a channel falls into is decided by the connect flow the provider offers, not
|
|
70
|
+
* by the listing. Hardcoding `userId: null` is what made the Discord AI
|
|
71
|
+
* auto-reply panel structurally empty (#5602): every route the product exposes
|
|
72
|
+
* for connecting a Discord bot writes `user_id = auth.sub`, so the filter
|
|
73
|
+
* excluded the only channels that can exist.
|
|
74
|
+
*
|
|
75
|
+
* The clause admits exactly what the assert allows and nothing more — shared
|
|
76
|
+
* channels, plus the caller's own personal ones. It grants no admin bypass, in
|
|
77
|
+
* line with v1 strict owner-only: a personal mailbox stays invisible to everyone
|
|
78
|
+
* but its owner. An anonymous caller (no user id) sees shared channels only.
|
|
79
|
+
*
|
|
80
|
+
* Spread into a MikroORM `where`, or nest under `$and` when another fragment in
|
|
81
|
+
* the same query already claims `$or` (`channelOrgScopeWhereFromFilter` does).
|
|
82
|
+
*/
|
|
83
|
+
export function channelOwnerScopeWhere(
|
|
84
|
+
currentUserId: string | null | undefined,
|
|
85
|
+
): Record<string, unknown> {
|
|
86
|
+
if (!currentUserId) return { userId: null }
|
|
87
|
+
return { $or: [{ userId: null }, { userId: currentUserId }] }
|
|
88
|
+
}
|
|
89
|
+
|
|
62
90
|
/**
|
|
63
91
|
* Throws when the caller may not access a specific channel. Returns silently
|
|
64
92
|
* when access is allowed.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
2
|
+
import { lookupHashCandidates } from '@open-mercato/shared/lib/encryption/aes'
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Sentinel UUID — used as a last-resort `senderUserId` for inbound channel
|
|
@@ -31,6 +32,12 @@ export function systemUserEmail(tenantId: string): string {
|
|
|
31
32
|
* sentinel. The inbound-processor must never refuse to ingest a message
|
|
32
33
|
* because the channel-bot user doesn't exist.
|
|
33
34
|
*
|
|
35
|
+
* That fail-soft design is also what hid #5599 for as long as it did — a broken
|
|
36
|
+
* lookup and an absent user produce the identical outcome — so the two
|
|
37
|
+
* properties the lookup depends on (the column it matches and the table it
|
|
38
|
+
* names) are pinned by `__tests__/system-user.test.ts` rather than left to the
|
|
39
|
+
* next reader to notice.
|
|
40
|
+
*
|
|
34
41
|
* @param em EntityManager scoped to the tenant.
|
|
35
42
|
* @param tenantId Tenant id for which to resolve the system user.
|
|
36
43
|
* @param fallbackId Optional caller-supplied fallback (e.g., the channel's
|
|
@@ -43,28 +50,39 @@ export async function resolveCommunicationChannelsSystemUserId(
|
|
|
43
50
|
): Promise<string> {
|
|
44
51
|
try {
|
|
45
52
|
const expectedEmail = systemUserEmail(tenantId)
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
53
|
+
// Match on `email_hash`, never on `email` (#5599). `users.email` is
|
|
54
|
+
// encrypted at rest with a per-row IV, so its ciphertext is
|
|
55
|
+
// non-deterministic and an equality filter against the plaintext can never
|
|
56
|
+
// hit — which is exactly why the tenant uniqueness index keys on
|
|
57
|
+
// `email_hash` instead (see `auth/data/entities.ts`). The candidate list
|
|
58
|
+
// covers both the keyed `v2:` digest and the legacy unkeyed one, so a
|
|
59
|
+
// deployment that has not yet backfilled still matches.
|
|
60
|
+
//
|
|
61
|
+
// The digests must be computed the same way `auth` writes them
|
|
62
|
+
// (`auth/lib/emailHash.ts` → `hashForLookup(email)`), i.e. with NO hash
|
|
63
|
+
// context. Reusing the shared primitive rather than importing the auth
|
|
64
|
+
// helper keeps this module free of a cross-module code dependency; the two
|
|
65
|
+
// must stay aligned if `auth` ever adopts a context.
|
|
66
|
+
const emailHashes = lookupHashCandidates(expectedEmail)
|
|
67
|
+
|
|
68
|
+
// Raw SQL against the table, not `createQueryBuilder('auth.users')`. That
|
|
69
|
+
// call names no registered entity — entities are discovered under their
|
|
70
|
+
// class names — so MikroORM reads the dot as a schema qualifier and looks
|
|
71
|
+
// for `users` in a schema named `auth`, which this project never creates
|
|
72
|
+
// (`Migration20251030150038` puts `users` in the default schema). The helper
|
|
73
|
+
// swallows every error by design, so the miss was indistinguishable from
|
|
74
|
+
// "no channel-bot user exists" and hid behind the same fallback as the
|
|
75
|
+
// plaintext-vs-ciphertext defect above. A parameterized statement names the
|
|
76
|
+
// table unambiguously and still pulls in no cross-module entity class,
|
|
77
|
+
// keeping the decoupling this helper was written for.
|
|
78
|
+
const placeholders = emailHashes.map(() => '?').join(', ')
|
|
79
|
+
const rows = await em
|
|
80
|
+
.getConnection()
|
|
81
|
+
.execute<Array<{ id?: string }>>(
|
|
82
|
+
`SELECT id FROM users WHERE email_hash IN (${placeholders}) AND tenant_id = ? AND deleted_at IS NULL LIMIT 1`,
|
|
83
|
+
[...emailHashes, tenantId],
|
|
84
|
+
)
|
|
85
|
+
const id = Array.isArray(rows) ? rows[0]?.id : undefined
|
|
68
86
|
if (typeof id === 'string' && id.length > 0) return id
|
|
69
87
|
} catch {
|
|
70
88
|
// ignore — fall through to fallback
|
|
@@ -9,6 +9,7 @@ import type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib
|
|
|
9
9
|
import { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
10
10
|
import { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'
|
|
11
11
|
import { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'
|
|
12
|
+
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
12
13
|
import { runRouteMutationGuards, type RouteMutationGuardResult } from '@open-mercato/shared/lib/crud/route-mutation-guard'
|
|
13
14
|
import { withScopedPayload } from '@open-mercato/shared/lib/api/scoped'
|
|
14
15
|
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
@@ -110,7 +111,7 @@ function isSuperAdmin(auth: CommandRuntimeContext['auth']): boolean {
|
|
|
110
111
|
function buildAuthContext(context: AssessRouteContext): AiChatRequestContext {
|
|
111
112
|
const userId = context.ctx.auth?.sub
|
|
112
113
|
if (!userId) {
|
|
113
|
-
throw new CrudHttpError(401, { error: 'warranty_claims.errors.unauthorized' })
|
|
114
|
+
throw new CrudHttpError(401, { error: context.translate('warranty_claims.errors.unauthorized', 'Unauthorized') })
|
|
114
115
|
}
|
|
115
116
|
return {
|
|
116
117
|
tenantId: context.tenantId,
|
|
@@ -124,7 +125,7 @@ function buildAuthContext(context: AssessRouteContext): AiChatRequestContext {
|
|
|
124
125
|
function buildAttachmentAuthContext(context: AssessRouteContext): Exclude<AuthContext, null> {
|
|
125
126
|
const auth = context.ctx.auth
|
|
126
127
|
if (!auth?.sub) {
|
|
127
|
-
throw new CrudHttpError(401, { error: 'warranty_claims.errors.unauthorized' })
|
|
128
|
+
throw new CrudHttpError(401, { error: context.translate('warranty_claims.errors.unauthorized', 'Unauthorized') })
|
|
128
129
|
}
|
|
129
130
|
return {
|
|
130
131
|
...auth,
|
|
@@ -134,16 +135,19 @@ function buildAttachmentAuthContext(context: AssessRouteContext): Exclude<AuthCo
|
|
|
134
135
|
}
|
|
135
136
|
}
|
|
136
137
|
|
|
137
|
-
async function resolveAssessContext(
|
|
138
|
+
async function resolveAssessContext(
|
|
139
|
+
req: Request,
|
|
140
|
+
translate: AssessRouteContext['translate'],
|
|
141
|
+
): Promise<AssessRouteContext> {
|
|
138
142
|
const container = await createRequestContainer()
|
|
139
143
|
const auth = await getAuthFromRequest(req)
|
|
140
144
|
if (!auth || !auth.tenantId) {
|
|
141
|
-
throw new CrudHttpError(401, { error: 'warranty_claims.errors.unauthorized' })
|
|
145
|
+
throw new CrudHttpError(401, { error: translate('warranty_claims.errors.unauthorized', 'Unauthorized') })
|
|
142
146
|
}
|
|
143
147
|
const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })
|
|
144
148
|
const organizationId = scope?.selectedId ?? auth.orgId ?? null
|
|
145
149
|
if (!organizationId) {
|
|
146
|
-
throw new CrudHttpError(400, { error: 'warranty_claims.errors.organization_required' })
|
|
150
|
+
throw new CrudHttpError(400, { error: translate('warranty_claims.errors.organization_required', 'Organization context is required.') })
|
|
147
151
|
}
|
|
148
152
|
return {
|
|
149
153
|
ctx: {
|
|
@@ -158,7 +162,7 @@ async function resolveAssessContext(req: Request): Promise<AssessRouteContext> {
|
|
|
158
162
|
organizationId,
|
|
159
163
|
container,
|
|
160
164
|
em: container.resolve<EntityManager>('em').fork(),
|
|
161
|
-
translate
|
|
165
|
+
translate,
|
|
162
166
|
}
|
|
163
167
|
}
|
|
164
168
|
|
|
@@ -173,7 +177,7 @@ async function runGuard(
|
|
|
173
177
|
): Promise<RouteMutationGuardResult> {
|
|
174
178
|
const userId = context.ctx.auth?.sub
|
|
175
179
|
if (!userId) {
|
|
176
|
-
throw new CrudHttpError(401, { error: 'warranty_claims.errors.unauthorized' })
|
|
180
|
+
throw new CrudHttpError(401, { error: context.translate('warranty_claims.errors.unauthorized', 'Unauthorized') })
|
|
177
181
|
}
|
|
178
182
|
return runRouteMutationGuards({
|
|
179
183
|
container: context.ctx.container,
|
|
@@ -197,6 +201,7 @@ async function loadScopedLine(
|
|
|
197
201
|
scope: WarrantyClaimScope,
|
|
198
202
|
claimId: string,
|
|
199
203
|
lineId: string,
|
|
204
|
+
translate: AssessRouteContext['translate'],
|
|
200
205
|
): Promise<WarrantyClaimLine> {
|
|
201
206
|
const line = await findOneWithDecryption(
|
|
202
207
|
em,
|
|
@@ -212,7 +217,7 @@ async function loadScopedLine(
|
|
|
212
217
|
scope,
|
|
213
218
|
)
|
|
214
219
|
if (!line) {
|
|
215
|
-
throw new CrudHttpError(404, { error: 'warranty_claims.errors.notFound' })
|
|
220
|
+
throw new CrudHttpError(404, { error: translate('warranty_claims.errors.notFound', 'Claim not found.') })
|
|
216
221
|
}
|
|
217
222
|
return line
|
|
218
223
|
}
|
|
@@ -238,7 +243,7 @@ async function verifyAttachmentLinkedToTarget(
|
|
|
238
243
|
],
|
|
239
244
|
}) : false
|
|
240
245
|
if (!linked) {
|
|
241
|
-
throw new CrudHttpError(400, { error: 'warranty_claims.errors.attachmentNotLinked' })
|
|
246
|
+
throw new CrudHttpError(400, { error: context.translate('warranty_claims.errors.attachmentNotLinked', 'The attachment is not linked to this claim.') })
|
|
242
247
|
}
|
|
243
248
|
}
|
|
244
249
|
|
|
@@ -279,12 +284,13 @@ async function persistAssessmentPayload(
|
|
|
279
284
|
}
|
|
280
285
|
|
|
281
286
|
export async function POST(req: Request) {
|
|
287
|
+
const { translate } = await resolveTranslations()
|
|
282
288
|
try {
|
|
283
|
-
const context = await resolveAssessContext(req)
|
|
289
|
+
const context = await resolveAssessContext(req, translate)
|
|
284
290
|
const payload = toRecord(await readJsonSafe(req, {}))
|
|
285
291
|
const parsed = toAssessInput(payload, context)
|
|
286
292
|
if (parsed.kind === 'damage' && !parsed.lineId) {
|
|
287
|
-
throw new CrudHttpError(400, { error: 'warranty_claims.errors.invalidInput' })
|
|
293
|
+
throw new CrudHttpError(400, { error: translate('warranty_claims.errors.invalidInput', 'Invalid input') })
|
|
288
294
|
}
|
|
289
295
|
|
|
290
296
|
const guarded = parsed.lineId ? await runGuard(req, context, parsed as AssessBodyInput & { lineId: string }) : null
|
|
@@ -296,9 +302,9 @@ export async function POST(req: Request) {
|
|
|
296
302
|
|
|
297
303
|
if (input.kind === 'damage') {
|
|
298
304
|
if (!input.lineId) {
|
|
299
|
-
throw new CrudHttpError(400, { error: 'warranty_claims.errors.invalidInput' })
|
|
305
|
+
throw new CrudHttpError(400, { error: translate('warranty_claims.errors.invalidInput', 'Invalid input') })
|
|
300
306
|
}
|
|
301
|
-
const line = await loadScopedLine(context.em, scope, input.claimId, input.lineId)
|
|
307
|
+
const line = await loadScopedLine(context.em, scope, input.claimId, input.lineId, translate)
|
|
302
308
|
await verifyAttachmentLinkedToTarget(context, {
|
|
303
309
|
attachmentId: input.attachmentId,
|
|
304
310
|
claimId: input.claimId,
|
|
@@ -318,8 +324,8 @@ export async function POST(req: Request) {
|
|
|
318
324
|
return NextResponse.json({ status: 'ok', assessment })
|
|
319
325
|
}
|
|
320
326
|
|
|
321
|
-
await requireScopedClaim(context.em, input.claimId, scope)
|
|
322
|
-
const line = input.lineId ? await loadScopedLine(context.em, scope, input.claimId, input.lineId) : null
|
|
327
|
+
await requireScopedClaim(context.em, input.claimId, scope, {}, translate('warranty_claims.errors.notFound', 'Claim not found.'))
|
|
328
|
+
const line = input.lineId ? await loadScopedLine(context.em, scope, input.claimId, input.lineId, translate) : null
|
|
323
329
|
await verifyAttachmentLinkedToTarget(context, {
|
|
324
330
|
attachmentId: input.attachmentId,
|
|
325
331
|
claimId: input.claimId,
|
|
@@ -350,10 +356,10 @@ export async function POST(req: Request) {
|
|
|
350
356
|
return NextResponse.json({ status: 'aiUnavailable' })
|
|
351
357
|
}
|
|
352
358
|
if (err instanceof z.ZodError) {
|
|
353
|
-
return NextResponse.json({ error: 'warranty_claims.errors.invalidInput' }, { status: 400 })
|
|
359
|
+
return NextResponse.json({ error: translate('warranty_claims.errors.invalidInput', 'Invalid input') }, { status: 400 })
|
|
354
360
|
}
|
|
355
361
|
logger.error('warranty_claims.ai.assess.post failed', { err })
|
|
356
|
-
return NextResponse.json({ error: 'warranty_claims.errors.save_failed' }, { status: 500 })
|
|
362
|
+
return NextResponse.json({ error: translate('warranty_claims.errors.save_failed', 'Failed to save warranty claim.') }, { status: 500 })
|
|
357
363
|
}
|
|
358
364
|
}
|
|
359
365
|
|
|
@@ -7,6 +7,7 @@ import type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib
|
|
|
7
7
|
import { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
8
8
|
import { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'
|
|
9
9
|
import { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'
|
|
10
|
+
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
10
11
|
import { runRouteMutationGuards, type RouteMutationGuardResult } from '@open-mercato/shared/lib/crud/route-mutation-guard'
|
|
11
12
|
import { withScopedPayload } from '@open-mercato/shared/lib/api/scoped'
|
|
12
13
|
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
@@ -59,24 +60,23 @@ export const metadata = {
|
|
|
59
60
|
POST: { requireAuth: true, requireFeatures: ['warranty_claims.claim.manage', 'sales.credit_memos.manage'] },
|
|
60
61
|
}
|
|
61
62
|
|
|
62
|
-
function translateKey(key: string): string {
|
|
63
|
-
return key
|
|
64
|
-
}
|
|
65
|
-
|
|
66
63
|
function toRecord(value: unknown): Record<string, unknown> {
|
|
67
64
|
return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : {}
|
|
68
65
|
}
|
|
69
66
|
|
|
70
|
-
async function resolveActionContext(
|
|
67
|
+
async function resolveActionContext(
|
|
68
|
+
req: Request,
|
|
69
|
+
translate: ActionRouteContext['translate'],
|
|
70
|
+
): Promise<ActionRouteContext> {
|
|
71
71
|
const container = await createRequestContainer()
|
|
72
72
|
const auth = await getAuthFromRequest(req)
|
|
73
73
|
if (!auth || !auth.tenantId) {
|
|
74
|
-
throw new CrudHttpError(401, { error: 'warranty_claims.errors.unauthorized' })
|
|
74
|
+
throw new CrudHttpError(401, { error: translate('warranty_claims.errors.unauthorized', 'Unauthorized') })
|
|
75
75
|
}
|
|
76
76
|
const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })
|
|
77
77
|
const organizationId = scope?.selectedId ?? auth.orgId ?? null
|
|
78
78
|
if (!organizationId) {
|
|
79
|
-
throw new CrudHttpError(400, { error: 'warranty_claims.errors.organization_required' })
|
|
79
|
+
throw new CrudHttpError(400, { error: translate('warranty_claims.errors.organization_required', 'Organization context is required.') })
|
|
80
80
|
}
|
|
81
81
|
return {
|
|
82
82
|
ctx: {
|
|
@@ -89,7 +89,7 @@ async function resolveActionContext(req: Request): Promise<ActionRouteContext> {
|
|
|
89
89
|
},
|
|
90
90
|
tenantId: auth.tenantId,
|
|
91
91
|
organizationId,
|
|
92
|
-
translate
|
|
92
|
+
translate,
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
|
|
@@ -115,7 +115,7 @@ async function runGuard(
|
|
|
115
115
|
): Promise<RouteMutationGuardResult> {
|
|
116
116
|
const userId = context.ctx.auth?.sub
|
|
117
117
|
if (!userId) {
|
|
118
|
-
throw new CrudHttpError(401, { error: 'warranty_claims.errors.unauthorized' })
|
|
118
|
+
throw new CrudHttpError(401, { error: context.translate('warranty_claims.errors.unauthorized', 'Unauthorized') })
|
|
119
119
|
}
|
|
120
120
|
return runRouteMutationGuards({
|
|
121
121
|
container: context.ctx.container,
|
|
@@ -135,8 +135,9 @@ async function runGuard(
|
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
export async function POST(req: Request) {
|
|
138
|
+
const { translate } = await resolveTranslations()
|
|
138
139
|
try {
|
|
139
|
-
const context = await resolveActionContext(req)
|
|
140
|
+
const context = await resolveActionContext(req, translate)
|
|
140
141
|
const requestInput = toCreditMemoRequest(toRecord(await readJsonSafe(req, {})))
|
|
141
142
|
const guarded = await runGuard(req, context, requestInput)
|
|
142
143
|
if (!guarded.ok) {
|
|
@@ -150,7 +151,7 @@ export async function POST(req: Request) {
|
|
|
150
151
|
'warranty_claims.claim.create_credit_memo',
|
|
151
152
|
{ input: toCommandInput(guardedInput, context), ctx: context.ctx },
|
|
152
153
|
)
|
|
153
|
-
if (!result) throw new CrudHttpError(400, { error: 'warranty_claims.errors.save_failed' })
|
|
154
|
+
if (!result) throw new CrudHttpError(400, { error: translate('warranty_claims.errors.save_failed', 'Failed to save warranty claim.') })
|
|
154
155
|
await guarded.runAfterSuccess()
|
|
155
156
|
return NextResponse.json({
|
|
156
157
|
creditMemoId: result.creditMemoId,
|
|
@@ -165,10 +166,10 @@ export async function POST(req: Request) {
|
|
|
165
166
|
return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status })
|
|
166
167
|
}
|
|
167
168
|
if (err instanceof z.ZodError) {
|
|
168
|
-
return NextResponse.json({ error: 'warranty_claims.errors.invalidInput' }, { status: 400 })
|
|
169
|
+
return NextResponse.json({ error: translate('warranty_claims.errors.invalidInput', 'Invalid input') }, { status: 400 })
|
|
169
170
|
}
|
|
170
171
|
logger.error('warranty_claims.credit-memo.post failed', { err })
|
|
171
|
-
return NextResponse.json({ error: 'warranty_claims.errors.save_failed' }, { status: 500 })
|
|
172
|
+
return NextResponse.json({ error: translate('warranty_claims.errors.save_failed', 'Failed to save warranty claim.') }, { status: 500 })
|
|
172
173
|
}
|
|
173
174
|
}
|
|
174
175
|
|
|
@@ -131,7 +131,7 @@ export async function GET(req: Request) {
|
|
|
131
131
|
const context = await resolveEventContext(req)
|
|
132
132
|
const url = new URL(req.url)
|
|
133
133
|
const query = eventListQuerySchema.parse({ claimId: url.searchParams.get('claimId') ?? undefined })
|
|
134
|
-
const claim = await requireScopedClaim(context.em, query.claimId, context.scope)
|
|
134
|
+
const claim = await requireScopedClaim(context.em, query.claimId, context.scope, {}, context.translate('warranty_claims.errors.notFound', 'Claim not found.'))
|
|
135
135
|
const events = await findWithDecryption(
|
|
136
136
|
context.em,
|
|
137
137
|
WarrantyClaimEvent,
|
|
@@ -9,7 +9,7 @@ import { runRouteMutationGuards, type RouteMutationGuardResult } from '@open-mer
|
|
|
9
9
|
import { emitCrudSideEffects } from '@open-mercato/shared/lib/commands/helpers'
|
|
10
10
|
import { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
11
11
|
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
12
|
-
import {
|
|
12
|
+
import type { CustomerAuthContext } from '@open-mercato/core/modules/customer_accounts/lib/customerAuth'
|
|
13
13
|
import { Attachment, AttachmentPartition } from '@open-mercato/core/modules/attachments/data/entities'
|
|
14
14
|
import { buildAttachmentImageUrl, slugifyAttachmentFileName } from '@open-mercato/core/modules/attachments/lib/imageUrls'
|
|
15
15
|
import { StorageDriverFactory } from '@open-mercato/core/modules/attachments/lib/drivers'
|
|
@@ -28,6 +28,7 @@ import { WARRANTY_CLAIM_RESOURCE_KIND } from '../../../commands/shared'
|
|
|
28
28
|
import { CUSTOMER_VISIBLE_ATTACHMENT_TAG, isCustomerVisibleAttachment } from '../../../lib/attachmentVisibility'
|
|
29
29
|
import { loadPortalOwnedClaim } from '../../../lib/portalClaimAccess'
|
|
30
30
|
import { resolvePortalAttachmentUploadService } from '../../../lib/portalAttachmentUpload'
|
|
31
|
+
import { resolveLinkedCustomerAuth } from '../../../lib/portalAuthGuard'
|
|
31
32
|
|
|
32
33
|
const CLAIM_ATTACHMENT_ENTITY_ID = 'warranty_claims:warranty_claim'
|
|
33
34
|
const logger = createLogger('warranty_claims').child({ route: 'portal-attachments' })
|
|
@@ -80,13 +81,9 @@ function toIso(value: Date | string | null | undefined): string | null {
|
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
async function resolvePortalContext(req: Request): Promise<PortalContext | Response> {
|
|
83
|
-
const
|
|
84
|
-
if (
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
if (!auth.customerEntityId) {
|
|
88
|
-
return NextResponse.json({ ok: false, error: 'warranty_claims.errors.customerAccountNotLinked' }, { status: 403 })
|
|
89
|
-
}
|
|
84
|
+
const authOrResponse = await resolveLinkedCustomerAuth(req)
|
|
85
|
+
if (authOrResponse instanceof Response) return authOrResponse
|
|
86
|
+
const auth = authOrResponse
|
|
90
87
|
const container = await createRequestContainer()
|
|
91
88
|
const em = container.resolve('em') as EntityManager
|
|
92
89
|
return {
|
|
@@ -240,19 +237,23 @@ async function deletePortalAttachment(context: PortalContext, attachment: Attach
|
|
|
240
237
|
}
|
|
241
238
|
}
|
|
242
239
|
|
|
243
|
-
async function streamOwnedAttachment(
|
|
240
|
+
async function streamOwnedAttachment(
|
|
241
|
+
context: PortalContext,
|
|
242
|
+
attachmentId: string,
|
|
243
|
+
translate: (key: string, fallback?: string) => string,
|
|
244
|
+
): Promise<Response> {
|
|
244
245
|
const owned = await loadOwnedVisibleAttachment(context, attachmentId)
|
|
245
246
|
if (!owned) {
|
|
246
|
-
return NextResponse.json({ ok: false, error: 'warranty_claims.errors.notFound' }, { status: 404 })
|
|
247
|
+
return NextResponse.json({ ok: false, error: translate('warranty_claims.errors.notFound', 'Claim not found.') }, { status: 404 })
|
|
247
248
|
}
|
|
248
249
|
const { attachment } = owned
|
|
249
250
|
const partition = await context.em.findOne(AttachmentPartition, { code: attachment.partitionCode })
|
|
250
251
|
if (!partition) {
|
|
251
|
-
return NextResponse.json({ ok: false, error: 'warranty_claims.errors.load_failed' }, { status: 500 })
|
|
252
|
+
return NextResponse.json({ ok: false, error: translate('warranty_claims.errors.load_failed', 'Failed to load warranty claim data.') }, { status: 500 })
|
|
252
253
|
}
|
|
253
254
|
const access = checkAttachmentAccess(attachmentAuth(context), attachment, partition, { requireAuthForPublic: true })
|
|
254
255
|
if (!access.ok) {
|
|
255
|
-
return NextResponse.json({ ok: false, error: 'warranty_claims.errors.notFound' }, { status: 404 })
|
|
256
|
+
return NextResponse.json({ ok: false, error: translate('warranty_claims.errors.notFound', 'Claim not found.') }, { status: 404 })
|
|
256
257
|
}
|
|
257
258
|
const driver = await (await resolveStorageDriverFactory(context)).resolveForPartition(attachment.partitionCode, {
|
|
258
259
|
tenantId: attachment.tenantId ?? '',
|
|
@@ -262,7 +263,7 @@ async function streamOwnedAttachment(context: PortalContext, attachmentId: strin
|
|
|
262
263
|
try {
|
|
263
264
|
buffer = (await driver.read(attachment.partitionCode, attachment.storagePath)).buffer
|
|
264
265
|
} catch {
|
|
265
|
-
return NextResponse.json({ ok: false, error: 'warranty_claims.errors.notFound' }, { status: 404 })
|
|
266
|
+
return NextResponse.json({ ok: false, error: translate('warranty_claims.errors.notFound', 'Claim not found.') }, { status: 404 })
|
|
266
267
|
}
|
|
267
268
|
const headers: Record<string, string> = {
|
|
268
269
|
'Cache-Control': 'private, max-age=60',
|
|
@@ -279,14 +280,15 @@ export async function GET(req: Request) {
|
|
|
279
280
|
const contextOrResponse = await resolvePortalContext(req)
|
|
280
281
|
if (contextOrResponse instanceof Response) return contextOrResponse
|
|
281
282
|
const context = contextOrResponse
|
|
283
|
+
const { translate } = await resolveTranslations()
|
|
282
284
|
const url = new URL(req.url)
|
|
283
285
|
const attachmentId = url.searchParams.get('attachmentId')
|
|
284
286
|
if (attachmentId) {
|
|
285
287
|
const attachmentIdParsed = z.string().uuid().safeParse(attachmentId)
|
|
286
288
|
if (!attachmentIdParsed.success) {
|
|
287
|
-
return NextResponse.json({ ok: false, error: 'warranty_claims.errors.invalidInput' }, { status: 400 })
|
|
289
|
+
return NextResponse.json({ ok: false, error: translate('warranty_claims.errors.invalidInput', 'Invalid input') }, { status: 400 })
|
|
288
290
|
}
|
|
289
|
-
return streamOwnedAttachment(context, attachmentIdParsed.data)
|
|
291
|
+
return streamOwnedAttachment(context, attachmentIdParsed.data, translate)
|
|
290
292
|
}
|
|
291
293
|
const parsed = attachmentQuerySchema.safeParse({
|
|
292
294
|
claimId: url.searchParams.get('claimId') ?? undefined,
|
|
@@ -294,11 +296,11 @@ export async function GET(req: Request) {
|
|
|
294
296
|
pageSize: url.searchParams.get('pageSize') ?? undefined,
|
|
295
297
|
})
|
|
296
298
|
if (!parsed.success) {
|
|
297
|
-
return NextResponse.json({ ok: false, error: 'warranty_claims.errors.invalidInput' }, { status: 400 })
|
|
299
|
+
return NextResponse.json({ ok: false, error: translate('warranty_claims.errors.invalidInput', 'Invalid input') }, { status: 400 })
|
|
298
300
|
}
|
|
299
301
|
const claim = await loadOwnedClaim(context, parsed.data.claimId)
|
|
300
302
|
if (!claim) {
|
|
301
|
-
return NextResponse.json({ ok: false, error: 'warranty_claims.errors.notFound' }, { status: 404 })
|
|
303
|
+
return NextResponse.json({ ok: false, error: translate('warranty_claims.errors.notFound', 'Claim not found.') }, { status: 404 })
|
|
302
304
|
}
|
|
303
305
|
const filter = {
|
|
304
306
|
entityId: CLAIM_ATTACHMENT_ENTITY_ID,
|
|
@@ -343,12 +345,12 @@ async function handleUpload(req: Request, replacement: boolean): Promise<Respons
|
|
|
343
345
|
const contextOrResponse = await resolvePortalContext(req)
|
|
344
346
|
if (contextOrResponse instanceof Response) return contextOrResponse
|
|
345
347
|
const context = contextOrResponse
|
|
348
|
+
const { t, translate } = await resolveTranslations()
|
|
346
349
|
const contentType = req.headers.get('content-type') || ''
|
|
347
350
|
if (!contentType.toLowerCase().includes('multipart/form-data')) {
|
|
348
|
-
return NextResponse.json({ ok: false, error: 'warranty_claims.errors.invalidInput' }, { status: 400 })
|
|
351
|
+
return NextResponse.json({ ok: false, error: translate('warranty_claims.errors.invalidInput', 'Invalid input') }, { status: 400 })
|
|
349
352
|
}
|
|
350
353
|
if (!isMultipartRequestWithinUploadLimit(req.headers.get('content-length'))) {
|
|
351
|
-
const { t } = await resolveTranslations()
|
|
352
354
|
return NextResponse.json({
|
|
353
355
|
ok: false,
|
|
354
356
|
error: t('attachments.errors.maxUploadSize', 'Attachment exceeds the maximum upload size.'),
|
|
@@ -364,15 +366,15 @@ async function handleUpload(req: Request, replacement: boolean): Promise<Respons
|
|
|
364
366
|
? z.string().uuid().safeParse(form.get('attachmentId') ?? undefined)
|
|
365
367
|
: null
|
|
366
368
|
if (!parsed.success || (replacement && !parsedAttachmentId?.success)) {
|
|
367
|
-
return NextResponse.json({ ok: false, error: 'warranty_claims.errors.invalidInput' }, { status: 400 })
|
|
369
|
+
return NextResponse.json({ ok: false, error: translate('warranty_claims.errors.invalidInput', 'Invalid input') }, { status: 400 })
|
|
368
370
|
}
|
|
369
371
|
const claim = await loadOwnedClaim(context, parsed.data.claimId)
|
|
370
372
|
if (!claim) {
|
|
371
|
-
return NextResponse.json({ ok: false, error: 'warranty_claims.errors.notFound' }, { status: 404 })
|
|
373
|
+
return NextResponse.json({ ok: false, error: translate('warranty_claims.errors.notFound', 'Claim not found.') }, { status: 404 })
|
|
372
374
|
}
|
|
373
375
|
const file = form.get('file')
|
|
374
376
|
if (!(file instanceof File)) {
|
|
375
|
-
return NextResponse.json({ ok: false, error: 'warranty_claims.portal.detail.fileRequired' }, { status: 400 })
|
|
377
|
+
return NextResponse.json({ ok: false, error: translate('warranty_claims.portal.detail.fileRequired', 'Choose a file before uploading.') }, { status: 400 })
|
|
376
378
|
}
|
|
377
379
|
const buffer = Buffer.from(await file.arrayBuffer())
|
|
378
380
|
|
|
@@ -381,7 +383,7 @@ async function handleUpload(req: Request, replacement: boolean): Promise<Respons
|
|
|
381
383
|
? await loadOwnedVisibleAttachment(context, attachmentId)
|
|
382
384
|
: null
|
|
383
385
|
if (replacement && (!replacementTarget || replacementTarget.claim.id !== claim.id)) {
|
|
384
|
-
return NextResponse.json({ ok: false, error: 'warranty_claims.errors.notFound' }, { status: 404 })
|
|
386
|
+
return NextResponse.json({ ok: false, error: translate('warranty_claims.errors.notFound', 'Claim not found.') }, { status: 404 })
|
|
385
387
|
}
|
|
386
388
|
|
|
387
389
|
const guarded = await runPortalAttachmentGuard(req, context, claim.id, replacement ? 'update' : 'create', {
|
|
@@ -397,7 +399,7 @@ async function handleUpload(req: Request, replacement: boolean): Promise<Respons
|
|
|
397
399
|
|
|
398
400
|
const uploadService = resolvePortalAttachmentUploadService(context.container)
|
|
399
401
|
if (!uploadService) {
|
|
400
|
-
return NextResponse.json({ ok: false, error: 'warranty_claims.portal.detail.attachmentError' }, { status: 503 })
|
|
402
|
+
return NextResponse.json({ ok: false, error: translate('warranty_claims.portal.detail.attachmentError', 'Attachment upload failed.') }, { status: 503 })
|
|
401
403
|
}
|
|
402
404
|
|
|
403
405
|
let attachment: Attachment
|
|
@@ -414,7 +416,6 @@ async function handleUpload(req: Request, replacement: boolean): Promise<Respons
|
|
|
414
416
|
})
|
|
415
417
|
} catch (error) {
|
|
416
418
|
if (isScopedAttachmentUploadError(error)) {
|
|
417
|
-
const { t } = await resolveTranslations()
|
|
418
419
|
const [key, fallback] = ATTACHMENT_ERROR_TRANSLATIONS[error.code]
|
|
419
420
|
?? ['warranty_claims.portal.detail.attachmentError', 'Attachment upload failed.']
|
|
420
421
|
return NextResponse.json(
|
|
@@ -451,16 +452,17 @@ export async function DELETE(req: Request) {
|
|
|
451
452
|
const contextOrResponse = await resolvePortalContext(req)
|
|
452
453
|
if (contextOrResponse instanceof Response) return contextOrResponse
|
|
453
454
|
const context = contextOrResponse
|
|
455
|
+
const { translate } = await resolveTranslations()
|
|
454
456
|
const url = new URL(req.url)
|
|
455
457
|
const parsed = deleteQuerySchema.safeParse({
|
|
456
458
|
attachmentId: url.searchParams.get('attachmentId') ?? undefined,
|
|
457
459
|
})
|
|
458
460
|
if (!parsed.success) {
|
|
459
|
-
return NextResponse.json({ ok: false, error: 'warranty_claims.errors.invalidInput' }, { status: 400 })
|
|
461
|
+
return NextResponse.json({ ok: false, error: translate('warranty_claims.errors.invalidInput', 'Invalid input') }, { status: 400 })
|
|
460
462
|
}
|
|
461
463
|
const owned = await loadOwnedVisibleAttachment(context, parsed.data.attachmentId)
|
|
462
464
|
if (!owned) {
|
|
463
|
-
return NextResponse.json({ ok: false, error: 'warranty_claims.errors.notFound' }, { status: 404 })
|
|
465
|
+
return NextResponse.json({ ok: false, error: translate('warranty_claims.errors.notFound', 'Claim not found.') }, { status: 404 })
|
|
464
466
|
}
|
|
465
467
|
const guarded = await runPortalAttachmentGuard(req, context, owned.claim.id, 'delete', {
|
|
466
468
|
claimId: owned.claim.id,
|
|
@@ -3,10 +3,12 @@ import { z } from 'zod'
|
|
|
3
3
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
4
4
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
5
5
|
import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
6
|
-
import {
|
|
6
|
+
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
7
|
+
import type { CustomerAuthContext } from '@open-mercato/core/modules/customer_accounts/lib/customerAuth'
|
|
7
8
|
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
8
9
|
import { WarrantyClaim, WarrantyClaimLine } from '../../../../data/entities'
|
|
9
10
|
import { loadPortalOwnedClaim } from '../../../../lib/portalClaimAccess'
|
|
11
|
+
import { resolveLinkedCustomerAuth } from '../../../../lib/portalAuthGuard'
|
|
10
12
|
|
|
11
13
|
export const metadata = {
|
|
12
14
|
GET: { requireAuth: false },
|
|
@@ -40,13 +42,9 @@ async function resolveClaimId(ctx: RouteContext): Promise<string | null> {
|
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
async function resolvePortalContext(req: Request): Promise<PortalContext | Response> {
|
|
43
|
-
const
|
|
44
|
-
if (
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
if (!auth.customerEntityId) {
|
|
48
|
-
return NextResponse.json({ ok: false, error: 'warranty_claims.errors.customerAccountNotLinked' }, { status: 403 })
|
|
49
|
-
}
|
|
45
|
+
const authOrResponse = await resolveLinkedCustomerAuth(req)
|
|
46
|
+
if (authOrResponse instanceof Response) return authOrResponse
|
|
47
|
+
const auth = authOrResponse
|
|
50
48
|
const container = await createRequestContainer()
|
|
51
49
|
const em = container.resolve('em') as EntityManager
|
|
52
50
|
return {
|
|
@@ -108,9 +106,10 @@ export async function GET(req: Request, ctx: RouteContext) {
|
|
|
108
106
|
const contextOrResponse = await resolvePortalContext(req)
|
|
109
107
|
if (contextOrResponse instanceof Response) return contextOrResponse
|
|
110
108
|
const context = contextOrResponse
|
|
109
|
+
const { translate } = await resolveTranslations()
|
|
111
110
|
const claimId = await resolveClaimId(ctx)
|
|
112
111
|
if (!claimId) {
|
|
113
|
-
return NextResponse.json({ ok: false, error: 'warranty_claims.errors.notFound' }, { status: 404 })
|
|
112
|
+
return NextResponse.json({ ok: false, error: translate('warranty_claims.errors.notFound', 'Claim not found.') }, { status: 404 })
|
|
114
113
|
}
|
|
115
114
|
const scope = { tenantId: context.tenantId, organizationId: context.organizationId }
|
|
116
115
|
const claim = await loadPortalOwnedClaim(
|
|
@@ -123,7 +122,7 @@ export async function GET(req: Request, ctx: RouteContext) {
|
|
|
123
122
|
claimId,
|
|
124
123
|
)
|
|
125
124
|
if (!claim) {
|
|
126
|
-
return NextResponse.json({ ok: false, error: 'warranty_claims.errors.notFound' }, { status: 404 })
|
|
125
|
+
return NextResponse.json({ ok: false, error: translate('warranty_claims.errors.notFound', 'Claim not found.') }, { status: 404 })
|
|
127
126
|
}
|
|
128
127
|
const lines = await findWithDecryption(
|
|
129
128
|
context.em,
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { NextResponse } from 'next/server'
|
|
2
1
|
import { z } from 'zod'
|
|
3
2
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
4
3
|
import type { AuthContext } from '@open-mercato/shared/lib/auth/server'
|
|
5
4
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
6
5
|
import type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
|
|
7
6
|
import { runRouteMutationGuards, type RouteMutationGuardResult } from '@open-mercato/shared/lib/crud/route-mutation-guard'
|
|
8
|
-
import {
|
|
7
|
+
import type { CustomerAuthContext } from '@open-mercato/core/modules/customer_accounts/lib/customerAuth'
|
|
9
8
|
import { WarrantyClaim } from '../../../../data/entities'
|
|
10
9
|
import { WARRANTY_CLAIM_RESOURCE_KIND } from '../../../../commands/shared'
|
|
11
10
|
import { loadPortalOwnedClaim } from '../../../../lib/portalClaimAccess'
|
|
11
|
+
import { resolveLinkedCustomerAuth } from '../../../../lib/portalAuthGuard'
|
|
12
12
|
|
|
13
13
|
export type PortalClaimActionRouteContext = { params: Promise<{ id: string }> }
|
|
14
14
|
|
|
@@ -34,13 +34,9 @@ export async function resolvePortalClaimId(ctx: PortalClaimActionRouteContext):
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
export async function resolvePortalActionContext(req: Request): Promise<PortalClaimActionContext | Response> {
|
|
37
|
-
const
|
|
38
|
-
if (
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
if (!auth.customerEntityId) {
|
|
42
|
-
return NextResponse.json({ ok: false, error: 'warranty_claims.errors.customerAccountNotLinked' }, { status: 403 })
|
|
43
|
-
}
|
|
37
|
+
const authOrResponse = await resolveLinkedCustomerAuth(req)
|
|
38
|
+
if (authOrResponse instanceof Response) return authOrResponse
|
|
39
|
+
const auth = authOrResponse
|
|
44
40
|
const container = await createRequestContainer()
|
|
45
41
|
const em = container.resolve('em') as EntityManager
|
|
46
42
|
const commandAuth: NonNullable<AuthContext> = {
|