@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.js CHANGED
@@ -427,7 +427,7 @@ async function submitApplication(db, chapter, fields, opts) {
427
427
  );
428
428
  return { ok: true, id: id2, duplicate, status: chapter.pipeline.initial };
429
429
  }
430
- function joinConfig(group, paymentsReady) {
430
+ function joinConfig(group, paymentsReady2) {
431
431
  return {
432
432
  id: group.id,
433
433
  name: group.name,
@@ -438,7 +438,7 @@ function joinConfig(group, paymentsReady) {
438
438
  trustCopy: group.trustCopy ?? "",
439
439
  commitmentText: group.commitmentText ?? "",
440
440
  normsText: group.normsText ?? "",
441
- paymentsReady
441
+ paymentsReady: paymentsReady2
442
442
  };
443
443
  }
444
444
 
@@ -587,6 +587,70 @@ async function verifyStripeSignature(payload, header, secret, opts = {}) {
587
587
  const mac = await crypto.subtle.sign("HMAC", key, enc.encode(`${t}.${payload}`));
588
588
  return timingSafeEqual(toHex(mac), v1);
589
589
  }
590
+ function paymentsReady(group, hasSecretKey) {
591
+ return Boolean(group.stripePublishableKey && group.stripePriceId && hasSecretKey);
592
+ }
593
+ function stripeForm(params) {
594
+ const out = new URLSearchParams();
595
+ for (const [k, v] of Object.entries(params)) {
596
+ if (v === void 0 || v === null) continue;
597
+ if (typeof v === "object") {
598
+ for (const [k2, v2] of Object.entries(v)) {
599
+ if (v2 !== void 0 && v2 !== null) out.append(`${k}[${k2}]`, String(v2));
600
+ }
601
+ } else {
602
+ out.append(k, String(v));
603
+ }
604
+ }
605
+ return out.toString();
606
+ }
607
+ function subscriptionIdempotencyKey(applicationId) {
608
+ return `sub:${applicationId}`;
609
+ }
610
+ function webhookMutationId(eventId) {
611
+ return `stripe:${eventId}`;
612
+ }
613
+ function findApplicationRef(obj) {
614
+ const metaOf = (v) => v && typeof v === "object" ? v.metadata ?? {} : {};
615
+ const pick = (m) => typeof m.applicationId === "string" ? m.applicationId : void 0;
616
+ const applicationId = pick(metaOf(obj)) ?? pick(metaOf(obj.subscription_details)) ?? pick(metaOf(obj.parent?.subscription_details));
617
+ const customerId = typeof obj.customer === "string" ? obj.customer : void 0;
618
+ return { ...applicationId ? { applicationId } : {}, ...customerId ? { customerId } : {} };
619
+ }
620
+ function normalizeWebhookEvent(event) {
621
+ const obj = event.data?.object ?? {};
622
+ const ref = findApplicationRef(obj);
623
+ switch (event.type) {
624
+ case "invoice.paid": {
625
+ const lines = obj.lines?.data ?? [];
626
+ const periodEnd = lines[0]?.period?.end;
627
+ const renewalAt = typeof periodEnd === "number" ? periodEnd * 1e3 : void 0;
628
+ const kind = obj.billing_reason === "subscription_create" ? "first_payment" : "renewal";
629
+ return { kind, ...ref, ...renewalAt !== void 0 ? { renewalAt } : {} };
630
+ }
631
+ case "charge.refunded":
632
+ return { kind: "refunded", ...ref };
633
+ case "customer.subscription.deleted":
634
+ return { kind: "canceled", ...ref };
635
+ default:
636
+ return { kind: "ignored", type: event.type };
637
+ }
638
+ }
639
+ function firstPaymentPatch(currentStatus, renewalAt) {
640
+ return {
641
+ ...currentStatus === "submitted" ? { status: "paid_pending_vetting" } : {},
642
+ ...renewalAt !== void 0 ? { renewalAt } : {}
643
+ };
644
+ }
645
+ function renewalPatch(renewalAt) {
646
+ return { renewalAt };
647
+ }
648
+ function refundedPatch() {
649
+ return { status: "refunded" };
650
+ }
651
+ function canceledPatch() {
652
+ return { canceled: true };
653
+ }
590
654
 
591
655
  // src/network.ts
592
656
  import { createRecord, updateRecord } from "@odla-ai/crm";
@@ -616,31 +680,218 @@ async function projectSharedRecord(deps, person) {
616
680
  const created = await createRecord(crmDeps, { type: "person", input, mutationId: `share:${person.hubRecordId}` });
617
681
  return { recordId: created.id };
618
682
  }
683
+
684
+ // src/session.ts
685
+ function applicationSummary(app) {
686
+ return {
687
+ id: app.id,
688
+ firstName: app.firstName ?? null,
689
+ lastName: app.lastName ?? null,
690
+ email: app.email ?? null,
691
+ status: app.status,
692
+ createdAt: app.createdAt ?? null,
693
+ meetingLink: app.meetingLink ?? null,
694
+ paid: Boolean(app.stripeSubscriptionId) && app.status !== "refunded",
695
+ renewalAt: app.renewalAt ?? null,
696
+ canceled: app.canceled === true
697
+ };
698
+ }
699
+ function memberApplication(app, meeting, defaultTimezone) {
700
+ const summary = applicationSummary(app);
701
+ let meetingAt = app.meetingAt ?? null;
702
+ let meetUrl = null;
703
+ let timezone = defaultTimezone;
704
+ if (meeting) {
705
+ timezone = meeting.timezone ?? timezone;
706
+ if (meeting.status === "scheduled") {
707
+ meetingAt = meeting.startAt ?? null;
708
+ meetUrl = meeting.meetUrl ?? null;
709
+ } else {
710
+ meetingAt = null;
711
+ }
712
+ }
713
+ return { ...summary, meetingAt, meetUrl, timezone };
714
+ }
715
+ function memberSession(user, opts) {
716
+ return {
717
+ userId: user.userId,
718
+ email: user.email ?? null,
719
+ role: user.role,
720
+ superAdmin: opts.superAdmin,
721
+ application: opts.application
722
+ };
723
+ }
724
+
725
+ // src/brand.ts
726
+ function paletteVar(key) {
727
+ return key.startsWith("--") ? key : `--${key}`;
728
+ }
729
+ function cleanValue(value) {
730
+ return value.replace(/[<>{};]/g, "").trim();
731
+ }
732
+ function brandTokens(brand) {
733
+ if (!brand) return "";
734
+ const decls = [];
735
+ for (const [key, value] of Object.entries(brand.palette ?? {})) {
736
+ if (typeof value === "string" && value.trim()) decls.push(`${paletteVar(key)}: ${cleanValue(value)};`);
737
+ }
738
+ const fonts = brand.fonts;
739
+ if (fonts?.display) decls.push(`--ui-font-display: ${cleanValue(fonts.display)};`);
740
+ if (fonts?.body) decls.push(`--ui-font-sans: ${cleanValue(fonts.body)};`);
741
+ if (fonts?.numeral) decls.push(`--ui-font-numeral: ${cleanValue(fonts.numeral)};`);
742
+ return decls.length ? `:root {
743
+ ${decls.join("\n ")}
744
+ }
745
+ ` : "";
746
+ }
747
+
748
+ // src/scheduling.ts
749
+ var SCHEDULING_DEFAULTS = {
750
+ slotMinutes: 45,
751
+ days: [1, 2, 3, 4, 5],
752
+ startHour: 9,
753
+ endHour: 17,
754
+ timezone: "America/Los_Angeles",
755
+ minNoticeHours: 24,
756
+ windowDays: 14,
757
+ summaryTemplate: "Introduction call with {{firstName}} {{lastName}}"
758
+ };
759
+ function isValidTimeZone(tz) {
760
+ try {
761
+ new Intl.DateTimeFormat(void 0, { timeZone: tz });
762
+ return true;
763
+ } catch {
764
+ return false;
765
+ }
766
+ }
767
+ function resolveScheduling(config) {
768
+ const d = config ?? {};
769
+ const c = {
770
+ slotMinutes: d.slotMinutes ?? SCHEDULING_DEFAULTS.slotMinutes,
771
+ days: d.days ?? SCHEDULING_DEFAULTS.days,
772
+ startHour: d.startHour ?? SCHEDULING_DEFAULTS.startHour,
773
+ endHour: d.endHour ?? SCHEDULING_DEFAULTS.endHour,
774
+ timezone: d.timezone ?? SCHEDULING_DEFAULTS.timezone,
775
+ minNoticeHours: d.minNoticeHours ?? SCHEDULING_DEFAULTS.minNoticeHours,
776
+ windowDays: d.windowDays ?? SCHEDULING_DEFAULTS.windowDays,
777
+ summaryTemplate: d.summaryTemplate ?? SCHEDULING_DEFAULTS.summaryTemplate
778
+ };
779
+ const fail = (msg) => {
780
+ throw new Error(`scheduling: ${msg}`);
781
+ };
782
+ if (!(c.slotMinutes >= 15 && c.slotMinutes <= 240)) fail("slotMinutes must be 15\u2013240");
783
+ if (!(c.windowDays >= 1 && c.windowDays <= 62)) fail("windowDays must be 1\u201362 (FreeBusy caps at 62)");
784
+ if (!(c.minNoticeHours >= 0 && c.minNoticeHours <= 336)) fail("minNoticeHours must be 0\u2013336");
785
+ if (!(c.startHour >= 0 && c.startHour < c.endHour && c.endHour <= 24)) fail("require 0 \u2264 startHour < endHour \u2264 24");
786
+ const days = [...c.days];
787
+ if (!days.length || !days.every((n) => Number.isInteger(n) && n >= 0 && n <= 6)) {
788
+ fail("days must be a non-empty list of weekday integers 0\u20136");
789
+ }
790
+ if (typeof c.timezone !== "string" || !isValidTimeZone(c.timezone)) fail(`invalid IANA timezone "${c.timezone}"`);
791
+ if (typeof c.summaryTemplate !== "string") fail("summaryTemplate must be a string");
792
+ return { ...c, days };
793
+ }
794
+ var BOOKABLE_STATUSES = ["submitted", "paid_pending_vetting", "call_scheduled"];
795
+ function canBookFrom(status) {
796
+ return BOOKABLE_STATUSES.includes(status);
797
+ }
798
+ function slotWindow(now, windowDays) {
799
+ return { from: now, to: now + windowDays * 864e5 };
800
+ }
801
+ function endForSlot(startAt, slotMinutes) {
802
+ return startAt + slotMinutes * 6e4;
803
+ }
804
+ function isSlotAvailable(slots, startAt) {
805
+ return slots.some((s) => s.startAt === startAt);
806
+ }
807
+ function renderSummary(template, app) {
808
+ return template.replace("{{firstName}}", app.firstName ?? "").replace("{{lastName}}", app.lastName ?? "");
809
+ }
810
+ function bookingDecision(existing) {
811
+ const eventId = existing?.googleEventId ?? null;
812
+ return { reschedule: Boolean(eventId), eventId };
813
+ }
814
+ function introIdempotencyKey(applicationId) {
815
+ return `application:${applicationId}:intro`;
816
+ }
817
+ function meetingCreateRow(i) {
818
+ return {
819
+ id: i.meetingId,
820
+ applicationId: i.applicationId,
821
+ groupId: i.groupId,
822
+ startAt: i.startAt,
823
+ endAt: i.endAt,
824
+ timezone: i.timezone,
825
+ status: "scheduled",
826
+ googleEventId: i.googleEventId,
827
+ ...i.meetUrl ? { meetUrl: i.meetUrl } : {},
828
+ ...i.htmlLink ? { htmlLink: i.htmlLink } : {},
829
+ drift: "none",
830
+ createdAt: i.createdAt
831
+ };
832
+ }
833
+ function meetingRescheduleUpdate(startAt, endAt) {
834
+ return { startAt, endAt, drift: "none" };
835
+ }
836
+ function applicationBookingUpdate(currentStatus, startAt, htmlLink) {
837
+ return {
838
+ meetingAt: startAt,
839
+ ...htmlLink ? { meetingLink: htmlLink } : {},
840
+ ...currentStatus !== "call_scheduled" ? { status: "call_scheduled" } : {}
841
+ };
842
+ }
619
843
  export {
844
+ BOOKABLE_STATUSES,
845
+ SCHEDULING_DEFAULTS,
846
+ applicationBookingUpdate,
847
+ applicationSummary,
848
+ bookingDecision,
849
+ brandTokens,
620
850
  buildGroupSeed,
621
851
  canApprove,
622
852
  canBook,
853
+ canBookFrom,
623
854
  canChangeRole,
624
855
  canTransition,
856
+ canceledPatch,
625
857
  chapterDb,
626
858
  createChapterIntegration,
627
859
  defaultCrm,
628
860
  defineChapter,
861
+ endForSlot,
862
+ findApplicationRef,
863
+ firstPaymentPatch,
629
864
  getVaultSecret,
865
+ introIdempotencyKey,
630
866
  isAdminRole,
631
867
  isAlreadySent,
868
+ isSlotAvailable,
632
869
  joinConfig,
870
+ meetingCreateRow,
871
+ meetingRescheduleUpdate,
872
+ memberApplication,
873
+ memberSession,
874
+ normalizeWebhookEvent,
875
+ paymentsReady,
633
876
  planDelivery,
634
877
  projectSharedRecord,
878
+ refundedPatch,
635
879
  render,
880
+ renderSummary,
636
881
  renderTemplateBody,
882
+ renewalPatch,
637
883
  resolveApplication,
638
884
  resolveAuth,
639
885
  resolvePipeline,
886
+ resolveScheduling,
640
887
  roleFromClaim,
641
888
  sharedPersonInput,
889
+ slotWindow,
642
890
  stageIndex,
891
+ stripeForm,
643
892
  submitApplication,
644
- verifyStripeSignature
893
+ subscriptionIdempotencyKey,
894
+ verifyStripeSignature,
895
+ webhookMutationId
645
896
  };
646
897
  //# sourceMappingURL=index.js.map