Class: SeriesService
Defined in: server/bookings/series-service.ts:127
Constructors
Constructor
new SeriesService(deps: SeriesServiceDeps): SeriesService;Defined in: server/bookings/series-service.ts:128
Parameters
| Parameter | Type |
|---|---|
deps | SeriesServiceDeps |
Returns
SeriesService
Properties
deps
protected readonly deps: SeriesServiceDeps;Defined in: server/bookings/series-service.ts:128
Methods
cancelOccurrence()
cancelOccurrence(bookingId: string, opts: {
actorId: string;
allowLateCancel?: boolean;
reason?: string;
}): Promise<{
amountOwedCents: number | null;
bookingTypeId: string;
bookingTypeVersionId: string;
capacityUnits: number;
createdAt: Date;
createdBy: string | null;
currency: string | null;
deletedAt: Date | null;
deletedBy: string | null;
groupId: string | null;
holdExpiresAt: Date | null;
id: string;
organizationId: string;
origin: BookingOrigin;
paid: boolean;
partyEmail: string | null;
partyName: string | null;
partyRef: string | null;
payload: JsonValue;
paymentRef: string | null;
pendingExpiresAt: Date | null;
status: BookingStatus;
statusUpdatedAt: Date;
statusUpdatedBy: string | null;
supersededById: string | null;
uid: string;
updatedAt: Date;
updatedBy: string | null;
version: number;
}>;Defined in: server/bookings/series-service.ts:202
THIS-OCCURRENCE skip (the EXDATE analogue, §6.8): cancel the one child and
flag its schedule isException — the cancelled-but-present row keeps
occupying its (series_id, occurrence_at) slot, so the occurrence is
never regenerated. Cancellation policy (and allowLateCancel) applies
exactly as for a standalone booking.
Parameters
| Parameter | Type |
|---|---|
bookingId | string |
opts | { actorId: string; allowLateCancel?: boolean; reason?: string; } |
opts.actorId | string |
opts.allowLateCancel? | boolean |
opts.reason? | string |
Returns
Promise<{
amountOwedCents: number | null;
bookingTypeId: string;
bookingTypeVersionId: string;
capacityUnits: number;
createdAt: Date;
createdBy: string | null;
currency: string | null;
deletedAt: Date | null;
deletedBy: string | null;
groupId: string | null;
holdExpiresAt: Date | null;
id: string;
organizationId: string;
origin: BookingOrigin;
paid: boolean;
partyEmail: string | null;
partyName: string | null;
partyRef: string | null;
payload: JsonValue;
paymentRef: string | null;
pendingExpiresAt: Date | null;
status: BookingStatus;
statusUpdatedAt: Date;
statusUpdatedBy: string | null;
supersededById: string | null;
uid: string;
updatedAt: Date;
updatedBy: string | null;
version: number;
}>
createSeries()
createSeries(raw: {
actorId: string;
allowOutsideHours?: boolean;
allowOverbook?: boolean;
bookingTypeId: string;
dtstartLocal: string;
durationMinutes: number;
horizon: unknown;
maxAdvanceMs: number;
organizationId: string;
partyRef?: string | null;
recurrenceRule: string;
resourceId: string;
timeZone: string;
}): Promise<CreateSeriesResult>;Defined in: server/bookings/series-service.ts:142
Create a BookingSeries and fan it out through horizon in ONE
transaction (spec §6.8): there is never a window where a confirmed series
exists with no materialized children blocking its slots.
The §6.8 fail-open guard: horizon must reach now + maxAdvanceMs, or
the create is REJECTED (SeriesHorizonError) — beyond the horizon the
series' occurrences are not rows and block nothing, so a short horizon
sells slots the series already owns. maxAdvanceMs is a per-call policy
value (mirroring SchedulerHealth.staleness), not a column — the
max-advance window is consumer-owned configuration.
Parameters
| Parameter | Type | Description |
|---|---|---|
raw | { actorId: string; allowOutsideHours?: boolean; allowOverbook?: boolean; bookingTypeId: string; dtstartLocal: string; durationMinutes: number; horizon: unknown; maxAdvanceMs: number; organizationId: string; partyRef?: string | null; recurrenceRule: string; resourceId: string; timeZone: string; } | - |
raw.actorId | string | - |
raw.allowOutsideHours? | boolean | §6.9 bypass-and-record — stamped onto each materialized child's claim. |
raw.allowOverbook? | boolean | §6.6 bypass-and-record — stamped onto each materialized child's claim. |
raw.bookingTypeId | string | - |
raw.dtstartLocal | string | Wall-clock ISO date-time, NO offset — e.g. 2026-06-15T17:00:00. |
raw.durationMinutes | number | - |
raw.horizon | unknown | Initial materialization horizon — children are fanned out through this instant at create. |
raw.maxAdvanceMs | number | The org's max-advance booking window; horizon must reach now + maxAdvanceMs. |
raw.organizationId | string | - |
raw.partyRef? | string | null | Null/omitted = a system/offering occupancy series (children get origin: "system"). |
raw.recurrenceRule | string | - |
raw.resourceId | string | - |
raw.timeZone | string | - |
Returns
Promise<CreateSeriesResult>
editAllFuture()
editAllFuture(
seriesId: string,
patch: {
resourceId: string;
},
opts: {
actorId: string;
allowOutsideHours?: boolean;
allowOverbook?: boolean;
}
): Promise<{
reconciled: number;
series: {
bookingTypeId: string;
bookingTypeVersionId: string;
createdAt: Date;
createdBy: string | null;
deletedAt: Date | null;
deletedBy: string | null;
dtstartLocal: string;
durationMinutes: number;
id: string;
materializedThrough: Date | null;
organizationId: string;
partyRef: string | null;
recurrenceRule: string;
resourceId: string;
status: string;
supersedesSeriesId: string | null;
timeZone: string;
updatedAt: Date;
updatedBy: string | null;
};
}>;Defined in: server/bookings/series-service.ts:465
ALL-FUTURE bulk edit (spec §6.8 table): apply a resource move to every
future non-exception, non-terminal child AND renew the series template so
newly materialized children follow. Time/rule changes are a
this-and-following split — use editThisAndFollowing.
Parameters
| Parameter | Type |
|---|---|
seriesId | string |
patch | { resourceId: string; } |
patch.resourceId | string |
opts | { actorId: string; allowOutsideHours?: boolean; allowOverbook?: boolean; } |
opts.actorId | string |
opts.allowOutsideHours? | boolean |
opts.allowOverbook? | boolean |
Returns
Promise<{
reconciled: number;
series: {
bookingTypeId: string;
bookingTypeVersionId: string;
createdAt: Date;
createdBy: string | null;
deletedAt: Date | null;
deletedBy: string | null;
dtstartLocal: string;
durationMinutes: number;
id: string;
materializedThrough: Date | null;
organizationId: string;
partyRef: string | null;
recurrenceRule: string;
resourceId: string;
status: string;
supersedesSeriesId: string | null;
timeZone: string;
updatedAt: Date;
updatedBy: string | null;
};
}>
editOccurrence()
editOccurrence(
bookingId: string,
patch: {
endsAt?: Date;
resourceId?: string;
startsAt?: Date;
},
opts: {
actorId: string;
allowOutsideHours?: boolean;
allowOverbook?: boolean;
}
): Promise<{
bookingId: string;
capacityModel: string;
endsAt: Date;
id: string;
isBlocking: boolean;
isException: boolean;
occurrenceAt: Date | null;
overbooked: boolean;
resourceId: string;
seriesId: string | null;
startsAt: Date;
timeZone: string;
}>;Defined in: server/bookings/series-service.ts:230
THIS-OCCURRENCE edit (the RECURRENCE-ID analogue, §6.8): mutate the one
child's slot and flag isException so series-wide edits skip it. The new
slot is capacity- and window-checked under the §6.6 locks. System
children mutate in place; customer children route through the §7.1
transfers seam (see reconcileCustomerChild).
Parameters
| Parameter | Type |
|---|---|
bookingId | string |
patch | { endsAt?: Date; resourceId?: string; startsAt?: Date; } |
patch.endsAt? | Date |
patch.resourceId? | string |
patch.startsAt? | Date |
opts | { actorId: string; allowOutsideHours?: boolean; allowOverbook?: boolean; } |
opts.actorId | string |
opts.allowOutsideHours? | boolean |
opts.allowOverbook? | boolean |
Returns
Promise<{
bookingId: string;
capacityModel: string;
endsAt: Date;
id: string;
isBlocking: boolean;
isException: boolean;
occurrenceAt: Date | null;
overbooked: boolean;
resourceId: string;
seriesId: string | null;
startsAt: Date;
timeZone: string;
}>
editThisAndFollowing()
editThisAndFollowing(seriesId: string, input: SeriesSplitInput): Promise<SeriesSplitResult>;Defined in: server/bookings/series-service.ts:303
THIS-AND-FOLLOWING split (spec §6.8 table). In ONE transaction:
- cap the old rule — lower
COUNTwhen COUNT-bounded (counting EVERY rule occurrence before the split: a cancelled exception consumed one of the purchased N), else cap withUNTIL; old series →ended; - mint the successor (
supersedesSeriesId→ old;COUNT= remaining purchased occurrences); - reconcile already-materialized future children by PAIRING them, in
occurrence order, with the successor's first instants: non-exception
children take the new slot (capacity/window-checked; system in
place, customer via the §7.1 seam); exception children keep their
diverged content (a skipped week stays skipped, special hours stay
special) but re-point their
(series_id, occurrence_at)identity so the successor never regenerates over them — which is what makes "10 lessons, week 3 skipped, move at week 5" yield exactly 10; - materialize the successor through the old cursor (no fail-open gap).
Parameters
| Parameter | Type |
|---|---|
seriesId | string |
input | SeriesSplitInput |
Returns
Promise<SeriesSplitResult>