@odla-ai/chapter 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -466,6 +466,88 @@ declare function verifyStripeSignature(payload: string, header: string, secret:
466
466
  now?: number;
467
467
  toleranceSec?: number;
468
468
  }): Promise<boolean>;
469
+ /** A group row's payment configuration, as far as readiness cares. */
470
+ interface PaymentsGroup {
471
+ stripePublishableKey?: string | null;
472
+ stripePriceId?: string | null;
473
+ }
474
+ /** Whether a group can take payment: a publishable key + a price id (both on the
475
+ * group row) AND a secret key (the vault). Anything missing drops the join
476
+ * flow's payment step (paymentsReady:false) rather than half-charging. */
477
+ declare function paymentsReady(group: PaymentsGroup, hasSecretKey: boolean): boolean;
478
+ /** Form-encode params for Stripe's x-www-form-urlencoded API, expanding one level
479
+ * of nested objects into bracket syntax (`metadata[applicationId]=...`). */
480
+ declare function stripeForm(params: Record<string, unknown>): string;
481
+ /** The Stripe idempotency key for creating an application's subscription — one
482
+ * per application, so a client retry can't orphan a second subscription (S&S
483
+ * lacked this; the package enforces it). */
484
+ declare function subscriptionIdempotencyKey(applicationId: string): string;
485
+ /** The db mutationId for a webhook-driven write — exactly-once per Stripe event,
486
+ * so replays are deduped at the db layer. */
487
+ declare function webhookMutationId(eventId: string): string;
488
+ /** A raw Stripe event, as far as normalization cares. */
489
+ interface StripeEvent {
490
+ id: string;
491
+ type: string;
492
+ data?: {
493
+ object?: Record<string, unknown>;
494
+ };
495
+ }
496
+ /** A normalized, provider-agnostic webhook event. `kind` drives the db write;
497
+ * the application is resolved from `applicationId` (metadata) or `customerId`. */
498
+ type WebhookEvent = {
499
+ kind: "first_payment";
500
+ applicationId?: string;
501
+ customerId?: string;
502
+ renewalAt?: number;
503
+ } | {
504
+ kind: "renewal";
505
+ applicationId?: string;
506
+ customerId?: string;
507
+ renewalAt?: number;
508
+ } | {
509
+ kind: "refunded";
510
+ applicationId?: string;
511
+ customerId?: string;
512
+ } | {
513
+ kind: "canceled";
514
+ applicationId?: string;
515
+ customerId?: string;
516
+ } | {
517
+ kind: "ignored";
518
+ type: string;
519
+ };
520
+ /** Resolve the application reference on a Stripe object: `applicationId` from
521
+ * metadata (direct, then subscription_details, then nested
522
+ * parent.subscription_details), plus the customer id for the db fallback. */
523
+ declare function findApplicationRef(obj: Record<string, unknown>): {
524
+ applicationId?: string;
525
+ customerId?: string;
526
+ };
527
+ /** Normalize a verified Stripe event into a {@link WebhookEvent}. `invoice.paid`
528
+ * splits into first_payment vs renewal by `billing_reason`; refunds and
529
+ * cancellations map directly; everything else is ignored (acked, not retried). */
530
+ declare function normalizeWebhookEvent(event: StripeEvent): WebhookEvent;
531
+ /** First-payment patch: advance submitted→paid_pending_vetting (never any other
532
+ * transition) and record the renewal date. Empty when nothing changed, so the
533
+ * caller can skip the write. */
534
+ declare function firstPaymentPatch(currentStatus: string, renewalAt?: number): {
535
+ status?: "paid_pending_vetting";
536
+ renewalAt?: number;
537
+ };
538
+ /** Renewal-invoice patch: just the new renewal date. */
539
+ declare function renewalPatch(renewalAt: number): {
540
+ renewalAt: number;
541
+ };
542
+ /** Refund patch — the SOLE writer of status "refunded" (the admin refund route
543
+ * issues the Stripe refund but never sets this; the webhook does). */
544
+ declare function refundedPatch(): {
545
+ status: "refunded";
546
+ };
547
+ /** Subscription-cancellation patch. */
548
+ declare function canceledPatch(): {
549
+ canceled: true;
550
+ };
469
551
 
