@odla-ai/chapter 0.5.0 → 0.7.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/README.md +63 -28
- package/dist/index.cjs +145 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +128 -8
- package/dist/index.d.ts +128 -8
- package/dist/index.js +145 -14
- package/dist/index.js.map +1 -1
- package/dist/ui/index.d.ts +5 -1
- package/dist/ui/index.js +3 -2
- package/dist/ui/index.js.map +1 -1
- package/dist/worker/index.cjs +344 -17
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +60 -6
- package/dist/worker/index.d.ts +60 -6
- package/dist/worker/index.js +344 -17
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -433,6 +433,54 @@ declare function planDelivery(input: {
|
|
|
433
433
|
force?: boolean;
|
|
434
434
|
}): DeliveryDecision;
|
|
435
435
|
|
|
436
|
+
/** The mail transport (the worker's Cloudflare SEND_EMAIL binding). */
|
|
437
|
+
interface MailSender {
|
|
438
|
+
send(payload: {
|
|
439
|
+
from: string;
|
|
440
|
+
to: string[];
|
|
441
|
+
subject: string;
|
|
442
|
+
text?: string;
|
|
443
|
+
replyTo?: string;
|
|
444
|
+
}): Promise<{
|
|
445
|
+
messageId: string;
|
|
446
|
+
}>;
|
|
447
|
+
}
|
|
448
|
+
/** Deps for {@link sendTemplated} — the db, the env name (drives the fail-safe),
|
|
449
|
+
* the transport + from (absent ⇒ log-only), and injected clock/id. */
|
|
450
|
+
interface NotifyDeps {
|
|
451
|
+
db: ChapterDb;
|
|
452
|
+
envName: string;
|
|
453
|
+
sender?: MailSender;
|
|
454
|
+
from?: string;
|
|
455
|
+
now: () => number;
|
|
456
|
+
newId: () => string;
|
|
457
|
+
}
|
|
458
|
+
/** One templated send. `dedupeKey` is the exactly-once key. */
|
|
459
|
+
interface NotifyInput {
|
|
460
|
+
group: EmailGroup;
|
|
461
|
+
template: string;
|
|
462
|
+
to: string;
|
|
463
|
+
vars: Record<string, string>;
|
|
464
|
+
dedupeKey: string;
|
|
465
|
+
applicationId?: string;
|
|
466
|
+
/** The admin test route may send a disabled template. */
|
|
467
|
+
force?: boolean;
|
|
468
|
+
}
|
|
469
|
+
/** The outcome. `sent:true` includes the already-sent short-circuit. */
|
|
470
|
+
interface NotifyResult {
|
|
471
|
+
sent: boolean;
|
|
472
|
+
reason?: string;
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Send a templated lifecycle email exactly once. Short-circuits on a prior
|
|
476
|
+
* successful send; otherwise plans delivery (dev fail-safe applies), sends via the
|
|
477
|
+
* transport when one is wired and the plan calls for it, and records an emailLog
|
|
478
|
+
* row either way (success keyed for exactly-once, failure unkeyed for retry).
|
|
479
|
+
*/
|
|
480
|
+
declare function sendTemplated(deps: NotifyDeps, input: NotifyInput): Promise<NotifyResult>;
|
|
481
|
+
/** Project a `groups` row into the {@link EmailGroup} the email pipeline reads. */
|
|
482
|
+
declare function emailGroupFrom(row: Record<string, unknown>): EmailGroup;
|
|
483
|
+
|
|
436
484
|
/**
|
|
437
485
|
* Apply defaults + validate the pipeline config. With no config, the full Silver
|
|
438
486
|
* & Salt pipeline. With `stages` given but the subsets omitted, the subsets
|
|
@@ -619,14 +667,50 @@ interface ProjectionDeps {
|
|
|
619
667
|
}
|
|
620
668
|
/**
|
|
621
669
|
* Upsert a hub-shared prospect into this chapter's `crm_record` (push
|
|
622
|
-
* projection)
|
|
623
|
-
*
|
|
624
|
-
* the chapter-side record id. Callers wrap this in `.catch` so a projection
|
|
625
|
-
* failure never fails the hub's share request.
|
|
670
|
+
* projection), idempotent by the hub record id and unified by email. Callers wrap
|
|
671
|
+
* this in `.catch` so a projection failure never fails the hub's share request.
|
|
626
672
|
*/
|
|
627
673
|
declare function projectSharedRecord(deps: ProjectionDeps, person: SharedPerson): Promise<{
|
|
628
674
|
recordId: string;
|
|
629
675
|
}>;
|
|
676
|
+
/** An arriving applicant, as far as the CRM projection cares. */
|
|
677
|
+
interface Applicant {
|
|
678
|
+
applicationId: string;
|
|
679
|
+
email: string;
|
|
680
|
+
firstName?: string;
|
|
681
|
+
lastName?: string;
|
|
682
|
+
phone?: string;
|
|
683
|
+
linkedin?: string;
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* Project an arriving applicant into this chapter's `crm_record`, so a new
|
|
687
|
+
* application shows up in the CRM immediately, unified by email with any prior
|
|
688
|
+
* record. Idempotent per application (`apply:${applicationId}`). Best-effort at
|
|
689
|
+
* the call site — a projection failure never fails the application.
|
|
690
|
+
*/
|
|
691
|
+
declare function projectApplicant(deps: ProjectionDeps, applicant: Applicant): Promise<{
|
|
692
|
+
recordId: string;
|
|
693
|
+
}>;
|
|
694
|
+
|
|
695
|
+
/** Inputs for a Clerk invitation. */
|
|
696
|
+
interface ClerkInviteInput {
|
|
697
|
+
email: string;
|
|
698
|
+
/** Where the accept-invitation link lands (usually the member area). */
|
|
699
|
+
redirectUrl?: string;
|
|
700
|
+
}
|
|
701
|
+
/** Build the Clerk Backend API invitation request (path + JSON body). Pure, so
|
|
702
|
+
* the wire shape is testable without a network call. */
|
|
703
|
+
declare function clerkInviteRequest(input: ClerkInviteInput): {
|
|
704
|
+
path: string;
|
|
705
|
+
body: Record<string, unknown>;
|
|
706
|
+
};
|
|
707
|
+
/** POST the invitation to the Clerk Backend API. `ignore_existing` isn't set, so
|
|
708
|
+
* a repeat invite for an already-invited/known email returns non-ok — the caller
|
|
709
|
+
* swallows that (idempotent-enough for a best-effort apply-time invite). */
|
|
710
|
+
declare function createClerkInvitation(secretKey: string, input: ClerkInviteInput, fetchImpl?: typeof fetch): Promise<{
|
|
711
|
+
ok: boolean;
|
|
712
|
+
status: number;
|
|
713
|
+
}>;
|
|
630
714
|
|
|
631
715
|
/** An application row, as far as the session cares about it. */
|
|
632
716
|
interface ApplicationRecord {
|
|
@@ -706,6 +790,45 @@ declare function memberSession(user: SessionUser, opts: {
|
|
|
706
790
|
*/
|
|
707
791
|
declare function brandTokens(brand: ChapterBrand | undefined): string;
|
|
708
792
|
|
|
793
|
+
/** A `meetings` row, as far as reconciliation cares. */
|
|
794
|
+
interface MeetingForReconcile {
|
|
795
|
+
id: string;
|
|
796
|
+
applicationId: string;
|
|
797
|
+
googleEventId?: string | null;
|
|
798
|
+
status: string;
|
|
799
|
+
startAt?: number | null;
|
|
800
|
+
endAt?: number | null;
|
|
801
|
+
}
|
|
802
|
+
/** One live calendar event (a subset of @odla-ai/calendar's Booking). */
|
|
803
|
+
interface LiveEvent {
|
|
804
|
+
eventId: string;
|
|
805
|
+
status?: string;
|
|
806
|
+
startAt?: number;
|
|
807
|
+
endAt?: number;
|
|
808
|
+
}
|
|
809
|
+
/** An adopt decision: mirror a Google move/cancel onto our rows. */
|
|
810
|
+
interface ReconcileDecision {
|
|
811
|
+
meetingId: string;
|
|
812
|
+
applicationId: string;
|
|
813
|
+
kind: "cancelled" | "moved";
|
|
814
|
+
/** Attrs to write onto the `meetings` row. */
|
|
815
|
+
meetingPatch: Record<string, unknown>;
|
|
816
|
+
/** Attrs to write onto the `applications` row (the projection). */
|
|
817
|
+
applicationPatch: Record<string, unknown>;
|
|
818
|
+
}
|
|
819
|
+
/** Meetings still worth reconciling: a scheduled booking with a Google event that
|
|
820
|
+
* starts in the future (or within the last hour, to catch a just-passed edit). */
|
|
821
|
+
declare function isReconcilable(meeting: MeetingForReconcile, now: number): boolean;
|
|
822
|
+
/**
|
|
823
|
+
* Diff canonical `meetings` against the live calendar events and return the adopt
|
|
824
|
+
* decisions — only for meetings that actually changed (a still-matching meeting
|
|
825
|
+
* is omitted). A meeting whose event vanished or is `cancelled` in Google is
|
|
826
|
+
* adopted as cancelled (application `meetingAt` zeroed — 0 means "was booked,
|
|
827
|
+
* then cancelled"); a meeting whose event moved adopts the new window (duration
|
|
828
|
+
* preserved when the event omits `endAt`).
|
|
829
|
+
*/
|
|
830
|
+
declare function reconcileMeetings(meetings: readonly MeetingForReconcile[], events: readonly LiveEvent[], now: number): ReconcileDecision[];
|
|
831
|
+
|
|
709
832
|
/** A fully-resolved scheduling config (every field present). */
|
|
710
833
|
interface ResolvedScheduling {
|
|
711
834
|
slotMinutes: number;
|
|
@@ -729,9 +852,6 @@ declare const SCHEDULING_DEFAULTS: ResolvedScheduling;
|
|
|
729
852
|
*/
|
|
730
853
|
declare function resolveScheduling(config?: ChapterScheduling): ResolvedScheduling;
|
|
731
854
|
/** 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
855
|
/** The availability window: `[now, now + windowDays]` in epoch ms. */
|
|
736
856
|
declare function slotWindow(now: number, windowDays: number): {
|
|
737
857
|
from: number;
|
|
@@ -814,4 +934,4 @@ type ApplicationBookingPatch = {
|
|
|
814
934
|
* already there (never backward). */
|
|
815
935
|
declare function applicationBookingUpdate(currentStatus: string, startAt: number, htmlLink?: string | null): ApplicationBookingPatch;
|
|
816
936
|
|
|
817
|
-
export { type ApplicationBookingPatch, type ApplicationRecord, type ApplicationSummary, type Attr, type AttrType,
|
|
937
|
+
export { type Applicant, type ApplicationBookingPatch, type ApplicationRecord, type ApplicationSummary, 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 ClerkInviteInput, type DbOp, type DbRules, type DbSchema, type DeliveryDecision, type EmailGroup, type EmailTemplate, type EmailTemplateRow, type Entity, type ExistingMeeting, type GuardResult, type JoinConfigGroup, type LiveEvent, type MailSender, type MeetingForReconcile, type MeetingRecord, type MeetingReschedulePatch, type MemberApplication, type MemberSession, type NewMeetingRow, type NotifyDeps, type NotifyInput, type NotifyResult, type PaymentsGroup, type ProjectionDeps, type ReconcileDecision, 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, canChangeRole, canTransition, canceledPatch, chapterDb, clerkInviteRequest, createChapterIntegration, createClerkInvitation, defaultCrm, defineChapter, emailGroupFrom, endForSlot, findApplicationRef, firstPaymentPatch, getVaultSecret, introIdempotencyKey, isAdminRole, isAlreadySent, isReconcilable, isSlotAvailable, joinConfig, meetingCreateRow, meetingRescheduleUpdate, memberApplication, memberSession, normalizeWebhookEvent, paymentsReady, planDelivery, projectApplicant, projectSharedRecord, reconcileMeetings, refundedPatch, render, renderSummary, renderTemplateBody, renewalPatch, resolveApplication, resolveAuth, resolvePipeline, resolveScheduling, roleFromClaim, sendTemplated, sharedPersonInput, slotWindow, stageIndex, stripeForm, submitApplication, subscriptionIdempotencyKey, verifyStripeSignature, webhookMutationId };
|
package/dist/index.d.ts
CHANGED
|
@@ -433,6 +433,54 @@ declare function planDelivery(input: {
|
|
|
433
433
|
force?: boolean;
|
|
434
434
|
}): DeliveryDecision;
|
|
435
435
|
|
|
436
|
+
/** The mail transport (the worker's Cloudflare SEND_EMAIL binding). */
|
|
437
|
+
interface MailSender {
|
|
438
|
+
send(payload: {
|
|
439
|
+
from: string;
|
|
440
|
+
to: string[];
|
|
441
|
+
subject: string;
|
|
442
|
+
text?: string;
|
|
443
|
+
replyTo?: string;
|
|
444
|
+
}): Promise<{
|
|
445
|
+
messageId: string;
|
|
446
|
+
}>;
|
|
447
|
+
}
|
|
448
|
+
/** Deps for {@link sendTemplated} — the db, the env name (drives the fail-safe),
|
|
449
|
+
* the transport + from (absent ⇒ log-only), and injected clock/id. */
|
|
450
|
+
interface NotifyDeps {
|
|
451
|
+
db: ChapterDb;
|
|
452
|
+
envName: string;
|
|
453
|
+
sender?: MailSender;
|
|
454
|
+
from?: string;
|
|
455
|
+
now: () => number;
|
|
456
|
+
newId: () => string;
|
|
457
|
+
}
|
|
458
|
+
/** One templated send. `dedupeKey` is the exactly-once key. */
|
|
459
|
+
interface NotifyInput {
|
|
460
|
+
group: EmailGroup;
|
|
461
|
+
template: string;
|
|
462
|
+
to: string;
|
|
463
|
+
vars: Record<string, string>;
|
|
464
|
+
dedupeKey: string;
|
|
465
|
+
applicationId?: string;
|
|
466
|
+
/** The admin test route may send a disabled template. */
|
|
467
|
+
force?: boolean;
|
|
468
|
+
}
|
|
469
|
+
/** The outcome. `sent:true` includes the already-sent short-circuit. */
|
|
470
|
+
interface NotifyResult {
|
|
471
|
+
sent: boolean;
|
|
472
|
+
reason?: string;
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Send a templated lifecycle email exactly once. Short-circuits on a prior
|
|
476
|
+
* successful send; otherwise plans delivery (dev fail-safe applies), sends via the
|
|
477
|
+
* transport when one is wired and the plan calls for it, and records an emailLog
|
|
478
|
+
* row either way (success keyed for exactly-once, failure unkeyed for retry).
|
|
479
|
+
*/
|
|
480
|
+
declare function sendTemplated(deps: NotifyDeps, input: NotifyInput): Promise<NotifyResult>;
|
|
481
|
+
/** Project a `groups` row into the {@link EmailGroup} the email pipeline reads. */
|
|
482
|
+
declare function emailGroupFrom(row: Record<string, unknown>): EmailGroup;
|
|
483
|
+
|
|
436
484
|
/**
|
|
437
485
|
* Apply defaults + validate the pipeline config. With no config, the full Silver
|
|
438
486
|
* & Salt pipeline. With `stages` given but the subsets omitted, the subsets
|
|
@@ -619,14 +667,50 @@ interface ProjectionDeps {
|
|
|
619
667
|
}
|
|
620
668
|
/**
|
|
621
669
|
* Upsert a hub-shared prospect into this chapter's `crm_record` (push
|
|
622
|
-
* projection)
|
|
623
|
-
*
|
|
624
|
-
* the chapter-side record id. Callers wrap this in `.catch` so a projection
|
|
625
|
-
* failure never fails the hub's share request.
|
|
670
|
+
* projection), idempotent by the hub record id and unified by email. Callers wrap
|
|
671
|
+
* this in `.catch` so a projection failure never fails the hub's share request.
|
|
626
672
|
*/
|
|
627
673
|
declare function projectSharedRecord(deps: ProjectionDeps, person: SharedPerson): Promise<{
|
|
628
674
|
recordId: string;
|
|
629
675
|
}>;
|
|
676
|
+
/** An arriving applicant, as far as the CRM projection cares. */
|
|
677
|
+
interface Applicant {
|
|
678
|
+
applicationId: string;
|
|
679
|
+
email: string;
|
|
680
|
+
firstName?: string;
|
|
681
|
+
lastName?: string;
|
|
682
|
+
phone?: string;
|
|
683
|
+
linkedin?: string;
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* Project an arriving applicant into this chapter's `crm_record`, so a new
|
|
687
|
+
* application shows up in the CRM immediately, unified by email with any prior
|
|
688
|
+
* record. Idempotent per application (`apply:${applicationId}`). Best-effort at
|
|
689
|
+
* the call site — a projection failure never fails the application.
|
|
690
|
+
*/
|
|
691
|
+
declare function projectApplicant(deps: ProjectionDeps, applicant: Applicant): Promise<{
|
|
692
|
+
recordId: string;
|
|
693
|
+
}>;
|
|
694
|
+
|
|
695
|
+
/** Inputs for a Clerk invitation. */
|
|
696
|
+
interface ClerkInviteInput {
|
|
697
|
+
email: string;
|
|
698
|
+
/** Where the accept-invitation link lands (usually the member area). */
|
|
699
|
+
redirectUrl?: string;
|
|
700
|
+
}
|
|
701
|
+
/** Build the Clerk Backend API invitation request (path + JSON body). Pure, so
|
|
702
|
+
* the wire shape is testable without a network call. */
|
|
703
|
+
declare function clerkInviteRequest(input: ClerkInviteInput): {
|
|
704
|
+
path: string;
|
|
705
|
+
body: Record<string, unknown>;
|
|
706
|
+
};
|
|
707
|
+
/** POST the invitation to the Clerk Backend API. `ignore_existing` isn't set, so
|
|
708
|
+
* a repeat invite for an already-invited/known email returns non-ok — the caller
|
|
709
|
+
* swallows that (idempotent-enough for a best-effort apply-time invite). */
|
|
710
|
+
declare function createClerkInvitation(secretKey: string, input: ClerkInviteInput, fetchImpl?: typeof fetch): Promise<{
|
|
711
|
+
ok: boolean;
|
|
712
|
+
status: number;
|
|
713
|
+
}>;
|
|
630
714
|
|
|
631
715
|
/** An application row, as far as the session cares about it. */
|
|
632
716
|
interface ApplicationRecord {
|
|
@@ -706,6 +790,45 @@ declare function memberSession(user: SessionUser, opts: {
|
|
|
706
790
|
*/
|
|
707
791
|
declare function brandTokens(brand: ChapterBrand | undefined): string;
|
|
708
792
|
|
|
793
|
+
/** A `meetings` row, as far as reconciliation cares. */
|
|
794
|
+
interface MeetingForReconcile {
|
|
795
|
+
id: string;
|
|
796
|
+
applicationId: string;
|
|
797
|
+
googleEventId?: string | null;
|
|
798
|
+
status: string;
|
|
799
|
+
startAt?: number | null;
|
|
800
|
+
endAt?: number | null;
|
|
801
|
+
}
|
|
802
|
+
/** One live calendar event (a subset of @odla-ai/calendar's Booking). */
|
|
803
|
+
interface LiveEvent {
|
|
804
|
+
eventId: string;
|
|
805
|
+
status?: string;
|
|
806
|
+
startAt?: number;
|
|
807
|
+
endAt?: number;
|
|
808
|
+
}
|
|
809
|
+
/** An adopt decision: mirror a Google move/cancel onto our rows. */
|
|
810
|
+
interface ReconcileDecision {
|
|
811
|
+
meetingId: string;
|
|
812
|
+
applicationId: string;
|
|
813
|
+
kind: "cancelled" | "moved";
|
|
814
|
+
/** Attrs to write onto the `meetings` row. */
|
|
815
|
+
meetingPatch: Record<string, unknown>;
|
|
816
|
+
/** Attrs to write onto the `applications` row (the projection). */
|
|
817
|
+
applicationPatch: Record<string, unknown>;
|
|
818
|
+
}
|
|
819
|
+
/** Meetings still worth reconciling: a scheduled booking with a Google event that
|
|
820
|
+
* starts in the future (or within the last hour, to catch a just-passed edit). */
|
|
821
|
+
declare function isReconcilable(meeting: MeetingForReconcile, now: number): boolean;
|
|
822
|
+
/**
|
|
823
|
+
* Diff canonical `meetings` against the live calendar events and return the adopt
|
|
824
|
+
* decisions — only for meetings that actually changed (a still-matching meeting
|
|
825
|
+
* is omitted). A meeting whose event vanished or is `cancelled` in Google is
|
|
826
|
+
* adopted as cancelled (application `meetingAt` zeroed — 0 means "was booked,
|
|
827
|
+
* then cancelled"); a meeting whose event moved adopts the new window (duration
|
|
828
|
+
* preserved when the event omits `endAt`).
|
|
829
|
+
*/
|
|
830
|
+
declare function reconcileMeetings(meetings: readonly MeetingForReconcile[], events: readonly LiveEvent[], now: number): ReconcileDecision[];
|
|
831
|
+
|
|
709
832
|
/** A fully-resolved scheduling config (every field present). */
|
|
710
833
|
interface ResolvedScheduling {
|
|
711
834
|
slotMinutes: number;
|
|
@@ -729,9 +852,6 @@ declare const SCHEDULING_DEFAULTS: ResolvedScheduling;
|
|
|
729
852
|
*/
|
|
730
853
|
declare function resolveScheduling(config?: ChapterScheduling): ResolvedScheduling;
|
|
731
854
|
/** 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
855
|
/** The availability window: `[now, now + windowDays]` in epoch ms. */
|
|
736
856
|
declare function slotWindow(now: number, windowDays: number): {
|
|
737
857
|
from: number;
|
|
@@ -814,4 +934,4 @@ type ApplicationBookingPatch = {
|
|
|
814
934
|
* already there (never backward). */
|
|
815
935
|
declare function applicationBookingUpdate(currentStatus: string, startAt: number, htmlLink?: string | null): ApplicationBookingPatch;
|
|
816
936
|
|
|
817
|
-
export { type ApplicationBookingPatch, type ApplicationRecord, type ApplicationSummary, type Attr, type AttrType,
|
|
937
|
+
export { type Applicant, type ApplicationBookingPatch, type ApplicationRecord, type ApplicationSummary, 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 ClerkInviteInput, type DbOp, type DbRules, type DbSchema, type DeliveryDecision, type EmailGroup, type EmailTemplate, type EmailTemplateRow, type Entity, type ExistingMeeting, type GuardResult, type JoinConfigGroup, type LiveEvent, type MailSender, type MeetingForReconcile, type MeetingRecord, type MeetingReschedulePatch, type MemberApplication, type MemberSession, type NewMeetingRow, type NotifyDeps, type NotifyInput, type NotifyResult, type PaymentsGroup, type ProjectionDeps, type ReconcileDecision, 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, canChangeRole, canTransition, canceledPatch, chapterDb, clerkInviteRequest, createChapterIntegration, createClerkInvitation, defaultCrm, defineChapter, emailGroupFrom, endForSlot, findApplicationRef, firstPaymentPatch, getVaultSecret, introIdempotencyKey, isAdminRole, isAlreadySent, isReconcilable, isSlotAvailable, joinConfig, meetingCreateRow, meetingRescheduleUpdate, memberApplication, memberSession, normalizeWebhookEvent, paymentsReady, planDelivery, projectApplicant, projectSharedRecord, reconcileMeetings, refundedPatch, render, renderSummary, renderTemplateBody, renewalPatch, resolveApplication, resolveAuth, resolvePipeline, resolveScheduling, roleFromClaim, sendTemplated, sharedPersonInput, slotWindow, stageIndex, stripeForm, submitApplication, subscriptionIdempotencyKey, verifyStripeSignature, webhookMutationId };
|
package/dist/index.js
CHANGED
|
@@ -556,6 +556,72 @@ function planDelivery(input) {
|
|
|
556
556
|
return { deliver: true, transport, to, subject, text, redirected: redirect };
|
|
557
557
|
}
|
|
558
558
|
|
|
559
|
+
// src/notify.ts
|
|
560
|
+
async function sendTemplated(deps, input) {
|
|
561
|
+
const { emailLog: emailLog2 } = await deps.db.query({ emailLog: { $: { where: { dedupeKey: input.dedupeKey } } } });
|
|
562
|
+
const prior = Array.isArray(emailLog2) ? emailLog2 : [];
|
|
563
|
+
if (isAlreadySent(prior)) return { sent: true, reason: "already-sent" };
|
|
564
|
+
const cloudflareReady = Boolean(deps.sender && deps.from);
|
|
565
|
+
const decision = planDelivery({
|
|
566
|
+
envName: deps.envName,
|
|
567
|
+
group: input.group,
|
|
568
|
+
template: input.template,
|
|
569
|
+
to: input.to,
|
|
570
|
+
vars: input.vars,
|
|
571
|
+
cloudflareReady,
|
|
572
|
+
force: input.force
|
|
573
|
+
});
|
|
574
|
+
if (!decision.deliver) return { sent: false, reason: decision.reason };
|
|
575
|
+
let error;
|
|
576
|
+
let messageId;
|
|
577
|
+
if (decision.transport === "cloudflare" && deps.sender && deps.from) {
|
|
578
|
+
try {
|
|
579
|
+
const res = await deps.sender.send({
|
|
580
|
+
from: deps.from,
|
|
581
|
+
to: [decision.to],
|
|
582
|
+
subject: decision.subject,
|
|
583
|
+
text: decision.text,
|
|
584
|
+
replyTo: input.group.replyTo
|
|
585
|
+
});
|
|
586
|
+
messageId = res.messageId;
|
|
587
|
+
} catch (e) {
|
|
588
|
+
error = e instanceof Error ? e.message : String(e);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
const id2 = deps.newId();
|
|
592
|
+
const row = {
|
|
593
|
+
id: id2,
|
|
594
|
+
groupId: input.group.id,
|
|
595
|
+
to: decision.to,
|
|
596
|
+
template: input.template,
|
|
597
|
+
subject: decision.subject,
|
|
598
|
+
body: decision.text,
|
|
599
|
+
transport: decision.transport,
|
|
600
|
+
redirected: decision.redirected,
|
|
601
|
+
dedupeKey: input.dedupeKey,
|
|
602
|
+
sentAt: deps.now(),
|
|
603
|
+
...input.applicationId ? { applicationId: input.applicationId } : {},
|
|
604
|
+
...messageId ? { messageId } : {},
|
|
605
|
+
...error ? { error } : {}
|
|
606
|
+
};
|
|
607
|
+
await deps.db.transact([{ t: "update", ns: "emailLog", id: id2, attrs: row }], error ? void 0 : { mutationId: `email:${input.dedupeKey}` });
|
|
608
|
+
return error ? { sent: false, reason: error } : { sent: true };
|
|
609
|
+
}
|
|
610
|
+
function emailGroupFrom(row) {
|
|
611
|
+
const str = (v) => typeof v === "string" ? v : void 0;
|
|
612
|
+
const templates = row.emailTemplates && typeof row.emailTemplates === "object" ? row.emailTemplates : {};
|
|
613
|
+
return {
|
|
614
|
+
id: String(row.id),
|
|
615
|
+
name: String(row.name ?? ""),
|
|
616
|
+
replyTo: str(row.replyTo) ?? "",
|
|
617
|
+
debugEmail: str(row.debugEmail),
|
|
618
|
+
refundPolicyText: str(row.refundPolicyText),
|
|
619
|
+
commitmentText: str(row.commitmentText),
|
|
620
|
+
normsText: str(row.normsText),
|
|
621
|
+
emailTemplates: templates
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
|
|
559
625
|
// src/payments.ts
|
|
560
626
|
function parseSigHeader(header) {
|
|
561
627
|
const parts = {};
|
|
@@ -665,21 +731,53 @@ function sharedPersonInput(person) {
|
|
|
665
731
|
if (person.linkedin) input.linkedin = person.linkedin;
|
|
666
732
|
return input;
|
|
667
733
|
}
|
|
668
|
-
async function
|
|
669
|
-
const email =
|
|
670
|
-
const input = sharedPersonInput(person);
|
|
734
|
+
async function upsertPerson(deps, opts) {
|
|
735
|
+
const email = opts.email.toLowerCase();
|
|
671
736
|
const crmDeps = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
|
|
672
|
-
const { crm_record } = await deps.db.query({
|
|
673
|
-
crm_record: { $: { where: { type: "person", primaryEmail: email }, limit: 1 } }
|
|
674
|
-
});
|
|
737
|
+
const { crm_record } = await deps.db.query({ crm_record: { $: { where: { type: "person", primaryEmail: email }, limit: 1 } } });
|
|
675
738
|
const existing = crm_record?.[0];
|
|
676
739
|
if (existing && typeof existing.id === "string") {
|
|
677
|
-
await updateRecord(crmDeps, { id: existing.id, input });
|
|
740
|
+
await updateRecord(crmDeps, { id: existing.id, input: opts.input });
|
|
678
741
|
return { recordId: existing.id };
|
|
679
742
|
}
|
|
680
|
-
const created = await createRecord(crmDeps, { type: "person", input, mutationId:
|
|
743
|
+
const created = await createRecord(crmDeps, { type: "person", input: opts.input, mutationId: opts.mutationId });
|
|
681
744
|
return { recordId: created.id };
|
|
682
745
|
}
|
|
746
|
+
async function projectSharedRecord(deps, person) {
|
|
747
|
+
return upsertPerson(deps, { email: person.email, input: sharedPersonInput(person), mutationId: `share:${person.hubRecordId}` });
|
|
748
|
+
}
|
|
749
|
+
async function projectApplicant(deps, applicant) {
|
|
750
|
+
const input = sharedPersonInput({
|
|
751
|
+
email: applicant.email,
|
|
752
|
+
firstName: applicant.firstName,
|
|
753
|
+
lastName: applicant.lastName,
|
|
754
|
+
phone: applicant.phone,
|
|
755
|
+
linkedin: applicant.linkedin,
|
|
756
|
+
hubRecordId: applicant.applicationId
|
|
757
|
+
});
|
|
758
|
+
return upsertPerson(deps, { email: applicant.email, input, mutationId: `apply:${applicant.applicationId}` });
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// src/clerk.ts
|
|
762
|
+
function clerkInviteRequest(input) {
|
|
763
|
+
return {
|
|
764
|
+
path: "/v1/invitations",
|
|
765
|
+
body: {
|
|
766
|
+
email_address: input.email,
|
|
767
|
+
notify: true,
|
|
768
|
+
...input.redirectUrl ? { redirect_url: input.redirectUrl } : {}
|
|
769
|
+
}
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
async function createClerkInvitation(secretKey, input, fetchImpl = fetch) {
|
|
773
|
+
const { path, body } = clerkInviteRequest(input);
|
|
774
|
+
const res = await fetchImpl(`https://api.clerk.com${path}`, {
|
|
775
|
+
method: "POST",
|
|
776
|
+
headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
|
|
777
|
+
body: JSON.stringify(body)
|
|
778
|
+
});
|
|
779
|
+
return { ok: res.ok, status: res.status };
|
|
780
|
+
}
|
|
683
781
|
|
|
684
782
|
// src/session.ts
|
|
685
783
|
function applicationSummary(app) {
|
|
@@ -745,6 +843,38 @@ function brandTokens(brand) {
|
|
|
745
843
|
` : "";
|
|
746
844
|
}
|
|
747
845
|
|
|
846
|
+
// src/reconcile.ts
|
|
847
|
+
function isReconcilable(meeting, now) {
|
|
848
|
+
return meeting.status === "scheduled" && Boolean(meeting.googleEventId) && (meeting.startAt ?? 0) > now - 36e5;
|
|
849
|
+
}
|
|
850
|
+
function reconcileMeetings(meetings2, events, now) {
|
|
851
|
+
const byEvent = new Map(events.map((e) => [e.eventId, e]));
|
|
852
|
+
const decisions = [];
|
|
853
|
+
for (const m of meetings2) {
|
|
854
|
+
if (!isReconcilable(m, now) || !m.googleEventId) continue;
|
|
855
|
+
const g = byEvent.get(m.googleEventId);
|
|
856
|
+
if (!g || g.status === "cancelled") {
|
|
857
|
+
decisions.push({
|
|
858
|
+
meetingId: m.id,
|
|
859
|
+
applicationId: m.applicationId,
|
|
860
|
+
kind: "cancelled",
|
|
861
|
+
meetingPatch: { status: "cancelled", drift: "none", adoptedFromGoogleAt: now },
|
|
862
|
+
applicationPatch: { meetingAt: 0, meetingLink: "" }
|
|
863
|
+
});
|
|
864
|
+
} else if (g.startAt !== void 0 && g.startAt !== m.startAt) {
|
|
865
|
+
const duration = (m.endAt ?? 0) - (m.startAt ?? 0);
|
|
866
|
+
decisions.push({
|
|
867
|
+
meetingId: m.id,
|
|
868
|
+
applicationId: m.applicationId,
|
|
869
|
+
kind: "moved",
|
|
870
|
+
meetingPatch: { startAt: g.startAt, endAt: g.endAt ?? g.startAt + duration, drift: "none", adoptedFromGoogleAt: now },
|
|
871
|
+
applicationPatch: { meetingAt: g.startAt }
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
return decisions;
|
|
876
|
+
}
|
|
877
|
+
|
|
748
878
|
// src/scheduling.ts
|
|
749
879
|
var SCHEDULING_DEFAULTS = {
|
|
750
880
|
slotMinutes: 45,
|
|
@@ -791,10 +921,6 @@ function resolveScheduling(config) {
|
|
|
791
921
|
if (typeof c.summaryTemplate !== "string") fail("summaryTemplate must be a string");
|
|
792
922
|
return { ...c, days };
|
|
793
923
|
}
|
|
794
|
-
var BOOKABLE_STATUSES = ["submitted", "paid_pending_vetting", "call_scheduled"];
|
|
795
|
-
function canBookFrom(status) {
|
|
796
|
-
return BOOKABLE_STATUSES.includes(status);
|
|
797
|
-
}
|
|
798
924
|
function slotWindow(now, windowDays) {
|
|
799
925
|
return { from: now, to: now + windowDays * 864e5 };
|
|
800
926
|
}
|
|
@@ -841,7 +967,6 @@ function applicationBookingUpdate(currentStatus, startAt, htmlLink) {
|
|
|
841
967
|
};
|
|
842
968
|
}
|
|
843
969
|
export {
|
|
844
|
-
BOOKABLE_STATUSES,
|
|
845
970
|
SCHEDULING_DEFAULTS,
|
|
846
971
|
applicationBookingUpdate,
|
|
847
972
|
applicationSummary,
|
|
@@ -850,14 +975,16 @@ export {
|
|
|
850
975
|
buildGroupSeed,
|
|
851
976
|
canApprove,
|
|
852
977
|
canBook,
|
|
853
|
-
canBookFrom,
|
|
854
978
|
canChangeRole,
|
|
855
979
|
canTransition,
|
|
856
980
|
canceledPatch,
|
|
857
981
|
chapterDb,
|
|
982
|
+
clerkInviteRequest,
|
|
858
983
|
createChapterIntegration,
|
|
984
|
+
createClerkInvitation,
|
|
859
985
|
defaultCrm,
|
|
860
986
|
defineChapter,
|
|
987
|
+
emailGroupFrom,
|
|
861
988
|
endForSlot,
|
|
862
989
|
findApplicationRef,
|
|
863
990
|
firstPaymentPatch,
|
|
@@ -865,6 +992,7 @@ export {
|
|
|
865
992
|
introIdempotencyKey,
|
|
866
993
|
isAdminRole,
|
|
867
994
|
isAlreadySent,
|
|
995
|
+
isReconcilable,
|
|
868
996
|
isSlotAvailable,
|
|
869
997
|
joinConfig,
|
|
870
998
|
meetingCreateRow,
|
|
@@ -874,7 +1002,9 @@ export {
|
|
|
874
1002
|
normalizeWebhookEvent,
|
|
875
1003
|
paymentsReady,
|
|
876
1004
|
planDelivery,
|
|
1005
|
+
projectApplicant,
|
|
877
1006
|
projectSharedRecord,
|
|
1007
|
+
reconcileMeetings,
|
|
878
1008
|
refundedPatch,
|
|
879
1009
|
render,
|
|
880
1010
|
renderSummary,
|
|
@@ -885,6 +1015,7 @@ export {
|
|
|
885
1015
|
resolvePipeline,
|
|
886
1016
|
resolveScheduling,
|
|
887
1017
|
roleFromClaim,
|
|
1018
|
+
sendTemplated,
|
|
888
1019
|
sharedPersonInput,
|
|
889
1020
|
slotWindow,
|
|
890
1021
|
stageIndex,
|