@open-mercato/core 0.10.1-develop.7833.1.d6cbf0e3ab → 0.10.1-develop.7836.1.0ef0f453c6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/cart/api/current/lines/bulk/route.js +1 -1
  3. package/dist/modules/cart/api/current/lines/bulk/route.js.map +2 -2
  4. package/dist/modules/cart/api/current/lines/route.js +1 -1
  5. package/dist/modules/cart/api/current/lines/route.js.map +2 -2
  6. package/dist/modules/cart/commands/lines.js +5 -10
  7. package/dist/modules/cart/commands/lines.js.map +2 -2
  8. package/dist/modules/cart/lib/currentCartRoute.js +8 -0
  9. package/dist/modules/cart/lib/currentCartRoute.js.map +3 -3
  10. package/dist/modules/cart/lib/lineMutationLock.js +52 -0
  11. package/dist/modules/cart/lib/lineMutationLock.js.map +7 -0
  12. package/dist/modules/sales/commands/documents.js +3 -3
  13. package/dist/modules/sales/commands/documents.js.map +2 -2
  14. package/dist/modules/sales/components/documents/ItemsSection.js +75 -21
  15. package/dist/modules/sales/components/documents/ItemsSection.js.map +2 -2
  16. package/dist/modules/sales/components/documents/lineItemUtils.js +2 -2
  17. package/dist/modules/sales/components/documents/lineItemUtils.js.map +2 -2
  18. package/package.json +7 -7
  19. package/src/modules/cart/api/current/lines/bulk/route.ts +1 -1
  20. package/src/modules/cart/api/current/lines/route.ts +1 -1
  21. package/src/modules/cart/commands/lines.ts +5 -10
  22. package/src/modules/cart/i18n/de.json +1 -0
  23. package/src/modules/cart/i18n/en.json +1 -0
  24. package/src/modules/cart/i18n/es.json +1 -0
  25. package/src/modules/cart/i18n/ko.json +1 -0
  26. package/src/modules/cart/i18n/pl.json +1 -0
  27. package/src/modules/cart/lib/currentCartRoute.ts +8 -0
  28. package/src/modules/cart/lib/lineMutationLock.ts +59 -0
  29. package/src/modules/sales/commands/documents.ts +3 -3
  30. package/src/modules/sales/components/documents/ItemsSection.tsx +82 -23
  31. package/src/modules/sales/components/documents/lineItemUtils.ts +2 -2
  32. package/src/modules/sales/i18n/de.json +1 -0
  33. package/src/modules/sales/i18n/en.json +1 -0
  34. package/src/modules/sales/i18n/es.json +1 -0
  35. package/src/modules/sales/i18n/ko.json +1 -0
  36. package/src/modules/sales/i18n/pl.json +1 -0
@@ -1,4 +1,4 @@
1
- [build:core] found 6230 entry points
1
+ [build:core] found 6231 entry points
2
2
  [build:core] built successfully
3
3
  [build:core:generated] found 271 entry points
4
4
  [build:core:generated] built successfully