470
552
  /** Apply defaults + validate the application config. Throws at import on bad shape. */
471
553
  declare function resolveApplication(a: ChapterApplication | undefined): ResolvedApplication;
@@ -546,4 +628,190 @@ declare function projectSharedRecord(deps: ProjectionDeps, person: SharedPerson)
546
628
  recordId: string;
547
629
  }>;
548
630
 
549
- export { type Attr, type AttrType, type Chapter, type ChapterApplication, type ChapterAuth, type ChapterBrand, type ChapterConfig, type ChapterDb, type ChapterEmails, type ChapterIntegrationDescriptor, type ChapterIntegrationOptions, type ChapterMode, type ChapterPipeline, type ChapterPolicy, type ChapterPrices, type ChapterScheduling, type DbOp, type DbRules, type DbSchema, type DeliveryDecision, type EmailGroup, type EmailTemplate, type EmailTemplateRow, type Entity, type GuardResult, type JoinConfigGroup, type ProjectionDeps, type ResolvedApplication, type ResolvedAuth, type ResolvedPipeline, type RoleChangeContext, type Rule, type SecretStore, type SharedPerson, type SubmitResult, buildGroupSeed, canApprove, canBook, canChangeRole, canTransition, chapterDb, createChapterIntegration, defaultCrm, defineChapter, getVaultSecret, isAdminRole, isAlreadySent, joinConfig, planDelivery, projectSharedRecord, render, renderTemplateBody, resolveApplication, resolveAuth, resolvePipeline, roleFromClaim, sharedPersonInput, stageIndex, submitApplication, verifyStripeSignature };
631
+ /** An application row, as far as the session cares about it. */
632
+ interface ApplicationRecord {
633
+ id: string;
634
+ firstName?: string | null;
635
+ lastName?: string | null;
636
+ email?: string | null;
637
+ status: string;
638
+ meetingAt?: number | null;
639
+ meetingLink?: string | null;
640
+ createdAt?: number | null;
641
+ stripeSubscriptionId?: string | null;
642
+ renewalAt?: number | null;
643
+ canceled?: boolean;
644
+ }
645
+ /** The reconciled meeting row (already adopted against the calendar), or null. */
646
+ interface MeetingRecord {
647
+ status: string;
648
+ startAt?: number | null;
649
+ meetUrl?: string | null;
650
+ timezone?: string | null;
651
+ }
652
+ /** The stable, non-meeting fields of an application (safe to expose to its own
653
+ * owner). */
654
+ interface ApplicationSummary {
655
+ id: string;
656
+ firstName: string | null;
657
+ lastName: string | null;
658
+ email: string | null;
659
+ status: string;
660
+ createdAt: number | null;
661
+ meetingLink: string | null;
662
+ paid: boolean;
663
+ renewalAt: number | null;
664
+ canceled: boolean;
665
+ }
666
+ /** A summary plus the reconciled meeting fields — the `application` the member
667
+ * area renders. */
668
+ interface MemberApplication extends ApplicationSummary {
669
+ meetingAt: number | null;
670
+ meetUrl: string | null;
671
+ timezone: string;
672
+ }
673
+ /** The full GET /api/me payload for a signed-in user. */
674
+ interface MemberSession {
675
+ userId: string;
676
+ email: string | null;
677
+ role: string;
678
+ superAdmin: boolean;
679
+ application: MemberApplication | null;
680
+ }
681
+ /** Derive the summary fields from an application row. `paid` is computed, not
682
+ * read, so it can never contradict Stripe. */
683
+ declare function applicationSummary(app: ApplicationRecord): ApplicationSummary;
684
+ /** Fold a (possibly absent, already-reconciled) meeting into the application the
685
+ * member area renders. The live meeting overrides the application's cached
686
+ * meeting fields; a non-`scheduled` meeting clears the booking. */
687
+ declare function memberApplication(app: ApplicationRecord, meeting: MeetingRecord | null | undefined, defaultTimezone: string): MemberApplication;
688
+ /** Identity of the signed-in user, from the verified session. */
689
+ interface SessionUser {
690
+ userId: string;
691
+ email?: string | null;
692
+ role: string;
693
+ }
694
+ /** Assemble the GET /api/me payload. `application` is null when the user has no
695
+ * application on file (an admin who never applied, or a brand-new account). */
696
+ declare function memberSession(user: SessionUser, opts: {
697
+ application: MemberApplication | null;
698
+ superAdmin: boolean;
699
+ }): MemberSession;
700
+
701
+ /**
702
+ * Build the `:root { … }` CSS that maps a chapter's brand onto the design tokens
703
+ * the UI reads: each `palette` entry becomes a custom property, and `fonts`
704
+ * (display/body/numeral) map to `--ui-font-display` / `--ui-font-sans` /
705
+ * `--ui-font-numeral`. Returns "" when there is nothing to theme.
706
+ */
707
+ declare function brandTokens(brand: ChapterBrand | undefined): string;
708
+
709
+ /** A fully-resolved scheduling config (every field present). */
710
+ interface ResolvedScheduling {
711
+ slotMinutes: number;
712
+ days: readonly number[];
713
+ startHour: number;
714
+ endHour: number;
715
+ timezone: string;
716
+ minNoticeHours: number;
717
+ windowDays: number;
718
+ summaryTemplate: string;
719
+ }
720
+ /** The S&S-proven defaults: 45-minute weekday slots, 9–5 Pacific, 24h notice,
721
+ * a 14-day window. The summary is generic (a chapter's group seed supplies a
722
+ * name-branded one). */
723
+ declare const SCHEDULING_DEFAULTS: ResolvedScheduling;
724
+ /**
725
+ * Resolve a group's scheduling config against the defaults, validating every
726
+ * bound the way S&S does at config-write time (throws on a bad config, so a
727
+ * misconfiguration surfaces immediately rather than yielding empty slots).
728
+ * `windowDays` is capped at 62 because Google FreeBusy is.
729
+ */
730
+ declare function resolveScheduling(config?: ChapterScheduling): ResolvedScheduling;
731
+ /** Statuses a member may book/reschedule from (early pipeline only). */
732
+ declare const BOOKABLE_STATUSES: readonly string[];
733
+ /** Whether an application at `status` may book a call. */
734
+ declare function canBookFrom(status: string): boolean;
735
+ /** The availability window: `[now, now + windowDays]` in epoch ms. */
736
+ declare function slotWindow(now: number, windowDays: number): {
737
+ from: number;
738
+ to: number;
739
+ };
740
+ /** The slot's end instant, always derived from its start (never client-supplied). */
741
+ declare function endForSlot(startAt: number, slotMinutes: number): number;
742
+ /** Double-book pre-check: the requested start must land exactly on a currently
743
+ * bookable slot boundary. */
744
+ declare function isSlotAvailable(slots: readonly {
745
+ startAt: number;
746
+ }[], startAt: number): boolean;
747
+ /** Render a meeting summary from its template (`{{firstName}}`/`{{lastName}}`). */
748
+ declare function renderSummary(template: string, app: {
749
+ firstName?: string | null;
750
+ lastName?: string | null;
751
+ }): string;
752
+ /** The prior scheduled meeting for an application, as far as booking cares. */
753
+ interface ExistingMeeting {
754
+ id: string;
755
+ googleEventId?: string | null;
756
+ meetUrl?: string | null;
757
+ htmlLink?: string | null;
758
+ }
759
+ /** Decide reschedule-vs-create: reschedule iff there's an existing event to move,
760
+ * so the Meet link + invite thread survive and no second event is minted. */
761
+ declare function bookingDecision(existing: ExistingMeeting | null | undefined): {
762
+ reschedule: boolean;
763
+ eventId: string | null;
764
+ };
765
+ /** The create idempotency key — one intro event per application, forever, so a
766
+ * retried create returns the same booking rather than a duplicate. */
767
+ declare function introIdempotencyKey(applicationId: string): string;
768
+ /** The canonical `meetings` row for a first booking. A `type` (not `interface`)
769
+ * so it stays assignable to a db op's `attrs` (Record<string, unknown>). */
770
+ type NewMeetingRow = {
771
+ id: string;
772
+ applicationId: string;
773
+ groupId: string;
774
+ startAt: number;
775
+ endAt: number;
776
+ timezone: string;
777
+ status: "scheduled";
778
+ googleEventId: string;
779
+ meetUrl?: string;
780
+ htmlLink?: string;
781
+ drift: "none";
782
+ createdAt: number;
783
+ };
784
+ /** Build the new `meetings` row after the calendar created the event. Optional
785
+ * scalars are omitted (never null), per the odla-db porting rule. */
786
+ declare function meetingCreateRow(i: {
787
+ meetingId: string;
788
+ applicationId: string;
789
+ groupId: string;
790
+ startAt: number;
791
+ endAt: number;
792
+ timezone: string;
793
+ googleEventId: string;
794
+ meetUrl?: string | null;
795
+ htmlLink?: string | null;
796
+ createdAt: number;
797
+ }): NewMeetingRow;
798
+ /** The `meetings`-row patch for a reschedule (same row id, moved in place). */
799
+ type MeetingReschedulePatch = {
800
+ startAt: number;
801
+ endAt: number;
802
+ drift: "none";
803
+ };
804
+ /** Patch to move an existing meeting to a new window. */
805
+ declare function meetingRescheduleUpdate(startAt: number, endAt: number): MeetingReschedulePatch;
806
+ /** The `applications`-row patch after a booking. */
807
+ type ApplicationBookingPatch = {
808
+ meetingAt: number;
809
+ meetingLink?: string;
810
+ status?: "call_scheduled";
811
+ };
812
+ /** Project the booking onto the application row: cache the time, adopt the
813
+ * calendar link if any, and advance the status to `call_scheduled` unless it is
814
+ * already there (never backward). */
815
+ declare function applicationBookingUpdate(currentStatus: string, startAt: number, htmlLink?: string | null): ApplicationBookingPatch;
816
+
817
+ export { type ApplicationBookingPatch, type ApplicationRecord, type ApplicationSummary, type Attr, type AttrType, BOOKABLE_STATUSES, type Chapter, type ChapterApplication, type ChapterAuth, type ChapterBrand, type ChapterConfig, type ChapterDb, type ChapterEmails, type ChapterIntegrationDescriptor, type ChapterIntegrationOptions, type ChapterMode, type ChapterPipeline, type ChapterPolicy, type ChapterPrices, type ChapterScheduling, type DbOp, type DbRules, type DbSchema, type DeliveryDecision, type EmailGroup, type EmailTemplate, type EmailTemplateRow, type Entity, type ExistingMeeting, type GuardResult, type JoinConfigGroup, type MeetingRecord, type MeetingReschedulePatch, type MemberApplication, type MemberSession, type NewMeetingRow, type PaymentsGroup, type ProjectionDeps, type ResolvedApplication, type ResolvedAuth, type ResolvedPipeline, type ResolvedScheduling, type RoleChangeContext, type Rule, SCHEDULING_DEFAULTS, type SecretStore, type SessionUser, type SharedPerson, type StripeEvent, type SubmitResult, type WebhookEvent, applicationBookingUpdate, applicationSummary, bookingDecision, brandTokens, buildGroupSeed, canApprove, canBook, canBookFrom, canChangeRole, canTransition, canceledPatch, chapterDb, createChapterIntegration, defaultCrm, defineChapter, endForSlot, findApplicationRef, firstPaymentPatch, getVaultSecret, introIdempotencyKey, isAdminRole, isAlreadySent, isSlotAvailable, joinConfig, meetingCreateRow, meetingRescheduleUpdate, memberApplication, memberSession, normalizeWebhookEvent, paymentsReady, planDelivery, projectSharedRecord, refundedPatch, render, renderSummary, renderTemplateBody, renewalPatch, resolveApplication, resolveAuth, resolvePipeline, resolveScheduling, roleFromClaim, sharedPersonInput, slotWindow, stageIndex, stripeForm, submitApplication, subscriptionIdempotencyKey, verifyStripeSignature, webhookMutationId };
package/dist/index.d.ts CHANGED
@@ -466,6 +466,88 @@ declare function verifyStripeSignature(payload: string, header: string, secret:
466
466
  now?: number;
467
467
  toleranceSec?: number;
468
468
  }): Promise<boolean>;
