@open-mercato/core 0.10.1-develop.7723.1.3b2ea82ea7 → 0.10.1-develop.7726.1.a447cc5525

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 (81) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/generated/entities/sales_order/index.js +2 -0
  3. package/dist/generated/entities/sales_order/index.js.map +2 -2
  4. package/dist/generated/entities/sales_order_line/index.js +2 -0
  5. package/dist/generated/entities/sales_order_line/index.js.map +2 -2
  6. package/dist/generated/entity-fields-registry.js +2 -0
  7. package/dist/generated/entity-fields-registry.js.map +2 -2
  8. package/dist/modules/planner/api/availability-date-specific.js +2 -7
  9. package/dist/modules/planner/api/availability-date-specific.js.map +2 -2
  10. package/dist/modules/planner/commands/availability-date-specific.js +14 -31
  11. package/dist/modules/planner/commands/availability-date-specific.js.map +2 -2
  12. package/dist/modules/planner/commands/availability-weekly.js +8 -22
  13. package/dist/modules/planner/commands/availability-weekly.js.map +2 -2
  14. package/dist/modules/planner/lib/availabilityMerge.js +10 -18
  15. package/dist/modules/planner/lib/availabilityMerge.js.map +2 -2
  16. package/dist/modules/planner/lib/availabilityTimezone.js +161 -0
  17. package/dist/modules/planner/lib/availabilityTimezone.js.map +7 -0
  18. package/dist/modules/sales/api/documents/factory.js +3 -0
  19. package/dist/modules/sales/api/documents/factory.js.map +2 -2
  20. package/dist/modules/sales/api/order-lines/route.js +2 -1
  21. package/dist/modules/sales/api/order-lines/route.js.map +2 -2
  22. package/dist/modules/sales/backend/sales/documents/[id]/page.js +83 -6
  23. package/dist/modules/sales/backend/sales/documents/[id]/page.js.map +2 -2
  24. package/dist/modules/sales/commands/documents.js +141 -18
  25. package/dist/modules/sales/commands/documents.js.map +2 -2
  26. package/dist/modules/sales/commands/returns.js +24 -7
  27. package/dist/modules/sales/commands/returns.js.map +2 -2
  28. package/dist/modules/sales/components/documents/AdjustmentsSection.js +17 -11
  29. package/dist/modules/sales/components/documents/AdjustmentsSection.js.map +2 -2
  30. package/dist/modules/sales/components/documents/DocumentTotals.js +21 -3
  31. package/dist/modules/sales/components/documents/DocumentTotals.js.map +2 -2
  32. package/dist/modules/sales/components/documents/ItemsSection.js +23 -12
  33. package/dist/modules/sales/components/documents/ItemsSection.js.map +2 -2
  34. package/dist/modules/sales/data/entities.js +10 -0
  35. package/dist/modules/sales/data/entities.js.map +2 -2
  36. package/dist/modules/sales/data/validators.js +9 -1
  37. package/dist/modules/sales/data/validators.js.map +2 -2
  38. package/dist/modules/sales/lib/calculations.js +85 -6
  39. package/dist/modules/sales/lib/calculations.js.map +2 -2
  40. package/dist/modules/sales/lib/externalAmounts.js +143 -0
  41. package/dist/modules/sales/lib/externalAmounts.js.map +7 -0
  42. package/dist/modules/sales/lib/lineSnapshots.js +6 -1
  43. package/dist/modules/sales/lib/lineSnapshots.js.map +2 -2
  44. package/dist/modules/sales/lib/makeSalesLineRoute.js +8 -2
  45. package/dist/modules/sales/lib/makeSalesLineRoute.js.map +2 -2
  46. package/dist/modules/sales/lib/providers/totals.js +3 -2
  47. package/dist/modules/sales/lib/providers/totals.js.map +2 -2
  48. package/dist/modules/sales/migrations/Migration20260910120000_sales_external_amounts_mode.js +19 -0
  49. package/dist/modules/sales/migrations/Migration20260910120000_sales_external_amounts_mode.js.map +7 -0
  50. package/generated/entities/sales_order/index.ts +1 -0
  51. package/generated/entities/sales_order_line/index.ts +1 -0
  52. package/generated/entity-fields-registry.ts +2 -0
  53. package/package.json +8 -7
  54. package/src/modules/planner/api/availability-date-specific.ts +5 -10
  55. package/src/modules/planner/commands/availability-date-specific.ts +14 -33
  56. package/src/modules/planner/commands/availability-weekly.ts +15 -24
  57. package/src/modules/planner/lib/availabilityMerge.ts +13 -19
  58. package/src/modules/planner/lib/availabilityTimezone.ts +242 -0
  59. package/src/modules/sales/api/documents/factory.ts +3 -0
  60. package/src/modules/sales/api/order-lines/route.ts +2 -1
  61. package/src/modules/sales/backend/sales/documents/[id]/page.tsx +96 -0
  62. package/src/modules/sales/commands/documents.ts +219 -44
  63. package/src/modules/sales/commands/returns.ts +27 -3
  64. package/src/modules/sales/components/documents/AdjustmentsSection.tsx +34 -25
  65. package/src/modules/sales/components/documents/DocumentTotals.tsx +26 -2
  66. package/src/modules/sales/components/documents/ItemsSection.tsx +27 -12
  67. package/src/modules/sales/data/entities.ts +14 -0
  68. package/src/modules/sales/data/validators.ts +10 -0
  69. package/src/modules/sales/i18n/de.json +12 -0
  70. package/src/modules/sales/i18n/en.json +12 -0
  71. package/src/modules/sales/i18n/es.json +12 -0
  72. package/src/modules/sales/i18n/ko.json +12 -0
  73. package/src/modules/sales/i18n/pl.json +12 -0
  74. package/src/modules/sales/lib/calculations.ts +99 -4
  75. package/src/modules/sales/lib/externalAmounts.ts +174 -0
  76. package/src/modules/sales/lib/lineSnapshots.ts +6 -1
  77. package/src/modules/sales/lib/makeSalesLineRoute.ts +8 -0
  78. package/src/modules/sales/lib/providers/totals.ts +5 -2
  79. package/src/modules/sales/lib/types.ts +19 -2
  80. package/src/modules/sales/migrations/.snapshot-open-mercato.json +33 -1
  81. package/src/modules/sales/migrations/Migration20260910120000_sales_external_amounts_mode.ts +25 -0
@@ -9,6 +9,7 @@ import { CrudHttpError, isCrudHttpError } from "@open-mercato/shared/lib/crud/er
9
9
  import { parseScopedCommandInput } from "@open-mercato/shared/lib/api/scoped";
