@open-mercato/core 0.10.1-develop.7827.1.5c631908e4 → 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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/helpers/integration/appRuntime.js +17 -0
- package/dist/helpers/integration/appRuntime.js.map +7 -0
- 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/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/warranty_claims/search.js +7 -6
- package/dist/modules/warranty_claims/search.js.map +2 -2
- package/package.json +7 -7
- package/src/helpers/integration/appRuntime.ts +26 -0
- 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/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/warranty_claims/i18n/de.json +1 -0
- package/src/modules/warranty_claims/i18n/en.json +1 -0
- package/src/modules/warranty_claims/i18n/es.json +1 -0
- package/src/modules/warranty_claims/i18n/ko.json +1 -0
- package/src/modules/warranty_claims/i18n/pl.json +1 -0
- package/src/modules/warranty_claims/search.ts +8 -5
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/cart/lib/currentCartRoute.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { AppContainer } from '@open-mercato/shared/lib/di/container'\nimport type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'\n\nimport { getCartCommand } from '../commands/cart'\nimport type { CartGetByTokenInput, CartSummary } from '../commands/cart'\nimport type { CartPriceChange } from './reprice'\nimport { isCartStoreContextUnavailableError, resolveCartRouteContext } from './routeContext'\nimport type { ResolveCartRouteContextOptions, ResolvedCartRouteContext } from './routeContext'\n\n// Step 4.7 \u2014 shared plumbing for the four `/api/cart/current/*` route files\n// (`current/route.ts`, `current/lines/route.ts`, `current/lines/[lineId]/route.ts`,\n// `current/lines/bulk/route.ts`). Kept here rather than duplicated per file because\n// every one of them needs the identical: resolve-the-store-context-at-the-edge\n// step (\u00A76a.1a), the `X-Cart-Token` header read (\u00A78.3/\u00A710, never a path segment or\n// query string), the token\u2192cart lookup via `cart.get`, and the same CrudHttpError /\n// interceptor-rejection \u2192 HTTP mapping.\n\n/**\n * \u00A78.1's literal resourceKind for every cart mutation's optimistic-lock AND\n * mutation-guard call \u2014 the lock helper's own `resourceKind` input, paired with\n * `resourceId: cart.id`, `current: cart.updatedAt` and `request: ctx.request`. Used\n * for line-level mutations too \u2014 a `CartLine` has its own `updated_at`, but the lock\n * token this module's concurrency contract tracks is the CART aggregate's, per\n * \u00A78.1's own text (which names `cart.cart` for every mutating command, not a\n * per-sub-resource kind).\n */\nexport const CART_RESOURCE_KIND = 'cart.cart'\n\n/** \u00A78.3/\u00A710: the ONLY place a cart token travels \u2014 never a path segment, never a query string. */\nexport const CART_TOKEN_HEADER = 'X-Cart-Token'\n\nexport function readCartToken(request: Request): string | null {\n const value = request.headers.get(CART_TOKEN_HEADER)\n const trimmed = value?.trim()\n return trimmed ? trimmed : null\n}\n\nexport type ResolvedContextResult =\n | { ok: true; routeContext: ResolvedCartRouteContext }\n | { ok: false; response: NextResponse }\n\n/**\n * Wraps `resolveCartRouteContext` (Step 4.3) and maps\n * `CartStoreContextUnavailableError` to a `503` \u2014 per that error's own doc comment\n * (\"the route layer must map this to a 503\") and this step's explicit brief.\n *\n * (Step 7.3) Accepts an optional `opts`, forwarded verbatim to\n * `resolveCartRouteContext` \u2014 D-R4-22's \"lock and merge routes resolve the buyer context\n * fresh (`{ fresh: true }`)\". Every pre-existing caller omits it and is unaffected\n * (`undefined` forwards to the same default `resolveCartRouteContext` already had).\n */\nexport async function resolveRouteContextOrError(\n request: Request,\n container: AppContainer,\n opts?: ResolveCartRouteContextOptions,\n): Promise<ResolvedContextResult> {\n try {\n const routeContext = await resolveCartRouteContext(request, container, opts)\n return { ok: true, routeContext }\n } catch (error) {\n if (isCartStoreContextUnavailableError(error)) {\n const { translate } = await resolveTranslations()\n return {\n ok: false,\n response: NextResponse.json(\n {\n error: translate(\n 'cart.errors.store_context_unavailable',\n 'Cart is unavailable for this store right now',\n ),\n code: 'cart_store_context_unavailable',\n },\n { status: 503 },\n ),\n }\n }\n throw error\n }\n}\n\n/**\n * Builds the `CommandRuntimeContext` every `/current/*` command call shares.\n * `auth` stays `null` \u2014 the storefront caller is anonymous, or a buyer with no\n * staff identity at all (`ResolvedCartRouteContext.customerUserId` is where an\n * identified buyer's identity already lives); this mirrors `cart.create`'s own\n * documented \"Pattern C\" assumption (`commands/cart.ts`). Tenant/organization scope\n * for the commands' own `ensureTenantScope`/`ensureOrganizationScope` defense-in-depth\n * checks comes from `selectedOrganizationId`, which the resolved store context\n * already pins \u2014 not from an (absent) staff `auth` scope.\n */\nexport function buildCommandContext(\n container: AppContainer,\n request: Request,\n routeContext: ResolvedCartRouteContext,\n): CommandRuntimeContext {\n return {\n container,\n auth: null,\n organizationScope: null,\n selectedOrganizationId: routeContext.organizationId,\n organizationIds: null,\n request,\n }\n}\n\n/**\n * (Step 5.1) `resolveCurrentCart`'s result. `priceChanges` is always an array (possibly\n * empty); `versionBumpReason` is set exactly when \u00A75.2 triggers 2-4 fired and `cart.get`\n * ran `cart.reprice` internally for this call \u2014 see `commands/cart.ts`'s `getCartCommand`.\n */\nexport type ResolvedCurrentCart = {\n cart: CartSummary\n priceChanges: CartPriceChange[]\n versionBumpReason?: 'repricing'\n}\n\n/**\n * Resolves the \"current\" cart addressed by the presented `X-Cart-Token`, scoped to\n * the resolved store's tenant/organization (\u00A78.3: `kind = 'basket'` only, via\n * `cart.get`). Returns `null` when the token does not resolve \u2014 a wrong token, a\n * cross-tenant token and a real `kind = 'proposal'` cart all collapse to the same\n * `null` here, matching `cart.get`'s own no-enumeration-oracle contract.\n *\n * (Step 5.1) Always supplies `freshContext: routeContext` \u2014 every one of the four\n * `/current/*` routes that calls this helper has already resolved a `StoreContext` at the\n * route edge, so `cart.get` always has a fresh buyer context to compare against, not only\n * on the literal `GET /current` route: \"per-request BuyerContext digest comparison (also\n * on GET)\" (PLAN.md Step 5.1) is implemented once, here, for every caller.\n */\nexport async function resolveCurrentCart(\n commandBus: CommandBus,\n ctx: CommandRuntimeContext,\n routeContext: ResolvedCartRouteContext,\n token: string,\n): Promise<ResolvedCurrentCart | null> {\n const input: CartGetByTokenInput = {\n tenantId: routeContext.tenantId,\n organizationId: routeContext.organizationId,\n token,\n freshContext: routeContext,\n }\n const { result } = await commandBus.execute<CartGetByTokenInput, Awaited<ReturnType<typeof getCartCommand.execute>>>(\n getCartCommand.id,\n { input, ctx },\n )\n if (!result.found) return null\n return {\n cart: result.cart,\n priceChanges: result.priceChanges ?? [],\n versionBumpReason: result.versionBumpReason,\n }\n}\n\nexport function notFoundCartResponse(): NextResponse {\n return NextResponse.json({ error: '[internal] cart not found' }, { status: 404 })\n}\n\nconst logger = createLogger('cart').child({ component: 'api' })\n\n/**\n * Standard catch-block mapping for every `/current/*` route: a thrown\n * `CrudHttpError` (badRequest/notFound/conflict from a command, or the standardized\n * optimistic-lock/idempotency-mismatch 409/422) echoes its own status/body; a\n * command-interceptor rejection (none exist for `cart` yet, but the mapping is the\n * same one `promotions/api/[id]/tree/route.ts` uses for an identical custom\n * command-backed route) echoes its own status/body; anything else is an\n * unexpected 500, logged and reported.\n */\nexport async function mapRouteErrorToResponse(error: unknown): Promise<NextResponse> {\n if (isCrudHttpError(error)) return NextResponse.json(error.body, { status: error.status })\n const interceptorRejection = getCommandInterceptorHttpRejection(error)\n if (interceptorRejection) return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status })\n logger.error('cart api route failed', { err: error })\n getTelemetryRuntime()?.reportError(error, { module: 'cart', code: 'cart.api_route_failed' })\n const { translate } = await resolveTranslations()\n return NextResponse.json({ error: translate('cart.errors.unexpected', 'Unexpected error') }, { status: 500 })\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAG7B,SAAS,0CAA0C;AACnD,SAAS,uBAAuB;AAChC,SAAS,2BAA2B;AACpC,SAAS,oBAAoB;AAC7B,SAAS,2BAA2B;AAEpC,SAAS,sBAAsB;AAG/B,SAAS,oCAAoC,+BAA+B;AAoBrE,MAAM,qBAAqB;AAG3B,MAAM,oBAAoB;AAE1B,SAAS,cAAc,SAAiC;AAC7D,QAAM,QAAQ,QAAQ,QAAQ,IAAI,iBAAiB;AACnD,QAAM,UAAU,OAAO,KAAK;AAC5B,SAAO,UAAU,UAAU;AAC7B;AAgBA,eAAsB,2BACpB,SACA,WACA,MACgC;AAChC,MAAI;AACF,UAAM,eAAe,MAAM,wBAAwB,SAAS,WAAW,IAAI;AAC3E,WAAO,EAAE,IAAI,MAAM,aAAa;AAAA,EAClC,SAAS,OAAO;AACd,QAAI,mCAAmC,KAAK,GAAG;AAC7C,YAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,aAAO;AAAA,QACL,IAAI;AAAA,QACJ,UAAU,aAAa;AAAA,UACrB;AAAA,YACE,OAAO;AAAA,cACL;AAAA,cACA;AAAA,YACF;AAAA,YACA,MAAM;AAAA,UACR;AAAA,UACA,EAAE,QAAQ,IAAI;AAAA,QAChB;AAAA,MACF;AAAA,IACF;AACA,UAAM;AAAA,EACR;AACF;AAYO,SAAS,oBACd,WACA,SACA,cACuB;AACvB,SAAO;AAAA,IACL;AAAA,IACA,MAAM;AAAA,IACN,mBAAmB;AAAA,IACnB,wBAAwB,aAAa;AAAA,IACrC,iBAAiB;AAAA,IACjB;AAAA,EACF;AACF;AA0BA,eAAsB,mBACpB,YACA,KACA,cACA,OACqC;AACrC,QAAM,QAA6B;AAAA,IACjC,UAAU,aAAa;AAAA,IACvB,gBAAgB,aAAa;AAAA,IAC7B;AAAA,IACA,cAAc;AAAA,EAChB;AACA,QAAM,EAAE,OAAO,IAAI,MAAM,WAAW;AAAA,IAClC,eAAe;AAAA,IACf,EAAE,OAAO,IAAI;AAAA,EACf;AACA,MAAI,CAAC,OAAO,MAAO,QAAO;AAC1B,SAAO;AAAA,IACL,MAAM,OAAO;AAAA,IACb,cAAc,OAAO,gBAAgB,CAAC;AAAA,IACtC,mBAAmB,OAAO;AAAA,EAC5B;AACF;AAEO,SAAS,uBAAqC;AACnD,SAAO,aAAa,KAAK,EAAE,OAAO,4BAA4B,GAAG,EAAE,QAAQ,IAAI,CAAC;AAClF;AAEA,MAAM,SAAS,aAAa,MAAM,EAAE,MAAM,EAAE,WAAW,MAAM,CAAC;AAW9D,eAAsB,wBAAwB,OAAuC;AACnF,MAAI,gBAAgB,KAAK,EAAG,QAAO,aAAa,KAAK,MAAM,MAAM,EAAE,QAAQ,MAAM,OAAO,CAAC;AACzF,QAAM,uBAAuB,mCAAmC,KAAK;AACrE,MAAI,qBAAsB,QAAO,aAAa,KAAK,qBAAqB,MAAM,EAAE,QAAQ,qBAAqB,OAAO,CAAC;AACrH,SAAO,MAAM,yBAAyB,EAAE,KAAK,MAAM,CAAC;AACpD,sBAAoB,GAAG,YAAY,OAAO,EAAE,QAAQ,QAAQ,MAAM,wBAAwB,CAAC;AAC3F,QAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,SAAO,aAAa,KAAK,EAAE,OAAO,UAAU,0BAA0B,kBAAkB,EAAE,GAAG,EAAE,QAAQ,IAAI,CAAC;AAC9G;",
|
|
6
|
-
"names": []
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { AppContainer } from '@open-mercato/shared/lib/di/container'\nimport type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'\n\nimport { getCartCommand } from '../commands/cart'\nimport type { CartGetByTokenInput, CartSummary } from '../commands/cart'\nimport type { CartPriceChange } from './reprice'\nimport { isCartLineMutationLockTimeoutError } from './lineMutationLock'\nimport { isCartStoreContextUnavailableError, resolveCartRouteContext } from './routeContext'\nimport type { ResolveCartRouteContextOptions, ResolvedCartRouteContext } from './routeContext'\n\n// Step 4.7 \u2014 shared plumbing for the four `/api/cart/current/*` route files\n// (`current/route.ts`, `current/lines/route.ts`, `current/lines/[lineId]/route.ts`,\n// `current/lines/bulk/route.ts`). Kept here rather than duplicated per file because\n// every one of them needs the identical: resolve-the-store-context-at-the-edge\n// step (\u00A76a.1a), the `X-Cart-Token` header read (\u00A78.3/\u00A710, never a path segment or\n// query string), the token\u2192cart lookup via `cart.get`, and the same CrudHttpError /\n// interceptor-rejection \u2192 HTTP mapping.\n\n/**\n * \u00A78.1's literal resourceKind for every cart mutation's optimistic-lock AND\n * mutation-guard call \u2014 the lock helper's own `resourceKind` input, paired with\n * `resourceId: cart.id`, `current: cart.updatedAt` and `request: ctx.request`. Used\n * for line-level mutations too \u2014 a `CartLine` has its own `updated_at`, but the lock\n * token this module's concurrency contract tracks is the CART aggregate's, per\n * \u00A78.1's own text (which names `cart.cart` for every mutating command, not a\n * per-sub-resource kind).\n */\nexport const CART_RESOURCE_KIND = 'cart.cart'\n\n/** \u00A78.3/\u00A710: the ONLY place a cart token travels \u2014 never a path segment, never a query string. */\nexport const CART_TOKEN_HEADER = 'X-Cart-Token'\n\nexport function readCartToken(request: Request): string | null {\n const value = request.headers.get(CART_TOKEN_HEADER)\n const trimmed = value?.trim()\n return trimmed ? trimmed : null\n}\n\nexport type ResolvedContextResult =\n | { ok: true; routeContext: ResolvedCartRouteContext }\n | { ok: false; response: NextResponse }\n\n/**\n * Wraps `resolveCartRouteContext` (Step 4.3) and maps\n * `CartStoreContextUnavailableError` to a `503` \u2014 per that error's own doc comment\n * (\"the route layer must map this to a 503\") and this step's explicit brief.\n *\n * (Step 7.3) Accepts an optional `opts`, forwarded verbatim to\n * `resolveCartRouteContext` \u2014 D-R4-22's \"lock and merge routes resolve the buyer context\n * fresh (`{ fresh: true }`)\". Every pre-existing caller omits it and is unaffected\n * (`undefined` forwards to the same default `resolveCartRouteContext` already had).\n */\nexport async function resolveRouteContextOrError(\n request: Request,\n container: AppContainer,\n opts?: ResolveCartRouteContextOptions,\n): Promise<ResolvedContextResult> {\n try {\n const routeContext = await resolveCartRouteContext(request, container, opts)\n return { ok: true, routeContext }\n } catch (error) {\n if (isCartStoreContextUnavailableError(error)) {\n const { translate } = await resolveTranslations()\n return {\n ok: false,\n response: NextResponse.json(\n {\n error: translate(\n 'cart.errors.store_context_unavailable',\n 'Cart is unavailable for this store right now',\n ),\n code: 'cart_store_context_unavailable',\n },\n { status: 503 },\n ),\n }\n }\n throw error\n }\n}\n\n/**\n * Builds the `CommandRuntimeContext` every `/current/*` command call shares.\n * `auth` stays `null` \u2014 the storefront caller is anonymous, or a buyer with no\n * staff identity at all (`ResolvedCartRouteContext.customerUserId` is where an\n * identified buyer's identity already lives); this mirrors `cart.create`'s own\n * documented \"Pattern C\" assumption (`commands/cart.ts`). Tenant/organization scope\n * for the commands' own `ensureTenantScope`/`ensureOrganizationScope` defense-in-depth\n * checks comes from `selectedOrganizationId`, which the resolved store context\n * already pins \u2014 not from an (absent) staff `auth` scope.\n */\nexport function buildCommandContext(\n container: AppContainer,\n request: Request,\n routeContext: ResolvedCartRouteContext,\n): CommandRuntimeContext {\n return {\n container,\n auth: null,\n organizationScope: null,\n selectedOrganizationId: routeContext.organizationId,\n organizationIds: null,\n request,\n }\n}\n\n/**\n * (Step 5.1) `resolveCurrentCart`'s result. `priceChanges` is always an array (possibly\n * empty); `versionBumpReason` is set exactly when \u00A75.2 triggers 2-4 fired and `cart.get`\n * ran `cart.reprice` internally for this call \u2014 see `commands/cart.ts`'s `getCartCommand`.\n */\nexport type ResolvedCurrentCart = {\n cart: CartSummary\n priceChanges: CartPriceChange[]\n versionBumpReason?: 'repricing'\n}\n\n/**\n * Resolves the \"current\" cart addressed by the presented `X-Cart-Token`, scoped to\n * the resolved store's tenant/organization (\u00A78.3: `kind = 'basket'` only, via\n * `cart.get`). Returns `null` when the token does not resolve \u2014 a wrong token, a\n * cross-tenant token and a real `kind = 'proposal'` cart all collapse to the same\n * `null` here, matching `cart.get`'s own no-enumeration-oracle contract.\n *\n * (Step 5.1) Always supplies `freshContext: routeContext` \u2014 every one of the four\n * `/current/*` routes that calls this helper has already resolved a `StoreContext` at the\n * route edge, so `cart.get` always has a fresh buyer context to compare against, not only\n * on the literal `GET /current` route: \"per-request BuyerContext digest comparison (also\n * on GET)\" (PLAN.md Step 5.1) is implemented once, here, for every caller.\n */\nexport async function resolveCurrentCart(\n commandBus: CommandBus,\n ctx: CommandRuntimeContext,\n routeContext: ResolvedCartRouteContext,\n token: string,\n): Promise<ResolvedCurrentCart | null> {\n const input: CartGetByTokenInput = {\n tenantId: routeContext.tenantId,\n organizationId: routeContext.organizationId,\n token,\n freshContext: routeContext,\n }\n const { result } = await commandBus.execute<CartGetByTokenInput, Awaited<ReturnType<typeof getCartCommand.execute>>>(\n getCartCommand.id,\n { input, ctx },\n )\n if (!result.found) return null\n return {\n cart: result.cart,\n priceChanges: result.priceChanges ?? [],\n versionBumpReason: result.versionBumpReason,\n }\n}\n\nexport function notFoundCartResponse(): NextResponse {\n return NextResponse.json({ error: '[internal] cart not found' }, { status: 404 })\n}\n\nconst logger = createLogger('cart').child({ component: 'api' })\n\n/**\n * Standard catch-block mapping for every `/current/*` route: a thrown\n * `CrudHttpError` (badRequest/notFound/conflict from a command, or the standardized\n * optimistic-lock/idempotency-mismatch 409/422) echoes its own status/body; a\n * command-interceptor rejection (none exist for `cart` yet, but the mapping is the\n * same one `promotions/api/[id]/tree/route.ts` uses for an identical custom\n * command-backed route) echoes its own status/body; anything else is an\n * unexpected 500, logged and reported.\n */\nexport async function mapRouteErrorToResponse(error: unknown): Promise<NextResponse> {\n if (isCartLineMutationLockTimeoutError(error)) {\n const { translate } = await resolveTranslations()\n return NextResponse.json(\n { error: translate('cart.errors.busy', 'Cart is busy. Please try again.'), code: 'cart_busy' },\n { status: 503, headers: { 'Retry-After': '1', 'Cache-Control': 'no-store' } },\n )\n }\n if (isCrudHttpError(error)) return NextResponse.json(error.body, { status: error.status })\n const interceptorRejection = getCommandInterceptorHttpRejection(error)\n if (interceptorRejection) return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status })\n logger.error('cart api route failed', { err: error })\n getTelemetryRuntime()?.reportError(error, { module: 'cart', code: 'cart.api_route_failed' })\n const { translate } = await resolveTranslations()\n return NextResponse.json({ error: translate('cart.errors.unexpected', 'Unexpected error') }, { status: 500 })\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["translate"]
|
|
7
7
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
var _a, _b;
|
|
2
|
+
import { withAtomicFlush } from "@open-mercato/shared/lib/commands/flush";
|
|
3
|
+
import { calculateBackoffDelayMs } from "@open-mercato/shared/lib/delivery/retry";
|
|
4
|
+
const CART_LINE_LOCK_TIMEOUT = /* @__PURE__ */ Symbol.for("@open-mercato/cart:LineMutationLockTimeout");
|
|
5
|
+
class CartLineMutationLockTimeoutError extends (_b = Error, _a = CART_LINE_LOCK_TIMEOUT, _b) {
|
|
6
|
+
constructor() {
|
|
7
|
+
super("[internal] Cart line mutation lock wait deadline exceeded");
|
|
8
|
+
this[_a] = true;
|
|
9
|
+
this.name = "CartLineMutationLockTimeoutError";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function isCartLineMutationLockTimeoutError(error) {
|
|
13
|
+
return typeof error === "object" && error !== null && Reflect.get(error, CART_LINE_LOCK_TIMEOUT) === true;
|
|
14
|
+
}
|
|
15
|
+
async function withCartLineMutationLock(em, cartId, phases, label, waitDeadlineMs = 15e3) {
|
|
16
|
+
const key = `cart:lines:${cartId}`;
|
|
17
|
+
const deadline = Date.now() + waitDeadlineMs;
|
|
18
|
+
const busy = new Error("[internal] Cart line mutation lock is busy");
|
|
19
|
+
const joinsTransaction = em.isInTransaction();
|
|
20
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
21
|
+
try {
|
|
22
|
+
await withAtomicFlush(em, [
|
|
23
|
+
async () => {
|
|
24
|
+
if (joinsTransaction) {
|
|
25
|
+
await em.execute("select pg_advisory_xact_lock(hashtextextended(?::text, 0))", [key]);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const rows = await em.execute(
|
|
29
|
+
"select pg_try_advisory_xact_lock(hashtextextended(?::text, 0)) as locked",
|
|
30
|
+
[key]
|
|
31
|
+
);
|
|
32
|
+
if (!rows[0]?.locked) throw busy;
|
|
33
|
+
},
|
|
34
|
+
...phases
|
|
35
|
+
], { transaction: true, label });
|
|
36
|
+
return;
|
|
37
|
+
} catch (error) {
|
|
38
|
+
if (error !== busy || em.isInTransaction()) throw error;
|
|
39
|
+
}
|
|
40
|
+
const remainingMs = deadline - Date.now();
|
|
41
|
+
if (remainingMs <= 0) throw new CartLineMutationLockTimeoutError();
|
|
42
|
+
const delayMs = calculateBackoffDelayMs(Math.min(attempt, 5), { baseDelayMs: 50, maxJitterMs: 25 });
|
|
43
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(delayMs, remainingMs)));
|
|
44
|
+
if (Date.now() >= deadline) throw new CartLineMutationLockTimeoutError();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export {
|
|
48
|
+
CartLineMutationLockTimeoutError,
|
|
49
|
+
isCartLineMutationLockTimeoutError,
|
|
50
|
+
withCartLineMutationLock
|
|
51
|
+
};
|
|
52
|
+
//# sourceMappingURL=lineMutationLock.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/cart/lib/lineMutationLock.ts"],
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'\nimport { calculateBackoffDelayMs } from '@open-mercato/shared/lib/delivery/retry'\n\nconst CART_LINE_LOCK_TIMEOUT = Symbol.for('@open-mercato/cart:LineMutationLockTimeout')\n\nexport class CartLineMutationLockTimeoutError extends Error {\n readonly [CART_LINE_LOCK_TIMEOUT] = true\n\n constructor() {\n super('[internal] Cart line mutation lock wait deadline exceeded')\n this.name = 'CartLineMutationLockTimeoutError'\n }\n}\n\nexport function isCartLineMutationLockTimeoutError(error: unknown): error is CartLineMutationLockTimeoutError {\n return typeof error === 'object' && error !== null && Reflect.get(error, CART_LINE_LOCK_TIMEOUT) === true\n}\n\nexport async function withCartLineMutationLock(\n em: EntityManager,\n cartId: string,\n phases: Array<() => void | Promise<void>>,\n label: string,\n waitDeadlineMs = 15_000,\n): Promise<void> {\n const key = `cart:lines:${cartId}`\n const deadline = Date.now() + waitDeadlineMs\n const busy = new Error('[internal] Cart line mutation lock is busy')\n const joinsTransaction = em.isInTransaction()\n\n for (let attempt = 1; ; attempt += 1) {\n try {\n await withAtomicFlush(em, [\n async () => {\n if (joinsTransaction) {\n await em.execute('select pg_advisory_xact_lock(hashtextextended(?::text, 0))', [key])\n return\n }\n const rows = await em.execute<Array<{ locked: boolean }>>(\n 'select pg_try_advisory_xact_lock(hashtextextended(?::text, 0)) as locked',\n [key],\n )\n if (!rows[0]?.locked) throw busy\n },\n ...phases,\n ], { transaction: true, label })\n return\n } catch (error) {\n if (error !== busy || em.isInTransaction()) throw error\n }\n\n const remainingMs = deadline - Date.now()\n if (remainingMs <= 0) throw new CartLineMutationLockTimeoutError()\n const delayMs = calculateBackoffDelayMs(Math.min(attempt, 5), { baseDelayMs: 50, maxJitterMs: 25 })\n await new Promise((resolve) => setTimeout(resolve, Math.min(delayMs, remainingMs)))\n if (Date.now() >= deadline) throw new CartLineMutationLockTimeoutError()\n }\n}\n"],
|
|
5
|
+
"mappings": "AAAA;AACA,SAAS,uBAAuB;AAChC,SAAS,+BAA+B;AAExC,MAAM,yBAAyB,uBAAO,IAAI,4CAA4C;AAE/E,MAAM,0CAAyC,YAC1C,6BAD0C,IAAM;AAAA,EAG1D,cAAc;AACZ,UAAM,2DAA2D;AAHnE,SAAU,MAA0B;AAIlC,SAAK,OAAO;AAAA,EACd;AACF;AAEO,SAAS,mCAAmC,OAA2D;AAC5G,SAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,QAAQ,IAAI,OAAO,sBAAsB,MAAM;AACvG;AAEA,eAAsB,yBACpB,IACA,QACA,QACA,OACA,iBAAiB,MACF;AACf,QAAM,MAAM,cAAc,MAAM;AAChC,QAAM,WAAW,KAAK,IAAI,IAAI;AAC9B,QAAM,OAAO,IAAI,MAAM,4CAA4C;AACnE,QAAM,mBAAmB,GAAG,gBAAgB;AAE5C,WAAS,UAAU,KAAK,WAAW,GAAG;AACpC,QAAI;AACF,YAAM,gBAAgB,IAAI;AAAA,QACxB,YAAY;AACV,cAAI,kBAAkB;AACpB,kBAAM,GAAG,QAAQ,8DAA8D,CAAC,GAAG,CAAC;AACpF;AAAA,UACF;AACA,gBAAM,OAAO,MAAM,GAAG;AAAA,YACpB;AAAA,YACA,CAAC,GAAG;AAAA,UACN;AACA,cAAI,CAAC,KAAK,CAAC,GAAG,OAAQ,OAAM;AAAA,QAC9B;AAAA,QACA,GAAG;AAAA,MACL,GAAG,EAAE,aAAa,MAAM,MAAM,CAAC;AAC/B;AAAA,IACF,SAAS,OAAO;AACd,UAAI,UAAU,QAAQ,GAAG,gBAAgB,EAAG,OAAM;AAAA,IACpD;AAEA,UAAM,cAAc,WAAW,KAAK,IAAI;AACxC,QAAI,eAAe,EAAG,OAAM,IAAI,iCAAiC;AACjE,UAAM,UAAU,wBAAwB,KAAK,IAAI,SAAS,CAAC,GAAG,EAAE,aAAa,IAAI,aAAa,GAAG,CAAC;AAClG,UAAM,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,KAAK,IAAI,SAAS,WAAW,CAAC,CAAC;AAClF,QAAI,KAAK,IAAI,KAAK,SAAU,OAAM,IAAI,iCAAiC;AAAA,EACzE;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -186,8 +186,9 @@ const documentUpdateFieldsSchema = z.object({
|
|
|
186
186
|
currencyCode: currencyCodeSchema.optional(),
|
|
187
187
|
// Order-only columns — SalesQuote declares none of them. Declared here
|
|
188
188
|
// because the create schema declares them and a caller reusing its create
|
|
189
|
-
// payload on an update otherwise lost them silently
|
|
190
|
-
// rejects
|
|
189
|
+
// payload on an update otherwise lost them silently. `applyDocumentUpdate`
|
|
190
|
+
// rejects them on a quote, except `totalsMode`, which is ignored to preserve
|
|
191
|
+
// quote-create compatibility without enabling external quote amounts.
|
|
191
192
|
exchangeRate: z.union([z.null(), exchangeRateSchema]).optional(),
|
|
192
193
|
paymentStatusEntryId: z.string().uuid().nullable().optional(),
|
|
193
194
|
fulfillmentStatusEntryId: z.string().uuid().nullable().optional(),
|
|
@@ -4110,7 +4111,6 @@ const updateQuoteCommand = {
|
|
|
4110
4111
|
},
|
|
4111
4112
|
async execute(rawInput, ctx) {
|
|
4112
4113
|
const parsed = documentUpdateSchema.parse(rawInput ?? {});
|
|
4113
|
-
await assertAmountsModeUnsupportedOnQuote([parsed]);
|
|
4114
4114
|
documentEditSchema.parse(rawInput ?? {});
|
|
4115
4115
|
const em = ctx.container.resolve("em").fork();
|
|
4116
4116
|
let quote;
|