469
+ /** A group row's payment configuration, as far as readiness cares. */
470
+ interface PaymentsGroup {
471
+ stripePublishableKey?: string | null;
472
+ stripePriceId?: string | null;
473
+ }
474
+ /** Whether a group can take payment: a publishable key + a price id (both on the
475
+ * group row) AND a secret key (the vault). Anything missing drops the join
476
+ * flow's payment step (paymentsReady:false) rather than half-charging. */
477
+ declare function paymentsReady(group: PaymentsGroup, hasSecretKey: boolean): boolean;
478
+ /** Form-encode params for Stripe's x-www-form-urlencoded API, expanding one level
479
+ * of nested objects into bracket syntax (`metadata[applicationId]=...`). */
480
+ declare function stripeForm(params: Record<string, unknown>): string;
481
+ /** The Stripe idempotency key for creating an application's subscription — one
482
+ * per application, so a client retry can't orphan a second subscription (S&S
483
+ * lacked this; the package enforces it). */
484
+ declare function subscriptionIdempotencyKey(applicationId: string): string;
485
+ /** The db mutationId for a webhook-driven write — exactly-once per Stripe event,
486
+ * so replays are deduped at the db layer. */
487
+ declare function webhookMutationId(eventId: string): string;
488
+ /** A raw Stripe event, as far as normalization cares. */
489
+ interface StripeEvent {
490
+ id: string;
491
+ type: string;
492
+ data?: {
493
+ object?: Record<string, unknown>;
494
+ };
495
+ }
496
+ /** A normalized, provider-agnostic webhook event. `kind` drives the db write;
497
+ * the application is resolved from `applicationId` (metadata) or `customerId`. */
498
+ type WebhookEvent = {
499
+ kind: "first_payment";
500
+ applicationId?: string;
501
+ customerId?: string;
502
+ renewalAt?: number;
503
+ } | {
504
+ kind: "renewal";
505
+ applicationId?: string;
506
+ customerId?: string;
507
+ renewalAt?: number;
508
+ } | {
509
+ kind: "refunded";
510
+ applicationId?: string;
511
+ customerId?: string;
512
+ } | {
513
+ kind: "canceled";
514
+ applicationId?: string;
515
+ customerId?: string;
516
+ } | {
517
+ kind: "ignored";
518
+ type: string;
519
+ };
520
+ /** Resolve the application reference on a Stripe object: `applicationId` from
521
+ * metadata (direct, then subscription_details, then nested
522
+ * parent.subscription_details), plus the customer id for the db fallback. */
523
+ declare function findApplicationRef(obj: Record<string, unknown>): {
524
+ applicationId?: string;
525
+ customerId?: string;
526
+ };
527
+ /** Normalize a verified Stripe event into a {@link WebhookEvent}. `invoice.paid`
528
+ * splits into first_payment vs renewal by `billing_reason`; refunds and
529
+ * cancellations map directly; everything else is ignored (acked, not retried). */
530
+ declare function normalizeWebhookEvent(event: StripeEvent): WebhookEvent;
531
+ /** First-payment patch: advance submitted→paid_pending_vetting (never any other
532
+ * transition) and record the renewal date. Empty when nothing changed, so the
533
+ * caller can skip the write. */
534
+ declare function firstPaymentPatch(currentStatus: string, renewalAt?: number): {
535
+ status?: "paid_pending_vetting";
536
+ renewalAt?: number;
537
+ };
538
+ /** Renewal-invoice patch: just the new renewal date. */
539
+ declare function renewalPatch(renewalAt: number): {
540
+ renewalAt: number;
541
+ };
542
+ /** Refund patch — the SOLE writer of status "refunded" (the admin refund route
543
+ * issues the Stripe refund but never sets this; the webhook does). */
544
+ declare function refundedPatch(): {
545
+ status: "refunded";
546
+ };
547
+ /** Subscription-cancellation patch. */
548
+ declare function canceledPatch(): {
549
+ canceled: true;
550
+ };
469
551
 
