@openhi/constructs 0.0.188 → 0.0.190

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 (38) hide show
  1. package/lib/{chunk-7GMTHOYF.mjs → chunk-2TLJKOJL.mjs} +2 -2
  2. package/lib/{chunk-VQY57NOV.mjs → chunk-3PS5R367.mjs} +2 -2
  3. package/lib/{chunk-DIVYB6GD.mjs → chunk-4WFHISV3.mjs} +4 -4
  4. package/lib/{chunk-3M4QTQH6.mjs → chunk-4YQD4PFR.mjs} +2 -2
  5. package/lib/{chunk-JJ3AQ6G5.mjs → chunk-63POQTMP.mjs} +2 -2
  6. package/lib/{chunk-F2LY4TEI.mjs → chunk-7WBLBIQJ.mjs} +4 -4
  7. package/lib/{chunk-4LQR32D2.mjs → chunk-AYHBFK3Y.mjs} +2 -2
  8. package/lib/{chunk-PIQISEGW.mjs → chunk-RGE4WDND.mjs} +2 -2
  9. package/lib/{chunk-V6KLFEHC.mjs → chunk-UWVPF6GB.mjs} +2 -2
  10. package/lib/{chunk-Q4KQD2NB.mjs → chunk-V6AO5T57.mjs} +9 -2
  11. package/lib/chunk-V6AO5T57.mjs.map +1 -0
  12. package/lib/counter-reconciliation.handler.js.map +1 -1
  13. package/lib/counter-reconciliation.handler.mjs +4 -4
  14. package/lib/index.js.map +1 -1
  15. package/lib/index.mjs +8 -8
  16. package/lib/pre-token-generation.handler.js.map +1 -1
  17. package/lib/pre-token-generation.handler.mjs +4 -4
  18. package/lib/provision-default-workspace.handler.js +4 -1
  19. package/lib/provision-default-workspace.handler.js.map +1 -1
  20. package/lib/provision-default-workspace.handler.mjs +3 -3
  21. package/lib/rest-api-lambda.handler.js +984 -1
  22. package/lib/rest-api-lambda.handler.js.map +1 -1
  23. package/lib/rest-api-lambda.handler.mjs +1020 -8
  24. package/lib/rest-api-lambda.handler.mjs.map +1 -1
  25. package/lib/seed-demo-data.handler.js +4 -1
  26. package/lib/seed-demo-data.handler.js.map +1 -1
  27. package/lib/seed-demo-data.handler.mjs +7 -7
  28. package/package.json +1 -1
  29. package/lib/chunk-Q4KQD2NB.mjs.map +0 -1
  30. /package/lib/{chunk-7GMTHOYF.mjs.map → chunk-2TLJKOJL.mjs.map} +0 -0
  31. /package/lib/{chunk-VQY57NOV.mjs.map → chunk-3PS5R367.mjs.map} +0 -0
  32. /package/lib/{chunk-DIVYB6GD.mjs.map → chunk-4WFHISV3.mjs.map} +0 -0
  33. /package/lib/{chunk-3M4QTQH6.mjs.map → chunk-4YQD4PFR.mjs.map} +0 -0
  34. /package/lib/{chunk-JJ3AQ6G5.mjs.map → chunk-63POQTMP.mjs.map} +0 -0
  35. /package/lib/{chunk-F2LY4TEI.mjs.map → chunk-7WBLBIQJ.mjs.map} +0 -0
  36. /package/lib/{chunk-4LQR32D2.mjs.map → chunk-AYHBFK3Y.mjs.map} +0 -0
  37. /package/lib/{chunk-PIQISEGW.mjs.map → chunk-RGE4WDND.mjs.map} +0 -0
  38. /package/lib/{chunk-V6KLFEHC.mjs.map → chunk-UWVPF6GB.mjs.map} +0 -0
