@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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/sales/lib/providers/totals.ts"],
4
- "sourcesContent": ["import { registerSalesTotalsCalculator, rebuildDocumentResult } from '../calculations'\nimport type { SalesAdjustmentDraft, SalesDocumentCalculationResult } from '../types'\nimport {\n getPaymentProvider,\n getShippingProvider,\n normalizeProviderSettings,\n} from './registry'\nimport type {\n PaymentMethodContext,\n ProviderAdjustment,\n ProviderAdjustmentResult,\n ShippingMethodContext,\n ShippingMetrics,\n} from './types'\n\nconst SHIPPING_PREFIX = 'shipping-provider:'\nconst PAYMENT_PREFIX = 'payment-provider:'\n\nlet totalsRegistered = false\n\nfunction toNumber(value: unknown, fallback = 0): number {\n if (typeof value === 'number' && Number.isFinite(value)) return value\n if (typeof value === 'string' && value.trim() !== '' && !Number.isNaN(Number(value))) {\n return Number(value)\n }\n return fallback\n}\n\nfunction isProviderAdjustment(adjustment: SalesAdjustmentDraft): boolean {\n const key = adjustment.calculatorKey ?? ''\n return typeof key === 'string' && (key.startsWith(SHIPPING_PREFIX) || key.startsWith(PAYMENT_PREFIX))\n}\n\nfunction isManualOverride(adjustment: SalesAdjustmentDraft): boolean {\n const metadata = adjustment.metadata\n if (!metadata || typeof metadata !== 'object') return false\n const manual =\n (metadata as Record<string, unknown>).manualOverride ??\n (metadata as Record<string, unknown>).manual_override\n return manual === true\n}\n\nfunction withoutPrefix(adjustments: SalesAdjustmentDraft[], prefix: string) {\n return adjustments.filter((adj) => {\n if (!(adj.calculatorKey ?? '').startsWith(prefix)) return true\n return isManualOverride(adj)\n })\n}\n\nfunction extractProviderSettings(\n method: ShippingMethodContext | PaymentMethodContext | null | undefined\n): Record<string, unknown> | null {\n if (!method) return null\n if (method.providerSettings && typeof method.providerSettings === 'object') {\n return method.providerSettings as Record<string, unknown>\n }\n if (method.metadata && typeof method.metadata === 'object') {\n const meta = method.metadata as Record<string, unknown>\n if (meta.providerSettings && typeof meta.providerSettings === 'object') {\n return meta.providerSettings as Record<string, unknown>\n }\n }\n return null\n}\n\nfunction computeMetrics(lines: SalesDocumentCalculationResult['lines']): ShippingMetrics {\n let itemCount = 0\n let totalWeight = 0\n let totalVolume = 0\n let subtotalNet = 0\n let subtotalGross = 0\n\n for (const entry of lines) {\n const { line } = entry\n const qty = toNumber(line.quantity, 0)\n itemCount += qty\n subtotalNet += toNumber(entry.netAmount, 0)\n subtotalGross += toNumber(entry.grossAmount, 0)\n\n const meta = (line.metadata ?? {}) as Record<string, unknown>\n const configuration = (line.configuration ?? {}) as Record<string, unknown>\n const weight =\n toNumber((meta.weight as number | string | undefined) ?? (configuration.weight as number | string | undefined), 0) *\n qty\n const volume =\n toNumber(\n (meta.volume as number | string | undefined) ??\n (configuration.volume as number | string | undefined),\n 0\n ) * qty\n\n totalWeight += weight\n totalVolume += volume\n }\n\n return {\n itemCount,\n totalWeight,\n totalVolume,\n subtotalNet,\n subtotalGross,\n }\n}\n\nfunction normalizeAdjustments(params: {\n providerKey: string\n calculatorKey: string\n adjustments: ProviderAdjustment[]\n currencyCode: string\n defaultKind: SalesAdjustmentDraft['kind']\n}) {\n return params.adjustments.map<SalesAdjustmentDraft>((adj, index) => ({\n scope: 'order',\n kind: adj.kind ?? params.defaultKind,\n code: adj.code ?? params.providerKey,\n label: adj.label ?? params.providerKey,\n calculatorKey: params.calculatorKey,\n promotionId: null,\n rate: null,\n amountNet: toNumber(adj.amountNet, 0),\n amountGross:\n adj.amountGross === undefined || adj.amountGross === null\n ? toNumber(adj.amountNet, 0)\n : toNumber(adj.amountGross, 0),\n currencyCode: (adj.currencyCode || params.currencyCode || '').toUpperCase() || params.currencyCode,\n metadata: adj.metadata ?? null,\n position: 10_000 + index,\n }))\n}\n\nfunction mergeMetadata(\n base: Record<string, unknown> | undefined,\n key: string,\n value: unknown\n): Record<string, unknown> {\n const next = { ...(base ?? {}) }\n next[key] = value\n return next\n}\n\nfunction applyProviderResult(\n current: SalesDocumentCalculationResult,\n adjustments: SalesAdjustmentDraft[],\n providerKey: string,\n calculatorKey: string,\n result: ProviderAdjustmentResult | null | undefined,\n defaultKind: SalesAdjustmentDraft['kind']\n): SalesDocumentCalculationResult {\n if (!result?.adjustments?.length) {\n return rebuildDocumentResult({\n documentKind: current.kind,\n currencyCode: current.currencyCode,\n lines: current.lines,\n adjustments,\n metadata: current.metadata,\n })\n }\n const normalized = normalizeAdjustments({\n providerKey,\n calculatorKey,\n adjustments: result.adjustments,\n currencyCode: current.currencyCode,\n defaultKind,\n })\n const nextAdjustments = [...adjustments, ...normalized]\n const next = rebuildDocumentResult({\n documentKind: current.kind,\n currencyCode: current.currencyCode,\n lines: current.lines,\n adjustments: nextAdjustments,\n metadata: current.metadata,\n })\n if (result.metadata) {\n next.metadata = mergeMetadata(next.metadata, calculatorKey, result.metadata)\n }\n return next\n}\n\nexport function ensureProviderTotalsCalculator() {\n if (totalsRegistered) return\n totalsRegistered = true\n\n registerSalesTotalsCalculator(async ({ documentKind, lines, context, current, eventBus }) => {\n const metadata = (context.metadata ?? {}) as Record<string, unknown>\n const shippingMethod = (metadata.shippingMethod ?? null) as ShippingMethodContext | null\n const paymentMethod = (metadata.paymentMethod ?? null) as PaymentMethodContext | null\n\n let runningAdjustments = (current.adjustments ?? []).filter(\n (adj) => !isProviderAdjustment(adj) || isManualOverride(adj)\n )\n let working = rebuildDocumentResult({\n documentKind,\n currencyCode: current.currencyCode,\n lines,\n adjustments: runningAdjustments,\n metadata: current.metadata,\n })\n\n if (shippingMethod?.providerKey) {\n const provider = getShippingProvider(shippingMethod.providerKey)\n if (provider?.calculate) {\n const calculatorKey = `${SHIPPING_PREFIX}${provider.key}`\n const hasManualOverride = runningAdjustments.some(\n (adj) => (adj.calculatorKey ?? '').startsWith(calculatorKey) && isManualOverride(adj)\n )\n if (hasManualOverride) {\n // Respect user overrides: keep existing provider-tagged adjustments and skip regeneration.\n working = rebuildDocumentResult({\n documentKind,\n currencyCode: working.currencyCode,\n lines: working.lines,\n adjustments: runningAdjustments,\n metadata: working.metadata,\n })\n } else {\n const rawSettings = extractProviderSettings(shippingMethod)\n const settings =\n normalizeProviderSettings('shipping', provider.key, rawSettings) ?? rawSettings ?? {}\n const metrics = computeMetrics(working.lines)\n let result = await provider.calculate({\n method: shippingMethod,\n settings,\n document: working,\n lines: working.lines,\n context,\n metrics,\n })\n if (eventBus) {\n await eventBus.emitEvent('sales.shipping.adjustments.apply.before', {\n documentKind,\n providerKey: provider.key,\n calculatorKey,\n method: shippingMethod,\n settings,\n document: working,\n result,\n setResult(next: ProviderAdjustmentResult | null | undefined) {\n result = next ?? null\n },\n })\n }\n working = applyProviderResult(working, runningAdjustments, provider.key, calculatorKey, result, 'shipping')\n runningAdjustments = working.adjustments\n if (eventBus) {\n let nextDocument = working\n await eventBus.emitEvent('sales.shipping.adjustments.apply.after', {\n documentKind,\n providerKey: provider.key,\n calculatorKey,\n method: shippingMethod,\n settings,\n document: nextDocument,\n adjustments: nextDocument.adjustments.filter((adj) =>\n (adj.calculatorKey ?? '').startsWith(calculatorKey)\n ),\n setDocument(next: SalesDocumentCalculationResult | null | undefined) {\n if (next) {\n nextDocument = next\n }\n },\n setAdjustments(next: SalesAdjustmentDraft[] | null | undefined) {\n if (!next) return\n const preserved = nextDocument.adjustments.filter(\n (adj) => !(adj.calculatorKey ?? '').startsWith(calculatorKey)\n )\n nextDocument = rebuildDocumentResult({\n documentKind,\n currencyCode: nextDocument.currencyCode,\n lines: nextDocument.lines,\n adjustments: [...preserved, ...next],\n metadata: nextDocument.metadata,\n })\n },\n })\n working = nextDocument\n runningAdjustments = working.adjustments\n }\n }\n }\n }\n\n if (paymentMethod?.providerKey) {\n const provider = getPaymentProvider(paymentMethod.providerKey)\n if (provider?.calculate) {\n const calculatorKey = `${PAYMENT_PREFIX}${provider.key}`\n const hasManualOverride = runningAdjustments.some(\n (adj) => (adj.calculatorKey ?? '').startsWith(calculatorKey) && isManualOverride(adj)\n )\n if (hasManualOverride) {\n working = rebuildDocumentResult({\n documentKind,\n currencyCode: working.currencyCode,\n lines: working.lines,\n adjustments: runningAdjustments,\n metadata: working.metadata,\n })\n } else {\n runningAdjustments = withoutPrefix(runningAdjustments, PAYMENT_PREFIX)\n const rawSettings = extractProviderSettings(paymentMethod)\n const settings =\n normalizeProviderSettings('payment', provider.key, rawSettings) ?? rawSettings ?? {}\n let result = await provider.calculate({\n method: paymentMethod,\n settings,\n document: working,\n lines: working.lines,\n context,\n })\n if (eventBus) {\n await eventBus.emitEvent('sales.payment.adjustments.apply.before', {\n documentKind,\n providerKey: provider.key,\n calculatorKey,\n method: paymentMethod,\n settings,\n document: working,\n result,\n setResult(next: ProviderAdjustmentResult | null | undefined) {\n result = next ?? null\n },\n })\n }\n working = applyProviderResult(\n working,\n runningAdjustments,\n provider.key,\n calculatorKey,\n result,\n 'surcharge'\n )\n runningAdjustments = working.adjustments\n if (eventBus) {\n let nextDocument = working\n await eventBus.emitEvent('sales.payment.adjustments.apply.after', {\n documentKind,\n providerKey: provider.key,\n calculatorKey,\n method: paymentMethod,\n settings,\n document: nextDocument,\n adjustments: nextDocument.adjustments.filter((adj) =>\n (adj.calculatorKey ?? '').startsWith(calculatorKey)\n ),\n setDocument(next: SalesDocumentCalculationResult | null | undefined) {\n if (next) {\n nextDocument = next\n }\n },\n setAdjustments(next: SalesAdjustmentDraft[] | null | undefined) {\n if (!next) return\n const preserved = nextDocument.adjustments.filter(\n (adj) => !(adj.calculatorKey ?? '').startsWith(calculatorKey)\n )\n nextDocument = rebuildDocumentResult({\n documentKind,\n currencyCode: nextDocument.currencyCode,\n lines: nextDocument.lines,\n adjustments: [...preserved, ...next],\n metadata: nextDocument.metadata,\n })\n },\n })\n working = nextDocument\n runningAdjustments = working.adjustments\n }\n }\n }\n }\n\n return working\n })\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { isExternalAmountsMode, registerSalesTotalsCalculator, rebuildDocumentResult } from '../calculations'\nimport type { SalesAdjustmentDraft, SalesDocumentCalculationResult } from '../types'\nimport {\n getPaymentProvider,\n getShippingProvider,\n normalizeProviderSettings,\n} from './registry'\nimport type {\n PaymentMethodContext,\n ProviderAdjustment,\n ProviderAdjustmentResult,\n ShippingMethodContext,\n ShippingMetrics,\n} from './types'\n\nconst SHIPPING_PREFIX = 'shipping-provider:'\nconst PAYMENT_PREFIX = 'payment-provider:'\n\nlet totalsRegistered = false\n\nfunction toNumber(value: unknown, fallback = 0): number {\n if (typeof value === 'number' && Number.isFinite(value)) return value\n if (typeof value === 'string' && value.trim() !== '' && !Number.isNaN(Number(value))) {\n return Number(value)\n }\n return fallback\n}\n\nfunction isProviderAdjustment(adjustment: SalesAdjustmentDraft): boolean {\n const key = adjustment.calculatorKey ?? ''\n return typeof key === 'string' && (key.startsWith(SHIPPING_PREFIX) || key.startsWith(PAYMENT_PREFIX))\n}\n\nfunction isManualOverride(adjustment: SalesAdjustmentDraft): boolean {\n const metadata = adjustment.metadata\n if (!metadata || typeof metadata !== 'object') return false\n const manual =\n (metadata as Record<string, unknown>).manualOverride ??\n (metadata as Record<string, unknown>).manual_override\n return manual === true\n}\n\nfunction withoutPrefix(adjustments: SalesAdjustmentDraft[], prefix: string) {\n return adjustments.filter((adj) => {\n if (!(adj.calculatorKey ?? '').startsWith(prefix)) return true\n return isManualOverride(adj)\n })\n}\n\nfunction extractProviderSettings(\n method: ShippingMethodContext | PaymentMethodContext | null | undefined\n): Record<string, unknown> | null {\n if (!method) return null\n if (method.providerSettings && typeof method.providerSettings === 'object') {\n return method.providerSettings as Record<string, unknown>\n }\n if (method.metadata && typeof method.metadata === 'object') {\n const meta = method.metadata as Record<string, unknown>\n if (meta.providerSettings && typeof meta.providerSettings === 'object') {\n return meta.providerSettings as Record<string, unknown>\n }\n }\n return null\n}\n\nfunction computeMetrics(lines: SalesDocumentCalculationResult['lines']): ShippingMetrics {\n let itemCount = 0\n let totalWeight = 0\n let totalVolume = 0\n let subtotalNet = 0\n let subtotalGross = 0\n\n for (const entry of lines) {\n const { line } = entry\n const qty = toNumber(line.quantity, 0)\n itemCount += qty\n subtotalNet += toNumber(entry.netAmount, 0)\n subtotalGross += toNumber(entry.grossAmount, 0)\n\n const meta = (line.metadata ?? {}) as Record<string, unknown>\n const configuration = (line.configuration ?? {}) as Record<string, unknown>\n const weight =\n toNumber((meta.weight as number | string | undefined) ?? (configuration.weight as number | string | undefined), 0) *\n qty\n const volume =\n toNumber(\n (meta.volume as number | string | undefined) ??\n (configuration.volume as number | string | undefined),\n 0\n ) * qty\n\n totalWeight += weight\n totalVolume += volume\n }\n\n return {\n itemCount,\n totalWeight,\n totalVolume,\n subtotalNet,\n subtotalGross,\n }\n}\n\nfunction normalizeAdjustments(params: {\n providerKey: string\n calculatorKey: string\n adjustments: ProviderAdjustment[]\n currencyCode: string\n defaultKind: SalesAdjustmentDraft['kind']\n}) {\n return params.adjustments.map<SalesAdjustmentDraft>((adj, index) => ({\n scope: 'order',\n kind: adj.kind ?? params.defaultKind,\n code: adj.code ?? params.providerKey,\n label: adj.label ?? params.providerKey,\n calculatorKey: params.calculatorKey,\n promotionId: null,\n rate: null,\n amountNet: toNumber(adj.amountNet, 0),\n amountGross:\n adj.amountGross === undefined || adj.amountGross === null\n ? toNumber(adj.amountNet, 0)\n : toNumber(adj.amountGross, 0),\n currencyCode: (adj.currencyCode || params.currencyCode || '').toUpperCase() || params.currencyCode,\n metadata: adj.metadata ?? null,\n position: 10_000 + index,\n }))\n}\n\nfunction mergeMetadata(\n base: Record<string, unknown> | undefined,\n key: string,\n value: unknown\n): Record<string, unknown> {\n const next = { ...(base ?? {}) }\n next[key] = value\n return next\n}\n\nfunction applyProviderResult(\n current: SalesDocumentCalculationResult,\n adjustments: SalesAdjustmentDraft[],\n providerKey: string,\n calculatorKey: string,\n result: ProviderAdjustmentResult | null | undefined,\n defaultKind: SalesAdjustmentDraft['kind']\n): SalesDocumentCalculationResult {\n if (!result?.adjustments?.length) {\n return rebuildDocumentResult({\n documentKind: current.kind,\n currencyCode: current.currencyCode,\n lines: current.lines,\n adjustments,\n metadata: current.metadata,\n })\n }\n const normalized = normalizeAdjustments({\n providerKey,\n calculatorKey,\n adjustments: result.adjustments,\n currencyCode: current.currencyCode,\n defaultKind,\n })\n const nextAdjustments = [...adjustments, ...normalized]\n const next = rebuildDocumentResult({\n documentKind: current.kind,\n currencyCode: current.currencyCode,\n lines: current.lines,\n adjustments: nextAdjustments,\n metadata: current.metadata,\n })\n if (result.metadata) {\n next.metadata = mergeMetadata(next.metadata, calculatorKey, result.metadata)\n }\n return next\n}\n\nexport function ensureProviderTotalsCalculator() {\n if (totalsRegistered) return\n totalsRegistered = true\n\n registerSalesTotalsCalculator(async ({ documentKind, lines, context, current, eventBus, totalsMode }) => {\n // A provider charge here could not move the caller's header, so it would show\n // in the breakdown and be absent from the total.\n if (isExternalAmountsMode(totalsMode)) return current\n const metadata = (context.metadata ?? {}) as Record<string, unknown>\n const shippingMethod = (metadata.shippingMethod ?? null) as ShippingMethodContext | null\n const paymentMethod = (metadata.paymentMethod ?? null) as PaymentMethodContext | null\n\n let runningAdjustments = (current.adjustments ?? []).filter(\n (adj) => !isProviderAdjustment(adj) || isManualOverride(adj)\n )\n let working = rebuildDocumentResult({\n documentKind,\n currencyCode: current.currencyCode,\n lines,\n adjustments: runningAdjustments,\n metadata: current.metadata,\n })\n\n if (shippingMethod?.providerKey) {\n const provider = getShippingProvider(shippingMethod.providerKey)\n if (provider?.calculate) {\n const calculatorKey = `${SHIPPING_PREFIX}${provider.key}`\n const hasManualOverride = runningAdjustments.some(\n (adj) => (adj.calculatorKey ?? '').startsWith(calculatorKey) && isManualOverride(adj)\n )\n if (hasManualOverride) {\n // Respect user overrides: keep existing provider-tagged adjustments and skip regeneration.\n working = rebuildDocumentResult({\n documentKind,\n currencyCode: working.currencyCode,\n lines: working.lines,\n adjustments: runningAdjustments,\n metadata: working.metadata,\n })\n } else {\n const rawSettings = extractProviderSettings(shippingMethod)\n const settings =\n normalizeProviderSettings('shipping', provider.key, rawSettings) ?? rawSettings ?? {}\n const metrics = computeMetrics(working.lines)\n let result = await provider.calculate({\n method: shippingMethod,\n settings,\n document: working,\n lines: working.lines,\n context,\n metrics,\n })\n if (eventBus) {\n await eventBus.emitEvent('sales.shipping.adjustments.apply.before', {\n documentKind,\n providerKey: provider.key,\n calculatorKey,\n method: shippingMethod,\n settings,\n document: working,\n result,\n setResult(next: ProviderAdjustmentResult | null | undefined) {\n result = next ?? null\n },\n })\n }\n working = applyProviderResult(working, runningAdjustments, provider.key, calculatorKey, result, 'shipping')\n runningAdjustments = working.adjustments\n if (eventBus) {\n let nextDocument = working\n await eventBus.emitEvent('sales.shipping.adjustments.apply.after', {\n documentKind,\n providerKey: provider.key,\n calculatorKey,\n method: shippingMethod,\n settings,\n document: nextDocument,\n adjustments: nextDocument.adjustments.filter((adj) =>\n (adj.calculatorKey ?? '').startsWith(calculatorKey)\n ),\n setDocument(next: SalesDocumentCalculationResult | null | undefined) {\n if (next) {\n nextDocument = next\n }\n },\n setAdjustments(next: SalesAdjustmentDraft[] | null | undefined) {\n if (!next) return\n const preserved = nextDocument.adjustments.filter(\n (adj) => !(adj.calculatorKey ?? '').startsWith(calculatorKey)\n )\n nextDocument = rebuildDocumentResult({\n documentKind,\n currencyCode: nextDocument.currencyCode,\n lines: nextDocument.lines,\n adjustments: [...preserved, ...next],\n metadata: nextDocument.metadata,\n })\n },\n })\n working = nextDocument\n runningAdjustments = working.adjustments\n }\n }\n }\n }\n\n if (paymentMethod?.providerKey) {\n const provider = getPaymentProvider(paymentMethod.providerKey)\n if (provider?.calculate) {\n const calculatorKey = `${PAYMENT_PREFIX}${provider.key}`\n const hasManualOverride = runningAdjustments.some(\n (adj) => (adj.calculatorKey ?? '').startsWith(calculatorKey) && isManualOverride(adj)\n )\n if (hasManualOverride) {\n working = rebuildDocumentResult({\n documentKind,\n currencyCode: working.currencyCode,\n lines: working.lines,\n adjustments: runningAdjustments,\n metadata: working.metadata,\n })\n } else {\n runningAdjustments = withoutPrefix(runningAdjustments, PAYMENT_PREFIX)\n const rawSettings = extractProviderSettings(paymentMethod)\n const settings =\n normalizeProviderSettings('payment', provider.key, rawSettings) ?? rawSettings ?? {}\n let result = await provider.calculate({\n method: paymentMethod,\n settings,\n document: working,\n lines: working.lines,\n context,\n })\n if (eventBus) {\n await eventBus.emitEvent('sales.payment.adjustments.apply.before', {\n documentKind,\n providerKey: provider.key,\n calculatorKey,\n method: paymentMethod,\n settings,\n document: working,\n result,\n setResult(next: ProviderAdjustmentResult | null | undefined) {\n result = next ?? null\n },\n })\n }\n working = applyProviderResult(\n working,\n runningAdjustments,\n provider.key,\n calculatorKey,\n result,\n 'surcharge'\n )\n runningAdjustments = working.adjustments\n if (eventBus) {\n let nextDocument = working\n await eventBus.emitEvent('sales.payment.adjustments.apply.after', {\n documentKind,\n providerKey: provider.key,\n calculatorKey,\n method: paymentMethod,\n settings,\n document: nextDocument,\n adjustments: nextDocument.adjustments.filter((adj) =>\n (adj.calculatorKey ?? '').startsWith(calculatorKey)\n ),\n setDocument(next: SalesDocumentCalculationResult | null | undefined) {\n if (next) {\n nextDocument = next\n }\n },\n setAdjustments(next: SalesAdjustmentDraft[] | null | undefined) {\n if (!next) return\n const preserved = nextDocument.adjustments.filter(\n (adj) => !(adj.calculatorKey ?? '').startsWith(calculatorKey)\n )\n nextDocument = rebuildDocumentResult({\n documentKind,\n currencyCode: nextDocument.currencyCode,\n lines: nextDocument.lines,\n adjustments: [...preserved, ...next],\n metadata: nextDocument.metadata,\n })\n },\n })\n working = nextDocument\n runningAdjustments = working.adjustments\n }\n }\n }\n }\n\n return working\n })\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,19 @@
1
+ import { Migration } from "@mikro-orm/migrations";
2
+ class Migration20260910120000_sales_external_amounts_mode extends Migration {
3
+ async up() {
4
+ this.addSql(
5
+ `alter table "sales_orders" add column if not exists "totals_mode" text not null default 'computed';`
6
+ );
7
+ this.addSql(
8
+ `alter table "sales_order_lines" add column if not exists "amounts_mode" text not null default 'computed';`
9
+ );
10
+ }
11
+ async down() {
12
+ this.addSql(`alter table "sales_order_lines" drop column if exists "amounts_mode";`);
13
+ this.addSql(`alter table "sales_orders" drop column if exists "totals_mode";`);
14
+ }
15
+ }
16
+ export {
17
+ Migration20260910120000_sales_external_amounts_mode
18
+ };
19
+ //# sourceMappingURL=Migration20260910120000_sales_external_amounts_mode.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/sales/migrations/Migration20260910120000_sales_external_amounts_mode.ts"],
4
+ "sourcesContent": ["import { Migration } from '@mikro-orm/migrations';\n\n// Opt-in `external` amounts mode for sales orders: the row-level statement that\n// the stored amounts are a caller's assertion mirrored from an external book of\n// record, not something core derived. Two defaulted columns, no backfill \u2014 every\n// existing row takes 'computed', which is exactly today's behaviour, so the\n// change is observably inert until a caller sets the mode.\n//\n// PostgreSQL has not rewritten a table for a defaulted column add since 11, so\n// both statements are metadata-only regardless of table size.\nexport class Migration20260910120000_sales_external_amounts_mode extends Migration {\n override async up(): Promise<void> {\n this.addSql(\n `alter table \"sales_orders\" add column if not exists \"totals_mode\" text not null default 'computed';`,\n );\n this.addSql(\n `alter table \"sales_order_lines\" add column if not exists \"amounts_mode\" text not null default 'computed';`,\n );\n }\n\n override async down(): Promise<void> {\n this.addSql(`alter table \"sales_order_lines\" drop column if exists \"amounts_mode\";`);\n this.addSql(`alter table \"sales_orders\" drop column if exists \"totals_mode\";`);\n }\n}\n"],
5
+ "mappings": "AAAA,SAAS,iBAAiB;AAUnB,MAAM,4DAA4D,UAAU;AAAA,EACjF,MAAe,KAAoB;AACjC,SAAK;AAAA,MACH;AAAA,IACF;AACA,SAAK;AAAA,MACH;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAe,OAAsB;AACnC,SAAK,OAAO,uEAAuE;AACnF,SAAK,OAAO,iEAAiE;AAAA,EAC/E;AACF;",
6
+ "names": []
7
+ }
@@ -43,6 +43,7 @@ export const totals_snapshot = "totals_snapshot";
43
43
  export const paid_total_amount = "paid_total_amount";
