@odla-ai/chapter 0.15.0 → 0.16.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 +31 -16
- package/dist/index.cjs +154 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +103 -6
- package/dist/index.d.ts +103 -6
- package/dist/index.js +154 -6
- package/dist/index.js.map +1 -1
- package/dist/worker/index.cjs +820 -23
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +48 -5
- package/dist/worker/index.d.ts +48 -5
- package/dist/worker/index.js +820 -23
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -242,18 +242,59 @@ interface ChapterConfig {
|
|
|
242
242
|
auth?: ChapterAuth;
|
|
243
243
|
/** odla services (db implied). Default `["db","calendar","o11y"]`. */
|
|
244
244
|
services?: readonly string[];
|
|
245
|
-
/** Apply-time account provisioning
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
*
|
|
245
|
+
/** Apply-time account provisioning. **Default `"none"`** — it provisions
|
|
246
|
+
* nothing, because the alternatives have an outbound side effect and a site
|
|
247
|
+
* that never made the choice must not be mailing people. `"create"` makes the
|
|
248
|
+
* Clerk account server-side (so join can say the account is ready);
|
|
249
|
+
* `"invite"` **emails the applicant a Clerk invitation**. Both non-default
|
|
250
|
+
* models need a `clerk_secret_key` vault secret to act. Opt in explicitly —
|
|
251
|
+
* leaving this unset provisions no accounts. */
|
|
249
252
|
account?: AccountModel;
|
|
250
253
|
/** WHEN lifecycle email fires. Addressing and content live on the group row
|
|
251
254
|
* (owner-editable at runtime); this is the trigger, which is a build-time
|
|
252
255
|
* decision. See {@link ChapterSends}. */
|
|
253
256
|
sends?: ChapterSends;
|
|
257
|
+
/** Site policy for admin operations (approve side effects, refund rules).
|
|
258
|
+
* See {@link ChapterOperations}. */
|
|
259
|
+
operations?: ChapterOperations;
|
|
254
260
|
}
|
|
255
261
|
/** Apply-time Clerk account provisioning model. */
|
|
256
262
|
type AccountModel = "invite" | "create" | "none";
|
|
263
|
+
/** Site policy for admin OPERATIONS: the DECISIONS, where the mechanics stay
|
|
264
|
+
* package-owned. Same model as {@link ChapterSends} — a site declares the rule,
|
|
265
|
+
* chapter enforces it. Distinct from {@link ChapterPolicy}, which is member-facing
|
|
266
|
+
* copy. (The privilege-escalation rules are NOT here: they are package-enforced
|
|
267
|
+
* in `canChangeRole`, gated on `auth.superAdmins`, so a site cannot weaken them.) */
|
|
268
|
+
interface ChapterOperations {
|
|
269
|
+
/** What approving an application does. */
|
|
270
|
+
onApprove?: {
|
|
271
|
+
/** Role to promote the applicant to in Clerk. Defaults to the ladder rung
|
|
272
|
+
* directly below admin (e.g. `"member"`); `false` promotes nobody. */
|
|
273
|
+
promoteTo?: string | false;
|
|
274
|
+
/** Group email template to send on approve. Default `"onboardingInvite"`;
|
|
275
|
+
* `false` sends nothing. */
|
|
276
|
+
send?: string | false;
|
|
277
|
+
};
|
|
278
|
+
/** Refund rules. */
|
|
279
|
+
refund?: {
|
|
280
|
+
/** Application statuses a refund may be issued from. Default: any status. */
|
|
281
|
+
allowedFrom?: readonly string[];
|
|
282
|
+
/** Also cancel the Stripe subscription. Default `true`. */
|
|
283
|
+
cancelSubscription?: boolean;
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
/** The fully-resolved {@link ChapterOperations} carried on the {@link Chapter}. */
|
|
287
|
+
interface ResolvedOperations {
|
|
288
|
+
onApprove: {
|
|
289
|
+
promoteTo: string | false;
|
|
290
|
+
send: string | false;
|
|
291
|
+
};
|
|
292
|
+
/** `allowedFrom: null` means "any status". */
|
|
293
|
+
refund: {
|
|
294
|
+
allowedFrom: readonly string[] | null;
|
|
295
|
+
cancelSubscription: boolean;
|
|
296
|
+
};
|
|
297
|
+
}
|
|
257
298
|
/** When the admin notification fires: on application `submit` (default), on the
|
|
258
299
|
* first successful `payment`, or `never` (the site drives it itself). */
|
|
259
300
|
type AdminNotificationTrigger = "submit" | "payment" | "never";
|
|
@@ -284,10 +325,12 @@ interface Chapter {
|
|
|
284
325
|
schema: DbSchema;
|
|
285
326
|
rules: DbRules;
|
|
286
327
|
services: readonly string[];
|
|
287
|
-
/** Resolved apply-time account provisioning model (default `"
|
|
328
|
+
/** Resolved apply-time account provisioning model (default `"none"`). */
|
|
288
329
|
account: AccountModel;
|
|
289
330
|
/** Resolved send policy — when each lifecycle email fires. */
|
|
290
331
|
sends: ResolvedSends;
|
|
332
|
+
/** Resolved admin-operation policy (approve side effects, refund rules). */
|
|
333
|
+
operations: ResolvedOperations;
|
|
291
334
|
/** The seed `groups` row derived from config (chapter mode), else `null`. */
|
|
292
335
|
groupSeed(): Record<string, unknown> | null;
|
|
293
336
|
}
|
|
@@ -781,6 +824,60 @@ declare function projectApplicant(deps: ProjectionDeps, applicant: Applicant): P
|
|
|
781
824
|
recordId: string;
|
|
782
825
|
}>;
|
|
783
826
|
|
|
827
|
+
/** The crm `person` input for one application row: the built-in identity/contact
|
|
828
|
+
* fields plus each configured `crmFields` value present on the row. */
|
|
829
|
+
declare function personInputFromApp(chapter: Chapter, app: Record<string, unknown>): Record<string, unknown>;
|
|
830
|
+
/** Upsert the person record for one application (or a synthetic `{ email,
|
|
831
|
+
* firstName }` account row) and mirror its stage, billing snapshot, and Clerk
|
|
832
|
+
* identity. `stage` is the `applications.status` to mirror — omit for
|
|
833
|
+
* account-only rows not in the pipeline. Throws on failure (the backfill route
|
|
834
|
+
* counts; the operational call sites wrap in `.catch`). */
|
|
835
|
+
declare function syncApplicationToCrm(deps: ProjectionDeps & {
|
|
836
|
+
chapter: Chapter;
|
|
837
|
+
}, opts: {
|
|
838
|
+
app: Record<string, unknown>;
|
|
839
|
+
stage?: string;
|
|
840
|
+
}): Promise<string | null>;
|
|
841
|
+
/** Backfill / repair: project every application (newest per person) and every
|
|
842
|
+
* account-only `$users` row into the CRM. Idempotent (safe to re-run). Dev
|
|
843
|
+
* volumes fit one 1000-row page. Returns `{ synced, errors }`. */
|
|
844
|
+
declare function backfillCrm(deps: ProjectionDeps & {
|
|
845
|
+
chapter: Chapter;
|
|
846
|
+
}): Promise<{
|
|
847
|
+
synced: number;
|
|
848
|
+
errors: Array<{
|
|
849
|
+
email: string;
|
|
850
|
+
error: string;
|
|
851
|
+
}>;
|
|
852
|
+
}>;
|
|
853
|
+
|
|
854
|
+
/** Bucket `{ t, v }` points into the last `weeks` weekly buckets ending at `now`
|
|
855
|
+
* (epoch ms), summing `v` per bucket. Returns oldest→newest `{ weekStart, value
|
|
856
|
+
* }`, so a caller renders a sparkline directly. Points outside the window are
|
|
857
|
+
* ignored. */
|
|
858
|
+
declare function bucketSeries(points: Array<{
|
|
859
|
+
t: number;
|
|
860
|
+
v: number;
|
|
861
|
+
}>, now: number, weeks?: number): Array<{
|
|
862
|
+
weekStart: number;
|
|
863
|
+
value: number;
|
|
864
|
+
}>;
|
|
865
|
+
/** Annualized cents for a Stripe subscription: sum each item's
|
|
866
|
+
* `unit_amount * quantity`, ×12 for monthly intervals. A yearly interval is
|
|
867
|
+
* taken as-is. Returns 0 for a shape with no priced items. */
|
|
868
|
+
declare function subAnnualCents(sub: Record<string, unknown>): number;
|
|
869
|
+
|
|
870
|
+
/** The result of a Stripe Backend API call: ok + status + parsed JSON body. */
|
|
871
|
+
type StripeResult = {
|
|
872
|
+
ok: boolean;
|
|
873
|
+
status: number;
|
|
874
|
+
body: Record<string, unknown>;
|
|
875
|
+
};
|
|
876
|
+
/** Call the Stripe Backend API (form-encoded, Bearer sk_). Exposed so the admin
|
|
877
|
+
* billing/dashboard reads and the refund route share one Stripe client instead
|
|
878
|
+
* of each re-deriving the auth + encoding. */
|
|
879
|
+
declare function stripeCall(sk: string, method: "GET" | "POST" | "DELETE", path: string, params?: Record<string, unknown>, idempotencyKey?: string): Promise<StripeResult>;
|
|
880
|
+
|
|
784
881
|
/** The outcome of a Clerk provisioning call. `existed` marks the heal case: Clerk
|
|
785
882
|
* rejected it because the account/invitation is already there, which is the end
|
|
786
883
|
* state we wanted anyway. */
|
|
@@ -1165,4 +1262,4 @@ type ApplicationBookingPatch = {
|
|
|
1165
1262
|
* already there (never backward). */
|
|
1166
1263
|
declare function applicationBookingUpdate(currentStatus: string, startAt: number, htmlLink?: string | null): ApplicationBookingPatch;
|
|
1167
1264
|
|
|
1168
|
-
export { type AccountModel, type AdminNotificationTrigger, 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 ChapterSends, type ClerkInviteInput, type ClerkResult, type ClerkUserInput, type ClerkUserRecord, type DbOp, type DbRules, type DbSchema, type DeliveryDecision, type EmailGroup, type EmailTemplate, type EmailTemplateRow, type Entity, type ExistingMeeting, type GuardResult, type IntegrationDescriptor, type IntegrationProvision, type IntegrationSecret, type IntegrationSetting, type IntegrationSync, 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 ResolvedSends, type RoleChangeContext, type Rule, SCHEDULING_DEFAULTS, type SchedulingErrors, type SecretMode, type SecretStore, type SessionUser, type SharedPerson, type StripeEvent, type SubmitResult, type WebhookEvent, applicantProfile, applicationBookingUpdate, applicationSummary, bookingDecision, brandTokens, buildGroupSeed, canApprove, canBook, canChangeRole, canTransition, canceledPatch, chapterDb, clampArray, clerkGetUser, clerkGetUserByEmail, clerkIntegration, clerkInviteRequest, clerkListUsers, clerkSetRole, clerkUserRequest, createChapterIntegration, createClerkInvitation, createClerkUser, defaultCrm, defineChapter, emailGroupFrom, endForSlot, findApplicationRef, firstPaymentPatch, getVaultSecret, hasDisclaimerAck, introIdempotencyKey, isAdminRole, isAlreadySent, isReconcilable, isSlotAvailable, isValidEmail, 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, validateScheduling, verifyStripeSignature, webhookMutationId };
|
|
1265
|
+
export { type AccountModel, type AdminNotificationTrigger, 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 ChapterOperations, type ChapterPipeline, type ChapterPolicy, type ChapterPrices, type ChapterScheduling, type ChapterSends, type ClerkInviteInput, type ClerkResult, type ClerkUserInput, type ClerkUserRecord, type DbOp, type DbRules, type DbSchema, type DeliveryDecision, type EmailGroup, type EmailTemplate, type EmailTemplateRow, type Entity, type ExistingMeeting, type GuardResult, type IntegrationDescriptor, type IntegrationProvision, type IntegrationSecret, type IntegrationSetting, type IntegrationSync, 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 ResolvedOperations, type ResolvedPipeline, type ResolvedScheduling, type ResolvedSends, type RoleChangeContext, type Rule, SCHEDULING_DEFAULTS, type SchedulingErrors, type SecretMode, type SecretStore, type SessionUser, type SharedPerson, type StripeEvent, type StripeResult, type SubmitResult, type WebhookEvent, applicantProfile, applicationBookingUpdate, applicationSummary, backfillCrm, bookingDecision, brandTokens, bucketSeries, buildGroupSeed, canApprove, canBook, canChangeRole, canTransition, canceledPatch, chapterDb, clampArray, clerkGetUser, clerkGetUserByEmail, clerkIntegration, clerkInviteRequest, clerkListUsers, clerkSetRole, clerkUserRequest, createChapterIntegration, createClerkInvitation, createClerkUser, defaultCrm, defineChapter, emailGroupFrom, endForSlot, findApplicationRef, firstPaymentPatch, getVaultSecret, hasDisclaimerAck, introIdempotencyKey, isAdminRole, isAlreadySent, isReconcilable, isSlotAvailable, isValidEmail, joinConfig, meetingCreateRow, meetingRescheduleUpdate, memberApplication, memberSession, normalizeWebhookEvent, paymentsReady, personInputFromApp, planDelivery, projectApplicant, projectSharedRecord, reconcileMeetings, refundedPatch, render, renderSummary, renderTemplateBody, renewalPatch, resolveApplication, resolveAuth, resolvePipeline, resolveScheduling, roleFromClaim, sendTemplated, sharedPersonInput, slotWindow, stageIndex, stripeCall, stripeForm, subAnnualCents, submitApplication, subscriptionIdempotencyKey, syncApplicationToCrm, validateScheduling, verifyStripeSignature, webhookMutationId };
|
package/dist/index.d.ts
CHANGED
|
@@ -242,18 +242,59 @@ interface ChapterConfig {
|
|
|
242
242
|
auth?: ChapterAuth;
|
|
243
243
|
/** odla services (db implied). Default `["db","calendar","o11y"]`. */
|
|
244
244
|
services?: readonly string[];
|
|
245
|
-
/** Apply-time account provisioning
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
*
|
|
245
|
+
/** Apply-time account provisioning. **Default `"none"`** — it provisions
|
|
246
|
+
* nothing, because the alternatives have an outbound side effect and a site
|
|
247
|
+
* that never made the choice must not be mailing people. `"create"` makes the
|
|
248
|
+
* Clerk account server-side (so join can say the account is ready);
|
|
249
|
+
* `"invite"` **emails the applicant a Clerk invitation**. Both non-default
|
|
250
|
+
* models need a `clerk_secret_key` vault secret to act. Opt in explicitly —
|
|
251
|
+
* leaving this unset provisions no accounts. */
|
|
249
252
|
account?: AccountModel;
|
|
250
253
|
/** WHEN lifecycle email fires. Addressing and content live on the group row
|
|
251
254
|
* (owner-editable at runtime); this is the trigger, which is a build-time
|
|
252
255
|
* decision. See {@link ChapterSends}. */
|
|
253
256
|
sends?: ChapterSends;
|
|
257
|
+
/** Site policy for admin operations (approve side effects, refund rules).
|
|
258
|
+
* See {@link ChapterOperations}. */
|
|
259
|
+
operations?: ChapterOperations;
|
|
254
260
|
}
|
|
255
261
|
/** Apply-time Clerk account provisioning model. */
|
|
256
262
|
type AccountModel = "invite" | "create" | "none";
|
|
263
|
+
/** Site policy for admin OPERATIONS: the DECISIONS, where the mechanics stay
|
|
264
|
+
* package-owned. Same model as {@link ChapterSends} — a site declares the rule,
|
|
265
|
+
* chapter enforces it. Distinct from {@link ChapterPolicy}, which is member-facing
|
|
266
|
+
* copy. (The privilege-escalation rules are NOT here: they are package-enforced
|
|
267
|
+
* in `canChangeRole`, gated on `auth.superAdmins`, so a site cannot weaken them.) */
|
|
268
|
+
interface ChapterOperations {
|
|
269
|
+
/** What approving an application does. */
|
|
270
|
+
onApprove?: {
|
|
271
|
+
/** Role to promote the applicant to in Clerk. Defaults to the ladder rung
|
|
272
|
+
* directly below admin (e.g. `"member"`); `false` promotes nobody. */
|
|
273
|
+
promoteTo?: string | false;
|
|
274
|
+
/** Group email template to send on approve. Default `"onboardingInvite"`;
|
|
275
|
+
* `false` sends nothing. */
|
|
276
|
+
send?: string | false;
|
|
277
|
+
};
|
|
278
|
+
/** Refund rules. */
|
|
279
|
+
refund?: {
|
|
280
|
+
/** Application statuses a refund may be issued from. Default: any status. */
|
|
281
|
+
allowedFrom?: readonly string[];
|
|
282
|
+
/** Also cancel the Stripe subscription. Default `true`. */
|
|
283
|
+
cancelSubscription?: boolean;
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
/** The fully-resolved {@link ChapterOperations} carried on the {@link Chapter}. */
|
|
287
|
+
interface ResolvedOperations {
|
|
288
|
+
onApprove: {
|
|
289
|
+
promoteTo: string | false;
|
|
290
|
+
send: string | false;
|
|
291
|
+
};
|
|
292
|
+
/** `allowedFrom: null` means "any status". */
|
|
293
|
+
refund: {
|
|
294
|
+
allowedFrom: readonly string[] | null;
|
|
295
|
+
cancelSubscription: boolean;
|
|
296
|
+
};
|
|
297
|
+
}
|
|
257
298
|
/** When the admin notification fires: on application `submit` (default), on the
|
|
258
299
|
* first successful `payment`, or `never` (the site drives it itself). */
|
|
259
300
|
type AdminNotificationTrigger = "submit" | "payment" | "never";
|
|
@@ -284,10 +325,12 @@ interface Chapter {
|
|
|
284
325
|
schema: DbSchema;
|
|
285
326
|
rules: DbRules;
|
|
286
327
|
services: readonly string[];
|
|
287
|
-
/** Resolved apply-time account provisioning model (default `"
|
|
328
|
+
/** Resolved apply-time account provisioning model (default `"none"`). */
|
|
288
329
|
account: AccountModel;
|
|
289
330
|
/** Resolved send policy — when each lifecycle email fires. */
|
|
290
331
|
sends: ResolvedSends;
|
|
332
|
+
/** Resolved admin-operation policy (approve side effects, refund rules). */
|
|
333
|
+
operations: ResolvedOperations;
|
|
291
334
|
/** The seed `groups` row derived from config (chapter mode), else `null`. */
|
|
292
335
|
groupSeed(): Record<string, unknown> | null;
|
|
293
336
|
}
|
|
@@ -781,6 +824,60 @@ declare function projectApplicant(deps: ProjectionDeps, applicant: Applicant): P
|
|
|
781
824
|
recordId: string;
|
|
782
825
|
}>;
|
|
783
826
|
|
|
827
|
+
/** The crm `person` input for one application row: the built-in identity/contact
|
|
828
|
+
* fields plus each configured `crmFields` value present on the row. */
|
|
829
|
+
declare function personInputFromApp(chapter: Chapter, app: Record<string, unknown>): Record<string, unknown>;
|
|
830
|
+
/** Upsert the person record for one application (or a synthetic `{ email,
|
|
831
|
+
* firstName }` account row) and mirror its stage, billing snapshot, and Clerk
|
|
832
|
+
* identity. `stage` is the `applications.status` to mirror — omit for
|
|
833
|
+
* account-only rows not in the pipeline. Throws on failure (the backfill route
|
|
834
|
+
* counts; the operational call sites wrap in `.catch`). */
|
|
835
|
+
declare function syncApplicationToCrm(deps: ProjectionDeps & {
|
|
836
|
+
chapter: Chapter;
|
|
837
|
+
}, opts: {
|
|
838
|
+
app: Record<string, unknown>;
|
|
839
|
+
stage?: string;
|
|
840
|
+
}): Promise<string | null>;
|
|
841
|
+
/** Backfill / repair: project every application (newest per person) and every
|
|
842
|
+
* account-only `$users` row into the CRM. Idempotent (safe to re-run). Dev
|
|
843
|
+
* volumes fit one 1000-row page. Returns `{ synced, errors }`. */
|
|
844
|
+
declare function backfillCrm(deps: ProjectionDeps & {
|
|
845
|
+
chapter: Chapter;
|
|
846
|
+
}): Promise<{
|
|
847
|
+
synced: number;
|
|
848
|
+
errors: Array<{
|
|
849
|
+
email: string;
|
|
850
|
+
error: string;
|
|
851
|
+
}>;
|
|
852
|
+
}>;
|
|
853
|
+
|
|
854
|
+
/** Bucket `{ t, v }` points into the last `weeks` weekly buckets ending at `now`
|
|
855
|
+
* (epoch ms), summing `v` per bucket. Returns oldest→newest `{ weekStart, value
|
|
856
|
+
* }`, so a caller renders a sparkline directly. Points outside the window are
|
|
857
|
+
* ignored. */
|
|
858
|
+
declare function bucketSeries(points: Array<{
|
|
859
|
+
t: number;
|
|
860
|
+
v: number;
|
|
861
|
+
}>, now: number, weeks?: number): Array<{
|
|
862
|
+
weekStart: number;
|
|
863
|
+
value: number;
|
|
864
|
+
}>;
|
|
865
|
+
/** Annualized cents for a Stripe subscription: sum each item's
|
|
866
|
+
* `unit_amount * quantity`, ×12 for monthly intervals. A yearly interval is
|
|
867
|
+
* taken as-is. Returns 0 for a shape with no priced items. */
|
|
868
|
+
declare function subAnnualCents(sub: Record<string, unknown>): number;
|
|
869
|
+
|
|
870
|
+
/** The result of a Stripe Backend API call: ok + status + parsed JSON body. */
|
|
871
|
+
type StripeResult = {
|
|
872
|
+
ok: boolean;
|
|
873
|
+
status: number;
|
|
874
|
+
body: Record<string, unknown>;
|
|
875
|
+
};
|
|
876
|
+
/** Call the Stripe Backend API (form-encoded, Bearer sk_). Exposed so the admin
|
|
877
|
+
* billing/dashboard reads and the refund route share one Stripe client instead
|
|
878
|
+
* of each re-deriving the auth + encoding. */
|
|
879
|
+
declare function stripeCall(sk: string, method: "GET" | "POST" | "DELETE", path: string, params?: Record<string, unknown>, idempotencyKey?: string): Promise<StripeResult>;
|
|
880
|
+
|
|
784
881
|
/** The outcome of a Clerk provisioning call. `existed` marks the heal case: Clerk
|
|
785
882
|
* rejected it because the account/invitation is already there, which is the end
|
|
786
883
|
* state we wanted anyway. */
|
|
@@ -1165,4 +1262,4 @@ type ApplicationBookingPatch = {
|
|
|
1165
1262
|
* already there (never backward). */
|
|
1166
1263
|
declare function applicationBookingUpdate(currentStatus: string, startAt: number, htmlLink?: string | null): ApplicationBookingPatch;
|
|
1167
1264
|
|
|
1168
|
-
export { type AccountModel, type AdminNotificationTrigger, 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 ChapterSends, type ClerkInviteInput, type ClerkResult, type ClerkUserInput, type ClerkUserRecord, type DbOp, type DbRules, type DbSchema, type DeliveryDecision, type EmailGroup, type EmailTemplate, type EmailTemplateRow, type Entity, type ExistingMeeting, type GuardResult, type IntegrationDescriptor, type IntegrationProvision, type IntegrationSecret, type IntegrationSetting, type IntegrationSync, 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 ResolvedSends, type RoleChangeContext, type Rule, SCHEDULING_DEFAULTS, type SchedulingErrors, type SecretMode, type SecretStore, type SessionUser, type SharedPerson, type StripeEvent, type SubmitResult, type WebhookEvent, applicantProfile, applicationBookingUpdate, applicationSummary, bookingDecision, brandTokens, buildGroupSeed, canApprove, canBook, canChangeRole, canTransition, canceledPatch, chapterDb, clampArray, clerkGetUser, clerkGetUserByEmail, clerkIntegration, clerkInviteRequest, clerkListUsers, clerkSetRole, clerkUserRequest, createChapterIntegration, createClerkInvitation, createClerkUser, defaultCrm, defineChapter, emailGroupFrom, endForSlot, findApplicationRef, firstPaymentPatch, getVaultSecret, hasDisclaimerAck, introIdempotencyKey, isAdminRole, isAlreadySent, isReconcilable, isSlotAvailable, isValidEmail, 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, validateScheduling, verifyStripeSignature, webhookMutationId };
|
|
1265
|
+
export { type AccountModel, type AdminNotificationTrigger, 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 ChapterOperations, type ChapterPipeline, type ChapterPolicy, type ChapterPrices, type ChapterScheduling, type ChapterSends, type ClerkInviteInput, type ClerkResult, type ClerkUserInput, type ClerkUserRecord, type DbOp, type DbRules, type DbSchema, type DeliveryDecision, type EmailGroup, type EmailTemplate, type EmailTemplateRow, type Entity, type ExistingMeeting, type GuardResult, type IntegrationDescriptor, type IntegrationProvision, type IntegrationSecret, type IntegrationSetting, type IntegrationSync, 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 ResolvedOperations, type ResolvedPipeline, type ResolvedScheduling, type ResolvedSends, type RoleChangeContext, type Rule, SCHEDULING_DEFAULTS, type SchedulingErrors, type SecretMode, type SecretStore, type SessionUser, type SharedPerson, type StripeEvent, type StripeResult, type SubmitResult, type WebhookEvent, applicantProfile, applicationBookingUpdate, applicationSummary, backfillCrm, bookingDecision, brandTokens, bucketSeries, buildGroupSeed, canApprove, canBook, canChangeRole, canTransition, canceledPatch, chapterDb, clampArray, clerkGetUser, clerkGetUserByEmail, clerkIntegration, clerkInviteRequest, clerkListUsers, clerkSetRole, clerkUserRequest, createChapterIntegration, createClerkInvitation, createClerkUser, defaultCrm, defineChapter, emailGroupFrom, endForSlot, findApplicationRef, firstPaymentPatch, getVaultSecret, hasDisclaimerAck, introIdempotencyKey, isAdminRole, isAlreadySent, isReconcilable, isSlotAvailable, isValidEmail, joinConfig, meetingCreateRow, meetingRescheduleUpdate, memberApplication, memberSession, normalizeWebhookEvent, paymentsReady, personInputFromApp, planDelivery, projectApplicant, projectSharedRecord, reconcileMeetings, refundedPatch, render, renderSummary, renderTemplateBody, renewalPatch, resolveApplication, resolveAuth, resolvePipeline, resolveScheduling, roleFromClaim, sendTemplated, sharedPersonInput, slotWindow, stageIndex, stripeCall, stripeForm, subAnnualCents, submitApplication, subscriptionIdempotencyKey, syncApplicationToCrm, validateScheduling, verifyStripeSignature, webhookMutationId };
|
package/dist/index.js
CHANGED
|
@@ -537,6 +537,27 @@ function defineChapter(config) {
|
|
|
537
537
|
);
|
|
538
538
|
}
|
|
539
539
|
const sends = { adminNotification };
|
|
540
|
+
const onApproveCfg = config.operations?.onApprove ?? {};
|
|
541
|
+
const belowAdmin = auth.ladder[auth.ladder.length - 2] ?? auth.adminRole;
|
|
542
|
+
const promoteTo = onApproveCfg.promoteTo === void 0 ? belowAdmin : onApproveCfg.promoteTo;
|
|
543
|
+
if (promoteTo !== false && !auth.ladder.includes(promoteTo)) {
|
|
544
|
+
throw new Error(
|
|
545
|
+
`defineChapter.operations.onApprove.promoteTo: "${String(promoteTo)}" is not in the auth ladder (${auth.ladder.join(", ")})`
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
const refundCfg = config.operations?.refund ?? {};
|
|
549
|
+
if (refundCfg.allowedFrom !== void 0) {
|
|
550
|
+
const unknown = refundCfg.allowedFrom.filter((s) => !pipeline.stages.includes(s));
|
|
551
|
+
if (unknown.length > 0) {
|
|
552
|
+
throw new Error(
|
|
553
|
+
`defineChapter.operations.refund.allowedFrom: ${unknown.map((s) => `"${s}"`).join(", ")} not in the pipeline (${pipeline.stages.join(", ")})`
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
const operations = {
|
|
558
|
+
onApprove: { promoteTo, send: onApproveCfg.send === void 0 ? "onboardingInvite" : onApproveCfg.send },
|
|
559
|
+
refund: { allowedFrom: refundCfg.allowedFrom ?? null, cancelSubscription: refundCfg.cancelSubscription ?? true }
|
|
560
|
+
};
|
|
540
561
|
const chapter = {
|
|
541
562
|
config,
|
|
542
563
|
id: id2,
|
|
@@ -551,6 +572,7 @@ function defineChapter(config) {
|
|
|
551
572
|
services,
|
|
552
573
|
account,
|
|
553
574
|
sends,
|
|
575
|
+
operations,
|
|
554
576
|
groupSeed: () => mode === "chapter" ? buildGroupSeed(config) : null
|
|
555
577
|
};
|
|
556
578
|
if (config.url !== void 0) chapter.url = config.url;
|
|
@@ -677,16 +699,16 @@ async function sendTemplated(deps, input) {
|
|
|
677
699
|
return error ? { sent: false, reason: error } : { sent: true };
|
|
678
700
|
}
|
|
679
701
|
function emailGroupFrom(row) {
|
|
680
|
-
const
|
|
702
|
+
const str2 = (v) => typeof v === "string" ? v : void 0;
|
|
681
703
|
const templates = row.emailTemplates && typeof row.emailTemplates === "object" ? row.emailTemplates : {};
|
|
682
704
|
return {
|
|
683
705
|
id: String(row.id),
|
|
684
706
|
name: String(row.name ?? ""),
|
|
685
|
-
replyTo:
|
|
686
|
-
debugEmail:
|
|
687
|
-
refundPolicyText:
|
|
688
|
-
commitmentText:
|
|
689
|
-
normsText:
|
|
707
|
+
replyTo: str2(row.replyTo) ?? "",
|
|
708
|
+
debugEmail: str2(row.debugEmail),
|
|
709
|
+
refundPolicyText: str2(row.refundPolicyText),
|
|
710
|
+
commitmentText: str2(row.commitmentText),
|
|
711
|
+
normsText: str2(row.normsText),
|
|
690
712
|
emailTemplates: templates
|
|
691
713
|
};
|
|
692
714
|
}
|
|
@@ -834,6 +856,126 @@ async function projectApplicant(deps, applicant) {
|
|
|
834
856
|
}
|
|
835
857
|
}
|
|
836
858
|
|
|
859
|
+
// src/crm-sync.ts
|
|
860
|
+
import { createRecord as createRecord2, updateRecord as updateRecord2, setStage, linkIdentity } from "@odla-ai/crm";
|
|
861
|
+
var str = (v) => typeof v === "string" ? v : v == null ? "" : String(v);
|
|
862
|
+
function personInputFromApp(chapter, app) {
|
|
863
|
+
const input = sharedPersonInput({
|
|
864
|
+
email: str(app.email),
|
|
865
|
+
firstName: str(app.firstName) || void 0,
|
|
866
|
+
lastName: str(app.lastName) || void 0,
|
|
867
|
+
phone: str(app.phone) || void 0,
|
|
868
|
+
linkedin: str(app.linkedin) || void 0,
|
|
869
|
+
hubRecordId: str(app.id)
|
|
870
|
+
});
|
|
871
|
+
for (const f of chapter.application.crmFields) {
|
|
872
|
+
if (app[f] !== void 0) input[f] = app[f];
|
|
873
|
+
}
|
|
874
|
+
if (app.id !== void 0) input.applicationId = str(app.id);
|
|
875
|
+
return input;
|
|
876
|
+
}
|
|
877
|
+
function billingColumns(app) {
|
|
878
|
+
const status = str(app.status);
|
|
879
|
+
const paid = Boolean(app.stripeSubscriptionId) && status !== "refunded";
|
|
880
|
+
const billingStatus = status === "refunded" ? "refunded" : app.canceled === true ? "canceled" : paid ? "active" : "none";
|
|
881
|
+
const cols = { billingStatus };
|
|
882
|
+
if (app.stripeCustomerId) cols.stripeCustomerId = str(app.stripeCustomerId);
|
|
883
|
+
if (app.stripeSubscriptionId) cols.subscriptionId = str(app.stripeSubscriptionId);
|
|
884
|
+
if (typeof app.renewalAt === "number") cols.renewalAt = app.renewalAt;
|
|
885
|
+
return cols;
|
|
886
|
+
}
|
|
887
|
+
async function syncApplicationToCrm(deps, opts) {
|
|
888
|
+
const emailKey = str(opts.app.email).toLowerCase();
|
|
889
|
+
if (!emailKey) return null;
|
|
890
|
+
const crmDeps = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
|
|
891
|
+
const input = personInputFromApp(deps.chapter, opts.app);
|
|
892
|
+
const { crm_record } = await deps.db.query({
|
|
893
|
+
crm_record: { $: { where: { type: "person", primaryEmail: emailKey }, limit: 1 } }
|
|
894
|
+
});
|
|
895
|
+
const existing = crm_record?.[0] ?? null;
|
|
896
|
+
const stage = opts.stage || void 0;
|
|
897
|
+
let recordId;
|
|
898
|
+
if (existing && typeof existing.id === "string") {
|
|
899
|
+
recordId = existing.id;
|
|
900
|
+
await updateRecord2(crmDeps, { id: recordId, input });
|
|
901
|
+
} else {
|
|
902
|
+
const created = await createRecord2(crmDeps, { type: "person", input, ...stage ? { stage } : {} });
|
|
903
|
+
recordId = created.id;
|
|
904
|
+
}
|
|
905
|
+
if (existing && stage && existing.stage !== stage) {
|
|
906
|
+
await setStage(crmDeps, { id: recordId, to: stage, authorId: "system", mutationId: `crm:stage:${recordId}:${stage}` }).catch(() => void 0);
|
|
907
|
+
}
|
|
908
|
+
await deps.db.transact([{ t: "update", ns: "crm_record", id: recordId, attrs: billingColumns(opts.app) }]);
|
|
909
|
+
await linkIdentity(crmDeps, { recordId, email: emailKey, mutationId: `crm:link:${recordId}:${emailKey}` }).catch(() => void 0);
|
|
910
|
+
return recordId;
|
|
911
|
+
}
|
|
912
|
+
async function backfillCrm(deps) {
|
|
913
|
+
const [appsRes, usersRes] = await Promise.all([
|
|
914
|
+
deps.db.query({ applications: { $: { order: { createdAt: "desc" }, limit: 1e3 } } }),
|
|
915
|
+
deps.db.query({ $users: { $: { limit: 1e3 } } })
|
|
916
|
+
]);
|
|
917
|
+
const seen = /* @__PURE__ */ new Set();
|
|
918
|
+
let synced = 0;
|
|
919
|
+
const errors = [];
|
|
920
|
+
const run = async (app, stage) => {
|
|
921
|
+
const key = str(app.email).toLowerCase();
|
|
922
|
+
if (!key || seen.has(key)) return;
|
|
923
|
+
seen.add(key);
|
|
924
|
+
try {
|
|
925
|
+
await syncApplicationToCrm(deps, { app, stage });
|
|
926
|
+
synced += 1;
|
|
927
|
+
} catch (err) {
|
|
928
|
+
errors.push({ email: key, error: err instanceof Error ? err.message : String(err) });
|
|
929
|
+
}
|
|
930
|
+
};
|
|
931
|
+
for (const a of appsRes.applications ?? []) await run(a, str(a.status));
|
|
932
|
+
for (const u of usersRes.$users ?? []) {
|
|
933
|
+
if (u.deleted === true) continue;
|
|
934
|
+
await run({ email: u.email, firstName: str(u.name) });
|
|
935
|
+
}
|
|
936
|
+
return { synced, errors };
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// src/series.ts
|
|
940
|
+
function bucketSeries(points, now, weeks = 12) {
|
|
941
|
+
const WEEK = 7 * 864e5;
|
|
942
|
+
const end = now;
|
|
943
|
+
const start = end - weeks * WEEK;
|
|
944
|
+
const buckets = Array.from({ length: weeks }, (_, i) => ({ weekStart: start + i * WEEK, value: 0 }));
|
|
945
|
+
for (const p of points) {
|
|
946
|
+
if (!Number.isFinite(p.t) || p.t < start || p.t > end) continue;
|
|
947
|
+
const idx = Math.min(weeks - 1, Math.floor((p.t - start) / WEEK));
|
|
948
|
+
const bucket = buckets[idx];
|
|
949
|
+
if (bucket) bucket.value += Number.isFinite(p.v) ? p.v : 0;
|
|
950
|
+
}
|
|
951
|
+
return buckets;
|
|
952
|
+
}
|
|
953
|
+
function subAnnualCents(sub) {
|
|
954
|
+
const items = sub.items?.data ?? [];
|
|
955
|
+
let cents = 0;
|
|
956
|
+
for (const it of items) {
|
|
957
|
+
const price = it.price ?? {};
|
|
958
|
+
const per = (price.unit_amount ?? 0) * (it.quantity ?? 1);
|
|
959
|
+
cents += price.recurring?.interval === "month" ? per * 12 : per;
|
|
960
|
+
}
|
|
961
|
+
return cents;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// src/payments-stripe.ts
|
|
965
|
+
async function stripeCall(sk, method, path, params, idempotencyKey) {
|
|
966
|
+
const qs = method === "GET" && params ? `?${stripeForm(params)}` : "";
|
|
967
|
+
const headers = { authorization: `Bearer ${sk}` };
|
|
968
|
+
if (idempotencyKey) headers["idempotency-key"] = idempotencyKey;
|
|
969
|
+
const init = { method, headers };
|
|
970
|
+
if (method === "POST" && params) {
|
|
971
|
+
headers["content-type"] = "application/x-www-form-urlencoded";
|
|
972
|
+
init.body = stripeForm(params);
|
|
973
|
+
}
|
|
974
|
+
const res = await fetch(`https://api.stripe.com${path}${qs}`, init);
|
|
975
|
+
const body = await res.json().catch(() => ({}));
|
|
976
|
+
return { ok: res.ok, status: res.status, body };
|
|
977
|
+
}
|
|
978
|
+
|
|
837
979
|
// src/clerk.ts
|
|
838
980
|
var heal = (status) => status === 422 ? { ok: true, status, existed: true } : { ok: false, status };
|
|
839
981
|
function clerkInviteRequest(input) {
|
|
@@ -1216,8 +1358,10 @@ export {
|
|
|
1216
1358
|
applicantProfile,
|
|
1217
1359
|
applicationBookingUpdate,
|
|
1218
1360
|
applicationSummary,
|
|
1361
|
+
backfillCrm,
|
|
1219
1362
|
bookingDecision,
|
|
1220
1363
|
brandTokens,
|
|
1364
|
+
bucketSeries,
|
|
1221
1365
|
buildGroupSeed,
|
|
1222
1366
|
canApprove,
|
|
1223
1367
|
canBook,
|
|
@@ -1257,6 +1401,7 @@ export {
|
|
|
1257
1401
|
memberSession,
|
|
1258
1402
|
normalizeWebhookEvent,
|
|
1259
1403
|
paymentsReady,
|
|
1404
|
+
personInputFromApp,
|
|
1260
1405
|
planDelivery,
|
|
1261
1406
|
projectApplicant,
|
|
1262
1407
|
projectSharedRecord,
|
|
@@ -1275,9 +1420,12 @@ export {
|
|
|
1275
1420
|
sharedPersonInput,
|
|
1276
1421
|
slotWindow,
|
|
1277
1422
|
stageIndex,
|
|
1423
|
+
stripeCall,
|
|
1278
1424
|
stripeForm,
|
|
1425
|
+
subAnnualCents,
|
|
1279
1426
|
submitApplication,
|
|
1280
1427
|
subscriptionIdempotencyKey,
|
|
1428
|
+
syncApplicationToCrm,
|
|
1281
1429
|
validateScheduling,
|
|
1282
1430
|
verifyStripeSignature,
|
|
1283
1431
|
webhookMutationId
|