@open-mercato/core 0.10.1-develop.7833.1.d6cbf0e3ab → 0.10.1-develop.7841.1.af87ac614d
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/modules/cart/api/current/lines/bulk/route.js +1 -1
- package/dist/modules/cart/api/current/lines/bulk/route.js.map +2 -2
- package/dist/modules/cart/api/current/lines/route.js +1 -1
- package/dist/modules/cart/api/current/lines/route.js.map +2 -2
- package/dist/modules/cart/commands/lines.js +5 -10
- package/dist/modules/cart/commands/lines.js.map +2 -2
- package/dist/modules/cart/lib/currentCartRoute.js +8 -0
- package/dist/modules/cart/lib/currentCartRoute.js.map +3 -3
- package/dist/modules/cart/lib/lineMutationLock.js +52 -0
- package/dist/modules/cart/lib/lineMutationLock.js.map +7 -0
- package/dist/modules/customer_accounts/api/admin/roles.js +25 -2
- package/dist/modules/customer_accounts/api/admin/roles.js.map +2 -2
- package/dist/modules/customer_accounts/api/admin/users.js +24 -7
- package/dist/modules/customer_accounts/api/admin/users.js.map +2 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/users/PortalUsersPageClient.js +77 -15
- package/dist/modules/customer_accounts/backend/customer_accounts/users/PortalUsersPageClient.js.map +2 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/users/page.js +12 -1
- package/dist/modules/customer_accounts/backend/customer_accounts/users/page.js.map +2 -2
- package/dist/modules/customer_accounts/data/validators.js +2 -1
- package/dist/modules/customer_accounts/data/validators.js.map +2 -2
- package/dist/modules/customer_accounts/lib/adminOrganizationScope.js +46 -0
- package/dist/modules/customer_accounts/lib/adminOrganizationScope.js.map +7 -0
- package/dist/modules/customers/api/customer-dictionary-entries/route.js +86 -0
- package/dist/modules/customers/api/customer-dictionary-entries/route.js.map +7 -0
- package/dist/modules/customers/api/dictionaries/[kind]/route.js +29 -7
- package/dist/modules/customers/api/dictionaries/[kind]/route.js.map +2 -2
- package/dist/modules/customers/cli.js +28 -110
- package/dist/modules/customers/cli.js.map +3 -3
- package/dist/modules/customers/components/DictionarySettings.js +1 -1
- package/dist/modules/customers/components/DictionarySettings.js.map +1 -1
- package/dist/modules/customers/components/detail/ManageTagsDialog.js +1 -1
- package/dist/modules/customers/components/detail/ManageTagsDialog.js.map +2 -2
- package/dist/modules/customers/components/detail/TaskDialog.js +14 -1
- package/dist/modules/customers/components/detail/TaskDialog.js.map +2 -2
- package/dist/modules/customers/components/detail/TaskForm.js +1 -16
- package/dist/modules/customers/components/detail/TaskForm.js.map +2 -2
- package/dist/modules/customers/components/detail/TasksSection.js +38 -10
- package/dist/modules/customers/components/detail/TasksSection.js.map +2 -2
- package/dist/modules/customers/components/detail/hooks/useCustomerDictionary.js +12 -5
- package/dist/modules/customers/components/detail/hooks/useCustomerDictionary.js.map +2 -2
- package/dist/modules/customers/components/detail/hooks/useInteractions.js +3 -13
- package/dist/modules/customers/components/detail/hooks/useInteractions.js.map +2 -2
- package/dist/modules/customers/lib/dictionaryDefaults.js +134 -0
- package/dist/modules/customers/lib/dictionaryDefaults.js.map +7 -0
- package/dist/modules/customers/lib/dictionaryLabels.js +59 -0
- package/dist/modules/customers/lib/dictionaryLabels.js.map +7 -0
- package/dist/modules/customers/lib/interactionCompatibility.js +1 -0
- package/dist/modules/customers/lib/interactionCompatibility.js.map +2 -2
- package/dist/modules/customers/translations.js +9 -0
- package/dist/modules/customers/translations.js.map +7 -0
- package/dist/modules/eudr/index.js +13 -0
- package/dist/modules/eudr/index.js.map +2 -2
- package/dist/modules/sales/commands/documents.js +3 -3
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/dist/modules/sales/components/documents/ItemsSection.js +75 -21
- package/dist/modules/sales/components/documents/ItemsSection.js.map +2 -2
- package/dist/modules/sales/components/documents/lineItemUtils.js +2 -2
- package/dist/modules/sales/components/documents/lineItemUtils.js.map +2 -2
- package/dist/modules/staff/api/timesheets/access-requests/route.js +11 -5
- package/dist/modules/staff/api/timesheets/access-requests/route.js.map +2 -2
- package/dist/modules/staff/api/timesheets/time-projects/route.js +19 -12
- package/dist/modules/staff/api/timesheets/time-projects/route.js.map +2 -2
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.js +21 -12
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js +14 -9
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/NoProjectAccess.js +31 -10
- package/dist/modules/staff/lib/time-tracking-ui/NoProjectAccess.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/projectAccessScope.js +64 -0
- package/dist/modules/staff/lib/time-tracking-ui/projectAccessScope.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/projectAccessScopeHeaders.js +13 -0
- package/dist/modules/staff/lib/time-tracking-ui/projectAccessScopeHeaders.js.map +7 -0
- package/dist/modules/sync_excel/index.js +11 -1
- package/dist/modules/sync_excel/index.js.map +2 -2
- package/dist/modules/translations/di.js +2 -0
- package/dist/modules/translations/di.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/cart/api/current/lines/bulk/route.ts +1 -1
- package/src/modules/cart/api/current/lines/route.ts +1 -1
- package/src/modules/cart/commands/lines.ts +5 -10
- package/src/modules/cart/i18n/de.json +1 -0
- package/src/modules/cart/i18n/en.json +1 -0
- package/src/modules/cart/i18n/es.json +1 -0
- package/src/modules/cart/i18n/ko.json +1 -0
- package/src/modules/cart/i18n/pl.json +1 -0
- package/src/modules/cart/lib/currentCartRoute.ts +8 -0
- package/src/modules/cart/lib/lineMutationLock.ts +59 -0
- package/src/modules/customer_accounts/api/admin/roles.ts +26 -2
- package/src/modules/customer_accounts/api/admin/users.ts +26 -7
- package/src/modules/customer_accounts/backend/customer_accounts/users/PortalUsersPageClient.tsx +96 -13
- package/src/modules/customer_accounts/backend/customer_accounts/users/page.tsx +9 -1
- package/src/modules/customer_accounts/data/validators.ts +1 -0
- package/src/modules/customer_accounts/i18n/de.json +10 -0
- package/src/modules/customer_accounts/i18n/en.json +10 -0
- package/src/modules/customer_accounts/i18n/es.json +10 -0
- package/src/modules/customer_accounts/i18n/ko.json +10 -0
- package/src/modules/customer_accounts/i18n/pl.json +10 -0
- package/src/modules/customer_accounts/lib/adminOrganizationScope.ts +71 -0
- package/src/modules/customers/api/customer-dictionary-entries/route.ts +82 -0
- package/src/modules/customers/api/dictionaries/[kind]/route.ts +29 -6
- package/src/modules/customers/cli.ts +23 -124
- package/src/modules/customers/components/DictionarySettings.tsx +1 -1
- package/src/modules/customers/components/detail/ManageTagsDialog.tsx +1 -1
- package/src/modules/customers/components/detail/TaskDialog.tsx +15 -1
- package/src/modules/customers/components/detail/TaskForm.tsx +1 -17
- package/src/modules/customers/components/detail/TasksSection.tsx +44 -14
- package/src/modules/customers/components/detail/hooks/useCustomerDictionary.ts +14 -8
- package/src/modules/customers/components/detail/hooks/useInteractions.ts +3 -4
- package/src/modules/customers/i18n/de.json +81 -0
- package/src/modules/customers/i18n/en.json +81 -0
- package/src/modules/customers/i18n/es.json +81 -0
- package/src/modules/customers/i18n/ko.json +81 -0
- package/src/modules/customers/i18n/pl.json +81 -0
- package/src/modules/customers/lib/dictionaryDefaults.ts +137 -0
- package/src/modules/customers/lib/dictionaryLabels.ts +73 -0
- package/src/modules/customers/lib/interactionCompatibility.ts +1 -0
- package/src/modules/customers/translations.ts +5 -0
- package/src/modules/eudr/index.ts +13 -0
- package/src/modules/sales/commands/documents.ts +3 -3
- package/src/modules/sales/components/documents/ItemsSection.tsx +82 -23
- package/src/modules/sales/components/documents/lineItemUtils.ts +2 -2
- package/src/modules/sales/i18n/de.json +1 -0
- package/src/modules/sales/i18n/en.json +1 -0
- package/src/modules/sales/i18n/es.json +1 -0
- package/src/modules/sales/i18n/ko.json +1 -0
- package/src/modules/sales/i18n/pl.json +1 -0
- package/src/modules/staff/api/timesheets/access-requests/route.ts +13 -14
- package/src/modules/staff/api/timesheets/time-projects/route.ts +22 -13
- package/src/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.tsx +25 -14
- package/src/modules/staff/backend/staff/time-tracking/projects/[id]/page.tsx +14 -13
- package/src/modules/staff/lib/time-tracking-ui/NoProjectAccess.tsx +30 -8
- package/src/modules/staff/lib/time-tracking-ui/projectAccessScope.ts +102 -0
- package/src/modules/staff/lib/time-tracking-ui/projectAccessScopeHeaders.ts +15 -0
- package/src/modules/sync_excel/index.ts +10 -0
- package/src/modules/translations/di.ts +2 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -130,7 +130,7 @@ const openApi = {
|
|
|
130
130
|
{ status: 409, description: "Optimistic-lock conflict" },
|
|
131
131
|
{ status: 422, description: "Idempotency key mismatch" },
|
|
132
132
|
{ status: 429, description: "Rate limited" },
|
|
133
|
-
{ status: 503, description: "No store context resolvable for
|
|
133
|
+
{ status: 503, description: "No store context resolvable, or cart_busy after 15 seconds waiting for the line lock. The requested line add/bulk-add phases did not run; earlier activity/context/repricing may have persisted. Retry the same request after Retry-After: 1 second, preserving Idempotency-Key." }
|
|
134
134
|
]
|
|
135
135
|
}
|
|
136
136
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../src/modules/cart/api/current/lines/bulk/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 type { CommandBus } from '@open-mercato/shared/lib/commands'\nimport { enforceCommandOptimisticLock } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport { runRouteMutationGuards } from '@open-mercato/shared/lib/crud/route-mutation-guard'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\n\nimport { bulkAddLinesCommand } from '../../../../commands/lines'\nimport type { CartLinesBulkAddEntryOutcome, CartLinesBulkAddInput, CartLinesBulkAddResult } from '../../../../commands/lines'\nimport { cartLineBulkAddRequestSchema } from '../../../../data/validators'\nimport {\n CART_RESOURCE_KIND,\n buildCommandContext,\n mapRouteErrorToResponse,\n notFoundCartResponse,\n readCartToken,\n resolveCurrentCart,\n resolveRouteContextOrError,\n} from '../../../../lib/currentCartRoute'\nimport { buildCartResponseEnvelope } from '../../../../lib/cartView'\nimport type { CartViewWarning } from '../../../../lib/cartView'\nimport { resolveCartWarningMessage } from '../../../../lib/warningMessages'\nimport {\n computeRequestDigest,\n findIdempotencyReplay,\n readIdempotencyKey,\n recordIdempotencyResponse,\n} from '../../../../lib/idempotency'\nimport { enforceCartRateLimit } from '../../../../lib/rateLimit'\n\n// Step 4.7 \u2014 `POST /current/lines/bulk` \u2192 `cart.lines.bulkAdd`. \u00A710: \"exists\n// because B2B quick-order pads and agent proposals add 50 lines at once ... 50\n// sequential mutations means 50 ... version conflicts\" \u2014 a single call therefore\n// carries ONE optimistic-lock check and ONE idempotency record for the whole\n// batch, not one per entry; per-entry outcomes (including per-entry rejections)\n// live inside the response body's `outcome.results` array.\nexport const metadata = {\n POST: { requireAuth: false },\n}\n\n/**\n * Always `200`: a batch call's HTTP status reports whether the REQUEST was\n * processed, not whether every entry was accepted \u2014 that is exactly what\n * `addedCount`/`rejectedCount` and each entry's own `status`/`reason` already\n * encode (\u00A715a has no bulk-specific status; per-entry statuses still follow the\n * same `product_unavailable` = warning / quantity = hard-stop vocabulary the\n * single-line route uses, surfaced in the body rather than the top-level status\n * code since a single HTTP status cannot represent N independent outcomes).\n */\nconst BULK_ADD_STATUS = 200\n\nexport async function POST(request: Request) {\n try {\n const container = await createRequestContainer()\n const token = readCartToken(request)\n\n const limited = await enforceCartRateLimit(container, request, 'mutate', token)\n if (limited) return limited\n\n const resolved = await resolveRouteContextOrError(request, container)\n if (!resolved.ok) return resolved.response\n if (!token) return notFoundCartResponse()\n\n const commandBus = container.resolve('commandBus') as CommandBus\n const ctx = buildCommandContext(container, request, resolved.routeContext)\n const resolvedCart = await resolveCurrentCart(commandBus, ctx, resolved.routeContext, token)\n if (!resolvedCart) return notFoundCartResponse()\n const { cart, priceChanges: contextPriceChanges, versionBumpReason: contextVersionBumpReason } = resolvedCart\n\n const rawBody = (await readJsonSafe<Record<string, unknown>>(request, {})) ?? {}\n let body: z.infer<typeof cartLineBulkAddRequestSchema>\n try {\n body = cartLineBulkAddRequestSchema.parse(rawBody)\n } catch (err) {\n if (err instanceof z.ZodError) {\n return NextResponse.json({ error: '[internal] invalid request body', issues: err.issues }, { status: 400 })\n }\n throw err\n }\n\n const scope = { tenantId: resolved.routeContext.tenantId, organizationId: resolved.routeContext.organizationId }\n const idempotencyKey = readIdempotencyKey(request)\n const requestDigest = computeRequestDigest(body)\n const em = (container.resolve('em') as EntityManager).fork()\n\n // \u00A78.2 ordering \u2014 see `lines/route.ts`'s identical note.\n if (idempotencyKey) {\n const replay = await findIdempotencyReplay(em, scope, cart.id, idempotencyKey, requestDigest)\n if (replay) return NextResponse.json(replay.body, { status: replay.status })\n }\n\n const guarded = await runRouteMutationGuards({\n container,\n req: request,\n auth: {\n userId: resolved.routeContext.customerUserId ?? 'anonymous',\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userFeatures: [],\n },\n input: { resourceKind: CART_RESOURCE_KIND, resourceId: cart.id, operation: 'create', mutationPayload: body },\n })\n if (!guarded.ok) return guarded.response\n\n enforceCommandOptimisticLock({\n resourceKind: CART_RESOURCE_KIND,\n resourceId: cart.id,\n current: cart.updatedAt,\n request,\n })\n\n const commandInput: CartLinesBulkAddInput = {\n routeContext: resolved.routeContext,\n cartId: cart.id,\n lines: body.lines,\n }\n const { result } = await commandBus.execute<CartLinesBulkAddInput, CartLinesBulkAddResult>(bulkAddLinesCommand.id, {\n input: commandInput,\n ctx,\n })\n\n await guarded.runAfterSuccess()\n\n // Step 5.1 \u2014 same reasoning as the single-line add route: `cart.lines.bulkAdd` never\n // itself produces a repricing side effect, so `priceChanges`/`versionBumpReason` here\n // can only come from the `resolveCurrentCart`-embedded \u00A75.2 trigger 2-4 check above.\n //\n // Step 5.2 \u2014 \u00A710.1's generic envelope has no slot for \"up to 100 independent per-entry\n // dispositions\"; collapsing `outcome.results` into the handful of top-level `warnings`\n // the envelope DOES support would discard information no batch-endpoint client can do\n // without (which of the 100 entries succeeded, which were rejected, and why). `outcome`\n // is therefore kept as a deliberate, documented ADDITION alongside the mandated `cart`\n // envelope \u2014 not a replacement \u2014 for this one route only. Only `added`/`merged` entries\n // contribute a top-level `quantity_capped` warning (one per affected line); a rejected\n // entry's reason is already fully disclosed in `outcome.results[index]`, so it is not\n // duplicated as a lineless top-level warning the way the single-line route's rejected-\n // add case needs to be (there, `outcome` itself is dropped for the warning-level case).\n const { translate } = await resolveTranslations()\n const warnings: CartViewWarning[] = result.results\n .filter((entry): entry is Extract<CartLinesBulkAddEntryOutcome, { status: 'added' | 'merged' }> => entry.status === 'added' || entry.status === 'merged')\n .flatMap((entry) =>\n entry.warnings.map((code) => ({\n code,\n lineId: entry.line.id,\n message: resolveCartWarningMessage('quantity_capped', translate),\n })),\n )\n const envelope = await buildCartResponseEnvelope({\n container,\n cartId: cart.id,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n priceChanges: contextPriceChanges,\n versionBumpReason: contextVersionBumpReason,\n warnings,\n })\n const responseBody: Record<string, unknown> = { ...envelope, outcome: result }\n if (idempotencyKey) {\n await recordIdempotencyResponse(em, scope, cart.id, idempotencyKey, requestDigest, BULK_ADD_STATUS, responseBody)\n }\n return NextResponse.json(responseBody, { status: BULK_ADD_STATUS })\n } catch (error) {\n return mapRouteErrorToResponse(error)\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Cart',\n summary: 'Batch-add lines to the current cart',\n methods: {\n POST: {\n summary: 'Add up to 100 product/variant lines in one call',\n description:\n 'One optimistic-lock check and one `Idempotency-Key` record for the whole batch; each entry\\'s own outcome (added/merged/rejected) is reported in `outcome.results`.',\n requestBody: { contentType: 'application/json', schema: cartLineBulkAddRequestSchema },\n responses: [{ status: 200, description: 'Batch processed \u2014 see `outcome.results` for per-entry outcomes' }],\n errors: [\n { status: 400, description: 'Invalid request body, or more than 100 entries' },\n { status: 404, description: 'No `X-Cart-Token` header, or the token does not resolve a cart' },\n { status: 409, description: 'Optimistic-lock conflict' },\n { status: 422, description: 'Idempotency key mismatch' },\n { status: 429, description: 'Rate limited' },\n { status: 503, description: 'No store context resolvable for
|
|
5
|
-
"mappings": "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
|
|
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 type { CommandBus } from '@open-mercato/shared/lib/commands'\nimport { enforceCommandOptimisticLock } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport { runRouteMutationGuards } from '@open-mercato/shared/lib/crud/route-mutation-guard'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\n\nimport { bulkAddLinesCommand } from '../../../../commands/lines'\nimport type { CartLinesBulkAddEntryOutcome, CartLinesBulkAddInput, CartLinesBulkAddResult } from '../../../../commands/lines'\nimport { cartLineBulkAddRequestSchema } from '../../../../data/validators'\nimport {\n CART_RESOURCE_KIND,\n buildCommandContext,\n mapRouteErrorToResponse,\n notFoundCartResponse,\n readCartToken,\n resolveCurrentCart,\n resolveRouteContextOrError,\n} from '../../../../lib/currentCartRoute'\nimport { buildCartResponseEnvelope } from '../../../../lib/cartView'\nimport type { CartViewWarning } from '../../../../lib/cartView'\nimport { resolveCartWarningMessage } from '../../../../lib/warningMessages'\nimport {\n computeRequestDigest,\n findIdempotencyReplay,\n readIdempotencyKey,\n recordIdempotencyResponse,\n} from '../../../../lib/idempotency'\nimport { enforceCartRateLimit } from '../../../../lib/rateLimit'\n\n// Step 4.7 \u2014 `POST /current/lines/bulk` \u2192 `cart.lines.bulkAdd`. \u00A710: \"exists\n// because B2B quick-order pads and agent proposals add 50 lines at once ... 50\n// sequential mutations means 50 ... version conflicts\" \u2014 a single call therefore\n// carries ONE optimistic-lock check and ONE idempotency record for the whole\n// batch, not one per entry; per-entry outcomes (including per-entry rejections)\n// live inside the response body's `outcome.results` array.\nexport const metadata = {\n POST: { requireAuth: false },\n}\n\n/**\n * Always `200`: a batch call's HTTP status reports whether the REQUEST was\n * processed, not whether every entry was accepted \u2014 that is exactly what\n * `addedCount`/`rejectedCount` and each entry's own `status`/`reason` already\n * encode (\u00A715a has no bulk-specific status; per-entry statuses still follow the\n * same `product_unavailable` = warning / quantity = hard-stop vocabulary the\n * single-line route uses, surfaced in the body rather than the top-level status\n * code since a single HTTP status cannot represent N independent outcomes).\n */\nconst BULK_ADD_STATUS = 200\n\nexport async function POST(request: Request) {\n try {\n const container = await createRequestContainer()\n const token = readCartToken(request)\n\n const limited = await enforceCartRateLimit(container, request, 'mutate', token)\n if (limited) return limited\n\n const resolved = await resolveRouteContextOrError(request, container)\n if (!resolved.ok) return resolved.response\n if (!token) return notFoundCartResponse()\n\n const commandBus = container.resolve('commandBus') as CommandBus\n const ctx = buildCommandContext(container, request, resolved.routeContext)\n const resolvedCart = await resolveCurrentCart(commandBus, ctx, resolved.routeContext, token)\n if (!resolvedCart) return notFoundCartResponse()\n const { cart, priceChanges: contextPriceChanges, versionBumpReason: contextVersionBumpReason } = resolvedCart\n\n const rawBody = (await readJsonSafe<Record<string, unknown>>(request, {})) ?? {}\n let body: z.infer<typeof cartLineBulkAddRequestSchema>\n try {\n body = cartLineBulkAddRequestSchema.parse(rawBody)\n } catch (err) {\n if (err instanceof z.ZodError) {\n return NextResponse.json({ error: '[internal] invalid request body', issues: err.issues }, { status: 400 })\n }\n throw err\n }\n\n const scope = { tenantId: resolved.routeContext.tenantId, organizationId: resolved.routeContext.organizationId }\n const idempotencyKey = readIdempotencyKey(request)\n const requestDigest = computeRequestDigest(body)\n const em = (container.resolve('em') as EntityManager).fork()\n\n // \u00A78.2 ordering \u2014 see `lines/route.ts`'s identical note.\n if (idempotencyKey) {\n const replay = await findIdempotencyReplay(em, scope, cart.id, idempotencyKey, requestDigest)\n if (replay) return NextResponse.json(replay.body, { status: replay.status })\n }\n\n const guarded = await runRouteMutationGuards({\n container,\n req: request,\n auth: {\n userId: resolved.routeContext.customerUserId ?? 'anonymous',\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userFeatures: [],\n },\n input: { resourceKind: CART_RESOURCE_KIND, resourceId: cart.id, operation: 'create', mutationPayload: body },\n })\n if (!guarded.ok) return guarded.response\n\n enforceCommandOptimisticLock({\n resourceKind: CART_RESOURCE_KIND,\n resourceId: cart.id,\n current: cart.updatedAt,\n request,\n })\n\n const commandInput: CartLinesBulkAddInput = {\n routeContext: resolved.routeContext,\n cartId: cart.id,\n lines: body.lines,\n }\n const { result } = await commandBus.execute<CartLinesBulkAddInput, CartLinesBulkAddResult>(bulkAddLinesCommand.id, {\n input: commandInput,\n ctx,\n })\n\n await guarded.runAfterSuccess()\n\n // Step 5.1 \u2014 same reasoning as the single-line add route: `cart.lines.bulkAdd` never\n // itself produces a repricing side effect, so `priceChanges`/`versionBumpReason` here\n // can only come from the `resolveCurrentCart`-embedded \u00A75.2 trigger 2-4 check above.\n //\n // Step 5.2 \u2014 \u00A710.1's generic envelope has no slot for \"up to 100 independent per-entry\n // dispositions\"; collapsing `outcome.results` into the handful of top-level `warnings`\n // the envelope DOES support would discard information no batch-endpoint client can do\n // without (which of the 100 entries succeeded, which were rejected, and why). `outcome`\n // is therefore kept as a deliberate, documented ADDITION alongside the mandated `cart`\n // envelope \u2014 not a replacement \u2014 for this one route only. Only `added`/`merged` entries\n // contribute a top-level `quantity_capped` warning (one per affected line); a rejected\n // entry's reason is already fully disclosed in `outcome.results[index]`, so it is not\n // duplicated as a lineless top-level warning the way the single-line route's rejected-\n // add case needs to be (there, `outcome` itself is dropped for the warning-level case).\n const { translate } = await resolveTranslations()\n const warnings: CartViewWarning[] = result.results\n .filter((entry): entry is Extract<CartLinesBulkAddEntryOutcome, { status: 'added' | 'merged' }> => entry.status === 'added' || entry.status === 'merged')\n .flatMap((entry) =>\n entry.warnings.map((code) => ({\n code,\n lineId: entry.line.id,\n message: resolveCartWarningMessage('quantity_capped', translate),\n })),\n )\n const envelope = await buildCartResponseEnvelope({\n container,\n cartId: cart.id,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n priceChanges: contextPriceChanges,\n versionBumpReason: contextVersionBumpReason,\n warnings,\n })\n const responseBody: Record<string, unknown> = { ...envelope, outcome: result }\n if (idempotencyKey) {\n await recordIdempotencyResponse(em, scope, cart.id, idempotencyKey, requestDigest, BULK_ADD_STATUS, responseBody)\n }\n return NextResponse.json(responseBody, { status: BULK_ADD_STATUS })\n } catch (error) {\n return mapRouteErrorToResponse(error)\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Cart',\n summary: 'Batch-add lines to the current cart',\n methods: {\n POST: {\n summary: 'Add up to 100 product/variant lines in one call',\n description:\n 'One optimistic-lock check and one `Idempotency-Key` record for the whole batch; each entry\\'s own outcome (added/merged/rejected) is reported in `outcome.results`.',\n requestBody: { contentType: 'application/json', schema: cartLineBulkAddRequestSchema },\n responses: [{ status: 200, description: 'Batch processed \u2014 see `outcome.results` for per-entry outcomes' }],\n errors: [\n { status: 400, description: 'Invalid request body, or more than 100 entries' },\n { status: 404, description: 'No `X-Cart-Token` header, or the token does not resolve a cart' },\n { status: 409, description: 'Optimistic-lock conflict' },\n { status: 422, description: 'Idempotency key mismatch' },\n { status: 429, description: 'Rate limited' },\n { status: 503, description: 'No store context resolvable, or cart_busy after 15 seconds waiting for the line lock. The requested line add/bulk-add phases did not run; earlier activity/context/repricing may have persisted. Retry the same request after Retry-After: 1 second, preserving Idempotency-Key.' },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -147,7 +147,7 @@ const openApi = {
|
|
|
147
147
|
{ status: 409, description: "Optimistic-lock conflict, or the cart is a `proposal` cart" },
|
|
148
148
|
{ status: 422, description: "Quantity rule violation, idempotency key mismatch, or an unpriceable product" },
|
|
149
149
|
{ status: 429, description: "Rate limited" },
|
|
150
|
-
{ status: 503, description: "No store context resolvable for
|
|
150
|
+
{ status: 503, description: "No store context resolvable, or cart_busy after 15 seconds waiting for the line lock. The requested line add/bulk-add phases did not run; earlier activity/context/repricing may have persisted. Retry the same request after Retry-After: 1 second, preserving Idempotency-Key." }
|
|
151
151
|
]
|
|
152
152
|
}
|
|
153
153
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/cart/api/current/lines/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 type { CommandBus } from '@open-mercato/shared/lib/commands'\nimport { enforceCommandOptimisticLock } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport { runRouteMutationGuards } from '@open-mercato/shared/lib/crud/route-mutation-guard'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\n\nimport { addLineCommand } from '../../../commands/lines'\nimport type { CartLinesAddInput, CartLinesAddOutcome } from '../../../commands/lines'\nimport { cartLineAddRequestSchema } from '../../../data/validators'\nimport {\n CART_RESOURCE_KIND,\n buildCommandContext,\n mapRouteErrorToResponse,\n notFoundCartResponse,\n readCartToken,\n resolveCurrentCart,\n resolveRouteContextOrError,\n} from '../../../lib/currentCartRoute'\nimport { buildCartResponseEnvelope } from '../../../lib/cartView'\nimport type { CartViewWarning } from '../../../lib/cartView'\nimport { resolveCartWarningMessage } from '../../../lib/warningMessages'\nimport {\n computeRequestDigest,\n findIdempotencyReplay,\n readIdempotencyKey,\n recordIdempotencyResponse,\n} from '../../../lib/idempotency'\nimport { enforceCartRateLimit } from '../../../lib/rateLimit'\n\n// Step 4.7 \u2014 `POST /current/lines` \u2192 `cart.lines.add`.\nexport const metadata = {\n POST: { requireAuth: false },\n}\n\n/**\n * \u00A715a's error-code catalogue maps the outcome union: `product_unavailable` is a\n * `200` warning (\u00A76a.3 \u2014 never a distinguishable error, restricted and\n * nonexistent collapse identically); `quantity_below_minimum`/\n * `quantity_invalid_increment` are hard `422`s (`quantity_below_min` /\n * `quantity_increment_violation`), carrying `nearestValid`.\n * `product_not_priceable` has no catalogue entry \u2014 treated as the same hard-stop\n * `422` family rather than invented as a silent `200`, since (unlike visibility)\n * the spec never names it a warning.\n */\nfunction statusForAddOutcome(outcome: CartLinesAddOutcome): number {\n if (outcome.status === 'added') return 201\n if (outcome.status === 'merged') return 200\n if (outcome.reason === 'product_unavailable') return 200\n return 422\n}\n\n/**\n * Step 5.2 \u2014 \u00A710.1's \"every endpoint returns the same envelope\" applies to every outcome\n * whose own status is a non-fatal 200/201 (added/merged/the product_unavailable warning);\n * a genuinely hard-stop 422 (quantity/pricing rule) keeps the pre-existing bare `{ outcome }`\n * body \u2014 that error-presentation design is \u00A715a's/Step 5.3's territory, not this step's, and\n * nothing in `outcome.results`-shaped data belongs in a `CartView` anyway (no line exists to\n * describe). See `lib/cartView.ts`'s own doc comment for the rest of this step's judgment\n * calls.\n */\nfunction isWarningLevelOutcome(outcome: CartLinesAddOutcome): boolean {\n return outcome.status !== 'rejected' || outcome.reason === 'product_unavailable'\n}\n\nexport async function POST(request: Request) {\n try {\n const container = await createRequestContainer()\n const token = readCartToken(request)\n\n const limited = await enforceCartRateLimit(container, request, 'mutate', token)\n if (limited) return limited\n\n const resolved = await resolveRouteContextOrError(request, container)\n if (!resolved.ok) return resolved.response\n if (!token) return notFoundCartResponse()\n\n const commandBus = container.resolve('commandBus') as CommandBus\n const ctx = buildCommandContext(container, request, resolved.routeContext)\n const resolvedCart = await resolveCurrentCart(commandBus, ctx, resolved.routeContext, token)\n if (!resolvedCart) return notFoundCartResponse()\n const { cart, priceChanges: contextPriceChanges, versionBumpReason: contextVersionBumpReason } = resolvedCart\n\n const rawBody = (await readJsonSafe<Record<string, unknown>>(request, {})) ?? {}\n let body: z.infer<typeof cartLineAddRequestSchema>\n try {\n body = cartLineAddRequestSchema.parse(rawBody)\n } catch (err) {\n if (err instanceof z.ZodError) {\n return NextResponse.json({ error: '[internal] invalid request body', issues: err.issues }, { status: 400 })\n }\n throw err\n }\n\n const scope = { tenantId: resolved.routeContext.tenantId, organizationId: resolved.routeContext.organizationId }\n const idempotencyKey = readIdempotencyKey(request)\n const requestDigest = computeRequestDigest(body)\n const em = (container.resolve('em') as EntityManager).fork()\n\n // \u00A78.2: \"the idempotency lookup precedes the optimistic-lock check\" \u2014 a retry\n // whose response was lost carries the PRE-mutation `updatedAt`, which a lock\n // check run first would 409 against the cart the original request itself\n // already changed. A replay also skips the mutation guard entirely: the guard\n // already ran (and passed, or this row would not exist) for the original call.\n if (idempotencyKey) {\n const replay = await findIdempotencyReplay(em, scope, cart.id, idempotencyKey, requestDigest)\n if (replay) return NextResponse.json(replay.body, { status: replay.status })\n }\n\n const guarded = await runRouteMutationGuards({\n container,\n req: request,\n auth: {\n userId: resolved.routeContext.customerUserId ?? 'anonymous',\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userFeatures: [],\n },\n input: { resourceKind: CART_RESOURCE_KIND, resourceId: cart.id, operation: 'create', mutationPayload: body },\n })\n if (!guarded.ok) return guarded.response\n\n enforceCommandOptimisticLock({\n resourceKind: CART_RESOURCE_KIND,\n resourceId: cart.id,\n current: cart.updatedAt,\n request,\n })\n\n const commandInput: CartLinesAddInput = {\n routeContext: resolved.routeContext,\n cartId: cart.id,\n productId: body.productId,\n variantId: body.variantId ?? null,\n quantity: body.quantity,\n configuration: body.configuration ?? null,\n metadata: body.metadata ?? null,\n }\n const { result } = await commandBus.execute<CartLinesAddInput, CartLinesAddOutcome>(addLineCommand.id, {\n input: commandInput,\n ctx,\n })\n\n await guarded.runAfterSuccess()\n\n const status = statusForAddOutcome(result)\n // Step 5.1 \u2014 `cart.lines.add` never itself produces a repricing side effect (a brand\n // new/merged line is priced, not re-priced); `priceChanges`/`versionBumpReason` here can\n // only come from the `resolveCurrentCart`-embedded \u00A75.2 trigger 2-4 check above.\n let responseBody: Record<string, unknown>\n if (isWarningLevelOutcome(result)) {\n const { translate } = await resolveTranslations()\n const warnings: CartViewWarning[] =\n result.status === 'added' || result.status === 'merged'\n ? result.warnings.map((code) => ({\n code,\n lineId: result.line.id,\n message: resolveCartWarningMessage('quantity_capped', translate),\n }))\n : [{ code: 'product_unavailable', message: resolveCartWarningMessage('product_unavailable', translate) }]\n responseBody = await buildCartResponseEnvelope({\n container,\n cartId: cart.id,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n priceChanges: contextPriceChanges,\n versionBumpReason: contextVersionBumpReason,\n warnings,\n })\n } else {\n responseBody = { outcome: result }\n if (contextPriceChanges.length > 0) responseBody.priceChanges = contextPriceChanges\n if (contextVersionBumpReason) responseBody.versionBumpReason = contextVersionBumpReason\n }\n if (idempotencyKey) {\n await recordIdempotencyResponse(em, scope, cart.id, idempotencyKey, requestDigest, status, responseBody)\n }\n return NextResponse.json(responseBody, { status })\n } catch (error) {\n return mapRouteErrorToResponse(error)\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Cart',\n summary: 'Add a line to the current cart',\n methods: {\n POST: {\n summary: 'Add a product/variant line to the current cart',\n description:\n 'Visibility-checked on every call (\u00A76a.1c). Accepts `Idempotency-Key` (\u00A78.2) and the standard optimistic-lock header (\u00A78.1).',\n requestBody: { contentType: 'application/json', schema: cartLineAddRequestSchema },\n responses: [{ status: 201, description: 'Line added' }, { status: 200, description: 'Line merged into an existing line, or rejected as a non-fatal warning' }],\n errors: [\n { status: 400, description: 'Invalid request body' },\n { status: 404, description: 'No `X-Cart-Token` header, or the token does not resolve a cart' },\n { status: 409, description: 'Optimistic-lock conflict, or the cart is a `proposal` cart' },\n { status: 422, description: 'Quantity rule violation, idempotency key mismatch, or an unpriceable product' },\n { status: 429, description: 'Rate limited' },\n { status: 503, description: 'No store context resolvable for
|
|
5
|
-
"mappings": "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
|
|
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 type { CommandBus } from '@open-mercato/shared/lib/commands'\nimport { enforceCommandOptimisticLock } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport { runRouteMutationGuards } from '@open-mercato/shared/lib/crud/route-mutation-guard'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\n\nimport { addLineCommand } from '../../../commands/lines'\nimport type { CartLinesAddInput, CartLinesAddOutcome } from '../../../commands/lines'\nimport { cartLineAddRequestSchema } from '../../../data/validators'\nimport {\n CART_RESOURCE_KIND,\n buildCommandContext,\n mapRouteErrorToResponse,\n notFoundCartResponse,\n readCartToken,\n resolveCurrentCart,\n resolveRouteContextOrError,\n} from '../../../lib/currentCartRoute'\nimport { buildCartResponseEnvelope } from '../../../lib/cartView'\nimport type { CartViewWarning } from '../../../lib/cartView'\nimport { resolveCartWarningMessage } from '../../../lib/warningMessages'\nimport {\n computeRequestDigest,\n findIdempotencyReplay,\n readIdempotencyKey,\n recordIdempotencyResponse,\n} from '../../../lib/idempotency'\nimport { enforceCartRateLimit } from '../../../lib/rateLimit'\n\n// Step 4.7 \u2014 `POST /current/lines` \u2192 `cart.lines.add`.\nexport const metadata = {\n POST: { requireAuth: false },\n}\n\n/**\n * \u00A715a's error-code catalogue maps the outcome union: `product_unavailable` is a\n * `200` warning (\u00A76a.3 \u2014 never a distinguishable error, restricted and\n * nonexistent collapse identically); `quantity_below_minimum`/\n * `quantity_invalid_increment` are hard `422`s (`quantity_below_min` /\n * `quantity_increment_violation`), carrying `nearestValid`.\n * `product_not_priceable` has no catalogue entry \u2014 treated as the same hard-stop\n * `422` family rather than invented as a silent `200`, since (unlike visibility)\n * the spec never names it a warning.\n */\nfunction statusForAddOutcome(outcome: CartLinesAddOutcome): number {\n if (outcome.status === 'added') return 201\n if (outcome.status === 'merged') return 200\n if (outcome.reason === 'product_unavailable') return 200\n return 422\n}\n\n/**\n * Step 5.2 \u2014 \u00A710.1's \"every endpoint returns the same envelope\" applies to every outcome\n * whose own status is a non-fatal 200/201 (added/merged/the product_unavailable warning);\n * a genuinely hard-stop 422 (quantity/pricing rule) keeps the pre-existing bare `{ outcome }`\n * body \u2014 that error-presentation design is \u00A715a's/Step 5.3's territory, not this step's, and\n * nothing in `outcome.results`-shaped data belongs in a `CartView` anyway (no line exists to\n * describe). See `lib/cartView.ts`'s own doc comment for the rest of this step's judgment\n * calls.\n */\nfunction isWarningLevelOutcome(outcome: CartLinesAddOutcome): boolean {\n return outcome.status !== 'rejected' || outcome.reason === 'product_unavailable'\n}\n\nexport async function POST(request: Request) {\n try {\n const container = await createRequestContainer()\n const token = readCartToken(request)\n\n const limited = await enforceCartRateLimit(container, request, 'mutate', token)\n if (limited) return limited\n\n const resolved = await resolveRouteContextOrError(request, container)\n if (!resolved.ok) return resolved.response\n if (!token) return notFoundCartResponse()\n\n const commandBus = container.resolve('commandBus') as CommandBus\n const ctx = buildCommandContext(container, request, resolved.routeContext)\n const resolvedCart = await resolveCurrentCart(commandBus, ctx, resolved.routeContext, token)\n if (!resolvedCart) return notFoundCartResponse()\n const { cart, priceChanges: contextPriceChanges, versionBumpReason: contextVersionBumpReason } = resolvedCart\n\n const rawBody = (await readJsonSafe<Record<string, unknown>>(request, {})) ?? {}\n let body: z.infer<typeof cartLineAddRequestSchema>\n try {\n body = cartLineAddRequestSchema.parse(rawBody)\n } catch (err) {\n if (err instanceof z.ZodError) {\n return NextResponse.json({ error: '[internal] invalid request body', issues: err.issues }, { status: 400 })\n }\n throw err\n }\n\n const scope = { tenantId: resolved.routeContext.tenantId, organizationId: resolved.routeContext.organizationId }\n const idempotencyKey = readIdempotencyKey(request)\n const requestDigest = computeRequestDigest(body)\n const em = (container.resolve('em') as EntityManager).fork()\n\n // \u00A78.2: \"the idempotency lookup precedes the optimistic-lock check\" \u2014 a retry\n // whose response was lost carries the PRE-mutation `updatedAt`, which a lock\n // check run first would 409 against the cart the original request itself\n // already changed. A replay also skips the mutation guard entirely: the guard\n // already ran (and passed, or this row would not exist) for the original call.\n if (idempotencyKey) {\n const replay = await findIdempotencyReplay(em, scope, cart.id, idempotencyKey, requestDigest)\n if (replay) return NextResponse.json(replay.body, { status: replay.status })\n }\n\n const guarded = await runRouteMutationGuards({\n container,\n req: request,\n auth: {\n userId: resolved.routeContext.customerUserId ?? 'anonymous',\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userFeatures: [],\n },\n input: { resourceKind: CART_RESOURCE_KIND, resourceId: cart.id, operation: 'create', mutationPayload: body },\n })\n if (!guarded.ok) return guarded.response\n\n enforceCommandOptimisticLock({\n resourceKind: CART_RESOURCE_KIND,\n resourceId: cart.id,\n current: cart.updatedAt,\n request,\n })\n\n const commandInput: CartLinesAddInput = {\n routeContext: resolved.routeContext,\n cartId: cart.id,\n productId: body.productId,\n variantId: body.variantId ?? null,\n quantity: body.quantity,\n configuration: body.configuration ?? null,\n metadata: body.metadata ?? null,\n }\n const { result } = await commandBus.execute<CartLinesAddInput, CartLinesAddOutcome>(addLineCommand.id, {\n input: commandInput,\n ctx,\n })\n\n await guarded.runAfterSuccess()\n\n const status = statusForAddOutcome(result)\n // Step 5.1 \u2014 `cart.lines.add` never itself produces a repricing side effect (a brand\n // new/merged line is priced, not re-priced); `priceChanges`/`versionBumpReason` here can\n // only come from the `resolveCurrentCart`-embedded \u00A75.2 trigger 2-4 check above.\n let responseBody: Record<string, unknown>\n if (isWarningLevelOutcome(result)) {\n const { translate } = await resolveTranslations()\n const warnings: CartViewWarning[] =\n result.status === 'added' || result.status === 'merged'\n ? result.warnings.map((code) => ({\n code,\n lineId: result.line.id,\n message: resolveCartWarningMessage('quantity_capped', translate),\n }))\n : [{ code: 'product_unavailable', message: resolveCartWarningMessage('product_unavailable', translate) }]\n responseBody = await buildCartResponseEnvelope({\n container,\n cartId: cart.id,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n priceChanges: contextPriceChanges,\n versionBumpReason: contextVersionBumpReason,\n warnings,\n })\n } else {\n responseBody = { outcome: result }\n if (contextPriceChanges.length > 0) responseBody.priceChanges = contextPriceChanges\n if (contextVersionBumpReason) responseBody.versionBumpReason = contextVersionBumpReason\n }\n if (idempotencyKey) {\n await recordIdempotencyResponse(em, scope, cart.id, idempotencyKey, requestDigest, status, responseBody)\n }\n return NextResponse.json(responseBody, { status })\n } catch (error) {\n return mapRouteErrorToResponse(error)\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Cart',\n summary: 'Add a line to the current cart',\n methods: {\n POST: {\n summary: 'Add a product/variant line to the current cart',\n description:\n 'Visibility-checked on every call (\u00A76a.1c). Accepts `Idempotency-Key` (\u00A78.2) and the standard optimistic-lock header (\u00A78.1).',\n requestBody: { contentType: 'application/json', schema: cartLineAddRequestSchema },\n responses: [{ status: 201, description: 'Line added' }, { status: 200, description: 'Line merged into an existing line, or rejected as a non-fatal warning' }],\n errors: [\n { status: 400, description: 'Invalid request body' },\n { status: 404, description: 'No `X-Cart-Token` header, or the token does not resolve a cart' },\n { status: 409, description: 'Optimistic-lock conflict, or the cart is a `proposal` cart' },\n { status: 422, description: 'Quantity rule violation, idempotency key mismatch, or an unpriceable product' },\n { status: 429, description: 'Rate limited' },\n { status: 503, description: 'No store context resolvable, or cart_busy after 15 seconds waiting for the line lock. The requested line add/bulk-add phases did not run; earlier activity/context/repricing may have persisted. Retry the same request after Retry-After: 1 second, preserving Idempotency-Key.' },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
isVisible
|
|
28
28
|
} from "../lib/visibility.js";
|
|
29
29
|
import { shouldEnforceAssortmentScope } from "../lib/routeContext.js";
|
|
30
|
+
import { withCartLineMutationLock } from "../lib/lineMutationLock.js";
|
|
30
31
|
import { buildRecomputeCartTotalsPhase } from "../lib/reprice.js";
|
|
31
32
|
import { buildPromotionsEffectsPhase } from "../lib/promotionContext.js";
|
|
32
33
|
import { assertCartNotLocked } from "../lib/lockGuard.js";
|
|
@@ -438,10 +439,7 @@ const addLineCommand = {
|
|
|
438
439
|
const removedGiftLines = [];
|
|
439
440
|
const promotionWarnings = [];
|
|
440
441
|
const shouldRecompute = () => !!outcome && outcome.status !== "rejected";
|
|
441
|
-
await
|
|
442
|
-
async () => {
|
|
443
|
-
await em.execute("select pg_advisory_xact_lock(hashtextextended(?::text, 0))", [`cart:lines:${cart.id}`]);
|
|
444
|
-
},
|
|
442
|
+
await withCartLineMutationLock(em, cart.id, [
|
|
445
443
|
async () => {
|
|
446
444
|
const existing = await findExistingCustomerLine(em, scope, cart.id, input.productId, variantId, input.configuration ?? null);
|
|
447
445
|
const pricingContext = buildCartPricingRowFilterContext(cart);
|
|
@@ -541,7 +539,7 @@ const addLineCommand = {
|
|
|
541
539
|
if (!shouldRecompute()) return;
|
|
542
540
|
await phase();
|
|
543
541
|
})
|
|
544
|
-
],
|
|
542
|
+
], "cart.lines.add");
|
|
545
543
|
if (!outcome) throw new Error("[internal] cart.lines.add produced no outcome");
|
|
546
544
|
const finalOutcome = outcome;
|
|
547
545
|
if (changedLine && changedAction) {
|
|
@@ -964,10 +962,7 @@ const bulkAddLinesCommand = {
|
|
|
964
962
|
const createdGiftLines = [];
|
|
965
963
|
const removedGiftLines = [];
|
|
966
964
|
const promotionWarnings = [];
|
|
967
|
-
await
|
|
968
|
-
async () => {
|
|
969
|
-
await em.execute("select pg_advisory_xact_lock(hashtextextended(?::text, 0))", [`cart:lines:${cart.id}`]);
|
|
970
|
-
},
|
|
965
|
+
await withCartLineMutationLock(em, cart.id, [
|
|
971
966
|
...phases,
|
|
972
967
|
...buildCartPromotionsAndTotalsPhases({
|
|
973
968
|
em,
|
|
@@ -980,7 +975,7 @@ const bulkAddLinesCommand = {
|
|
|
980
975
|
removedGiftLines,
|
|
981
976
|
promotionWarnings
|
|
982
977
|
})
|
|
983
|
-
],
|
|
978
|
+
], "cart.lines.bulkAdd");
|
|
984
979
|
await emitVisibilityRejectedEvent(cart, visibilityRejectedCount, "lines.bulkAdd");
|
|
985
980
|
const dataEngine = ctx.container.resolve("dataEngine");
|
|
986
981
|
for (let i = 0; i < changedLines.length; i += 1) {
|