470
552
  /** Apply defaults + validate the application config. Throws at import on bad shape. */
471
553
  declare function resolveApplication(a: ChapterApplication | undefined): ResolvedApplication;
@@ -546,4 +628,190 @@ declare function projectSharedRecord(deps: ProjectionDeps, person: SharedPerson)
546
628
  recordId: string;
547
629
  }>;
548
630
 
549
- export { type Attr, type AttrType, type Chapter, type ChapterApplication, type ChapterAuth, type ChapterBrand, type ChapterConfig, type ChapterDb, type ChapterEmails, type ChapterIntegrationDescriptor, type ChapterIntegrationOptions, type ChapterMode, type ChapterPipeline, type ChapterPolicy, type ChapterPrices, type ChapterScheduling, type DbOp, type DbRules, type DbSchema, type DeliveryDecision, type EmailGroup, type EmailTemplate, type EmailTemplateRow, type Entity, type GuardResult, type JoinConfigGroup, type ProjectionDeps, type ResolvedApplication, type ResolvedAuth, type ResolvedPipeline, type RoleChangeContext, type Rule, type SecretStore, type SharedPerson, type SubmitResult, buildGroupSeed, canApprove, canBook, canChangeRole, canTransition, chapterDb, createChapterIntegration, defaultCrm, defineChapter, getVaultSecret, isAdminRole, isAlreadySent, joinConfig, planDelivery, projectSharedRecord, render, renderTemplateBody, resolveApplication, resolveAuth, resolvePipeline, roleFromClaim, sharedPersonInput, stageIndex, submitApplication, verifyStripeSignature };
631
+ /** An application row, as far as the session cares about it. */
632
+ interface ApplicationRecord {
633
+ id: string;
634
+ firstName?: string | null;
635
+ lastName?: string | null;
636
+ email?: string | null;
637
+ status: string;
638
+ meetingAt?: number | null;
639
+ meetingLink?: string | null;
640
+ createdAt?: number | null;
641
+ stripeSubscriptionId?: string | null;
642
+ renewalAt?: number | null;
643
+ canceled?: boolean;
644
+ }
645
+ /** The reconciled meeting row (already adopted against the calendar), or null. */
646
+ interface MeetingRecord {
647
+ status: string;
648
+ startAt?: number | null;
649
+ meetUrl?: string | null;
650
+ timezone?: string | null;
651
+ }
652
+ /** The stable, non-meeting fields of an application (safe to expose to its own
653
+ * owner). */
654
+ interface ApplicationSummary {
655
+ id: string;
656
+ firstName: string | null;
657
+ lastName: string | null;
658
+ email: string | null;
659
+ status: string;
660
+ createdAt: number | null;
661
+ meetingLink: string | null;
662
+ paid: boolean;
663
+ renewalAt: number | null;
664
+ canceled: boolean;
665
+ }
666
+ /** A summary plus the reconciled meeting fields — the `application` the member
667
+ * area renders. */
668
+ interface MemberApplication extends ApplicationSummary {
669
+ meetingAt: number | null;
670
+ meetUrl: string | null;
671
+ timezone: string;
672
+ }
673
+ /** The full GET /api/me payload for a signed-in user. */
674
+ interface MemberSession {
675
+ userId: string;
676
+ email: string | null;
677
+ role: string;
678
+ superAdmin: boolean;
679
+ application: MemberApplication | null;
680
+ }
681
+ /** Derive the summary fields from an application row. `paid` is computed, not
682
+ * read, so it can never contradict Stripe. */
683
+ declare function applicationSummary(app: ApplicationRecord): ApplicationSummary;
684
+ /** Fold a (possibly absent, already-reconciled) meeting into the application the
685
+ * member area renders. The live meeting overrides the application's cached
686
+ * meeting fields; a non-`scheduled` meeting clears the booking. */
687
+ declare function memberApplication(app: ApplicationRecord, meeting: MeetingRecord | null | undefined, defaultTimezone: string): MemberApplication;
688
+ /** Identity of the signed-in user, from the verified session. */
689
+ interface SessionUser {
690
+ userId: string;
691
+ email?: string | null;
692
+ role: string;
693
+ }
694
+ /** Assemble the GET /api/me payload. `application` is null when the user has no
695
+ * application on file (an admin who never applied, or a brand-new account). */
696
+ declare function memberSession(user: SessionUser, opts: {
697
+ application: MemberApplication | null;
698
+ superAdmin: boolean;
699
+ }): MemberSession;
700
+
701
+ /**
702
+ * Build the `:root { … }` CSS that maps a chapter's brand onto the design tokens
703
+ * the UI reads: each `palette` entry becomes a custom property, and `fonts`
704
+ * (display/body/numeral) map to `--ui-font-display` / `--ui-font-sans` /
705
+ * `--ui-font-numeral`. Returns "" when there is nothing to theme.
706
+ */
707
+ declare function brandTokens(brand: ChapterBrand | undefined): string;
708
+
709
+ /** A fully-resolved scheduling config (every field present). */
710
+ interface ResolvedScheduling {
711
+ slotMinutes: number;
712
+ days: readonly number[];
713
+ startHour: number;
714
+ endHour: number;
715
+ timezone: string;
716
+ minNoticeHours: number;
717
+ windowDays: number;
718
+ summaryTemplate: string;
719
+ }
720
+ /** The S&S-proven defaults: 45-minute weekday slots, 9–5 Pacific, 24h notice,
721
+ * a 14-day window. The summary is generic (a chapter's group seed supplies a
722
+ * name-branded one). */
723
+ declare const SCHEDULING_DEFAULTS: ResolvedScheduling;
724
+ /**
725
+ * Resolve a group's scheduling config against the defaults, validating every
726
+ * bound the way S&S does at config-write time (throws on a bad config, so a
727
+ * misconfiguration surfaces immediately rather than yielding empty slots).
728
+ * `windowDays` is capped at 62 because Google FreeBusy is.
729
+ */
730
+ declare function resolveScheduling(config?: ChapterScheduling): ResolvedScheduling;
731
+ /** Statuses a member may book/reschedule from (early pipeline only). */
732
+ declare const BOOKABLE_STATUSES: readonly string[];
733
+ /** Whether an application at `status` may book a call. */
734
+ declare function canBookFrom(status: string): boolean;
735
+ /** The availability window: `[now, now + windowDays]` in epoch ms. */
736
+ declare function slotWindow(now: number, windowDays: number): {
737
+ from: number;
738
+ to: number;
739
+ };
740
+ /** The slot's end instant, always derived from its start (never client-supplied). */
741
+ declare function endForSlot(startAt: number, slotMinutes: number): number;
742
+ /** Double-book pre-check: the requested start must land exactly on a currently
743
+ * bookable slot boundary. */
744
+ declare function isSlotAvailable(slots: readonly {
745
+ startAt: number;
746
+ }[], startAt: number): boolean;
747
+ /** Render a meeting summary from its template (`{{firstName}}`/`{{lastName}}`). */
748
+ declare function renderSummary(template: string, app: {
749
+ firstName?: string | null;
750
+ lastName?: string | null;
751
+ }): string;
752
+ /** The prior scheduled meeting for an application, as far as booking cares. */
753
+ interface ExistingMeeting {
754
+ id: string;
755
+ googleEventId?: string | null;
756
+ meetUrl?: string | null;
757
+ htmlLink?: string | null;
758
+ }
759
+ /** Decide reschedule-vs-create: reschedule iff there's an existing event to move,
760
+ * so the Meet link + invite thread survive and no second event is minted. */
761
+ declare function bookingDecision(existing: ExistingMeeting | null | undefined): {
762
+ reschedule: boolean;
763
+ eventId: string | null;
764
+ };
765
+ /** The create idempotency key — one intro event per application, forever, so a
766
+ * retried create returns the same booking rather than a duplicate. */
767
+ declare function introIdempotencyKey(applicationId: string): string;
768
+ /** The canonical `meetings` row for a first booking. A `type` (not `interface`)
769
+ * so it stays assignable to a db op's `attrs` (Record<string, unknown>). */
770
+ type NewMeetingRow = {
771
+ id: string;
772
+ applicationId: string;
773
+ groupId: string;
774
+ startAt: number;
775
+ endAt: number;
776
+ timezone: string;
777
+ status: "scheduled";
778
+ googleEventId: string;
779
+ meetUrl?: string;
780
+ htmlLink?: string;
781
+ drift: "none";
782
+ createdAt: number;
783
+ };
784
+ /** Build the new `meetings` row after the calendar created the event. Optional
785
+ * scalars are omitted (never null), per the odla-db porting rule. */
786
+ declare function meetingCreateRow(i: {
787
+ meetingId: string;
788
+ applicationId: string;
789
+ groupId: string;
790
+ startAt: number;
791
+ endAt: number;
792
+ timezone: string;
793
+ googleEventId: string;
794
+ meetUrl?: string | null;
795
+ htmlLink?: string | null;
796
+ createdAt: number;
797
+ }): NewMeetingRow;
798
+ /** The `meetings`-row patch for a reschedule (same row id, moved in place). */
799
+ type MeetingReschedulePatch = {
800
+ startAt: number;
801
+ endAt: number;
802
+ drift: "none";
803
+ };
804
+ /** Patch to move an existing meeting to a new window. */
805
+ declare function meetingRescheduleUpdate(startAt: number, endAt: number): MeetingReschedulePatch;
806
+ /** The `applications`-row patch after a booking. */
807
+ type ApplicationBookingPatch = {
808
+ meetingAt: number;
809
+ meetingLink?: string;
810
+ status?: "call_scheduled";
811
+ };
812
+ /** Project the booking onto the application row: cache the time, adopt the
813
+ * calendar link if any, and advance the status to `call_scheduled` unless it is
814
+ * already there (never backward). */
815
+ declare function applicationBookingUpdate(currentStatus: string, startAt: number, htmlLink?: string | null): ApplicationBookingPatch;
816
+
817
+ export { type ApplicationBookingPatch, type ApplicationRecord, type ApplicationSummary, type Attr, type AttrType, BOOKABLE_STATUSES, type Chapter, type ChapterApplication, type ChapterAuth, type ChapterBrand, type ChapterConfig, type ChapterDb, type ChapterEmails, type ChapterIntegrationDescriptor, type ChapterIntegrationOptions, type ChapterMode, type ChapterPipeline, type ChapterPolicy, type ChapterPrices, type ChapterScheduling, type DbOp, type DbRules, type DbSchema, type DeliveryDecision, type EmailGroup, type EmailTemplate, type EmailTemplateRow, type Entity, type ExistingMeeting, type GuardResult, type JoinConfigGroup, type MeetingRecord, type MeetingReschedulePatch, type MemberApplication, type MemberSession, type NewMeetingRow, type PaymentsGroup, type ProjectionDeps, type ResolvedApplication, type ResolvedAuth, type ResolvedPipeline, type ResolvedScheduling, type RoleChangeContext, type Rule, SCHEDULING_DEFAULTS, type SecretStore, type SessionUser, type SharedPerson, type StripeEvent, type SubmitResult, type WebhookEvent, applicationBookingUpdate, applicationSummary, bookingDecision, brandTokens, buildGroupSeed, canApprove, canBook, canBookFrom, canChangeRole, canTransition, canceledPatch, chapterDb, createChapterIntegration, defaultCrm, defineChapter, endForSlot, findApplicationRef, firstPaymentPatch, getVaultSecret, introIdempotencyKey, isAdminRole, isAlreadySent, isSlotAvailable, joinConfig, meetingCreateRow, meetingRescheduleUpdate, memberApplication, memberSession, normalizeWebhookEvent, paymentsReady, planDelivery, projectSharedRecord, refundedPatch, render, renderSummary, renderTemplateBody, renewalPatch, resolveApplication, resolveAuth, resolvePipeline, resolveScheduling, roleFromClaim, sharedPersonInput, slotWindow, stageIndex, stripeForm, submitApplication, subscriptionIdempotencyKey, verifyStripeSignature, webhookMutationId };