44
44
  export const refunded_total_amount = "refunded_total_amount";
45
45
  export const outstanding_amount = "outstanding_amount";
46
+ export const totals_mode = "totals_mode";
46
47
  export const line_item_count = "line_item_count";
47
48
  export const metadata = "metadata";
48
49
  export const custom_field_set_id = "custom_field_set_id";
@@ -30,6 +30,7 @@ export const tax_rate = "tax_rate";
30
30
  export const tax_amount = "tax_amount";
31
31
  export const total_net_amount = "total_net_amount";
32
32
  export const total_gross_amount = "total_gross_amount";
33
+ export const amounts_mode = "amounts_mode";
33
34
  export const configuration = "configuration";
34
35
  export const promotion_code = "promotion_code";
35
36
  export const promotion_snapshot = "promotion_snapshot";
@@ -3054,6 +3054,7 @@ export const entityFieldsRegistry: Record<string, Record<string, string>> = {
3054
3054
  "paid_total_amount": "paid_total_amount",
3055
3055
  "refunded_total_amount": "refunded_total_amount",
3056
3056
  "outstanding_amount": "outstanding_amount",
3057
+ "totals_mode": "totals_mode",
3057
3058
  "line_item_count": "line_item_count",
3058
3059
  "metadata": "metadata",
3059
3060
  "custom_field_set_id": "custom_field_set_id",
@@ -3136,6 +3137,7 @@ export const entityFieldsRegistry: Record<string, Record<string, string>> = {
3136
3137
  "tax_amount": "tax_amount",
3137
3138
  "total_net_amount": "total_net_amount",
3138
3139
  "total_gross_amount": "total_gross_amount",
3140
+ "amounts_mode": "amounts_mode",
3139
3141
  "configuration": "configuration",
3140
3142
  "promotion_code": "promotion_code",
3141
3143
  "promotion_snapshot": "promotion_snapshot",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.10.1-develop.7723.1.3b2ea82ea7",
3
+ "version": "0.10.1-develop.7726.1.a447cc5525",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -9,6 +9,7 @@
9
9
  "watch": "node watch.mjs",
10
10
  "test": "jest --config jest.config.cjs",
11
11
  "test:pg-integration": "cross-env OM_PG_INTEGRATION=1 jest --config jest.config.cjs --runInBand 'src/modules/(integrations|staff)/.+\\.integration\\.test\\.ts$'",
12
+ "test:tz": "cross-env TZ=Pacific/Auckland jest --config jest.config.cjs",
12
13
  "typecheck": "tsc --noEmit"
13
14
  },
14
15
  "imports": {
@@ -265,16 +266,16 @@
265
266
  "zod": "^4.4.3"
266
267
  },
267
268
  "peerDependencies": {
268
- "@open-mercato/ai-assistant": "0.10.1-develop.7723.1.3b2ea82ea7",
269
- "@open-mercato/shared": "0.10.1-develop.7723.1.3b2ea82ea7",
270
- "@open-mercato/ui": "0.10.1-develop.7723.1.3b2ea82ea7",
269
+ "@open-mercato/ai-assistant": "0.10.1-develop.7726.1.a447cc5525",
270
+ "@open-mercato/shared": "0.10.1-develop.7726.1.a447cc5525",
271
+ "@open-mercato/ui": "0.10.1-develop.7726.1.a447cc5525",
271
272
  "react": "^19.0.0",
272
273
  "react-dom": "^19.0.0"
273
274
  },
274
275
  "devDependencies": {
275
- "@open-mercato/ai-assistant": "0.10.1-develop.7723.1.3b2ea82ea7",
276
- "@open-mercato/shared": "0.10.1-develop.7723.1.3b2ea82ea7",
277
- "@open-mercato/ui": "0.10.1-develop.7723.1.3b2ea82ea7",
276
+ "@open-mercato/ai-assistant": "0.10.1-develop.7726.1.a447cc5525",
277
+ "@open-mercato/shared": "0.10.1-develop.7726.1.a447cc5525",
278
+ "@open-mercato/ui": "0.10.1-develop.7726.1.a447cc5525",
278
279
  "@testing-library/dom": "^10.4.1",
279
280
  "@testing-library/jest-dom": "^7.0.1",
280
281
  "@testing-library/react": "^16.3.3",
@@ -11,6 +11,7 @@ import { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/er
11
11
  import { parseScopedCommandInput } from '@open-mercato/shared/lib/api/scoped'
12
12
  import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
13
13
  import { plannerAvailabilityDateSpecificReplaceSchema } from '../data/validators'
14
+ import { zonedDateKey } from '../lib/availabilityTimezone'
14
15
  import { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'
15
16
  import {
16
17
  runCrudMutationGuardAfterSuccess,
@@ -26,11 +27,9 @@ const logger = createLogger('planner').child({ component: 'availability' })
26
27
  export const metadata = {
27
28
  POST: { requireAuth: true },
28
29
  }
29
-
30
30
  type RequestContext = {
31
31
  ctx: CommandRuntimeContext
32
32
  }
33
-
34
33
  async function resolveRequestContext(req: Request): Promise<RequestContext> {
35
34
  const container = await createRequestContainer()
36
35
  const auth = await getAuthFromRequest(req)
@@ -94,7 +93,10 @@ export async function POST(req: Request) {
94
93
  const blocked = rules.some((rule) => {
95
94
  const window = parseAvailabilityRuleWindow(rule)
96
95
  if (window.repeat !== 'once') return false
97
- return dateSet.has(formatDateKey(window.startAt))
96
+ // Resolved in the rule's own zone so this gate agrees with
97
+ // `planner.availability.date-specific.replace` and does not depend
98
+ // on the host clock.
99
+ return dateSet.has(zonedDateKey(window.startAt, rule.timezone))
98
100
  })
99
101
  if (blocked) {
100
102
  throw new CrudHttpError(403, { error: translate('planner.availability.errors.unauthorized', 'Unauthorized') })
@@ -215,10 +217,3 @@ function resolveDateSet(input: { date?: string; dates?: string[] }): Set<string>
215
217
  }
216
218
  return dates
217
219
  }
218
-
219
- function formatDateKey(value: Date): string {
220
- const year = value.getFullYear()
221
- const month = String(value.getMonth() + 1).padStart(2, '0')
222
- const day = String(value.getDate()).padStart(2, '0')
223
- return `${year}-${month}-${day}`
224
- }
@@ -3,6 +3,7 @@ import { registerCommand } from '@open-mercato/shared/lib/commands'
3
3
  import type { EntityManager } from '@mikro-orm/postgresql'
4
4
  import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
5
5
  import { parseAvailabilityRuleWindow } from '@open-mercato/core/modules/planner/lib/availabilitySchedule'
6
+ import { warnOnUnresolvableTimeZone, zonedDateKey, zonedDayBounds, zonedWallTimeToInstant } from '@open-mercato/core/modules/planner/lib/availabilityTimezone'
6
7
  import { PlannerAvailabilityRule } from '../data/entities'
7
8
  import {
8
9
  plannerAvailabilityDateSpecificReplaceSchema,
@@ -52,22 +53,9 @@ type DateSpecificUndoPayload = {
52
53
  after: AvailabilityRuleSnapshot[]
53
54
  }
54
55
 
55
- function parseTimeInput(value: string): { hours: number; minutes: number } | null {
56
- const [hours, minutes] = value.split(':').map((part) => Number(part))
57
- if (!Number.isFinite(hours) || !Number.isFinite(minutes)) return null
58
- if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) return null
59
- return { hours, minutes }
60
- }
61
-
62
- function toDateForDay(value: string, time: string): Date | null {
63
- if (!value) return null
64
- const parsed = parseTimeInput(time)
65
- if (!parsed) return null
66
- const parts = value.split('-').map((part) => Number(part))
67
- if (parts.length !== 3 || parts.some((part) => Number.isNaN(part))) return null
68
- const [year, month, day] = parts
69
- const date = new Date(year, month - 1, day, parsed.hours, parsed.minutes, 0, 0)
70
- return Number.isNaN(date.getTime()) ? null : date
56
+ function toDateForDay(dateKey: string, time: string, timeZone: string): Date | null {
57
+ if (!dateKey) return null
58
+ return zonedWallTimeToInstant(dateKey, time, timeZone)
71
59
  }
72
60
 
73
61
  function formatDuration(minutes: number): string {
@@ -86,18 +74,10 @@ function buildAvailabilityRrule(start: Date, end: Date): string {
86
74
  return `DTSTART:${dtStart}\nDURATION:${duration}\nRRULE:FREQ=DAILY;COUNT=1`
87
75
  }
88
76
 
89
- function buildFullDayRrule(date: string): string | null {
90
- const start = toDateForDay(date, '00:00')
91
- if (!start) return null
92
- const end = new Date(start.getTime() + 24 * 60 * 60 * 1000)
93
- return buildAvailabilityRrule(start, end)
94
- }
95
-
96
- function formatDateKey(value: Date): string {
97
- const year = value.getFullYear()
98
- const month = String(value.getMonth() + 1).padStart(2, '0')
99
- const day = String(value.getDate()).padStart(2, '0')
100
- return `${year}-${month}-${day}`
77
+ function buildFullDayRrule(date: string, timeZone: string): string | null {
78
+ const bounds = zonedDayBounds(date, timeZone)
79
+ if (!bounds) return null
80
+ return buildAvailabilityRrule(bounds.start, bounds.end)
101
81
  }
102
82
 
103
83
  function toAvailabilityRuleSnapshot(record: PlannerAvailabilityRule): AvailabilityRuleSnapshot {
@@ -140,7 +120,7 @@ async function loadDateSpecificSnapshots(
140
120
  .filter((rule) => {
141
121
  const window = parseAvailabilityRuleWindow(rule)
142
122
  if (window.repeat !== 'once') return false
143
- return params.dates.has(formatDateKey(window.startAt))
123
+ return params.dates.has(zonedDateKey(window.startAt, rule.timezone))
144
124
  })
145
125
  .map(toAvailabilityRuleSnapshot)
146
126
  }
@@ -199,6 +179,7 @@ const replaceDateSpecificAvailabilityCommand: CommandHandler<PlannerAvailability
199
179
  },
200
180
  async execute(input, ctx) {
201
181
  const parsed = plannerAvailabilityDateSpecificReplaceSchema.parse(input)
182
+ warnOnUnresolvableTimeZone(parsed.timezone, 'planner.availability.date-specific.replace')
202
183
  ensureTenantScope(ctx, parsed.tenantId)
203
184
  ensureOrganizationScope(ctx, parsed.organizationId)
204
185
  const dates = resolveDateSet(parsed)
@@ -223,7 +204,7 @@ const replaceDateSpecificAvailabilityCommand: CommandHandler<PlannerAvailability
223
204
  const toDelete = existing.filter((rule) => {
224
205
  const window = parseAvailabilityRuleWindow(rule)
225
206
  if (window.repeat !== 'once') return false
226
- return dates.has(formatDateKey(window.startAt))
207
+ return dates.has(zonedDateKey(window.startAt, rule.timezone))
227
208
  })
228
209
  enforceCommandOptimisticLock({
229
210
  resourceKind: AVAILABILITY_RULE_RESOURCE_KIND,
@@ -244,7 +225,7 @@ const replaceDateSpecificAvailabilityCommand: CommandHandler<PlannerAvailability
244
225
 
245
226
  if (!isAvailable) {
246
227
  dates.forEach((date) => {
247
- const rrule = buildFullDayRrule(date)
228
+ const rrule = buildFullDayRrule(date, parsed.timezone)
248
229
  if (!rrule) return
249
230
  const record = trx.create(PlannerAvailabilityRule, {
250
231
  tenantId: parsed.tenantId,
@@ -266,8 +247,8 @@ const replaceDateSpecificAvailabilityCommand: CommandHandler<PlannerAvailability
266
247
  } else {
267
248
  dates.forEach((date) => {
268
249
  windows.forEach((window) => {
269
- const start = toDateForDay(date, window.start)
270
- const end = toDateForDay(date, window.end)
250
+ const start = toDateForDay(date, window.start, parsed.timezone)
251
+ const end = toDateForDay(date, window.end, parsed.timezone)
271
252
  if (!start || !end || start >= end) return
272
253
  const rrule = buildAvailabilityRrule(start, end)
273
254
  const record = trx.create(PlannerAvailabilityRule, {
@@ -2,6 +2,7 @@ import type { CommandHandler } from '@open-mercato/shared/lib/commands'
2
2
  import { registerCommand } from '@open-mercato/shared/lib/commands'
3
3
  import type { EntityManager } from '@mikro-orm/postgresql'
4
4
  import { parseAvailabilityRuleWindow } from '@open-mercato/core/modules/planner/lib/availabilitySchedule'
5
+ import { warnOnUnresolvableTimeZone, zonedWeeklyAnchor, zonedWeekday } from '@open-mercato/core/modules/planner/lib/availabilityTimezone'
5
6
  import { PlannerAvailabilityRule, PlannerAvailabilityRuleSet } from '../data/entities'
6
7
  import {
7
8
  plannerAvailabilityWeeklyReplaceSchema,
@@ -54,24 +55,6 @@ type WeeklyUndoPayload = {
54
55
  after: AvailabilityRuleSnapshot[]
55
56
  }
56
57
 
57
- function parseTimeInput(value: string): { hours: number; minutes: number } | null {
58
- const [hours, minutes] = value.split(':').map((part) => Number(part))
59
- if (!Number.isFinite(hours) || !Number.isFinite(minutes)) return null
60
- if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) return null
61
- return { hours, minutes }
62
- }
63
-
64
- function toDateForWeekday(weekday: number, time: string): Date | null {
65
- const parsed = parseTimeInput(time)
66
- if (!parsed) return null
67
- const now = new Date()
68
- const base = new Date(now.getFullYear(), now.getMonth(), now.getDate())
69
- const diff = (weekday - base.getDay() + 7) % 7
70
- const target = new Date(base.getTime() + diff * 24 * 60 * 60 * 1000)
71
- target.setHours(parsed.hours, parsed.minutes, 0, 0)
72
- return target
73
- }
74
-
75
58
  function formatDuration(minutes: number): string {
76
59
  const clamped = Math.max(1, minutes)
77
60
  const hours = Math.floor(clamped / 60)
@@ -81,11 +64,18 @@ function formatDuration(minutes: number): string {
81
64
  return `PT${mins}M`
82
65
  }
83
66
 
84
- function buildWeeklyRrule(start: Date, end: Date): string {
67
+ function buildWeeklyRrule(start: Date, end: Date, timeZone: string): string {
85
68
  const dtStart = start.toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z'
86
69
  const durationMinutes = Math.max(1, Math.round((end.getTime() - start.getTime()) / 60000))
87
70
  const duration = formatDuration(durationMinutes)
88
- const dayCode = DAY_CODES[start.getDay()] ?? 'MO'
71
+ // `BYDAY` names the weekday the window falls on in its DECLARED zone, which
72
+ // is the meaningful label for a recurring local-time window. It is therefore
73
+ // deliberately not consistent with the `Z`-suffixed `DTSTART` beside it: a
74
+ // Pacific/Auckland Monday 09:00 ships DTSTART:...T210000Z, a Sunday in UTC,
75
+ // with BYDAY=MO. Nothing in the planner reads BYDAY today (neither the
76
+ // expander nor `parseAvailabilityRuleWindow`); a future iCal export must
77
+ // re-derive it from the zone rather than trust it against DTSTART.
78
+ const dayCode = DAY_CODES[zonedWeekday(start, timeZone)] ?? 'MO'
89
79
  return `DTSTART:${dtStart}\nDURATION:${duration}\nRRULE:FREQ=WEEKLY;BYDAY=${dayCode}`
90
80
  }
91
81
 
@@ -185,6 +175,7 @@ const replaceWeeklyAvailabilityCommand: CommandHandler<PlannerAvailabilityWeekly
185
175
  },
186
176
  async execute(input, ctx) {
187
177
  const parsed = plannerAvailabilityWeeklyReplaceSchema.parse(input)
178
+ warnOnUnresolvableTimeZone(parsed.timezone, 'planner.availability.weekly.replace')
188
179
  ensureTenantScope(ctx, parsed.tenantId)
189
180
  ensureOrganizationScope(ctx, parsed.organizationId)
190
181
 
@@ -247,10 +238,10 @@ const replaceWeeklyAvailabilityCommand: CommandHandler<PlannerAvailabilityWeekly
247
238
  }
248
239
 
249
240
  parsed.windows.forEach((window) => {
250
- const start = toDateForWeekday(window.weekday, window.start)
251
- const end = toDateForWeekday(window.weekday, window.end)
252
- if (!start || !end || start >= end) return
253
- const rrule = buildWeeklyRrule(start, end)
241
+ const anchor = zonedWeeklyAnchor(window.weekday, window.start, window.end, parsed.timezone, now)
242
+ if (!anchor || anchor.start >= anchor.end) return
243
+ const { start, end } = anchor
244
+ const rrule = buildWeeklyRrule(start, end, parsed.timezone)
254
245
  const record = trx.create(PlannerAvailabilityRule, {
255
246
  tenantId: parsed.tenantId,
256
247
  organizationId: parsed.organizationId,
@@ -1,3 +1,5 @@
1
+ import { resolveTimeZone, zonedWallDateToInstant } from './availabilityTimezone'
2
+
1
3
  type AvailabilityKind = 'availability' | 'unavailability'
2
4
 
3
5
  export type AvailabilityRuleLike = {
@@ -46,21 +48,8 @@ function zonedParts(instant: Date, timezone: string): { year: number; month: num
46
48
  return { year: number('year'), month: number('month'), day: number('day'), hour: number('hour'), minute: number('minute'), second: number('second') }
47
49
  }
48
50
 
49
- function zonedWallToInstant(wall: Date, timezone: string): Date | null {
50
- const target = wall.getTime()
51
- let candidate = target
52
- for (let attempt = 0; attempt < 4; attempt += 1) {
53
- const parts = zonedParts(new Date(candidate), timezone)
54
- const displayed = Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, parts.second)
55
- const next = candidate + target - displayed
56
- if (next === candidate) return new Date(candidate)
57
- candidate = next
58
- }
59
- return null
60
- }
61
-
62
51
  function zonedStartOfDay(dayKey: string, timezone: string): Date | null {
63
- return zonedWallToInstant(new Date(`${dayKey}T00:00:00Z`), timezone)
52
+ return zonedWallDateToInstant(new Date(`${dayKey}T00:00:00Z`), timezone)
64
53
  }
65
54
 
66
55
  function dayKeyForRule(instant: Date, timezone?: string): string {
@@ -146,10 +135,10 @@ function expandZonedRule(rule: AvailabilityRuleLike, parsed: ParsedRule, range:
146
135
  let wall = new Date(anchorWall + skipped * step)
147
136
  let remaining = (parsed.count ?? Number.POSITIVE_INFINITY) - skipped
148
137
  while (wall.getTime() <= range.end.getTime() + DAY_MS && remaining > 0) {
149
- const start = zonedWallToInstant(wall, timezone)
150
- if (start && !shouldExcludeOccurrence(start, rule.exdates, timezone)) {
151
- const end = new Date(start.getTime() + duration)
152
- if (end > range.start && start < range.end) windows.push({ start, end, ruleId: rule.id })
138
+ const start = zonedWallDateToInstant(wall, timezone)
139
+ if (!shouldExcludeOccurrence(start, rule.exdates, timezone)) {
140
+ const end = zonedWallDateToInstant(new Date(wall.getTime() + duration), timezone)
141
+ if (end > start && end > range.start && start < range.end) windows.push({ start, end, ruleId: rule.id })
153
142
  }
154
143
  wall = new Date(wall.getTime() + step)
155
144
  remaining -= 1
@@ -231,7 +220,12 @@ export function getMergedAvailabilityWindows(params: {
231
220
  respectTimezone?: boolean
232
221
  weeklyScheduleTemplate?: boolean
233
222
  }): AvailabilityWindow[] {
234
- const rules = params.respectTimezone ? params.rules : params.rules.map((rule) => ({ ...rule, timezone: undefined }))
223
+ const rules: AvailabilityRuleLike[] = params.respectTimezone
224
+ ? params.rules.map((rule) => ({
225
+ ...rule,
226
+ timezone: rule.timezone ? resolveTimeZone(rule.timezone) : rule.timezone,
227
+ }))
228
+ : params.rules.map((rule) => ({ ...rule, timezone: undefined }))
235
229
  const parsedRules = rules
236
230
  .map((rule) => {
237
231
  const parsed = parseRrule(rule.rrule)