@@ -3550,6 +3550,9 @@ function mergeAuditIntoMeta(meta, audit) {
3550
3550
 
3551
3551
  // src/data/operations/data-operations-common.ts
3552
3552
  var DATA_ENTITY_SK = "CURRENT";
3553
+ function deriveVid(lastUpdated) {
3554
+ return lastUpdated.replace(/\D/g, "") || Date.now().toString(36);
3555
+ }
3553
3556
  async function getDataEntityById(entity, tenantId, workspaceId, id, resourceLabel) {
3554
3557
  const result = await entity.get({
3555
3558
  tenantId,
@@ -3678,7 +3681,7 @@ async function listDataEntitiesByWorkspace(entity, tenantId, workspaceId, mode =
3678
3681
  }
3679
3682
  async function createDataEntityRecord(entity, tenantId, workspaceId, id, resourceWithAudit, fallbackDate) {
3680
3683
  const lastUpdated = resourceWithAudit.meta?.lastUpdated ?? fallbackDate ?? (/* @__PURE__ */ new Date()).toISOString();
3681
- const vid = lastUpdated.replace(/[-:T.Z]/g, "").slice(0, 12) || Date.now().toString(36);
3684
+ const vid = deriveVid(lastUpdated);
3682
3685
  const resourceLike = resourceWithAudit;
3683
3686
  const summary = JSON.stringify((0, import_types4.extractSummary)(resourceLike));
3684
3687
  const gsi1sk = (0, import_types4.extractSortKey)(resourceLike);
@@ -3749,6 +3752,10 @@ async function updateDataEntityById(entity, tenantId, workspaceId, id, resourceL
3749
3752
  }).set({
3750
3753
  resource: compressResource(JSON.stringify(resource)),
3751
3754
  summary,
3755
+ // A fresh vid is what makes this UPDATE visible to the search tier:
3756
+ // the replication upsert rejects any event whose version does not
3757
+ // strictly exceed the stored row's. See deriveVid.
3758
+ vid: deriveVid(lastUpdated),
3752
3759
  lastUpdated,
3753
3760
  gsi1sk
3754
3761
  }).go();
@@ -12823,6 +12830,338 @@ async function listAppointmentsRoute(req, res) {
12823
12830
  }
12824
12831
  }
12825
12832
 
12833
+ // src/data/recurrence/constants.ts
12834
+ var import_types25 = require("@openhi/types");
12835
+
12836
+ // src/data/recurrence/rrule.ts
12837
+ var import_tz = require("@date-fns/tz");
12838
+ var import_date_fns = require("date-fns");
12839
+ var FREQ_TO_RRULE = {
12840
+ daily: "DAILY",
12841
+ weekly: "WEEKLY",
12842
+ monthly: "MONTHLY"
12843
+ };
12844
+ var RRULE_TO_FREQ = {
12845
+ DAILY: "daily",
12846
+ WEEKLY: "weekly",
12847
+ MONTHLY: "monthly"
12848
+ };
12849
+ var WEEKDAYS = ["MO", "TU", "WE", "TH", "FR", "SA", "SU"];
12850
+ var WEEKDAY_OFFSET_FROM_MONDAY = {
12851
+ MO: 0,
12852
+ TU: 1,
12853
+ WE: 2,
12854
+ TH: 3,
12855
+ FR: 4,
12856
+ SA: 5,
12857
+ SU: 6
12858
+ };
12859
+ var JS_DAY_TO_WEEKDAY = {
12860
+ 0: "SU",
12861
+ 1: "MO",
12862
+ 2: "TU",
12863
+ 3: "WE",
12864
+ 4: "TH",
12865
+ 5: "FR",
12866
+ 6: "SA"
12867
+ };
12868
+ var DEFAULT_MAX_COUNT = 1e3;
12869
+ function pad(value, length) {
12870
+ return String(value).padStart(length, "0");
12871
+ }
12872
+ function formatRruleUtc(date) {
12873
+ return `${pad(date.getUTCFullYear(), 4)}${pad(date.getUTCMonth() + 1, 2)}${pad(date.getUTCDate(), 2)}T${pad(date.getUTCHours(), 2)}${pad(date.getUTCMinutes(), 2)}${pad(date.getUTCSeconds(), 2)}Z`;
12874
+ }
12875
+ function parseRruleUtc(value) {
12876
+ const match = value.match(/^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z$/);
12877
+ if (!match) {
12878
+ throw new Error(`Unsupported RRULE UNTIL value: ${value}`);
12879
+ }
12880
+ const [, y, mo, d, h, mi, s] = match;
12881
+ return new Date(
12882
+ Date.UTC(
12883
+ Number(y),
12884
+ Number(mo) - 1,
12885
+ Number(d),
12886
+ Number(h),
12887
+ Number(mi),
12888
+ Number(s)
12889
+ )
12890
+ );
12891
+ }
12892
+ function buildRruleString(pattern) {
12893
+ const parts = [`FREQ=${FREQ_TO_RRULE[pattern.frequency]}`];
12894
+ if (pattern.interval > 1) {
12895
+ parts.push(`INTERVAL=${pattern.interval}`);
12896
+ }
12897
+ if (pattern.frequency === "weekly" && pattern.byWeekday?.length) {
12898
+ const ordered = WEEKDAYS.filter((day) => pattern.byWeekday?.includes(day));
12899
+ parts.push(`BYDAY=${ordered.join(",")}`);
12900
+ }
12901
+ if (pattern.end.kind === "count") {
12902
+ parts.push(`COUNT=${pattern.end.count}`);
12903
+ } else if (pattern.end.kind === "until") {
12904
+ parts.push(`UNTIL=${formatRruleUtc(pattern.end.until)}`);
12905
+ }
12906
+ return parts.join(";");
12907
+ }
12908
+ var SUPPORTED_RRULE_KEYS = /* @__PURE__ */ new Set([
12909
+ "FREQ",
12910
+ "INTERVAL",
12911
+ "BYDAY",
12912
+ "COUNT",
12913
+ "UNTIL"
12914
+ ]);
12915
+ var WEEKDAY_SET = new Set(WEEKDAYS);
12916
+ function parsePositiveInt(value, key) {
12917
+ const parsed = Number(value);
12918
+ if (!Number.isInteger(parsed) || parsed < 1) {
12919
+ throw new Error(`Invalid RRULE ${key}: ${value}`);
12920
+ }
12921
+ return parsed;
12922
+ }
12923
+ function parseRruleString(rrule) {
12924
+ const map = /* @__PURE__ */ new Map();
12925
+ for (const segment of rrule.split(";")) {
12926
+ const [key, value] = segment.split("=");
12927
+ if (!key || !value) continue;
12928
+ const normalizedKey = key.trim().toUpperCase();
12929
+ if (!SUPPORTED_RRULE_KEYS.has(normalizedKey)) {
12930
+ throw new Error(`Unsupported RRULE part: ${key}`);
12931
+ }
12932
+ map.set(normalizedKey, value.trim());
12933
+ }
12934
+ const freqRaw = map.get("FREQ");
12935
+ const frequency = freqRaw ? RRULE_TO_FREQ[freqRaw.toUpperCase()] : void 0;
12936
+ if (!frequency) {
12937
+ throw new Error(`Unsupported RRULE FREQ: ${freqRaw ?? "(missing)"}`);
12938
+ }
12939
+ const interval = map.has("INTERVAL") ? parsePositiveInt(map.get("INTERVAL"), "INTERVAL") : 1;
12940
+ let byWeekday;
12941
+ const byDay = map.get("BYDAY");
12942
+ if (byDay) {
12943
+ byWeekday = byDay.split(",").map((code) => {
12944
+ const normalized = code.trim().toUpperCase();
12945
+ if (!WEEKDAY_SET.has(normalized)) {
12946
+ throw new Error(`Unsupported RRULE BYDAY value: ${code}`);
12947
+ }
12948
+ return normalized;
12949
+ });
12950
+ }
12951
+ let end = { kind: "open" };
12952
+ if (map.has("COUNT")) {
12953
+ end = {
12954
+ kind: "count",
12955
+ count: parsePositiveInt(map.get("COUNT"), "COUNT")
12956
+ };
12957
+ } else if (map.has("UNTIL")) {
12958
+ end = { kind: "until", until: parseRruleUtc(map.get("UNTIL")) };
12959
+ }
12960
+ return { frequency, interval, byWeekday, end };
12961
+ }
12962
+ function setTimeOfDay(day, source) {
12963
+ return (0, import_date_fns.setMilliseconds)(
12964
+ (0, import_date_fns.setSeconds)((0, import_date_fns.setMinutes)((0, import_date_fns.setHours)(day, source.h), source.m), source.s),
12965
+ source.ms
12966
+ );
12967
+ }
12968
+ function expandOccurrences(args) {
12969
+ const { pattern, seriesStart, timezone, window } = args;
12970
+ const maxCount = args.maxCount ?? DEFAULT_MAX_COUNT;
12971
+ const startZ = new import_tz.TZDate(seriesStart.getTime(), timezone);
12972
+ const timeOfDay = {
12973
+ h: startZ.getHours(),
12974
+ m: startZ.getMinutes(),
12975
+ s: startZ.getSeconds(),
12976
+ ms: startZ.getMilliseconds()
12977
+ };
12978
+ const startMs = seriesStart.getTime();
12979
+ const fromMs = window.from.getTime();
12980
+ const toMs = window.to.getTime();
12981
+ const untilMs = pattern.end.kind === "until" ? pattern.end.until.getTime() : Infinity;
12982
+ const countLimit = pattern.end.kind === "count" ? pattern.end.count : Infinity;
12983
+ const results = [];
12984
+ let produced = 0;
12985
+ const accept = (candidate) => {
12986
+ const ms = candidate.getTime();
12987
+ if (ms < startMs) return true;
12988
+ if (ms > untilMs) return false;
12989
+ if (produced >= countLimit) return false;
12990
+ produced += 1;
12991
+ if (ms > toMs) return false;
12992
+ if (ms >= fromMs) results.push(new Date(ms));
12993
+ return true;
12994
+ };
12995
+ let examined = 0;
12996
+ const interval = Math.max(1, pattern.interval);
12997
+ let finished = false;
12998
+ if (pattern.frequency === "daily") {
12999
+ for (let i = 0; examined <= maxCount; i += 1, examined += 1) {
13000
+ if (!accept((0, import_date_fns.addDays)(startZ, i * interval))) {
13001
+ finished = true;
13002
+ break;
13003
+ }
13004
+ }
13005
+ } else if (pattern.frequency === "monthly") {
13006
+ for (let i = 0; examined <= maxCount; i += 1, examined += 1) {
13007
+ if (!accept((0, import_date_fns.addMonths)(startZ, i * interval))) {
13008
+ finished = true;
13009
+ break;
13010
+ }
13011
+ }
13012
+ } else {
13013
+ const days = pattern.byWeekday?.length ? pattern.byWeekday : [JS_DAY_TO_WEEKDAY[startZ.getDay()]];
13014
+ const offsets = WEEKDAYS.filter((day) => days.includes(day)).map(
13015
+ (day) => WEEKDAY_OFFSET_FROM_MONDAY[day]
13016
+ );
13017
+ const weekAnchor = (0, import_date_fns.startOfWeek)(startZ, { weekStartsOn: 1 });
13018
+ for (let w = 0; !finished && examined <= maxCount; w += interval) {
13019
+ const weekStart = (0, import_date_fns.addWeeks)(weekAnchor, w);
13020
+ for (const offset of offsets) {
13021
+ examined += 1;
13022
+ const occurrence = setTimeOfDay((0, import_date_fns.addDays)(weekStart, offset), timeOfDay);
13023
+ if (!accept(occurrence)) {
13024
+ finished = true;
13025
+ break;
13026
+ }
13027
+ }
13028
+ }
13029
+ }
13030
+ if (!finished) {
13031
+ throw new Error(
13032
+ `Recurrence expansion hit the ${maxCount}-candidate safety cap before reaching the end of the window. Raise maxCount or narrow the window.`
13033
+ );
13034
+ }
13035
+ return results;
13036
+ }
13037
+
13038
+ // src/data/recurrence/seriesInstances.ts
13039
+ var import_date_fns2 = require("date-fns");
13040
+
13041
+ // src/data/recurrence/membership.ts
13042
+ var import_types26 = require("@openhi/types");
13043
+
13044
+ // src/data/recurrence/seriesInstances.ts
13045
+ function buildSeriesInstances(args) {
13046
+ const {
13047
+ base,
13048
+ occurrences,
13049
+ durationMinutes,
13050
+ seriesId,
13051
+ rrule,
13052
+ timezone,
13053
+ anchor
13054
+ } = args;
13055
+ const seriesIdentifier = (0, import_types26.buildSeriesIdentifier)(seriesId);
13056
+ const seriesExtensions = [
13057
+ (0, import_types26.buildRecurrenceRuleExtension)(rrule),
13058
+ (0, import_types26.buildRecurrenceTimezoneExtension)(timezone),
13059
+ (0, import_types26.buildRecurrenceAnchorExtension)(anchor)
13060
+ ];
13061
+ const baseIdentifiers = base.identifier ?? [];
13062
+ const baseExtensions = base.extension ?? [];
13063
+ return occurrences.map((start) => ({
13064
+ ...base,
13065
+ start: start.toISOString(),
13066
+ end: (0, import_date_fns2.addMinutes)(start, durationMinutes).toISOString(),
13067
+ minutesDuration: durationMinutes,
13068
+ identifier: [
13069
+ ...baseIdentifiers,
13070
+ seriesIdentifier,
13071
+ (0, import_types26.buildOccurrenceIdentifier)(seriesId, start)
13072
+ ],
13073
+ extension: [...baseExtensions, ...seriesExtensions]
13074
+ }));
13075
+ }
13076
+
13077
+ // src/data/operations/data/appointment/appointment-series-common.ts
13078
+ var SeriesRequestError = class extends Error {
13079
+ };
13080
+ var SeriesNotFoundError = class extends Error {
13081
+ };
13082
+ var LOCATE_LIMIT = 500;
13083
+ async function locateSeriesInstances(opts) {
13084
+ const { context, seriesId, tableName } = opts;
13085
+ const found = await genericSearchOperation({
13086
+ resourceType: "Appointment",
13087
+ tenantId: context.tenantId,
13088
+ workspaceId: context.workspaceId,
13089
+ query: { identifier: `${import_types25.APPOINTMENT_SERIES_SYSTEM}|${seriesId}` },
13090
+ resolver: defaultSearchParameterResolver,
13091
+ limit: LOCATE_LIMIT
13092
+ });
13093
+ const instances = [];
13094
+ for (const entry of found.entries) {
13095
+ let resource;
13096
+ try {
13097
+ const hydrated = await getAppointmentByIdOperation({
13098
+ context,
13099
+ id: entry.id,
13100
+ tableName
13101
+ });
13102
+ resource = hydrated.resource;
13103
+ } catch {
13104
+ continue;
13105
+ }
13106
+ instances.push({
13107
+ id: entry.id,
13108
+ resource,
13109
+ occurrenceInstant: safeOccurrenceInstant(resource)
13110
+ });
13111
+ }
13112
+ instances.sort((a, b) => {
13113
+ if (a.occurrenceInstant === null && b.occurrenceInstant === null) {
13114
+ return a.id.localeCompare(b.id);
13115
+ }
13116
+ if (a.occurrenceInstant === null) return 1;
13117
+ if (b.occurrenceInstant === null) return -1;
13118
+ return Date.parse(a.occurrenceInstant) - Date.parse(b.occurrenceInstant) || a.id.localeCompare(b.id);
13119
+ });
13120
+ return instances;
13121
+ }
13122
+ function safeOccurrenceInstant(resource) {
13123
+ try {
13124
+ return (0, import_types26.getOccurrenceInstant)(resource);
13125
+ } catch {
13126
+ return null;
13127
+ }
13128
+ }
13129
+ function resolveFromOccurrence(instances, fromOccurrence) {
13130
+ const fromMs = Date.parse(fromOccurrence);
13131
+ if (Number.isNaN(fromMs)) {
13132
+ throw new SeriesRequestError(
13133
+ `fromOccurrence is not a valid ISO 8601 instant: "${fromOccurrence}".`
13134
+ );
13135
+ }
13136
+ const match = instances.some(
13137
+ (i) => i.occurrenceInstant !== null && Date.parse(i.occurrenceInstant) === fromMs
13138
+ );
13139
+ if (!match) {
13140
+ throw new SeriesRequestError(
13141
+ `fromOccurrence "${fromOccurrence}" does not match any occurrence key in the series.`
13142
+ );
13143
+ }
13144
+ return fromMs;
13145
+ }
13146
+ function isAtOrAfter(instance, boundaryMs) {
13147
+ return instance.occurrenceInstant !== null && Date.parse(instance.occurrenceInstant) >= boundaryMs;
13148
+ }
13149
+ function validateSeriesSelector(body) {
13150
+ if (typeof body.seriesId !== "string" || body.seriesId.length === 0) {
13151
+ throw new SeriesRequestError("seriesId is required.");
13152
+ }
13153
+ if (body.mode !== "following" && body.mode !== "all") {
13154
+ throw new SeriesRequestError(
13155
+ `mode must be "following" or "all"; received "${String(body.mode)}".`
13156
+ );
13157
+ }
13158
+ if (body.mode === "following" && !body.fromOccurrence) {
13159
+ throw new SeriesRequestError(
13160
+ 'fromOccurrence is required when mode is "following".'
13161
+ );
13162
+ }
13163
+ }
13164
+
12826
13165
  // src/data/operations/data/appointment/appointment-update-operation.ts
12827
13166
  async function updateAppointmentOperation(params) {
12828
13167
  const { context, id, body, tableName } = params;
@@ -12847,6 +13186,647 @@ async function updateAppointmentOperation(params) {
12847
13186
  );
12848
13187
  }
12849
13188
 
13189
+ // src/data/operations/data/appointment/appointment-cancel-series-operation.ts
13190
+ async function cancelSeriesOperation(params) {
13191
+ const { context, body, tableName } = params;
13192
+ validateSeriesSelector(body);
13193
+ const instances = await locateSeriesInstances({
13194
+ context,
13195
+ seriesId: body.seriesId,
13196
+ tableName
13197
+ });
13198
+ if (instances.length === 0) {
13199
+ throw new SeriesNotFoundError(
13200
+ `No Appointment instances found for series "${body.seriesId}".`
13201
+ );
13202
+ }
13203
+ let targets;
13204
+ if (body.mode === "following") {
13205
+ const boundaryMs = resolveFromOccurrence(instances, body.fromOccurrence);
13206
+ targets = instances.filter((i) => isAtOrAfter(i, boundaryMs));
13207
+ } else {
13208
+ targets = [...instances];
13209
+ }
13210
+ const ledger = [];
13211
+ for (const instance of targets) {
13212
+ const occurrence = instance.occurrenceInstant ?? void 0;
13213
+ if (instance.resource.status === "cancelled") {
13214
+ ledger.push({
13215
+ id: instance.id,
13216
+ occurrence,
13217
+ action: "skipped-already-cancelled"
13218
+ });
13219
+ continue;
13220
+ }
13221
+ try {
13222
+ const cancelled = {
13223
+ ...instance.resource,
13224
+ status: "cancelled"
13225
+ };
13226
+ await updateAppointmentOperation({
13227
+ context,
13228
+ id: instance.id,
13229
+ body: cancelled,
13230
+ tableName
13231
+ });
13232
+ ledger.push({ id: instance.id, occurrence, action: "cancelled" });
13233
+ } catch (err) {
13234
+ ledger.push({
13235
+ id: instance.id,
13236
+ occurrence,
13237
+ action: "failed",
13238
+ diagnostics: String(err)
13239
+ });
13240
+ }
13241
+ }
13242
+ return { seriesId: body.seriesId, mode: body.mode, ledger };
13243
+ }
13244
+
13245
+ // src/data/operations/data/appointment/appointment-create-series-operation.ts
13246
+ var SERIES_MAX_OCCURRENCES = 400;
13247
+ var FREQUENCIES = /* @__PURE__ */ new Set(["daily", "weekly", "monthly"]);
13248
+ var WEEKDAYS2 = /* @__PURE__ */ new Set(["MO", "TU", "WE", "TH", "FR", "SA", "SU"]);
13249
+ function parseSeriesPattern(pattern) {
13250
+ if (!FREQUENCIES.has(pattern.frequency)) {
13251
+ throw new SeriesRequestError(
13252
+ `Unsupported pattern.frequency "${pattern.frequency}"; expected daily | weekly | monthly.`
13253
+ );
13254
+ }
13255
+ if (!Number.isInteger(pattern.interval) || pattern.interval < 1) {
13256
+ throw new SeriesRequestError(
13257
+ `pattern.interval must be an integer >= 1; received ${pattern.interval}.`
13258
+ );
13259
+ }
13260
+ let byWeekday;
13261
+ if (pattern.byWeekday !== void 0) {
13262
+ for (const day of pattern.byWeekday) {
13263
+ if (!WEEKDAYS2.has(day)) {
13264
+ throw new SeriesRequestError(
13265
+ `Unsupported pattern.byWeekday value "${day}".`
13266
+ );
13267
+ }
13268
+ }
13269
+ byWeekday = [...pattern.byWeekday];
13270
+ }
13271
+ const end = pattern.end;
13272
+ if (end.kind === "count") {
13273
+ if (!Number.isInteger(end.count) || end.count < 1) {
13274
+ throw new SeriesRequestError(
13275
+ `pattern.end.count must be an integer >= 1; received ${end.count}.`
13276
+ );
13277
+ }
13278
+ return {
13279
+ frequency: pattern.frequency,
13280
+ interval: pattern.interval,
13281
+ byWeekday,
13282
+ end: { kind: "count", count: end.count }
13283
+ };
13284
+ }
13285
+ if (end.kind === "until") {
13286
+ const until = new Date(end.until ?? "");
13287
+ if (Number.isNaN(until.getTime())) {
13288
+ throw new SeriesRequestError(
13289
+ `pattern.end.until is not a valid ISO 8601 instant: "${end.until}".`
13290
+ );
13291
+ }
13292
+ return {
13293
+ frequency: pattern.frequency,
13294
+ interval: pattern.interval,
13295
+ byWeekday,
13296
+ end: { kind: "until", until }
13297
+ };
13298
+ }
13299
+ throw new SeriesRequestError(
13300
+ `Unsupported pattern.end.kind "${end.kind}"; v1 accepts "count" or "until" only (open-ended series are not supported).`
13301
+ );
13302
+ }
13303
+ var WINDOW_END = /* @__PURE__ */ new Date("9999-12-31T00:00:00Z");
13304
+ function expandSeries(opts) {
13305
+ let occurrences;
13306
+ try {
13307
+ occurrences = expandOccurrences({
13308
+ pattern: opts.pattern,
13309
+ seriesStart: opts.anchor,
13310
+ timezone: opts.timezone,
13311
+ window: { from: opts.anchor, to: WINDOW_END },
13312
+ // The cap throws (never silently truncates); +1 distinguishes
13313
+ // "exactly at the ceiling" from "over it".
13314
+ maxCount: SERIES_MAX_OCCURRENCES + 1
13315
+ });
13316
+ } catch (err) {
13317
+ throw new SeriesRequestError(
13318
+ `Series expansion exceeded the ${SERIES_MAX_OCCURRENCES}-occurrence server ceiling: ${String(err)}`
13319
+ );
13320
+ }
13321
+ if (occurrences.length > SERIES_MAX_OCCURRENCES) {
13322
+ throw new SeriesRequestError(
13323
+ `Series expands to ${occurrences.length} occurrences; the server ceiling is ${SERIES_MAX_OCCURRENCES}.`
13324
+ );
13325
+ }
13326
+ return occurrences;
13327
+ }
13328
+ async function createSeriesOperation(params) {
13329
+ const { context, body, tableName } = params;
13330
+ if (typeof body.seriesId !== "string" || body.seriesId.length === 0) {
13331
+ throw new SeriesRequestError("seriesId is required.");
13332
+ }
13333
+ if (body.template === void 0 || typeof body.template !== "object") {
13334
+ throw new SeriesRequestError("template is required.");
13335
+ }
13336
+ if (!Number.isInteger(body.durationMinutes) || body.durationMinutes < 1) {
13337
+ throw new SeriesRequestError(
13338
+ `durationMinutes must be an integer >= 1; received ${body.durationMinutes}.`
13339
+ );
13340
+ }
13341
+ if (typeof body.timezone !== "string" || body.timezone.length === 0) {
13342
+ throw new SeriesRequestError("timezone is required (IANA zone).");
13343
+ }
13344
+ const anchor = new Date(body.anchor ?? "");
13345
+ if (Number.isNaN(anchor.getTime())) {
13346
+ throw new SeriesRequestError(
13347
+ `anchor is not a valid ISO 8601 instant: "${body.anchor}".`
13348
+ );
13349
+ }
13350
+ const pattern = parseSeriesPattern(
13351
+ body.pattern ?? {}
13352
+ );
13353
+ const rrule = buildRruleString(pattern);
13354
+ const occurrences = expandSeries({
13355
+ pattern,
13356
+ anchor,
13357
+ timezone: body.timezone
13358
+ });
13359
+ if (body.dryRun === true) {
13360
+ return {
13361
+ seriesId: body.seriesId,
13362
+ rrule,
13363
+ occurrences: occurrences.map((o) => o.toISOString())
13364
+ };
13365
+ }
13366
+ const existing = await locateSeriesInstances({
13367
+ context,
13368
+ seriesId: body.seriesId,
13369
+ tableName
13370
+ });
13371
+ const existingInstants = new Set(
13372
+ existing.map((i) => i.occurrenceInstant).filter((v) => v !== null).map((v) => Date.parse(v))
13373
+ );
13374
+ const {
13375
+ id: _id,
13376
+ start: _start,
13377
+ end: _end,
13378
+ ...template
13379
+ } = body.template;
13380
+ const instances = buildSeriesInstances({
13381
+ base: template,
13382
+ occurrences,
13383
+ durationMinutes: body.durationMinutes,
13384
+ seriesId: body.seriesId,
13385
+ rrule,
13386
+ timezone: body.timezone,
13387
+ anchor
13388
+ });
13389
+ const ledger = [];
13390
+ for (let i = 0; i < instances.length; i++) {
13391
+ const occurrence = occurrences[i].toISOString();
13392
+ if (existingInstants.has(occurrences[i].getTime())) {
13393
+ ledger.push({ occurrence, action: "skipped-existing" });
13394
+ continue;
13395
+ }
13396
+ try {
13397
+ const created = await createAppointmentOperation({
13398
+ context,
13399
+ body: instances[i],
13400
+ tableName
13401
+ });
13402
+ ledger.push({ id: created.id, occurrence, action: "created" });
13403
+ } catch (err) {
13404
+ ledger.push({ occurrence, action: "failed", diagnostics: String(err) });
13405
+ }
13406
+ }
13407
+ return {
13408
+ seriesId: body.seriesId,
13409
+ rrule,
13410
+ occurrences: occurrences.map((o) => o.toISOString()),
13411
+ ledger
13412
+ };
13413
+ }
13414
+
13415
+ // src/data/operations/data/appointment/appointment-edit-series-operation.ts
13416
+ var import_node_crypto = require("crypto");
13417
+ var import_tz2 = require("@date-fns/tz");
13418
+ var import_date_fns3 = require("date-fns");
13419
+ var LOCAL_TIME_PATTERN = /^([01]\d|2[0-3]):[0-5]\d$/;
13420
+ function validateChanges(changes) {
13421
+ if (changes === void 0 || typeof changes !== "object") {
13422
+ throw new SeriesRequestError("changes is required.");
13423
+ }
13424
+ const named = changes.startTime !== void 0 || changes.practitioners !== void 0 || changes.patients !== void 0 || changes.appointmentType !== void 0 || changes.description !== void 0;
13425
+ if (!named) {
13426
+ throw new SeriesRequestError(
13427
+ "changes must name at least one of: startTime, practitioners, patients, appointmentType, description."
13428
+ );
13429
+ }
13430
+ if (changes.startTime !== void 0) {
13431
+ if (!LOCAL_TIME_PATTERN.test(changes.startTime.localTime ?? "")) {
13432
+ throw new SeriesRequestError(
13433
+ `changes.startTime.localTime must be wall-clock "HH:mm"; received "${changes.startTime.localTime}".`
13434
+ );
13435
+ }
13436
+ if (!Number.isInteger(changes.startTime.durationMinutes) || changes.startTime.durationMinutes < 1) {
13437
+ throw new SeriesRequestError(
13438
+ "changes.startTime.durationMinutes must be an integer >= 1."
13439
+ );
13440
+ }
13441
+ }
13442
+ validateRoleParticipants(changes.practitioners, "Practitioner/");
13443
+ validateRoleParticipants(changes.patients, "Patient/");
13444
+ }
13445
+ function validateRoleParticipants(participants, rolePrefix) {
13446
+ if (participants === void 0) return;
13447
+ for (const p of participants) {
13448
+ const reference = p.actor?.reference ?? "";
13449
+ if (!reference.startsWith(rolePrefix)) {
13450
+ throw new SeriesRequestError(
13451
+ `Every entry in the ${rolePrefix.slice(0, -1)} participant list must reference a ${rolePrefix.slice(0, -1)} (got "${reference}").`
13452
+ );
13453
+ }
13454
+ }
13455
+ }
13456
+ function replaceRoleParticipants(existing, rolePrefix, replacements) {
13457
+ const others = existing.filter(
13458
+ (p) => !(p.actor?.reference ?? "").startsWith(rolePrefix)
13459
+ );
13460
+ return [...others, ...replacements];
13461
+ }
13462
+ function deriveWallClockStart(occurrenceInstant, timezone, localTime) {
13463
+ const [hours, minutes] = localTime.split(":").map(Number);
13464
+ const onDate = new import_tz2.TZDate(Date.parse(occurrenceInstant), timezone);
13465
+ const adjusted = (0, import_date_fns3.setMilliseconds)(
13466
+ (0, import_date_fns3.setSeconds)((0, import_date_fns3.setMinutes)((0, import_date_fns3.setHours)(onDate, hours), minutes), 0),
13467
+ 0
13468
+ );
13469
+ return new Date(adjusted.getTime());
13470
+ }
13471
+ function applyChanges(instance, changes, timezone) {
13472
+ const resource = { ...instance.resource };
13473
+ if (changes.startTime !== void 0) {
13474
+ if (instance.occurrenceInstant === null) {
13475
+ throw new SeriesRequestError(
13476
+ `Instance ${instance.id} has no occurrence identifier; cannot re-derive its start time.`
13477
+ );
13478
+ }
13479
+ const start = deriveWallClockStart(
13480
+ instance.occurrenceInstant,
13481
+ timezone,
13482
+ changes.startTime.localTime
13483
+ );
13484
+ resource.start = start.toISOString();
13485
+ resource.end = (0, import_date_fns3.addMinutes)(
13486
+ start,
13487
+ changes.startTime.durationMinutes
13488
+ ).toISOString();
13489
+ resource.minutesDuration = changes.startTime.durationMinutes;
13490
+ }
13491
+ if (changes.practitioners !== void 0 || changes.patients !== void 0) {
13492
+ let participants = [...resource.participant ?? []];
13493
+ if (changes.practitioners !== void 0) {
13494
+ participants = replaceRoleParticipants(
13495
+ participants,
13496
+ "Practitioner/",
13497
+ changes.practitioners
13498
+ );
13499
+ }
13500
+ if (changes.patients !== void 0) {
13501
+ participants = replaceRoleParticipants(
13502
+ participants,
13503
+ "Patient/",
13504
+ changes.patients
13505
+ );
13506
+ }
13507
+ resource.participant = participants;
13508
+ }
13509
+ if (changes.appointmentType !== void 0) {
13510
+ resource.appointmentType = changes.appointmentType;
13511
+ }
13512
+ if (changes.description !== void 0) {
13513
+ resource.description = changes.description;
13514
+ }
13515
+ return resource;
13516
+ }
13517
+ function readSeriesRecurrence(instances) {
13518
+ for (const instance of instances) {
13519
+ const rule = (0, import_types26.getRecurrenceRule)(instance.resource);
13520
+ const timezone = (0, import_types26.getRecurrenceTimezone)(instance.resource);
13521
+ const anchor = (0, import_types26.getRecurrenceAnchor)(instance.resource);
13522
+ if (rule !== null && timezone !== null && anchor !== null) {
13523
+ return { pattern: parseRruleString(rule), timezone, anchor };
13524
+ }
13525
+ }
13526
+ throw new SeriesRequestError(
13527
+ "Series instances are missing the recurrence-rule / recurrence-timezone / recurrence-anchor extensions; the series cannot be edited as a series."
13528
+ );
13529
+ }
13530
+ function withTruncatedRule(resource, rule) {
13531
+ const extension = (resource.extension ?? []).map(
13532
+ (ext) => ext.url === import_types25.RECURRENCE_RULE_EXTENSION_URL ? { ...ext, valueString: rule } : ext
13533
+ );
13534
+ return { ...resource, extension };
13535
+ }
13536
+ function buildTailTemplate(resource) {
13537
+ const { id: _id, start: _start, end: _end, meta: _meta, ...rest } = resource;
13538
+ const identifier = (rest.identifier ?? []).filter(
13539
+ (i) => i.system !== import_types25.APPOINTMENT_SERIES_SYSTEM && i.system !== import_types25.APPOINTMENT_SERIES_OCCURRENCE_SYSTEM
13540
+ );
13541
+ const stripped = /* @__PURE__ */ new Set([
13542
+ import_types25.RECURRENCE_RULE_EXTENSION_URL,
13543
+ import_types25.RECURRENCE_TIMEZONE_EXTENSION_URL,
13544
+ import_types25.RECURRENCE_ANCHOR_EXTENSION_URL,
13545
+ import_types25.SERIES_INSTANCE_OVERRIDDEN_EXTENSION_URL
13546
+ ]);
13547
+ const extension = (rest.extension ?? []).filter(
13548
+ (e) => !stripped.has(e.url ?? "")
13549
+ );
13550
+ return {
13551
+ ...rest,
13552
+ identifier,
13553
+ extension,
13554
+ // A tail regenerated from a cancelled instance must not be born cancelled.
13555
+ status: rest.status === "cancelled" ? "booked" : rest.status
13556
+ };
13557
+ }
13558
+ async function editSeriesOperation(params) {
13559
+ const { context, body, tableName } = params;
13560
+ validateSeriesSelector(body);
13561
+ validateChanges(body.changes);
13562
+ if (body.newPattern !== void 0 && body.mode !== "following") {
13563
+ throw new SeriesRequestError(
13564
+ 'newPattern is only valid with mode="following".'
13565
+ );
13566
+ }
13567
+ const instances = await locateSeriesInstances({
13568
+ context,
13569
+ seriesId: body.seriesId,
13570
+ tableName
13571
+ });
13572
+ if (instances.length === 0) {
13573
+ throw new SeriesNotFoundError(
13574
+ `No Appointment instances found for series "${body.seriesId}".`
13575
+ );
13576
+ }
13577
+ const { pattern, timezone, anchor } = readSeriesRecurrence(instances);
13578
+ if (body.mode === "all") {
13579
+ return editAll({ context, body, instances, timezone, tableName });
13580
+ }
13581
+ return editFollowing({
13582
+ context,
13583
+ body,
13584
+ instances,
13585
+ pattern,
13586
+ timezone,
13587
+ anchor,
13588
+ tableName
13589
+ });
13590
+ }
13591
+ async function editAll(opts) {
13592
+ const { context, body, instances, timezone, tableName } = opts;
13593
+ const ledger = [];
13594
+ for (const instance of instances) {
13595
+ const occurrence = instance.occurrenceInstant ?? void 0;
13596
+ if (instance.resource.status === "cancelled") {
13597
+ ledger.push({
13598
+ id: instance.id,
13599
+ occurrence,
13600
+ action: "skipped-already-cancelled"
13601
+ });
13602
+ continue;
13603
+ }
13604
+ if ((0, import_types26.isSeriesInstanceOverridden)(instance.resource)) {
13605
+ ledger.push({
13606
+ id: instance.id,
13607
+ occurrence,
13608
+ action: "skipped-overridden"
13609
+ });
13610
+ continue;
13611
+ }
13612
+ try {
13613
+ const updated = applyChanges(instance, body.changes, timezone);
13614
+ await updateAppointmentOperation({
13615
+ context,
13616
+ id: instance.id,
13617
+ body: updated,
13618
+ tableName
13619
+ });
13620
+ ledger.push({ id: instance.id, occurrence, action: "updated" });
13621
+ } catch (err) {
13622
+ if (err instanceof SeriesRequestError) throw err;
13623
+ ledger.push({
13624
+ id: instance.id,
13625
+ occurrence,
13626
+ action: "failed",
13627
+ diagnostics: String(err)
13628
+ });
13629
+ }
13630
+ }
13631
+ return { seriesId: body.seriesId, mode: "all", ledger };
13632
+ }
13633
+ async function editFollowing(opts) {
13634
+ const { context, body, instances, pattern, timezone, anchor, tableName } = opts;
13635
+ const boundaryMs = resolveFromOccurrence(instances, body.fromOccurrence);
13636
+ const kept = instances.filter((i) => !isAtOrAfter(i, boundaryMs));
13637
+ const replaced = instances.filter((i) => isAtOrAfter(i, boundaryMs));
13638
+ if (kept.length === 0) {
13639
+ throw new SeriesRequestError(
13640
+ 'fromOccurrence is the first occurrence of the series \u2014 there is nothing to split. Use mode="all" instead.'
13641
+ );
13642
+ }
13643
+ const lastKeptMs = Math.max(
13644
+ ...kept.map((i) => i.occurrenceInstant).filter((v) => v !== null).map((v) => Date.parse(v))
13645
+ );
13646
+ const truncatedPattern = {
13647
+ ...pattern,
13648
+ end: { kind: "until", until: new Date(lastKeptMs) }
13649
+ };
13650
+ const truncatedRule = buildRruleString(truncatedPattern);
13651
+ let tailPattern;
13652
+ if (body.newPattern !== void 0) {
13653
+ tailPattern = parseSeriesPattern(body.newPattern);
13654
+ } else if (pattern.end.kind === "count") {
13655
+ const producedBefore = expandOccurrences({
13656
+ pattern,
13657
+ seriesStart: anchor,
13658
+ timezone,
13659
+ window: { from: anchor, to: new Date(boundaryMs - 1) }
13660
+ }).length;
13661
+ const remaining = pattern.end.count - producedBefore;
13662
+ if (remaining < 1) {
13663
+ throw new SeriesRequestError(
13664
+ "The original series has no occurrences remaining at fromOccurrence; nothing to regenerate."
13665
+ );
13666
+ }
13667
+ tailPattern = { ...pattern, end: { kind: "count", count: remaining } };
13668
+ } else {
13669
+ tailPattern = pattern;
13670
+ }
13671
+ const boundaryInstant = new Date(boundaryMs).toISOString();
13672
+ const newAnchor = body.changes.startTime !== void 0 ? deriveWallClockStart(
13673
+ boundaryInstant,
13674
+ timezone,
13675
+ body.changes.startTime.localTime
13676
+ ) : new Date(boundaryMs);
13677
+ const tailOccurrences = expandSeries({
13678
+ pattern: tailPattern,
13679
+ anchor: newAnchor,
13680
+ timezone
13681
+ });
13682
+ const templateSource = replaced[0];
13683
+ const edited = applyChanges(
13684
+ { ...templateSource, occurrenceInstant: boundaryInstant },
13685
+ body.changes,
13686
+ timezone
13687
+ );
13688
+ const template = buildTailTemplate(edited);
13689
+ const durationMinutes = body.changes.startTime?.durationMinutes ?? templateSource.resource.minutesDuration ?? defaultDurationMinutes(templateSource.resource);
13690
+ const newSeriesId = (0, import_node_crypto.randomUUID)();
13691
+ const newRrule = buildRruleString(tailPattern);
13692
+ const tailInstances = buildSeriesInstances({
13693
+ base: template,
13694
+ occurrences: tailOccurrences,
13695
+ durationMinutes,
13696
+ seriesId: newSeriesId,
13697
+ rrule: newRrule,
13698
+ timezone,
13699
+ anchor: newAnchor
13700
+ });
13701
+ const ledger = [];
13702
+ for (const instance of kept) {
13703
+ try {
13704
+ await updateAppointmentOperation({
13705
+ context,
13706
+ id: instance.id,
13707
+ body: withTruncatedRule(instance.resource, truncatedRule),
13708
+ tableName
13709
+ });
13710
+ ledger.push({
13711
+ id: instance.id,
13712
+ occurrence: instance.occurrenceInstant ?? void 0,
13713
+ action: "rule-truncated"
13714
+ });
13715
+ } catch (err) {
13716
+ ledger.push({
13717
+ id: instance.id,
13718
+ occurrence: instance.occurrenceInstant ?? void 0,
13719
+ action: "failed",
13720
+ diagnostics: String(err)
13721
+ });
13722
+ }
13723
+ }
13724
+ for (let i = 0; i < tailInstances.length; i++) {
13725
+ const occurrence = tailOccurrences[i].toISOString();
13726
+ try {
13727
+ const created = await createAppointmentOperation({
13728
+ context,
13729
+ body: tailInstances[i],
13730
+ tableName
13731
+ });
13732
+ ledger.push({ id: created.id, occurrence, action: "created" });
13733
+ } catch (err) {
13734
+ ledger.push({ occurrence, action: "failed", diagnostics: String(err) });
13735
+ }
13736
+ }
13737
+ for (const instance of replaced) {
13738
+ try {
13739
+ await deleteAppointmentOperation({ context, id: instance.id, tableName });
13740
+ ledger.push({
13741
+ id: instance.id,
13742
+ occurrence: instance.occurrenceInstant ?? void 0,
13743
+ action: "deleted"
13744
+ });
13745
+ } catch (err) {
13746
+ ledger.push({
13747
+ id: instance.id,
13748
+ occurrence: instance.occurrenceInstant ?? void 0,
13749
+ action: "failed",
13750
+ diagnostics: String(err)
13751
+ });
13752
+ }
13753
+ }
13754
+ return {
13755
+ seriesId: body.seriesId,
13756
+ mode: "following",
13757
+ newSeriesId,
13758
+ truncatedRule,
13759
+ newRrule,
13760
+ ledger
13761
+ };
13762
+ }
13763
+ function defaultDurationMinutes(resource) {
13764
+ const start = Date.parse(resource.start ?? "");
13765
+ const end = Date.parse(resource.end ?? "");
13766
+ if (!Number.isNaN(start) && !Number.isNaN(end) && end > start) {
13767
+ return Math.round((end - start) / 6e4);
13768
+ }
13769
+ throw new SeriesRequestError(
13770
+ "Cannot derive the tail's duration: supply changes.startTime.durationMinutes (the template instance has no usable minutesDuration or start/end)."
13771
+ );
13772
+ }
13773
+
13774
+ // src/data/rest-api/routes/data/appointment/appointment-series-routes.ts
13775
+ function sendOperationOutcome(res, status, code, diagnostics) {
13776
+ return res.status(status).json({
13777
+ resourceType: "OperationOutcome",
13778
+ issue: [{ severity: "error", code, diagnostics }]
13779
+ });
13780
+ }
13781
+ function sendSeriesError(res, err, logContext) {
13782
+ if (err instanceof SeriesRequestError) {
13783
+ return sendOperationOutcome(res, 400, "invalid", err.message);
13784
+ }
13785
+ if (err instanceof SeriesNotFoundError) {
13786
+ return sendOperationOutcome(res, 404, "not-found", err.message);
13787
+ }
13788
+ return sendOperationOutcome500(res, err, logContext);
13789
+ }
13790
+ async function createSeriesRoute(req, res) {
13791
+ const bodyResult = requireJsonBodyAs(req, res);
13792
+ if ("errorResponse" in bodyResult) return bodyResult.errorResponse;
13793
+ try {
13794
+ const result = await createSeriesOperation({
13795
+ context: req.openhiContext,
13796
+ body: bodyResult.body
13797
+ });
13798
+ return res.status(200).json(result);
13799
+ } catch (err) {
13800
+ return sendSeriesError(res, err, "POST /Appointment/$create-series error:");
13801
+ }
13802
+ }
13803
+ async function editSeriesRoute(req, res) {
13804
+ const bodyResult = requireJsonBodyAs(req, res);
13805
+ if ("errorResponse" in bodyResult) return bodyResult.errorResponse;
13806
+ try {
13807
+ const result = await editSeriesOperation({
13808
+ context: req.openhiContext,
13809
+ body: bodyResult.body
13810
+ });
13811
+ return res.status(200).json(result);
13812
+ } catch (err) {
13813
+ return sendSeriesError(res, err, "POST /Appointment/$edit-series error:");
13814
+ }
13815
+ }
13816
+ async function cancelSeriesRoute(req, res) {
13817
+ const bodyResult = requireJsonBodyAs(req, res);
13818
+ if ("errorResponse" in bodyResult) return bodyResult.errorResponse;
13819
+ try {
13820
+ const result = await cancelSeriesOperation({
13821
+ context: req.openhiContext,
13822
+ body: bodyResult.body
13823
+ });
13824
+ return res.status(200).json(result);
13825
+ } catch (err) {
13826
+ return sendSeriesError(res, err, "POST /Appointment/$cancel-series error:");
13827
+ }
13828
+ }
13829
+
12850
13830
  // src/data/rest-api/routes/data/appointment/appointment-update-route.ts
12851
13831
  async function updateAppointmentRoute(req, res) {
12852
13832
  const bodyResult = requireJsonBodyAs(req, res);
@@ -12881,6 +13861,9 @@ var router14 = import_express14.default.Router();
12881
13861
  router14.get("/", listAppointmentsRoute);
12882
13862
  router14.get("/:id", getAppointmentByIdRoute);
12883
13863
  router14.post("/", createAppointmentRoute);
13864
+ router14.post("/$create-series", createSeriesRoute);
13865
+ router14.post("/$edit-series", editSeriesRoute);
13866
+ router14.post("/$cancel-series", cancelSeriesRoute);
12884
13867
  router14.put("/:id", updateAppointmentRoute);
12885
13868
  router14.delete("/:id", deleteAppointmentRoute);
12886
13869