10
10
  import { findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
11
11
  import { plannerAvailabilityDateSpecificReplaceSchema } from "../data/validators.js";
12
+ import { zonedDateKey } from "../lib/availabilityTimezone.js";
12
13
  import { serializeOperationMetadata } from "@open-mercato/shared/lib/commands/operationMetadata";
13
14
  import {
14
15
  runCrudMutationGuardAfterSuccess,
@@ -80,7 +81,7 @@ async function POST(req) {
80
81
  const blocked = rules.some((rule) => {
81
82
  const window = parseAvailabilityRuleWindow(rule);
82
83
  if (window.repeat !== "once") return false;
83
- return dateSet.has(formatDateKey(window.startAt));
84
+ return dateSet.has(zonedDateKey(window.startAt, rule.timezone));
84
85
  });
85
86
  if (blocked) {
86
87
  throw new CrudHttpError(403, { error: translate("planner.availability.errors.unauthorized", "Unauthorized") });
@@ -190,12 +191,6 @@ function resolveDateSet(input) {
190
191
  }
191
192
  return dates;
192
193
  }
193
- function formatDateKey(value) {
194
- const year = value.getFullYear();
195
- const month = String(value.getMonth() + 1).padStart(2, "0");
196
- const day = String(value.getDate()).padStart(2, "0");
197
- return `${year}-${month}-${day}`;
198
- }
199
194
  export {
200
195
  POST,
201
196
  metadata,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/planner/api/availability-date-specific.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { resolveSingleOrganizationIdOrDeny } from '@open-mercato/core/modules/directory/utils/organizationScopeFilter'\nimport type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { parseScopedCommandInput } from '@open-mercato/shared/lib/api/scoped'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { plannerAvailabilityDateSpecificReplaceSchema } from '../data/validators'\nimport { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport { PlannerAvailabilityRule } from '../data/entities'\nimport { parseAvailabilityRuleWindow } from '../lib/availabilitySchedule'\nimport { assertAvailabilityWriteAccess, resolveAvailabilityActorId } from './access'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('planner').child({ component: 'availability' })\n\nexport const metadata = {\n POST: { requireAuth: true },\n}\n\ntype RequestContext = {\n ctx: CommandRuntimeContext\n}\n\nasync function resolveRequestContext(req: Request): Promise<RequestContext> {\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n const { translate } = await resolveTranslations()\n\n if (!auth || !auth.tenantId) {\n throw new CrudHttpError(401, { error: translate('planner.availability.errors.unauthorized', 'Unauthorized') })\n }\n\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const organizationId = resolveSingleOrganizationIdOrDeny(scope, auth) ?? null\n if (!organizationId) {\n throw new CrudHttpError(400, {\n error: translate('planner.availability.errors.organizationRequired', 'Organization context is required'),\n })\n }\n\n const ctx: CommandRuntimeContext = {\n container,\n auth,\n organizationScope: scope,\n selectedOrganizationId: organizationId,\n organizationIds: scope?.filterIds ?? (auth.orgId ? [auth.orgId] : null),\n request: req,\n }\n\n return { ctx }\n}\n\nexport async function POST(req: Request) {\n try {\n const { ctx } = await resolveRequestContext(req)\n const { translate } = await resolveTranslations()\n const payload = await req.json().catch(() => ({}))\n const normalized = normalizeDateSpecificPayload(payload)\n const input = parseScopedCommandInput(plannerAvailabilityDateSpecificReplaceSchema, normalized, ctx, translate)\n const isUnavailability = input.kind === 'unavailability' || input.isAvailable === false\n const access = await assertAvailabilityWriteAccess(\n ctx,\n { subjectType: input.subjectType, subjectId: input.subjectId, requiresUnavailability: isUnavailability },\n translate,\n )\n if (!access.canManageAll && !access.canManageUnavailability) {\n const dateSet = resolveDateSet(input)\n if (dateSet.size) {\n const em = ctx.container.resolve('em') as EntityManager\n const rules = await findWithDecryption(\n em,\n PlannerAvailabilityRule,\n {\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n subjectType: input.subjectType,\n subjectId: input.subjectId,\n kind: 'unavailability',\n deletedAt: null,\n },\n undefined,\n { tenantId: input.tenantId, organizationId: input.organizationId },\n )\n const blocked = rules.some((rule) => {\n const window = parseAvailabilityRuleWindow(rule)\n if (window.repeat !== 'once') return false\n return dateSet.has(formatDateKey(window.startAt))\n })\n if (blocked) {\n throw new CrudHttpError(403, { error: translate('planner.availability.errors.unauthorized', 'Unauthorized') })\n }\n }\n }\n const guardInput = {\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n userId: resolveAvailabilityActorId(ctx.auth),\n resourceKind: 'planner.availability',\n resourceId: input.subjectId,\n operation: 'custom' as const,\n requestMethod: req.method,\n requestHeaders: req.headers,\n }\n const guardResult = await validateCrudMutationGuard(ctx.container, { ...guardInput, mutationPayload: input })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n const commandBus = ctx.container.resolve('commandBus') as CommandBus\n const { logEntry } = await commandBus.execute('planner.availability.date-specific.replace', { input, ctx })\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(ctx.container, { ...guardInput, metadata: guardResult.metadata ?? null })\n }\n const response = NextResponse.json({ ok: true })\n if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {\n response.headers.set(\n 'x-om-operation',\n serializeOperationMetadata({\n id: logEntry.id,\n undoToken: logEntry.undoToken,\n commandId: logEntry.commandId,\n actionLabel: logEntry.actionLabel ?? null,\n resourceKind: logEntry.resourceKind ?? 'planner.availability',\n resourceId: logEntry.resourceId ?? null,\n executedAt: logEntry.createdAt instanceof Date ? logEntry.createdAt.toISOString() : undefined,\n })\n )\n }\n return response\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n const { translate } = await resolveTranslations()\n logger.error('Date-specific availability replace failed', { err })\n return NextResponse.json(\n { error: translate('planner.availability.errors.updateDateSpecific', 'Failed to save date-specific availability.') },\n { status: 400 },\n )\n }\n}\n\nexport const openApi = {\n tag: 'Planner',\n summary: 'Replace date-specific availability',\n methods: {\n POST: {\n summary: 'Replace date-specific availability',\n description: 'Replaces date-specific availability rules for the subject in a single request.',\n requestBody: {\n contentType: 'application/json',\n schema: plannerAvailabilityDateSpecificReplaceSchema,\n },\n responses: [\n { status: 200, description: 'Date-specific availability updated', schema: z.object({ ok: z.literal(true) }) },\n { status: 400, description: 'Invalid payload', schema: z.object({ error: z.string() }) },\n { status: 401, description: 'Unauthorized', schema: z.object({ error: z.string() }) },\n { status: 403, description: 'Forbidden', schema: z.object({ error: z.string() }) },\n {\n status: 409,\n description: 'Conflict \u2014 the subject/date availability was modified by another edit',\n schema: z.object({\n error: z.string(),\n code: z.string(),\n currentUpdatedAt: z.string(),\n expectedUpdatedAt: z.string(),\n }),\n },\n ],\n },\n },\n}\n\ntype DateSpecificPayload = {\n date?: string\n dates?: string[]\n kind?: string\n isAvailable?: boolean\n [key: string]: unknown\n}\n\nfunction normalizeDateSpecificPayload(payload: unknown): DateSpecificPayload {\n if (!payload || typeof payload !== 'object') return {}\n const data = { ...(payload as Record<string, unknown>) } as DateSpecificPayload\n if (!data.date && Array.isArray(data.dates) && data.dates.length > 0) {\n const first = data.dates.find((value) => typeof value === 'string' && value.length > 0)\n if (first) data.date = first\n }\n if (data.isAvailable === undefined && typeof data.kind === 'string') {\n data.isAvailable = data.kind !== 'unavailability'\n }\n return data\n}\n\nfunction resolveDateSet(input: { date?: string; dates?: string[] }): Set<string> {\n const dates = new Set<string>()\n if (typeof input.date === 'string' && input.date.length > 0) {\n dates.add(input.date)\n }\n if (Array.isArray(input.dates)) {\n input.dates.forEach((value) => {\n if (typeof value === 'string' && value.length > 0) {\n dates.add(value)\n }\n })\n }\n return dates\n}\n\nfunction formatDateKey(value: Date): string {\n const year = value.getFullYear()\n const month = String(value.getMonth() + 1).padStart(2, '0')\n const day = String(value.getDate()).padStart(2, '0')\n return `${year}-${month}-${day}`\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 { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { resolveSingleOrganizationIdOrDeny } from '@open-mercato/core/modules/directory/utils/organizationScopeFilter'\nimport type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { parseScopedCommandInput } from '@open-mercato/shared/lib/api/scoped'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { plannerAvailabilityDateSpecificReplaceSchema } from '../data/validators'\nimport { zonedDateKey } from '../lib/availabilityTimezone'\nimport { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport { PlannerAvailabilityRule } from '../data/entities'\nimport { parseAvailabilityRuleWindow } from '../lib/availabilitySchedule'\nimport { assertAvailabilityWriteAccess, resolveAvailabilityActorId } from './access'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('planner').child({ component: 'availability' })\n\nexport const metadata = {\n POST: { requireAuth: true },\n}\ntype RequestContext = {\n ctx: CommandRuntimeContext\n}\nasync function resolveRequestContext(req: Request): Promise<RequestContext> {\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n const { translate } = await resolveTranslations()\n\n if (!auth || !auth.tenantId) {\n throw new CrudHttpError(401, { error: translate('planner.availability.errors.unauthorized', 'Unauthorized') })\n }\n\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const organizationId = resolveSingleOrganizationIdOrDeny(scope, auth) ?? null\n if (!organizationId) {\n throw new CrudHttpError(400, {\n error: translate('planner.availability.errors.organizationRequired', 'Organization context is required'),\n })\n }\n\n const ctx: CommandRuntimeContext = {\n container,\n auth,\n organizationScope: scope,\n selectedOrganizationId: organizationId,\n organizationIds: scope?.filterIds ?? (auth.orgId ? [auth.orgId] : null),\n request: req,\n }\n\n return { ctx }\n}\n\nexport async function POST(req: Request) {\n try {\n const { ctx } = await resolveRequestContext(req)\n const { translate } = await resolveTranslations()\n const payload = await req.json().catch(() => ({}))\n const normalized = normalizeDateSpecificPayload(payload)\n const input = parseScopedCommandInput(plannerAvailabilityDateSpecificReplaceSchema, normalized, ctx, translate)\n const isUnavailability = input.kind === 'unavailability' || input.isAvailable === false\n const access = await assertAvailabilityWriteAccess(\n ctx,\n { subjectType: input.subjectType, subjectId: input.subjectId, requiresUnavailability: isUnavailability },\n translate,\n )\n if (!access.canManageAll && !access.canManageUnavailability) {\n const dateSet = resolveDateSet(input)\n if (dateSet.size) {\n const em = ctx.container.resolve('em') as EntityManager\n const rules = await findWithDecryption(\n em,\n PlannerAvailabilityRule,\n {\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n subjectType: input.subjectType,\n subjectId: input.subjectId,\n kind: 'unavailability',\n deletedAt: null,\n },\n undefined,\n { tenantId: input.tenantId, organizationId: input.organizationId },\n )\n const blocked = rules.some((rule) => {\n const window = parseAvailabilityRuleWindow(rule)\n if (window.repeat !== 'once') return false\n // Resolved in the rule's own zone so this gate agrees with\n // `planner.availability.date-specific.replace` and does not depend\n // on the host clock.\n return dateSet.has(zonedDateKey(window.startAt, rule.timezone))\n })\n if (blocked) {\n throw new CrudHttpError(403, { error: translate('planner.availability.errors.unauthorized', 'Unauthorized') })\n }\n }\n }\n const guardInput = {\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n userId: resolveAvailabilityActorId(ctx.auth),\n resourceKind: 'planner.availability',\n resourceId: input.subjectId,\n operation: 'custom' as const,\n requestMethod: req.method,\n requestHeaders: req.headers,\n }\n const guardResult = await validateCrudMutationGuard(ctx.container, { ...guardInput, mutationPayload: input })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n const commandBus = ctx.container.resolve('commandBus') as CommandBus\n const { logEntry } = await commandBus.execute('planner.availability.date-specific.replace', { input, ctx })\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(ctx.container, { ...guardInput, metadata: guardResult.metadata ?? null })\n }\n const response = NextResponse.json({ ok: true })\n if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {\n response.headers.set(\n 'x-om-operation',\n serializeOperationMetadata({\n id: logEntry.id,\n undoToken: logEntry.undoToken,\n commandId: logEntry.commandId,\n actionLabel: logEntry.actionLabel ?? null,\n resourceKind: logEntry.resourceKind ?? 'planner.availability',\n resourceId: logEntry.resourceId ?? null,\n executedAt: logEntry.createdAt instanceof Date ? logEntry.createdAt.toISOString() : undefined,\n })\n )\n }\n return response\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n const { translate } = await resolveTranslations()\n logger.error('Date-specific availability replace failed', { err })\n return NextResponse.json(\n { error: translate('planner.availability.errors.updateDateSpecific', 'Failed to save date-specific availability.') },\n { status: 400 },\n )\n }\n}\n\nexport const openApi = {\n tag: 'Planner',\n summary: 'Replace date-specific availability',\n methods: {\n POST: {\n summary: 'Replace date-specific availability',\n description: 'Replaces date-specific availability rules for the subject in a single request.',\n requestBody: {\n contentType: 'application/json',\n schema: plannerAvailabilityDateSpecificReplaceSchema,\n },\n responses: [\n { status: 200, description: 'Date-specific availability updated', schema: z.object({ ok: z.literal(true) }) },\n { status: 400, description: 'Invalid payload', schema: z.object({ error: z.string() }) },\n { status: 401, description: 'Unauthorized', schema: z.object({ error: z.string() }) },\n { status: 403, description: 'Forbidden', schema: z.object({ error: z.string() }) },\n {\n status: 409,\n description: 'Conflict \u2014 the subject/date availability was modified by another edit',\n schema: z.object({\n error: z.string(),\n code: z.string(),\n currentUpdatedAt: z.string(),\n expectedUpdatedAt: z.string(),\n }),\n },\n ],\n },\n },\n}\n\ntype DateSpecificPayload = {\n date?: string\n dates?: string[]\n kind?: string\n isAvailable?: boolean\n [key: string]: unknown\n}\n\nfunction normalizeDateSpecificPayload(payload: unknown): DateSpecificPayload {\n if (!payload || typeof payload !== 'object') return {}\n const data = { ...(payload as Record<string, unknown>) } as DateSpecificPayload\n if (!data.date && Array.isArray(data.dates) && data.dates.length > 0) {\n const first = data.dates.find((value) => typeof value === 'string' && value.length > 0)\n if (first) data.date = first\n }\n if (data.isAvailable === undefined && typeof data.kind === 'string') {\n data.isAvailable = data.kind !== 'unavailability'\n }\n return data\n}\n\nfunction resolveDateSet(input: { date?: string; dates?: string[] }): Set<string> {\n const dates = new Set<string>()\n if (typeof input.date === 'string' && input.date.length > 0) {\n dates.add(input.date)\n }\n if (Array.isArray(input.dates)) {\n input.dates.forEach((value) => {\n if (typeof value === 'string' && value.length > 0) {\n dates.add(value)\n }\n })\n }\n return dates\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,6 +1,7 @@
1
1
  import { registerCommand } from "@open-mercato/shared/lib/commands";
2
2
  import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
3
3
  import { parseAvailabilityRuleWindow } from "@open-mercato/core/modules/planner/lib/availabilitySchedule";
4
+ import { warnOnUnresolvableTimeZone, zonedDateKey, zonedDayBounds, zonedWallTimeToInstant } from "@open-mercato/core/modules/planner/lib/availabilityTimezone";
4
5
  import { PlannerAvailabilityRule } from "../data/entities.js";
5
6
  import {
6
7
  plannerAvailabilityDateSpecificReplaceSchema
@@ -17,21 +18,9 @@ function resolveAggregateLockVersion(rules) {
17
18
  }
18
19
  return latest;
19
20
  }
20
- function parseTimeInput(value) {
21
- const [hours, minutes] = value.split(":").map((part) => Number(part));
22
- if (!Number.isFinite(hours) || !Number.isFinite(minutes)) return null;
23
- if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) return null;
24
- return { hours, minutes };
25
- }
26
- function toDateForDay(value, time) {
27
- if (!value) return null;
28
- const parsed = parseTimeInput(time);
29
- if (!parsed) return null;
30
- const parts = value.split("-").map((part) => Number(part));
31
- if (parts.length !== 3 || parts.some((part) => Number.isNaN(part))) return null;
32
- const [year, month, day] = parts;
33
- const date = new Date(year, month - 1, day, parsed.hours, parsed.minutes, 0, 0);
34
- return Number.isNaN(date.getTime()) ? null : date;
21
+ function toDateForDay(dateKey, time, timeZone) {
22
+ if (!dateKey) return null;
23
+ return zonedWallTimeToInstant(dateKey, time, timeZone);
35
24
  }
36
25
  function formatDuration(minutes) {
37
26
  const clamped = Math.max(1, minutes);
@@ -49,17 +38,10 @@ function buildAvailabilityRrule(start, end) {
49
38
  DURATION:${duration}
50
39
  RRULE:FREQ=DAILY;COUNT=1`;
51
40
  }
52
- function buildFullDayRrule(date) {
53
- const start = toDateForDay(date, "00:00");
54
- if (!start) return null;
55
- const end = new Date(start.getTime() + 24 * 60 * 60 * 1e3);
56
- return buildAvailabilityRrule(start, end);
57
- }
58
- function formatDateKey(value) {
59
- const year = value.getFullYear();
60
- const month = String(value.getMonth() + 1).padStart(2, "0");
61
- const day = String(value.getDate()).padStart(2, "0");
62
- return `${year}-${month}-${day}`;
41
+ function buildFullDayRrule(date, timeZone) {
42
+ const bounds = zonedDayBounds(date, timeZone);
43
+ if (!bounds) return null;
44
+ return buildAvailabilityRrule(bounds.start, bounds.end);
63
45
  }
64
46
  function toAvailabilityRuleSnapshot(record) {
65
47
  return {
@@ -90,7 +72,7 @@ async function loadDateSpecificSnapshots(em, params) {
90
72
  return existing.filter((rule) => {
91
73
  const window = parseAvailabilityRuleWindow(rule);
92
74
  if (window.repeat !== "once") return false;
93
- return params.dates.has(formatDateKey(window.startAt));
75
+ return params.dates.has(zonedDateKey(window.startAt, rule.timezone));
94
76
  }).map(toAvailabilityRuleSnapshot);
95
77
  }
96
78
  async function restoreAvailabilityRuleFromSnapshot(em, snapshot) {
@@ -146,6 +128,7 @@ const replaceDateSpecificAvailabilityCommand = {
146
128
  },
147
129
  async execute(input, ctx) {
148
130
  const parsed = plannerAvailabilityDateSpecificReplaceSchema.parse(input);
131
+ warnOnUnresolvableTimeZone(parsed.timezone, "planner.availability.date-specific.replace");
149
132
  ensureTenantScope(ctx, parsed.tenantId);
150
133
  ensureOrganizationScope(ctx, parsed.organizationId);
151
134
  const dates = resolveDateSet(parsed);
@@ -169,7 +152,7 @@ const replaceDateSpecificAvailabilityCommand = {
169
152
  const toDelete = existing.filter((rule) => {
170
153
  const window = parseAvailabilityRuleWindow(rule);
171
154
  if (window.repeat !== "once") return false;
172
- return dates.has(formatDateKey(window.startAt));
155
+ return dates.has(zonedDateKey(window.startAt, rule.timezone));
173
156
  });
174
157
  enforceCommandOptimisticLock({
175
158
  resourceKind: AVAILABILITY_RULE_RESOURCE_KIND,
@@ -188,7 +171,7 @@ const replaceDateSpecificAvailabilityCommand = {
188
171
  if (!dates.size) return;
189
172
  if (!isAvailable) {
190
173
  dates.forEach((date) => {
191
- const rrule = buildFullDayRrule(date);
174
+ const rrule = buildFullDayRrule(date, parsed.timezone);
192
175
  if (!rrule) return;
193
176
  const record = trx.create(PlannerAvailabilityRule, {
194
177
  tenantId: parsed.tenantId,
@@ -210,8 +193,8 @@ const replaceDateSpecificAvailabilityCommand = {
210
193
  } else {
211
194
  dates.forEach((date) => {
212
195
  windows.forEach((window) => {
213
- const start = toDateForDay(date, window.start);
214
- const end = toDateForDay(date, window.end);
196
+ const start = toDateForDay(date, window.start, parsed.timezone);
197
+ const end = toDateForDay(date, window.end, parsed.timezone);
215
198
  if (!start || !end || start >= end) return;
216
199
  const rrule = buildAvailabilityRrule(start, end);
217
200
  const record = trx.create(PlannerAvailabilityRule, {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/planner/commands/availability-date-specific.ts"],
4
- "sourcesContent": ["import type { CommandHandler } from '@open-mercato/shared/lib/commands'\nimport { registerCommand } from '@open-mercato/shared/lib/commands'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { parseAvailabilityRuleWindow } from '@open-mercato/core/modules/planner/lib/availabilitySchedule'\nimport { PlannerAvailabilityRule } from '../data/entities'\nimport {\n plannerAvailabilityDateSpecificReplaceSchema,\n type PlannerAvailabilityDateSpecificReplaceInput,\n} from '../data/validators'\nimport { ensureOrganizationScope, ensureTenantScope, extractUndoPayload } from './shared'\nimport { enforceCommandOptimisticLock } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport type { PlannerAvailabilityKind, PlannerAvailabilitySubjectType } from '../data/entities'\n\nconst AVAILABILITY_RULE_RESOURCE_KIND = 'planner.availability.rule'\n\n/**\n * The date-specific aggregate (the one-off rules for a subject/date) has no\n * single parent row, so its optimistic-lock version is the latest `updated_at`\n * of the rules being replaced. The editor sends that token via the extension\n * header; an empty set means \"nothing to clobber\" \u2192 no current version \u2192 the\n * guard is a no-op (strictly additive, fail-open).\n */\nfunction resolveAggregateLockVersion(rules: PlannerAvailabilityRule[]): Date | null {\n let latest: Date | null = null\n for (const rule of rules) {\n const value = rule.updatedAt\n if (!value || Number.isNaN(value.getTime())) continue\n if (!latest || value.getTime() > latest.getTime()) latest = value\n }\n return latest\n}\n\ntype AvailabilityRuleSnapshot = {\n id: string\n tenantId: string\n organizationId: string\n subjectType: PlannerAvailabilitySubjectType\n subjectId: string\n timezone: string\n rrule: string\n exdates: string[]\n kind: PlannerAvailabilityKind\n note: string | null\n unavailabilityReasonEntryId: string | null\n unavailabilityReasonValue: string | null\n deletedAt: Date | null\n}\n\ntype DateSpecificUndoPayload = {\n before: AvailabilityRuleSnapshot[]\n after: AvailabilityRuleSnapshot[]\n}\n\nfunction parseTimeInput(value: string): { hours: number; minutes: number } | null {\n const [hours, minutes] = value.split(':').map((part) => Number(part))\n if (!Number.isFinite(hours) || !Number.isFinite(minutes)) return null\n if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) return null\n return { hours, minutes }\n}\n\nfunction toDateForDay(value: string, time: string): Date | null {\n if (!value) return null\n const parsed = parseTimeInput(time)\n if (!parsed) return null\n const parts = value.split('-').map((part) => Number(part))\n if (parts.length !== 3 || parts.some((part) => Number.isNaN(part))) return null\n const [year, month, day] = parts\n const date = new Date(year, month - 1, day, parsed.hours, parsed.minutes, 0, 0)\n return Number.isNaN(date.getTime()) ? null : date\n}\n\nfunction formatDuration(minutes: number): string {\n const clamped = Math.max(1, minutes)\n const hours = Math.floor(clamped / 60)\n const mins = clamped % 60\n if (hours > 0 && mins > 0) return `PT${hours}H${mins}M`\n if (hours > 0) return `PT${hours}H`\n return `PT${mins}M`\n}\n\nfunction buildAvailabilityRrule(start: Date, end: Date): string {\n const dtStart = start.toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z'\n const durationMinutes = Math.max(1, Math.round((end.getTime() - start.getTime()) / 60000))\n const duration = formatDuration(durationMinutes)\n return `DTSTART:${dtStart}\\nDURATION:${duration}\\nRRULE:FREQ=DAILY;COUNT=1`\n}\n\nfunction buildFullDayRrule(date: string): string | null {\n const start = toDateForDay(date, '00:00')\n if (!start) return null\n const end = new Date(start.getTime() + 24 * 60 * 60 * 1000)\n return buildAvailabilityRrule(start, end)\n}\n\nfunction formatDateKey(value: Date): string {\n const year = value.getFullYear()\n const month = String(value.getMonth() + 1).padStart(2, '0')\n const day = String(value.getDate()).padStart(2, '0')\n return `${year}-${month}-${day}`\n}\n\nfunction toAvailabilityRuleSnapshot(record: PlannerAvailabilityRule): AvailabilityRuleSnapshot {\n return {\n id: record.id,\n tenantId: record.tenantId,\n organizationId: record.organizationId,\n subjectType: record.subjectType,\n subjectId: record.subjectId,\n timezone: record.timezone,\n rrule: record.rrule,\n exdates: [...(record.exdates ?? [])],\n kind: record.kind,\n note: record.note ?? null,\n unavailabilityReasonEntryId: record.unavailabilityReasonEntryId ?? null,\n unavailabilityReasonValue: record.unavailabilityReasonValue ?? null,\n deletedAt: record.deletedAt ?? null,\n }\n}\n\nasync function loadDateSpecificSnapshots(\n em: EntityManager,\n params: {\n tenantId: string\n organizationId: string\n subjectType: PlannerAvailabilitySubjectType\n subjectId: string\n dates: Set<string>\n }\n): Promise<AvailabilityRuleSnapshot[]> {\n if (!params.dates.size) return []\n const existing = await em.find(PlannerAvailabilityRule, {\n tenantId: params.tenantId,\n organizationId: params.organizationId,\n subjectType: params.subjectType,\n subjectId: params.subjectId,\n deletedAt: null,\n })\n return existing\n .filter((rule) => {\n const window = parseAvailabilityRuleWindow(rule)\n if (window.repeat !== 'once') return false\n return params.dates.has(formatDateKey(window.startAt))\n })\n .map(toAvailabilityRuleSnapshot)\n}\n\nasync function restoreAvailabilityRuleFromSnapshot(em: EntityManager, snapshot: AvailabilityRuleSnapshot): Promise<void> {\n let record = await em.findOne(PlannerAvailabilityRule, { id: snapshot.id })\n if (!record) {\n record = em.create(PlannerAvailabilityRule, {\n id: snapshot.id,\n tenantId: snapshot.tenantId,\n organizationId: snapshot.organizationId,\n subjectType: snapshot.subjectType,\n subjectId: snapshot.subjectId,\n timezone: snapshot.timezone,\n rrule: snapshot.rrule,\n exdates: snapshot.exdates ?? [],\n kind: snapshot.kind ?? 'availability',\n note: snapshot.note ?? null,\n unavailabilityReasonEntryId: snapshot.unavailabilityReasonEntryId ?? null,\n unavailabilityReasonValue: snapshot.unavailabilityReasonValue ?? null,\n createdAt: new Date(),\n updatedAt: new Date(),\n deletedAt: snapshot.deletedAt ?? null,\n })\n em.persist(record)\n } else {\n record.subjectType = snapshot.subjectType\n record.subjectId = snapshot.subjectId\n record.timezone = snapshot.timezone\n record.rrule = snapshot.rrule\n record.exdates = snapshot.exdates ?? []\n record.kind = snapshot.kind ?? 'availability'\n record.note = snapshot.note ?? null\n record.unavailabilityReasonEntryId = snapshot.unavailabilityReasonEntryId ?? null\n record.unavailabilityReasonValue = snapshot.unavailabilityReasonValue ?? null\n record.deletedAt = snapshot.deletedAt ?? null\n }\n}\n\nconst replaceDateSpecificAvailabilityCommand: CommandHandler<PlannerAvailabilityDateSpecificReplaceInput, { ok: true }> = {\n id: 'planner.availability.date-specific.replace',\n async prepare(input, ctx) {\n const parsed = plannerAvailabilityDateSpecificReplaceSchema.parse(input)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n const dates = resolveDateSet(parsed)\n const em = (ctx.container.resolve('em') as EntityManager)\n const before = await loadDateSpecificSnapshots(em, {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n subjectType: parsed.subjectType,\n subjectId: parsed.subjectId,\n dates,\n })\n return { before }\n },\n async execute(input, ctx) {\n const parsed = plannerAvailabilityDateSpecificReplaceSchema.parse(input)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n const dates = resolveDateSet(parsed)\n const windows = parsed.windows ?? []\n const kind = parsed.kind ?? (parsed.isAvailable === false ? 'unavailability' : 'availability')\n const isAvailable = kind !== 'unavailability'\n const note = typeof parsed.note === 'string' && parsed.note.trim().length ? parsed.note.trim() : null\n const unavailabilityReasonEntryId = isAvailable ? null : parsed.unavailabilityReasonEntryId ?? null\n const unavailabilityReasonValue = isAvailable ? null : parsed.unavailabilityReasonValue ?? null\n const now = new Date()\n\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n await em.transactional(async (trx) => {\n if (dates.size) {\n const existing = await trx.find(PlannerAvailabilityRule, {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n subjectType: parsed.subjectType,\n subjectId: parsed.subjectId,\n deletedAt: null,\n })\n const toDelete = existing.filter((rule) => {\n const window = parseAvailabilityRuleWindow(rule)\n if (window.repeat !== 'once') return false\n return dates.has(formatDateKey(window.startAt))\n })\n enforceCommandOptimisticLock({\n resourceKind: AVAILABILITY_RULE_RESOURCE_KIND,\n resourceId: `${parsed.subjectType}:${parsed.subjectId}`,\n current: resolveAggregateLockVersion(toDelete),\n request: ctx.request ?? null,\n })\n toDelete.forEach((rule) => {\n rule.deletedAt = now\n rule.updatedAt = now\n })\n if (toDelete.length) {\n trx.persist(toDelete)\n }\n }\n\n if (!dates.size) return\n\n if (!isAvailable) {\n dates.forEach((date) => {\n const rrule = buildFullDayRrule(date)\n if (!rrule) return\n const record = trx.create(PlannerAvailabilityRule, {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n subjectType: parsed.subjectType,\n subjectId: parsed.subjectId,\n timezone: parsed.timezone,\n rrule,\n exdates: [],\n kind: 'unavailability',\n note,\n unavailabilityReasonEntryId,\n unavailabilityReasonValue,\n createdAt: now,\n updatedAt: now,\n })\n trx.persist(record)\n })\n } else {\n dates.forEach((date) => {\n windows.forEach((window) => {\n const start = toDateForDay(date, window.start)\n const end = toDateForDay(date, window.end)\n if (!start || !end || start >= end) return\n const rrule = buildAvailabilityRrule(start, end)\n const record = trx.create(PlannerAvailabilityRule, {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n subjectType: parsed.subjectType,\n subjectId: parsed.subjectId,\n timezone: parsed.timezone,\n rrule,\n exdates: [],\n kind: 'availability',\n note: null,\n unavailabilityReasonEntryId: null,\n unavailabilityReasonValue: null,\n createdAt: now,\n updatedAt: now,\n })\n trx.persist(record)\n })\n })\n }\n\n await trx.flush()\n })\n return { ok: true }\n },\n buildLog: async ({ input, snapshots, ctx }) => {\n const parsed = plannerAvailabilityDateSpecificReplaceSchema.parse(input)\n const dates = resolveDateSet(parsed)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const after = await loadDateSpecificSnapshots(em, {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n subjectType: parsed.subjectType,\n subjectId: parsed.subjectId,\n dates,\n })\n const before = (snapshots.before as AvailabilityRuleSnapshot[] | undefined) ?? []\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate('planner.audit.availability.dateSpecific.replace', 'Replace date-specific availability'),\n resourceKind: AVAILABILITY_RULE_RESOURCE_KIND,\n resourceId: null,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n snapshotBefore: before,\n snapshotAfter: after,\n payload: {\n undo: {\n before,\n after,\n } satisfies DateSpecificUndoPayload,\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<DateSpecificUndoPayload>(logEntry)\n const before = payload?.before ?? []\n const after = payload?.after ?? []\n if (!before.length && !after.length) return\n\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n await em.transactional(async (trx) => {\n if (after.length) {\n const ids = after.map((rule) => rule.id)\n const records = await trx.find(PlannerAvailabilityRule, { id: { $in: ids } })\n records.forEach((record) => {\n record.deletedAt = new Date()\n })\n if (records.length) trx.persist(records)\n }\n\n for (const snapshot of before) {\n await restoreAvailabilityRuleFromSnapshot(trx, { ...snapshot, deletedAt: null })\n }\n\n await trx.flush()\n })\n },\n}\n\nfunction resolveDateSet(input: PlannerAvailabilityDateSpecificReplaceInput): Set<string> {\n const dates = Array.isArray(input.dates) ? input.dates.filter((value) => typeof value === 'string' && value.length > 0) : []\n if (dates.length) return new Set(dates)\n if (typeof input.date === 'string' && input.date.length > 0) return new Set([input.date])\n return new Set()\n}\n\nregisterCommand(replaceDateSpecificAvailabilityCommand)\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { CommandHandler } from '@open-mercato/shared/lib/commands'\nimport { registerCommand } from '@open-mercato/shared/lib/commands'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { parseAvailabilityRuleWindow } from '@open-mercato/core/modules/planner/lib/availabilitySchedule'\nimport { warnOnUnresolvableTimeZone, zonedDateKey, zonedDayBounds, zonedWallTimeToInstant } from '@open-mercato/core/modules/planner/lib/availabilityTimezone'\nimport { PlannerAvailabilityRule } from '../data/entities'\nimport {\n plannerAvailabilityDateSpecificReplaceSchema,\n type PlannerAvailabilityDateSpecificReplaceInput,\n} from '../data/validators'\nimport { ensureOrganizationScope, ensureTenantScope, extractUndoPayload } from './shared'\nimport { enforceCommandOptimisticLock } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport type { PlannerAvailabilityKind, PlannerAvailabilitySubjectType } from '../data/entities'\n\nconst AVAILABILITY_RULE_RESOURCE_KIND = 'planner.availability.rule'\n\n/**\n * The date-specific aggregate (the one-off rules for a subject/date) has no\n * single parent row, so its optimistic-lock version is the latest `updated_at`\n * of the rules being replaced. The editor sends that token via the extension\n * header; an empty set means \"nothing to clobber\" \u2192 no current version \u2192 the\n * guard is a no-op (strictly additive, fail-open).\n */\nfunction resolveAggregateLockVersion(rules: PlannerAvailabilityRule[]): Date | null {\n let latest: Date | null = null\n for (const rule of rules) {\n const value = rule.updatedAt\n if (!value || Number.isNaN(value.getTime())) continue\n if (!latest || value.getTime() > latest.getTime()) latest = value\n }\n return latest\n}\n\ntype AvailabilityRuleSnapshot = {\n id: string\n tenantId: string\n organizationId: string\n subjectType: PlannerAvailabilitySubjectType\n subjectId: string\n timezone: string\n rrule: string\n exdates: string[]\n kind: PlannerAvailabilityKind\n note: string | null\n unavailabilityReasonEntryId: string | null\n unavailabilityReasonValue: string | null\n deletedAt: Date | null\n}\n\ntype DateSpecificUndoPayload = {\n before: AvailabilityRuleSnapshot[]\n after: AvailabilityRuleSnapshot[]\n}\n\nfunction toDateForDay(dateKey: string, time: string, timeZone: string): Date | null {\n if (!dateKey) return null\n return zonedWallTimeToInstant(dateKey, time, timeZone)\n}\n\nfunction formatDuration(minutes: number): string {\n const clamped = Math.max(1, minutes)\n const hours = Math.floor(clamped / 60)\n const mins = clamped % 60\n if (hours > 0 && mins > 0) return `PT${hours}H${mins}M`\n if (hours > 0) return `PT${hours}H`\n return `PT${mins}M`\n}\n\nfunction buildAvailabilityRrule(start: Date, end: Date): string {\n const dtStart = start.toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z'\n const durationMinutes = Math.max(1, Math.round((end.getTime() - start.getTime()) / 60000))\n const duration = formatDuration(durationMinutes)\n return `DTSTART:${dtStart}\\nDURATION:${duration}\\nRRULE:FREQ=DAILY;COUNT=1`\n}\n\nfunction buildFullDayRrule(date: string, timeZone: string): string | null {\n const bounds = zonedDayBounds(date, timeZone)\n if (!bounds) return null\n return buildAvailabilityRrule(bounds.start, bounds.end)\n}\n\nfunction toAvailabilityRuleSnapshot(record: PlannerAvailabilityRule): AvailabilityRuleSnapshot {\n return {\n id: record.id,\n tenantId: record.tenantId,\n organizationId: record.organizationId,\n subjectType: record.subjectType,\n subjectId: record.subjectId,\n timezone: record.timezone,\n rrule: record.rrule,\n exdates: [...(record.exdates ?? [])],\n kind: record.kind,\n note: record.note ?? null,\n unavailabilityReasonEntryId: record.unavailabilityReasonEntryId ?? null,\n unavailabilityReasonValue: record.unavailabilityReasonValue ?? null,\n deletedAt: record.deletedAt ?? null,\n }\n}\n\nasync function loadDateSpecificSnapshots(\n em: EntityManager,\n params: {\n tenantId: string\n organizationId: string\n subjectType: PlannerAvailabilitySubjectType\n subjectId: string\n dates: Set<string>\n }\n): Promise<AvailabilityRuleSnapshot[]> {\n if (!params.dates.size) return []\n const existing = await em.find(PlannerAvailabilityRule, {\n tenantId: params.tenantId,\n organizationId: params.organizationId,\n subjectType: params.subjectType,\n subjectId: params.subjectId,\n deletedAt: null,\n })\n return existing\n .filter((rule) => {\n const window = parseAvailabilityRuleWindow(rule)\n if (window.repeat !== 'once') return false\n return params.dates.has(zonedDateKey(window.startAt, rule.timezone))\n })\n .map(toAvailabilityRuleSnapshot)\n}\n\nasync function restoreAvailabilityRuleFromSnapshot(em: EntityManager, snapshot: AvailabilityRuleSnapshot): Promise<void> {\n let record = await em.findOne(PlannerAvailabilityRule, { id: snapshot.id })\n if (!record) {\n record = em.create(PlannerAvailabilityRule, {\n id: snapshot.id,\n tenantId: snapshot.tenantId,\n organizationId: snapshot.organizationId,\n subjectType: snapshot.subjectType,\n subjectId: snapshot.subjectId,\n timezone: snapshot.timezone,\n rrule: snapshot.rrule,\n exdates: snapshot.exdates ?? [],\n kind: snapshot.kind ?? 'availability',\n note: snapshot.note ?? null,\n unavailabilityReasonEntryId: snapshot.unavailabilityReasonEntryId ?? null,\n unavailabilityReasonValue: snapshot.unavailabilityReasonValue ?? null,\n createdAt: new Date(),\n updatedAt: new Date(),\n deletedAt: snapshot.deletedAt ?? null,\n })\n em.persist(record)\n } else {\n record.subjectType = snapshot.subjectType\n record.subjectId = snapshot.subjectId\n record.timezone = snapshot.timezone\n record.rrule = snapshot.rrule\n record.exdates = snapshot.exdates ?? []\n record.kind = snapshot.kind ?? 'availability'\n record.note = snapshot.note ?? null\n record.unavailabilityReasonEntryId = snapshot.unavailabilityReasonEntryId ?? null\n record.unavailabilityReasonValue = snapshot.unavailabilityReasonValue ?? null\n record.deletedAt = snapshot.deletedAt ?? null\n }\n}\n\nconst replaceDateSpecificAvailabilityCommand: CommandHandler<PlannerAvailabilityDateSpecificReplaceInput, { ok: true }> = {\n id: 'planner.availability.date-specific.replace',\n async prepare(input, ctx) {\n const parsed = plannerAvailabilityDateSpecificReplaceSchema.parse(input)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n const dates = resolveDateSet(parsed)\n const em = (ctx.container.resolve('em') as EntityManager)\n const before = await loadDateSpecificSnapshots(em, {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n subjectType: parsed.subjectType,\n subjectId: parsed.subjectId,\n dates,\n })\n return { before }\n },\n async execute(input, ctx) {\n const parsed = plannerAvailabilityDateSpecificReplaceSchema.parse(input)\n warnOnUnresolvableTimeZone(parsed.timezone, 'planner.availability.date-specific.replace')\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n const dates = resolveDateSet(parsed)\n const windows = parsed.windows ?? []\n const kind = parsed.kind ?? (parsed.isAvailable === false ? 'unavailability' : 'availability')\n const isAvailable = kind !== 'unavailability'\n const note = typeof parsed.note === 'string' && parsed.note.trim().length ? parsed.note.trim() : null\n const unavailabilityReasonEntryId = isAvailable ? null : parsed.unavailabilityReasonEntryId ?? null\n const unavailabilityReasonValue = isAvailable ? null : parsed.unavailabilityReasonValue ?? null\n const now = new Date()\n\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n await em.transactional(async (trx) => {\n if (dates.size) {\n const existing = await trx.find(PlannerAvailabilityRule, {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n subjectType: parsed.subjectType,\n subjectId: parsed.subjectId,\n deletedAt: null,\n })\n const toDelete = existing.filter((rule) => {\n const window = parseAvailabilityRuleWindow(rule)\n if (window.repeat !== 'once') return false\n return dates.has(zonedDateKey(window.startAt, rule.timezone))\n })\n enforceCommandOptimisticLock({\n resourceKind: AVAILABILITY_RULE_RESOURCE_KIND,\n resourceId: `${parsed.subjectType}:${parsed.subjectId}`,\n current: resolveAggregateLockVersion(toDelete),\n request: ctx.request ?? null,\n })\n toDelete.forEach((rule) => {\n rule.deletedAt = now\n rule.updatedAt = now\n })\n if (toDelete.length) {\n trx.persist(toDelete)\n }\n }\n\n if (!dates.size) return\n\n if (!isAvailable) {\n dates.forEach((date) => {\n const rrule = buildFullDayRrule(date, parsed.timezone)\n if (!rrule) return\n const record = trx.create(PlannerAvailabilityRule, {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n subjectType: parsed.subjectType,\n subjectId: parsed.subjectId,\n timezone: parsed.timezone,\n rrule,\n exdates: [],\n kind: 'unavailability',\n note,\n unavailabilityReasonEntryId,\n unavailabilityReasonValue,\n createdAt: now,\n updatedAt: now,\n })\n trx.persist(record)\n })\n } else {\n dates.forEach((date) => {\n windows.forEach((window) => {\n const start = toDateForDay(date, window.start, parsed.timezone)\n const end = toDateForDay(date, window.end, parsed.timezone)\n if (!start || !end || start >= end) return\n const rrule = buildAvailabilityRrule(start, end)\n const record = trx.create(PlannerAvailabilityRule, {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n subjectType: parsed.subjectType,\n subjectId: parsed.subjectId,\n timezone: parsed.timezone,\n rrule,\n exdates: [],\n kind: 'availability',\n note: null,\n unavailabilityReasonEntryId: null,\n unavailabilityReasonValue: null,\n createdAt: now,\n updatedAt: now,\n })\n trx.persist(record)\n })\n })\n }\n\n await trx.flush()\n })\n return { ok: true }\n },\n buildLog: async ({ input, snapshots, ctx }) => {\n const parsed = plannerAvailabilityDateSpecificReplaceSchema.parse(input)\n const dates = resolveDateSet(parsed)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const after = await loadDateSpecificSnapshots(em, {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n subjectType: parsed.subjectType,\n subjectId: parsed.subjectId,\n dates,\n })\n const before = (snapshots.before as AvailabilityRuleSnapshot[] | undefined) ?? []\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate('planner.audit.availability.dateSpecific.replace', 'Replace date-specific availability'),\n resourceKind: AVAILABILITY_RULE_RESOURCE_KIND,\n resourceId: null,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n snapshotBefore: before,\n snapshotAfter: after,\n payload: {\n undo: {\n before,\n after,\n } satisfies DateSpecificUndoPayload,\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<DateSpecificUndoPayload>(logEntry)\n const before = payload?.before ?? []\n const after = payload?.after ?? []\n if (!before.length && !after.length) return\n\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n await em.transactional(async (trx) => {\n if (after.length) {\n const ids = after.map((rule) => rule.id)\n const records = await trx.find(PlannerAvailabilityRule, { id: { $in: ids } })\n records.forEach((record) => {\n record.deletedAt = new Date()\n })\n if (records.length) trx.persist(records)\n }\n\n for (const snapshot of before) {\n await restoreAvailabilityRuleFromSnapshot(trx, { ...snapshot, deletedAt: null })\n }\n\n await trx.flush()\n })\n },\n}\n\nfunction resolveDateSet(input: PlannerAvailabilityDateSpecificReplaceInput): Set<string> {\n const dates = Array.isArray(input.dates) ? input.dates.filter((value) => typeof value === 'string' && value.length > 0) : []\n if (dates.length) return new Set(dates)\n if (typeof input.date === 'string' && input.date.length > 0) return new Set([input.date])\n return new Set()\n}\n\nregisterCommand(replaceDateSpecificAvailabilityCommand)\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,5 +1,6 @@
1
1
  import { registerCommand } from "@open-mercato/shared/lib/commands";
2
2
  import { parseAvailabilityRuleWindow } from "@open-mercato/core/modules/planner/lib/availabilitySchedule";
3
+ import { warnOnUnresolvableTimeZone, zonedWeeklyAnchor, zonedWeekday } from "@open-mercato/core/modules/planner/lib/availabilityTimezone";
3
4
  import { PlannerAvailabilityRule, PlannerAvailabilityRuleSet } from "../data/entities.js";
4
5
  import {
5
6
  plannerAvailabilityWeeklyReplaceSchema
@@ -20,22 +21,6 @@ const availabilityRuleSetCrudIndexer = {
20
21
  };
21
22
  const AVAILABILITY_RULE_SET_RESOURCE_KIND = "planner.availability.rule.set";
22
23
  const DAY_CODES = ["SU", "MO", "TU", "WE", "TH", "FR", "SA"];
23
- function parseTimeInput(value) {
24
- const [hours, minutes] = value.split(":").map((part) => Number(part));
25
- if (!Number.isFinite(hours) || !Number.isFinite(minutes)) return null;
26
- if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) return null;
27
- return { hours, minutes };
28
- }
29
- function toDateForWeekday(weekday, time) {
30
- const parsed = parseTimeInput(time);
31
- if (!parsed) return null;
32
- const now = /* @__PURE__ */ new Date();
33
- const base = new Date(now.getFullYear(), now.getMonth(), now.getDate());
34
- const diff = (weekday - base.getDay() + 7) % 7;
35
- const target = new Date(base.getTime() + diff * 24 * 60 * 60 * 1e3);
36
- target.setHours(parsed.hours, parsed.minutes, 0, 0);
37
- return target;
38
- }
39
24
  function formatDuration(minutes) {
40
25
  const clamped = Math.max(1, minutes);
41
26
  const hours = Math.floor(clamped / 60);
@@ -44,11 +29,11 @@ function formatDuration(minutes) {
44
29
  if (hours > 0) return `PT${hours}H`;
45
30
  return `PT${mins}M`;
46
31
  }
47
- function buildWeeklyRrule(start, end) {
32
+ function buildWeeklyRrule(start, end, timeZone) {
48
33
  const dtStart = start.toISOString().replace(/[-:]/g, "").split(".")[0] + "Z";
49
34
  const durationMinutes = Math.max(1, Math.round((end.getTime() - start.getTime()) / 6e4));
50
35
  const duration = formatDuration(durationMinutes);
51
- const dayCode = DAY_CODES[start.getDay()] ?? "MO";
36
+ const dayCode = DAY_CODES[zonedWeekday(start, timeZone)] ?? "MO";
52
37
  return `DTSTART:${dtStart}
53
38
  DURATION:${duration}
54
39
  RRULE:FREQ=WEEKLY;BYDAY=${dayCode}`;
@@ -135,6 +120,7 @@ const replaceWeeklyAvailabilityCommand = {
135
120
  },
136
121
  async execute(input, ctx) {
137
122
  const parsed = plannerAvailabilityWeeklyReplaceSchema.parse(input);
123
+ warnOnUnresolvableTimeZone(parsed.timezone, "planner.availability.weekly.replace");
138
124
  ensureTenantScope(ctx, parsed.tenantId);
139
125
  ensureOrganizationScope(ctx, parsed.organizationId);
140
126
  const em = ctx.container.resolve("em").fork();
@@ -182,10 +168,10 @@ const replaceWeeklyAvailabilityCommand = {
182
168
  trx.persist(toDelete);
183
169
  }
184
170
  parsed.windows.forEach((window) => {
185
- const start = toDateForWeekday(window.weekday, window.start);
186
- const end = toDateForWeekday(window.weekday, window.end);
187
- if (!start || !end || start >= end) return;
188
- const rrule = buildWeeklyRrule(start, end);
171
+ const anchor = zonedWeeklyAnchor(window.weekday, window.start, window.end, parsed.timezone, now);
172
+ if (!anchor || anchor.start >= anchor.end) return;
173
+ const { start, end } = anchor;
174
+ const rrule = buildWeeklyRrule(start, end, parsed.timezone);
189
175
  const record = trx.create(PlannerAvailabilityRule, {
190
176
  tenantId: parsed.tenantId,
191
177
  organizationId: parsed.organizationId,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/planner/commands/availability-weekly.ts"],
4
- "sourcesContent": ["import type { CommandHandler } from '@open-mercato/shared/lib/commands'\nimport { registerCommand } from '@open-mercato/shared/lib/commands'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { parseAvailabilityRuleWindow } from '@open-mercato/core/modules/planner/lib/availabilitySchedule'\nimport { PlannerAvailabilityRule, PlannerAvailabilityRuleSet } from '../data/entities'\nimport {\n plannerAvailabilityWeeklyReplaceSchema,\n type PlannerAvailabilityWeeklyReplaceInput,\n} from '../data/validators'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { emitCrudSideEffects } from '@open-mercato/shared/lib/commands/helpers'\nimport {\n enforceCommandOptimisticLock,\n enforceRecordGoneIsConflict,\n} from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport type { DataEngine } from '@open-mercato/shared/lib/data/engine'\nimport type { CrudIndexerConfig } from '@open-mercato/shared/lib/crud/types'\nimport type { PlannerAvailabilityKind, PlannerAvailabilitySubjectType } from '../data/entities'\nimport { ensureOrganizationScope, ensureTenantScope, extractUndoPayload } from './shared'\nimport { plannerAvailabilityRuleSetCrudEvents } from '../lib/crud'\nimport { E } from '#generated/entities.ids.generated'\n\nconst AVAILABILITY_RULE_RESOURCE_KIND = 'planner.availability.rule'\nconst AVAILABILITY_RULE_SET_CACHE_RESOURCE_KIND = 'planner.availability-rule-set'\n\nconst availabilityRuleSetCrudIndexer: CrudIndexerConfig<PlannerAvailabilityRuleSet> = {\n entityType: E.planner.planner_availability_rule_set,\n}\n\n// Canonical resource kind for the parent rule set, matching the tag the CRUD\n// factory derives for `planner.availability-rule-sets.*` commands. Weekly\n// replace mutates the rule set's child `availability_rules`, so the parent is\n// the optimistic-lock consistency boundary (document-aggregate pattern).\nconst AVAILABILITY_RULE_SET_RESOURCE_KIND = 'planner.availability.rule.set'\n\nconst DAY_CODES = ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA']\n\ntype AvailabilityRuleSnapshot = {\n id: string\n tenantId: string\n organizationId: string\n subjectType: PlannerAvailabilitySubjectType\n subjectId: string\n timezone: string\n rrule: string\n exdates: string[]\n kind: PlannerAvailabilityKind\n note: string | null\n deletedAt: Date | null\n}\n\ntype WeeklyUndoPayload = {\n before: AvailabilityRuleSnapshot[]\n after: AvailabilityRuleSnapshot[]\n}\n\nfunction parseTimeInput(value: string): { hours: number; minutes: number } | null {\n const [hours, minutes] = value.split(':').map((part) => Number(part))\n if (!Number.isFinite(hours) || !Number.isFinite(minutes)) return null\n if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) return null\n return { hours, minutes }\n}\n\nfunction toDateForWeekday(weekday: number, time: string): Date | null {\n const parsed = parseTimeInput(time)\n if (!parsed) return null\n const now = new Date()\n const base = new Date(now.getFullYear(), now.getMonth(), now.getDate())\n const diff = (weekday - base.getDay() + 7) % 7\n const target = new Date(base.getTime() + diff * 24 * 60 * 60 * 1000)\n target.setHours(parsed.hours, parsed.minutes, 0, 0)\n return target\n}\n\nfunction formatDuration(minutes: number): string {\n const clamped = Math.max(1, minutes)\n const hours = Math.floor(clamped / 60)\n const mins = clamped % 60\n if (hours > 0 && mins > 0) return `PT${hours}H${mins}M`\n if (hours > 0) return `PT${hours}H`\n return `PT${mins}M`\n}\n\nfunction buildWeeklyRrule(start: Date, end: Date): string {\n const dtStart = start.toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z'\n const durationMinutes = Math.max(1, Math.round((end.getTime() - start.getTime()) / 60000))\n const duration = formatDuration(durationMinutes)\n const dayCode = DAY_CODES[start.getDay()] ?? 'MO'\n return `DTSTART:${dtStart}\\nDURATION:${duration}\\nRRULE:FREQ=WEEKLY;BYDAY=${dayCode}`\n}\n\nfunction toAvailabilityRuleSnapshot(record: PlannerAvailabilityRule): AvailabilityRuleSnapshot {\n return {\n id: record.id,\n tenantId: record.tenantId,\n organizationId: record.organizationId,\n subjectType: record.subjectType,\n subjectId: record.subjectId,\n timezone: record.timezone,\n rrule: record.rrule,\n exdates: [...(record.exdates ?? [])],\n kind: record.kind,\n note: record.note ?? null,\n deletedAt: record.deletedAt ?? null,\n }\n}\n\nfunction nextRuleSetUpdatedAt(current: Date | null | undefined, fallback: Date): Date {\n const currentMs = current instanceof Date ? current.getTime() : Number.NaN\n if (Number.isFinite(currentMs) && fallback.getTime() <= currentMs) {\n return new Date(currentMs + 1)\n }\n return fallback\n}\n\nasync function loadWeeklySnapshots(\n em: EntityManager,\n params: {\n tenantId: string\n organizationId: string\n subjectType: PlannerAvailabilitySubjectType\n subjectId: string\n }\n): Promise<AvailabilityRuleSnapshot[]> {\n const existing = await em.find(PlannerAvailabilityRule, {\n tenantId: params.tenantId,\n organizationId: params.organizationId,\n subjectType: params.subjectType,\n subjectId: params.subjectId,\n deletedAt: null,\n })\n return existing\n .filter((rule) => {\n const repeat = parseAvailabilityRuleWindow(rule).repeat\n return repeat === 'weekly' || repeat === 'daily'\n })\n .map(toAvailabilityRuleSnapshot)\n}\n\nasync function restoreAvailabilityRuleFromSnapshot(em: EntityManager, snapshot: AvailabilityRuleSnapshot): Promise<void> {\n let record = await em.findOne(PlannerAvailabilityRule, { id: snapshot.id })\n if (!record) {\n record = em.create(PlannerAvailabilityRule, {\n id: snapshot.id,\n tenantId: snapshot.tenantId,\n organizationId: snapshot.organizationId,\n subjectType: snapshot.subjectType,\n subjectId: snapshot.subjectId,\n timezone: snapshot.timezone,\n rrule: snapshot.rrule,\n exdates: snapshot.exdates ?? [],\n kind: snapshot.kind ?? 'availability',\n note: snapshot.note ?? null,\n createdAt: new Date(),\n updatedAt: new Date(),\n deletedAt: snapshot.deletedAt ?? null,\n })\n em.persist(record)\n } else {\n record.subjectType = snapshot.subjectType\n record.subjectId = snapshot.subjectId\n record.timezone = snapshot.timezone\n record.rrule = snapshot.rrule\n record.exdates = snapshot.exdates ?? []\n record.kind = snapshot.kind ?? 'availability'\n record.note = snapshot.note ?? null\n record.deletedAt = snapshot.deletedAt ?? null\n }\n}\n\nconst replaceWeeklyAvailabilityCommand: CommandHandler<PlannerAvailabilityWeeklyReplaceInput, { ok: true }> = {\n id: 'planner.availability.weekly.replace',\n async prepare(input, ctx) {\n const parsed = plannerAvailabilityWeeklyReplaceSchema.parse(input)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n const em = (ctx.container.resolve('em') as EntityManager)\n const before = await loadWeeklySnapshots(em, {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n subjectType: parsed.subjectType,\n subjectId: parsed.subjectId,\n })\n return { before }\n },\n async execute(input, ctx) {\n const parsed = plannerAvailabilityWeeklyReplaceSchema.parse(input)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const now = new Date()\n\n // The weekly rules of a rule set are a sub-resource of that rule set: the\n // parent is the optimistic-lock consistency boundary. Guard the parent's\n // version (so a stale weekly save loses to a concurrent rule-set\n // change/delete) and bump its `updated_at` after the replace (so a\n // concurrent rule-set delete/update with a stale token conflicts). See #2927.\n const touchedRuleSet = await em.transactional(async (trx): Promise<PlannerAvailabilityRuleSet | null> => {\n let ruleSet: PlannerAvailabilityRuleSet | null = null\n if (parsed.subjectType === 'ruleset') {\n ruleSet = await trx.findOne(PlannerAvailabilityRuleSet, {\n id: parsed.subjectId,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n deletedAt: null,\n })\n if (ruleSet) {\n enforceCommandOptimisticLock({\n resourceKind: AVAILABILITY_RULE_SET_RESOURCE_KIND,\n resourceId: ruleSet.id,\n current: ruleSet.updatedAt,\n request: ctx.request ?? null,\n })\n } else {\n // The rule set was deleted concurrently. When the client opted into\n // optimistic locking, surface the unified conflict instead of\n // silently writing orphan rules; otherwise preserve legacy behavior.\n enforceRecordGoneIsConflict({\n resourceKind: AVAILABILITY_RULE_SET_RESOURCE_KIND,\n resourceId: parsed.subjectId,\n request: ctx.request ?? null,\n })\n }\n }\n\n const existing = await trx.find(PlannerAvailabilityRule, {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n subjectType: parsed.subjectType,\n subjectId: parsed.subjectId,\n deletedAt: null,\n })\n\n const toDelete = existing.filter((rule) => {\n const repeat = parseAvailabilityRuleWindow(rule).repeat\n return repeat === 'weekly' || repeat === 'daily'\n })\n\n toDelete.forEach((rule) => {\n rule.deletedAt = now\n rule.updatedAt = now\n })\n\n if (toDelete.length) {\n trx.persist(toDelete)\n }\n\n parsed.windows.forEach((window) => {\n const start = toDateForWeekday(window.weekday, window.start)\n const end = toDateForWeekday(window.weekday, window.end)\n if (!start || !end || start >= end) return\n const rrule = buildWeeklyRrule(start, end)\n const record = trx.create(PlannerAvailabilityRule, {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n subjectType: parsed.subjectType,\n subjectId: parsed.subjectId,\n timezone: parsed.timezone,\n rrule,\n exdates: [],\n kind: 'availability',\n note: null,\n createdAt: now,\n updatedAt: now,\n })\n trx.persist(record)\n })\n\n if (ruleSet) {\n ruleSet.updatedAt = nextRuleSetUpdatedAt(ruleSet.updatedAt, now)\n trx.persist(ruleSet)\n }\n\n await trx.flush()\n return ruleSet\n })\n\n if (touchedRuleSet) {\n const dataEngine = ctx.container.resolve('dataEngine') as DataEngine\n await emitCrudSideEffects({\n dataEngine,\n action: 'updated',\n entity: touchedRuleSet,\n identifiers: {\n id: touchedRuleSet.id,\n organizationId: touchedRuleSet.organizationId,\n tenantId: touchedRuleSet.tenantId,\n },\n events: plannerAvailabilityRuleSetCrudEvents,\n indexer: availabilityRuleSetCrudIndexer,\n })\n }\n\n return { ok: true }\n },\n buildLog: async ({ input, snapshots, ctx }) => {\n const parsed = plannerAvailabilityWeeklyReplaceSchema.parse(input)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const after = await loadWeeklySnapshots(em, {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n subjectType: parsed.subjectType,\n subjectId: parsed.subjectId,\n })\n const before = (snapshots.before as AvailabilityRuleSnapshot[] | undefined) ?? []\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate('planner.audit.availability.weekly.replace', 'Replace weekly availability'),\n resourceKind: AVAILABILITY_RULE_RESOURCE_KIND,\n resourceId: null,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n snapshotBefore: before,\n snapshotAfter: after,\n payload: {\n undo: {\n before,\n after,\n } satisfies WeeklyUndoPayload,\n },\n context: parsed.subjectType === 'ruleset'\n ? { cacheAliases: [AVAILABILITY_RULE_SET_CACHE_RESOURCE_KIND] }\n : null,\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<WeeklyUndoPayload>(logEntry)\n const before = payload?.before ?? []\n const after = payload?.after ?? []\n if (!before.length && !after.length) return\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n await em.transactional(async (trx) => {\n if (after.length) {\n const ids = after.map((rule) => rule.id)\n const records = await trx.find(PlannerAvailabilityRule, { id: { $in: ids } })\n records.forEach((record) => {\n record.deletedAt = new Date()\n })\n if (records.length) trx.persist(records)\n }\n\n for (const snapshot of before) {\n await restoreAvailabilityRuleFromSnapshot(trx, { ...snapshot, deletedAt: null })\n }\n\n await trx.flush()\n })\n },\n}\n\nregisterCommand(replaceWeeklyAvailabilityCommand)\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { CommandHandler } from '@open-mercato/shared/lib/commands'\nimport { registerCommand } from '@open-mercato/shared/lib/commands'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { parseAvailabilityRuleWindow } from '@open-mercato/core/modules/planner/lib/availabilitySchedule'\nimport { warnOnUnresolvableTimeZone, zonedWeeklyAnchor, zonedWeekday } from '@open-mercato/core/modules/planner/lib/availabilityTimezone'\nimport { PlannerAvailabilityRule, PlannerAvailabilityRuleSet } from '../data/entities'\nimport {\n plannerAvailabilityWeeklyReplaceSchema,\n type PlannerAvailabilityWeeklyReplaceInput,\n} from '../data/validators'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { emitCrudSideEffects } from '@open-mercato/shared/lib/commands/helpers'\nimport {\n enforceCommandOptimisticLock,\n enforceRecordGoneIsConflict,\n} from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport type { DataEngine } from '@open-mercato/shared/lib/data/engine'\nimport type { CrudIndexerConfig } from '@open-mercato/shared/lib/crud/types'\nimport type { PlannerAvailabilityKind, PlannerAvailabilitySubjectType } from '../data/entities'\nimport { ensureOrganizationScope, ensureTenantScope, extractUndoPayload } from './shared'\nimport { plannerAvailabilityRuleSetCrudEvents } from '../lib/crud'\nimport { E } from '#generated/entities.ids.generated'\n\nconst AVAILABILITY_RULE_RESOURCE_KIND = 'planner.availability.rule'\nconst AVAILABILITY_RULE_SET_CACHE_RESOURCE_KIND = 'planner.availability-rule-set'\n\nconst availabilityRuleSetCrudIndexer: CrudIndexerConfig<PlannerAvailabilityRuleSet> = {\n entityType: E.planner.planner_availability_rule_set,\n}\n\n// Canonical resource kind for the parent rule set, matching the tag the CRUD\n// factory derives for `planner.availability-rule-sets.*` commands. Weekly\n// replace mutates the rule set's child `availability_rules`, so the parent is\n// the optimistic-lock consistency boundary (document-aggregate pattern).\nconst AVAILABILITY_RULE_SET_RESOURCE_KIND = 'planner.availability.rule.set'\n\nconst DAY_CODES = ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA']\n\ntype AvailabilityRuleSnapshot = {\n id: string\n tenantId: string\n organizationId: string\n subjectType: PlannerAvailabilitySubjectType\n subjectId: string\n timezone: string\n rrule: string\n exdates: string[]\n kind: PlannerAvailabilityKind\n note: string | null\n deletedAt: Date | null\n}\n\ntype WeeklyUndoPayload = {\n before: AvailabilityRuleSnapshot[]\n after: AvailabilityRuleSnapshot[]\n}\n\nfunction formatDuration(minutes: number): string {\n const clamped = Math.max(1, minutes)\n const hours = Math.floor(clamped / 60)\n const mins = clamped % 60\n if (hours > 0 && mins > 0) return `PT${hours}H${mins}M`\n if (hours > 0) return `PT${hours}H`\n return `PT${mins}M`\n}\n\nfunction buildWeeklyRrule(start: Date, end: Date, timeZone: string): string {\n const dtStart = start.toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z'\n const durationMinutes = Math.max(1, Math.round((end.getTime() - start.getTime()) / 60000))\n const duration = formatDuration(durationMinutes)\n // `BYDAY` names the weekday the window falls on in its DECLARED zone, which\n // is the meaningful label for a recurring local-time window. It is therefore\n // deliberately not consistent with the `Z`-suffixed `DTSTART` beside it: a\n // Pacific/Auckland Monday 09:00 ships DTSTART:...T210000Z, a Sunday in UTC,\n // with BYDAY=MO. Nothing in the planner reads BYDAY today (neither the\n // expander nor `parseAvailabilityRuleWindow`); a future iCal export must\n // re-derive it from the zone rather than trust it against DTSTART.\n const dayCode = DAY_CODES[zonedWeekday(start, timeZone)] ?? 'MO'\n return `DTSTART:${dtStart}\\nDURATION:${duration}\\nRRULE:FREQ=WEEKLY;BYDAY=${dayCode}`\n}\n\nfunction toAvailabilityRuleSnapshot(record: PlannerAvailabilityRule): AvailabilityRuleSnapshot {\n return {\n id: record.id,\n tenantId: record.tenantId,\n organizationId: record.organizationId,\n subjectType: record.subjectType,\n subjectId: record.subjectId,\n timezone: record.timezone,\n rrule: record.rrule,\n exdates: [...(record.exdates ?? [])],\n kind: record.kind,\n note: record.note ?? null,\n deletedAt: record.deletedAt ?? null,\n }\n}\n\nfunction nextRuleSetUpdatedAt(current: Date | null | undefined, fallback: Date): Date {\n const currentMs = current instanceof Date ? current.getTime() : Number.NaN\n if (Number.isFinite(currentMs) && fallback.getTime() <= currentMs) {\n return new Date(currentMs + 1)\n }\n return fallback\n}\n\nasync function loadWeeklySnapshots(\n em: EntityManager,\n params: {\n tenantId: string\n organizationId: string\n subjectType: PlannerAvailabilitySubjectType\n subjectId: string\n }\n): Promise<AvailabilityRuleSnapshot[]> {\n const existing = await em.find(PlannerAvailabilityRule, {\n tenantId: params.tenantId,\n organizationId: params.organizationId,\n subjectType: params.subjectType,\n subjectId: params.subjectId,\n deletedAt: null,\n })\n return existing\n .filter((rule) => {\n const repeat = parseAvailabilityRuleWindow(rule).repeat\n return repeat === 'weekly' || repeat === 'daily'\n })\n .map(toAvailabilityRuleSnapshot)\n}\n\nasync function restoreAvailabilityRuleFromSnapshot(em: EntityManager, snapshot: AvailabilityRuleSnapshot): Promise<void> {\n let record = await em.findOne(PlannerAvailabilityRule, { id: snapshot.id })\n if (!record) {\n record = em.create(PlannerAvailabilityRule, {\n id: snapshot.id,\n tenantId: snapshot.tenantId,\n organizationId: snapshot.organizationId,\n subjectType: snapshot.subjectType,\n subjectId: snapshot.subjectId,\n timezone: snapshot.timezone,\n rrule: snapshot.rrule,\n exdates: snapshot.exdates ?? [],\n kind: snapshot.kind ?? 'availability',\n note: snapshot.note ?? null,\n createdAt: new Date(),\n updatedAt: new Date(),\n deletedAt: snapshot.deletedAt ?? null,\n })\n em.persist(record)\n } else {\n record.subjectType = snapshot.subjectType\n record.subjectId = snapshot.subjectId\n record.timezone = snapshot.timezone\n record.rrule = snapshot.rrule\n record.exdates = snapshot.exdates ?? []\n record.kind = snapshot.kind ?? 'availability'\n record.note = snapshot.note ?? null\n record.deletedAt = snapshot.deletedAt ?? null\n }\n}\n\nconst replaceWeeklyAvailabilityCommand: CommandHandler<PlannerAvailabilityWeeklyReplaceInput, { ok: true }> = {\n id: 'planner.availability.weekly.replace',\n async prepare(input, ctx) {\n const parsed = plannerAvailabilityWeeklyReplaceSchema.parse(input)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n const em = (ctx.container.resolve('em') as EntityManager)\n const before = await loadWeeklySnapshots(em, {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n subjectType: parsed.subjectType,\n subjectId: parsed.subjectId,\n })\n return { before }\n },\n async execute(input, ctx) {\n const parsed = plannerAvailabilityWeeklyReplaceSchema.parse(input)\n warnOnUnresolvableTimeZone(parsed.timezone, 'planner.availability.weekly.replace')\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const now = new Date()\n\n // The weekly rules of a rule set are a sub-resource of that rule set: the\n // parent is the optimistic-lock consistency boundary. Guard the parent's\n // version (so a stale weekly save loses to a concurrent rule-set\n // change/delete) and bump its `updated_at` after the replace (so a\n // concurrent rule-set delete/update with a stale token conflicts). See #2927.\n const touchedRuleSet = await em.transactional(async (trx): Promise<PlannerAvailabilityRuleSet | null> => {\n let ruleSet: PlannerAvailabilityRuleSet | null = null\n if (parsed.subjectType === 'ruleset') {\n ruleSet = await trx.findOne(PlannerAvailabilityRuleSet, {\n id: parsed.subjectId,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n deletedAt: null,\n })\n if (ruleSet) {\n enforceCommandOptimisticLock({\n resourceKind: AVAILABILITY_RULE_SET_RESOURCE_KIND,\n resourceId: ruleSet.id,\n current: ruleSet.updatedAt,\n request: ctx.request ?? null,\n })\n } else {\n // The rule set was deleted concurrently. When the client opted into\n // optimistic locking, surface the unified conflict instead of\n // silently writing orphan rules; otherwise preserve legacy behavior.\n enforceRecordGoneIsConflict({\n resourceKind: AVAILABILITY_RULE_SET_RESOURCE_KIND,\n resourceId: parsed.subjectId,\n request: ctx.request ?? null,\n })\n }\n }\n\n const existing = await trx.find(PlannerAvailabilityRule, {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n subjectType: parsed.subjectType,\n subjectId: parsed.subjectId,\n deletedAt: null,\n })\n\n const toDelete = existing.filter((rule) => {\n const repeat = parseAvailabilityRuleWindow(rule).repeat\n return repeat === 'weekly' || repeat === 'daily'\n })\n\n toDelete.forEach((rule) => {\n rule.deletedAt = now\n rule.updatedAt = now\n })\n\n if (toDelete.length) {\n trx.persist(toDelete)\n }\n\n parsed.windows.forEach((window) => {\n const anchor = zonedWeeklyAnchor(window.weekday, window.start, window.end, parsed.timezone, now)\n if (!anchor || anchor.start >= anchor.end) return\n const { start, end } = anchor\n const rrule = buildWeeklyRrule(start, end, parsed.timezone)\n const record = trx.create(PlannerAvailabilityRule, {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n subjectType: parsed.subjectType,\n subjectId: parsed.subjectId,\n timezone: parsed.timezone,\n rrule,\n exdates: [],\n kind: 'availability',\n note: null,\n createdAt: now,\n updatedAt: now,\n })\n trx.persist(record)\n })\n\n if (ruleSet) {\n ruleSet.updatedAt = nextRuleSetUpdatedAt(ruleSet.updatedAt, now)\n trx.persist(ruleSet)\n }\n\n await trx.flush()\n return ruleSet\n })\n\n if (touchedRuleSet) {\n const dataEngine = ctx.container.resolve('dataEngine') as DataEngine\n await emitCrudSideEffects({\n dataEngine,\n action: 'updated',\n entity: touchedRuleSet,\n identifiers: {\n id: touchedRuleSet.id,\n organizationId: touchedRuleSet.organizationId,\n tenantId: touchedRuleSet.tenantId,\n },\n events: plannerAvailabilityRuleSetCrudEvents,\n indexer: availabilityRuleSetCrudIndexer,\n })\n }\n\n return { ok: true }\n },\n buildLog: async ({ input, snapshots, ctx }) => {\n const parsed = plannerAvailabilityWeeklyReplaceSchema.parse(input)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const after = await loadWeeklySnapshots(em, {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n subjectType: parsed.subjectType,\n subjectId: parsed.subjectId,\n })\n const before = (snapshots.before as AvailabilityRuleSnapshot[] | undefined) ?? []\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate('planner.audit.availability.weekly.replace', 'Replace weekly availability'),\n resourceKind: AVAILABILITY_RULE_RESOURCE_KIND,\n resourceId: null,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n snapshotBefore: before,\n snapshotAfter: after,\n payload: {\n undo: {\n before,\n after,\n } satisfies WeeklyUndoPayload,\n },\n context: parsed.subjectType === 'ruleset'\n ? { cacheAliases: [AVAILABILITY_RULE_SET_CACHE_RESOURCE_KIND] }\n : null,\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<WeeklyUndoPayload>(logEntry)\n const before = payload?.before ?? []\n const after = payload?.after ?? []\n if (!before.length && !after.length) return\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n await em.transactional(async (trx) => {\n if (after.length) {\n const ids = after.map((rule) => rule.id)\n const records = await trx.find(PlannerAvailabilityRule, { id: { $in: ids } })\n records.forEach((record) => {\n record.deletedAt = new Date()\n })\n if (records.length) trx.persist(records)\n }\n\n for (const snapshot of before) {\n await restoreAvailabilityRuleFromSnapshot(trx, { ...snapshot, deletedAt: null })\n }\n\n await trx.flush()\n })\n },\n}\n\nregisterCommand(replaceWeeklyAvailabilityCommand)\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,3 +1,4 @@
1
+ import { resolveTimeZone, zonedWallDateToInstant } from "./availabilityTimezone.js";
1
2
  const DAY_MS = 24 * 60 * 60 * 1e3;
2
3
  const timezoneFormatters = /* @__PURE__ */ new Map();
3
4
  function zonedParts(instant, timezone) {
@@ -20,20 +21,8 @@ function zonedParts(instant, timezone) {
20
21
  const number = (kind) => Number(parts.find((part) => part.type === kind)?.value);
21
22
  return { year: number("year"), month: number("month"), day: number("day"), hour: number("hour"), minute: number("minute"), second: number("second") };
22
23
  }
23
- function zonedWallToInstant(wall, timezone) {
24
- const target = wall.getTime();
25
- let candidate = target;
26
- for (let attempt = 0; attempt < 4; attempt += 1) {
27
- const parts = zonedParts(new Date(candidate), timezone);
28
- const displayed = Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, parts.second);
29
- const next = candidate + target - displayed;
30
- if (next === candidate) return new Date(candidate);
31
- candidate = next;
32
- }
33
- return null;
34
- }
35
24
  function zonedStartOfDay(dayKey, timezone) {
36
- return zonedWallToInstant(/* @__PURE__ */ new Date(`${dayKey}T00:00:00Z`), timezone);
25
+ return zonedWallDateToInstant(/* @__PURE__ */ new Date(`${dayKey}T00:00:00Z`), timezone);
37
26
  }
38
27
  function dayKeyForRule(instant, timezone) {
39
28
  if (!timezone || timezone === "UTC" || timezone === "Etc/UTC" || timezone === "Etc/GMT") return toDayKey(instant);
@@ -106,10 +95,10 @@ function expandZonedRule(rule, parsed, range, weeklyScheduleTemplate) {
106
95
  let wall = new Date(anchorWall + skipped * step);
107
96
  let remaining = (parsed.count ?? Number.POSITIVE_INFINITY) - skipped;
108
97
  while (wall.getTime() <= range.end.getTime() + DAY_MS && remaining > 0) {
109
- const start = zonedWallToInstant(wall, timezone);
110
- if (start && !shouldExcludeOccurrence(start, rule.exdates, timezone)) {
111
- const end = new Date(start.getTime() + duration);
112
- if (end > range.start && start < range.end) windows.push({ start, end, ruleId: rule.id });
98
+ const start = zonedWallDateToInstant(wall, timezone);
99
+ if (!shouldExcludeOccurrence(start, rule.exdates, timezone)) {
100
+ const end = zonedWallDateToInstant(new Date(wall.getTime() + duration), timezone);
101
+ if (end > start && end > range.start && start < range.end) windows.push({ start, end, ruleId: rule.id });
113
102
  }
114
103
  wall = new Date(wall.getTime() + step);
115
104
  remaining -= 1;
@@ -181,7 +170,10 @@ function subtractWindow(window, blockers) {
181
170
  return segments;
182
171
  }
183
172
  function getMergedAvailabilityWindows(params) {
184
- const rules = params.respectTimezone ? params.rules : params.rules.map((rule) => ({ ...rule, timezone: void 0 }));
173
+ const rules = params.respectTimezone ? params.rules.map((rule) => ({
174
+ ...rule,
175
+ timezone: rule.timezone ? resolveTimeZone(rule.timezone) : rule.timezone
176
+ })) : params.rules.map((rule) => ({ ...rule, timezone: void 0 }));
185
177
  const parsedRules = rules.map((rule) => {
186
178
  const parsed = parseRrule(rule.rrule);
187
179
  if (!parsed) return null;