@@ -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 this request (\xA76a.1a)" }
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 this request (\u00A76a.1a)' },\n ],\n },\n },\n}\n"],
5
- "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,8BAA8B;AAEvC,SAAS,oCAAoC;AAC7C,SAAS,8BAA8B;AACvC,SAAS,oBAAoB;AAE7B,SAAS,2BAA2B;AAEpC,SAAS,2BAA2B;AAEpC,SAAS,oCAAoC;AAC7C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,iCAAiC;AAE1C,SAAS,iCAAiC;AAC1C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,4BAA4B;AAQ9B,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,MAAM;AAC7B;AAWA,MAAM,kBAAkB;AAExB,eAAsB,KAAK,SAAkB;AAC3C,MAAI;AACF,UAAM,YAAY,MAAM,uBAAuB;AAC/C,UAAM,QAAQ,cAAc,OAAO;AAEnC,UAAM,UAAU,MAAM,qBAAqB,WAAW,SAAS,UAAU,KAAK;AAC9E,QAAI,QAAS,QAAO;AAEpB,UAAM,WAAW,MAAM,2BAA2B,SAAS,SAAS;AACpE,QAAI,CAAC,SAAS,GAAI,QAAO,SAAS;AAClC,QAAI,CAAC,MAAO,QAAO,qBAAqB;AAExC,UAAM,aAAa,UAAU,QAAQ,YAAY;AACjD,UAAM,MAAM,oBAAoB,WAAW,SAAS,SAAS,YAAY;AACzE,UAAM,eAAe,MAAM,mBAAmB,YAAY,KAAK,SAAS,cAAc,KAAK;AAC3F,QAAI,CAAC,aAAc,QAAO,qBAAqB;AAC/C,UAAM,EAAE,MAAM,cAAc,qBAAqB,mBAAmB,yBAAyB,IAAI;AAEjG,UAAM,UAAW,MAAM,aAAsC,SAAS,CAAC,CAAC,KAAM,CAAC;AAC/E,QAAI;AACJ,QAAI;AACF,aAAO,6BAA6B,MAAM,OAAO;AAAA,IACnD,SAAS,KAAK;AACZ,UAAI,eAAe,EAAE,UAAU;AAC7B,eAAO,aAAa,KAAK,EAAE,OAAO,mCAAmC,QAAQ,IAAI,OAAO,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,MAC5G;AACA,YAAM;AAAA,IACR;AAEA,UAAM,QAAQ,EAAE,UAAU,SAAS,aAAa,UAAU,gBAAgB,SAAS,aAAa,eAAe;AAC/G,UAAM,iBAAiB,mBAAmB,OAAO;AACjD,UAAM,gBAAgB,qBAAqB,IAAI;AAC/C,UAAM,KAAM,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAG3D,QAAI,gBAAgB;AAClB,YAAM,SAAS,MAAM,sBAAsB,IAAI,OAAO,KAAK,IAAI,gBAAgB,aAAa;AAC5F,UAAI,OAAQ,QAAO,aAAa,KAAK,OAAO,MAAM,EAAE,QAAQ,OAAO,OAAO,CAAC;AAAA,IAC7E;AAEA,UAAM,UAAU,MAAM,uBAAuB;AAAA,MAC3C;AAAA,MACA,KAAK;AAAA,MACL,MAAM;AAAA,QACJ,QAAQ,SAAS,aAAa,kBAAkB;AAAA,QAChD,UAAU,MAAM;AAAA,QAChB,gBAAgB,MAAM;AAAA,QACtB,cAAc,CAAC;AAAA,MACjB;AAAA,MACA,OAAO,EAAE,cAAc,oBAAoB,YAAY,KAAK,IAAI,WAAW,UAAU,iBAAiB,KAAK;AAAA,IAC7G,CAAC;AACD,QAAI,CAAC,QAAQ,GAAI,QAAO,QAAQ;AAEhC,iCAA6B;AAAA,MAC3B,cAAc;AAAA,MACd,YAAY,KAAK;AAAA,MACjB,SAAS,KAAK;AAAA,MACd;AAAA,IACF,CAAC;AAED,UAAM,eAAsC;AAAA,MAC1C,cAAc,SAAS;AAAA,MACvB,QAAQ,KAAK;AAAA,MACb,OAAO,KAAK;AAAA,IACd;AACA,UAAM,EAAE,OAAO,IAAI,MAAM,WAAW,QAAuD,oBAAoB,IAAI;AAAA,MACjH,OAAO;AAAA,MACP;AAAA,IACF,CAAC;AAED,UAAM,QAAQ,gBAAgB;AAgB9B,UAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,UAAM,WAA8B,OAAO,QACxC,OAAO,CAAC,UAA0F,MAAM,WAAW,WAAW,MAAM,WAAW,QAAQ,EACvJ;AAAA,MAAQ,CAAC,UACR,MAAM,SAAS,IAAI,CAAC,UAAU;AAAA,QAC5B;AAAA,QACA,QAAQ,MAAM,KAAK;AAAA,QACnB,SAAS,0BAA0B,mBAAmB,SAAS;AAAA,MACjE,EAAE;AAAA,IACJ;AACF,UAAM,WAAW,MAAM,0BAA0B;AAAA,MAC/C;AAAA,MACA,QAAQ,KAAK;AAAA,MACb,UAAU,MAAM;AAAA,MAChB,gBAAgB,MAAM;AAAA,MACtB,cAAc;AAAA,MACd,mBAAmB;AAAA,MACnB;AAAA,IACF,CAAC;AACD,UAAM,eAAwC,EAAE,GAAG,UAAU,SAAS,OAAO;AAC7E,QAAI,gBAAgB;AAClB,YAAM,0BAA0B,IAAI,OAAO,KAAK,IAAI,gBAAgB,eAAe,iBAAiB,YAAY;AAAA,IAClH;AACA,WAAO,aAAa,KAAK,cAAc,EAAE,QAAQ,gBAAgB,CAAC;AAAA,EACpE,SAAS,OAAO;AACd,WAAO,wBAAwB,KAAK;AAAA,EACtC;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,aACE;AAAA,MACF,aAAa,EAAE,aAAa,oBAAoB,QAAQ,6BAA6B;AAAA,MACrF,WAAW,CAAC,EAAE,QAAQ,KAAK,aAAa,sEAAiE,CAAC;AAAA,MAC1G,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,iDAAiD;AAAA,QAC7E,EAAE,QAAQ,KAAK,aAAa,iEAAiE;AAAA,QAC7F,EAAE,QAAQ,KAAK,aAAa,2BAA2B;AAAA,QACvD,EAAE,QAAQ,KAAK,aAAa,2BAA2B;AAAA,QACvD,EAAE,QAAQ,KAAK,aAAa,eAAe;AAAA,QAC3C,EAAE,QAAQ,KAAK,aAAa,2DAAwD;AAAA,MACtF;AAAA,IACF;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport 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 this request (\xA76a.1a)" }
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 this request (\u00A76a.1a)' },\n ],\n },\n },\n}\n"],
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 withAtomicFlush(em, [
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
- ], { transaction: true, label: "cart.lines.add" });
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 withAtomicFlush(em, [
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
- ], { transaction: true, label: "cart.lines.bulkAdd" });
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) {