@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.cjs CHANGED
@@ -20,31 +20,58 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ BOOKABLE_STATUSES: () => BOOKABLE_STATUSES,
24
+ SCHEDULING_DEFAULTS: () => SCHEDULING_DEFAULTS,
25
+ applicationBookingUpdate: () => applicationBookingUpdate,
26
+ applicationSummary: () => applicationSummary,
27
+ bookingDecision: () => bookingDecision,
28
+ brandTokens: () => brandTokens,
23
29
  buildGroupSeed: () => buildGroupSeed,
24
30
  canApprove: () => canApprove,
25
31
  canBook: () => canBook,
32
+ canBookFrom: () => canBookFrom,
26
33
  canChangeRole: () => canChangeRole,
27
34
  canTransition: () => canTransition,
35
+ canceledPatch: () => canceledPatch,
28
36
  chapterDb: () => chapterDb,
29
37
  createChapterIntegration: () => createChapterIntegration,
30
38
  defaultCrm: () => defaultCrm,
31
39
  defineChapter: () => defineChapter,
40
+ endForSlot: () => endForSlot,
41
+ findApplicationRef: () => findApplicationRef,
42
+ firstPaymentPatch: () => firstPaymentPatch,
32
43
  getVaultSecret: () => getVaultSecret,
44
+ introIdempotencyKey: () => introIdempotencyKey,
33
45
  isAdminRole: () => isAdminRole,
34
46
  isAlreadySent: () => isAlreadySent,
47
+ isSlotAvailable: () => isSlotAvailable,
35
48
  joinConfig: () => joinConfig,
49
+ meetingCreateRow: () => meetingCreateRow,
50
+ meetingRescheduleUpdate: () => meetingRescheduleUpdate,
51
+ memberApplication: () => memberApplication,
52
+ memberSession: () => memberSession,
53
+ normalizeWebhookEvent: () => normalizeWebhookEvent,
54
+ paymentsReady: () => paymentsReady,
36
55
  planDelivery: () => planDelivery,
37
56
  projectSharedRecord: () => projectSharedRecord,
57
+ refundedPatch: () => refundedPatch,
38
58
  render: () => render,
59
+ renderSummary: () => renderSummary,
39
60
  renderTemplateBody: () => renderTemplateBody,
61
+ renewalPatch: () => renewalPatch,
40
62
  resolveApplication: () => resolveApplication,
41
63
  resolveAuth: () => resolveAuth,
42
64
  resolvePipeline: () => resolvePipeline,
65
+ resolveScheduling: () => resolveScheduling,
43
66
  roleFromClaim: () => roleFromClaim,
44
67
  sharedPersonInput: () => sharedPersonInput,
68
+ slotWindow: () => slotWindow,
45
69
  stageIndex: () => stageIndex,
70
+ stripeForm: () => stripeForm,
46
71
  submitApplication: () => submitApplication,
47
- verifyStripeSignature: () => verifyStripeSignature
72
+ subscriptionIdempotencyKey: () => subscriptionIdempotencyKey,
73
+ verifyStripeSignature: () => verifyStripeSignature,
74
+ webhookMutationId: () => webhookMutationId
48
75
  });
49
76
  module.exports = __toCommonJS(index_exports);
50
77
 
@@ -477,7 +504,7 @@ async function submitApplication(db, chapter, fields, opts) {
477
504
  );
478
505
  return { ok: true, id: id2, duplicate, status: chapter.pipeline.initial };
479
506
  }
480
- function joinConfig(group, paymentsReady) {
507
+ function joinConfig(group, paymentsReady2) {
481
508
  return {
482
509
  id: group.id,
483
510
  name: group.name,
@@ -488,7 +515,7 @@ function joinConfig(group, paymentsReady) {
488
515
  trustCopy: group.trustCopy ?? "",
489
516
  commitmentText: group.commitmentText ?? "",
490
517
  normsText: group.normsText ?? "",
491
- paymentsReady
518
+ paymentsReady: paymentsReady2
492
519
  };
493
520
  }
494
521
 
@@ -637,6 +664,70 @@ async function verifyStripeSignature(payload, header, secret, opts = {}) {
637
664
  const mac = await crypto.subtle.sign("HMAC", key, enc.encode(`${t}.${payload}`));
638
665
  return timingSafeEqual(toHex(mac), v1);
639
666
  }
667
+ function paymentsReady(group, hasSecretKey) {
668
+ return Boolean(group.stripePublishableKey && group.stripePriceId && hasSecretKey);
669
+ }
670
+ function stripeForm(params) {
671
+ const out = new URLSearchParams();
672
+ for (const [k, v] of Object.entries(params)) {
673
+ if (v === void 0 || v === null) continue;
674
+ if (typeof v === "object") {
675
+ for (const [k2, v2] of Object.entries(v)) {
676
+ if (v2 !== void 0 && v2 !== null) out.append(`${k}[${k2}]`, String(v2));
677
+ }
678
+ } else {
679
+ out.append(k, String(v));
680
+ }
681
+ }
682
+ return out.toString();
683
+ }
684
+ function subscriptionIdempotencyKey(applicationId) {
685
+ return `sub:${applicationId}`;
686
+ }
687
+ function webhookMutationId(eventId) {
688
+ return `stripe:${eventId}`;
689
+ }
690
+ function findApplicationRef(obj) {
691
+ const metaOf = (v) => v && typeof v === "object" ? v.metadata ?? {} : {};
692
+ const pick = (m) => typeof m.applicationId === "string" ? m.applicationId : void 0;
693
+ const applicationId = pick(metaOf(obj)) ?? pick(metaOf(obj.subscription_details)) ?? pick(metaOf(obj.parent?.subscription_details));
694
+ const customerId = typeof obj.customer === "string" ? obj.customer : void 0;
695
+ return { ...applicationId ? { applicationId } : {}, ...customerId ? { customerId } : {} };
696
+ }
697
+ function normalizeWebhookEvent(event) {
698
+ const obj = event.data?.object ?? {};
699
+ const ref = findApplicationRef(obj);
700
+ switch (event.type) {
701
+ case "invoice.paid": {
702
+ const lines = obj.lines?.data ?? [];
703
+ const periodEnd = lines[0]?.period?.end;
704
+ const renewalAt = typeof periodEnd === "number" ? periodEnd * 1e3 : void 0;
705
+ const kind = obj.billing_reason === "subscription_create" ? "first_payment" : "renewal";
706
+ return { kind, ...ref, ...renewalAt !== void 0 ? { renewalAt } : {} };
707
+ }
708
+ case "charge.refunded":
709
+ return { kind: "refunded", ...ref };
710
+ case "customer.subscription.deleted":
711
+ return { kind: "canceled", ...ref };
712
+ default:
713
+ return { kind: "ignored", type: event.type };
714
+ }
715
+ }
716
+ function firstPaymentPatch(currentStatus, renewalAt) {
717
+ return {
718
+ ...currentStatus === "submitted" ? { status: "paid_pending_vetting" } : {},
719
+ ...renewalAt !== void 0 ? { renewalAt } : {}
720
+ };
721
+ }
722
+ function renewalPatch(renewalAt) {
723
+ return { renewalAt };
724
+ }
725
+ function refundedPatch() {
726
+ return { status: "refunded" };
727
+ }
728
+ function canceledPatch() {
729
+ return { canceled: true };
730
+ }
640
731
 
641
732
  // src/network.ts
642
733
  var import_crm3 = require("@odla-ai/crm");
@@ -666,4 +757,164 @@ async function projectSharedRecord(deps, person) {
666
757
  const created = await (0, import_crm3.createRecord)(crmDeps, { type: "person", input, mutationId: `share:${person.hubRecordId}` });
667
758
  return { recordId: created.id };
668
759
  }
760
+
761
+ // src/session.ts
762
+ function applicationSummary(app) {
763
+ return {
764
+ id: app.id,
765
+ firstName: app.firstName ?? null,
766
+ lastName: app.lastName ?? null,
767
+ email: app.email ?? null,
768
+ status: app.status,
769
+ createdAt: app.createdAt ?? null,
770
+ meetingLink: app.meetingLink ?? null,
771
+ paid: Boolean(app.stripeSubscriptionId) && app.status !== "refunded",
772
+ renewalAt: app.renewalAt ?? null,
773
+ canceled: app.canceled === true
774
+ };
775
+ }
776
+ function memberApplication(app, meeting, defaultTimezone) {
777
+ const summary = applicationSummary(app);
778
+ let meetingAt = app.meetingAt ?? null;
779
+ let meetUrl = null;
780
+ let timezone = defaultTimezone;
781
+ if (meeting) {
782
+ timezone = meeting.timezone ?? timezone;
783
+ if (meeting.status === "scheduled") {
784
+ meetingAt = meeting.startAt ?? null;
785
+ meetUrl = meeting.meetUrl ?? null;
786
+ } else {
787
+ meetingAt = null;
788
+ }
789
+ }
790
+ return { ...summary, meetingAt, meetUrl, timezone };
791
+ }
792
+ function memberSession(user, opts) {
793
+ return {
794
+ userId: user.userId,
795
+ email: user.email ?? null,
796
+ role: user.role,
797
+ superAdmin: opts.superAdmin,
798
+ application: opts.application
799
+ };
800
+ }
801
+
802
+ // src/brand.ts
803
+ function paletteVar(key) {
804
+ return key.startsWith("--") ? key : `--${key}`;
805
+ }
806
+ function cleanValue(value) {
807
+ return value.replace(/[<>{};]/g, "").trim();
808
+ }
809
+ function brandTokens(brand) {
810
+ if (!brand) return "";
811
+ const decls = [];
812
+ for (const [key, value] of Object.entries(brand.palette ?? {})) {
813
+ if (typeof value === "string" && value.trim()) decls.push(`${paletteVar(key)}: ${cleanValue(value)};`);
814
+ }
815
+ const fonts = brand.fonts;
816
+ if (fonts?.display) decls.push(`--ui-font-display: ${cleanValue(fonts.display)};`);
817
+ if (fonts?.body) decls.push(`--ui-font-sans: ${cleanValue(fonts.body)};`);
818
+ if (fonts?.numeral) decls.push(`--ui-font-numeral: ${cleanValue(fonts.numeral)};`);
819
+ return decls.length ? `:root {
820
+ ${decls.join("\n ")}
821
+ }
822
+ ` : "";
823
+ }
824
+
825
+ // src/scheduling.ts
826
+ var SCHEDULING_DEFAULTS = {
827
+ slotMinutes: 45,
828
+ days: [1, 2, 3, 4, 5],
829
+ startHour: 9,
830
+ endHour: 17,
831
+ timezone: "America/Los_Angeles",
832
+ minNoticeHours: 24,
833
+ windowDays: 14,
834
+ summaryTemplate: "Introduction call with {{firstName}} {{lastName}}"
835
+ };
836
+ function isValidTimeZone(tz) {
837
+ try {
838
+ new Intl.DateTimeFormat(void 0, { timeZone: tz });
839
+ return true;
840
+ } catch {
841
+ return false;
842
+ }
843
+ }
844
+ function resolveScheduling(config) {
845
+ const d = config ?? {};
846
+ const c = {
847
+ slotMinutes: d.slotMinutes ?? SCHEDULING_DEFAULTS.slotMinutes,
848
+ days: d.days ?? SCHEDULING_DEFAULTS.days,
849
+ startHour: d.startHour ?? SCHEDULING_DEFAULTS.startHour,
850
+ endHour: d.endHour ?? SCHEDULING_DEFAULTS.endHour,
851
+ timezone: d.timezone ?? SCHEDULING_DEFAULTS.timezone,
852
+ minNoticeHours: d.minNoticeHours ?? SCHEDULING_DEFAULTS.minNoticeHours,
853
+ windowDays: d.windowDays ?? SCHEDULING_DEFAULTS.windowDays,
854
+ summaryTemplate: d.summaryTemplate ?? SCHEDULING_DEFAULTS.summaryTemplate
855
+ };
856
+ const fail = (msg) => {
857
+ throw new Error(`scheduling: ${msg}`);
858
+ };
859
+ if (!(c.slotMinutes >= 15 && c.slotMinutes <= 240)) fail("slotMinutes must be 15\u2013240");
860
+ if (!(c.windowDays >= 1 && c.windowDays <= 62)) fail("windowDays must be 1\u201362 (FreeBusy caps at 62)");
861
+ if (!(c.minNoticeHours >= 0 && c.minNoticeHours <= 336)) fail("minNoticeHours must be 0\u2013336");
862
+ if (!(c.startHour >= 0 && c.startHour < c.endHour && c.endHour <= 24)) fail("require 0 \u2264 startHour < endHour \u2264 24");
863
+ const days = [...c.days];
864
+ if (!days.length || !days.every((n) => Number.isInteger(n) && n >= 0 && n <= 6)) {
865
+ fail("days must be a non-empty list of weekday integers 0\u20136");
866
+ }
867
+ if (typeof c.timezone !== "string" || !isValidTimeZone(c.timezone)) fail(`invalid IANA timezone "${c.timezone}"`);
868
+ if (typeof c.summaryTemplate !== "string") fail("summaryTemplate must be a string");
869
+ return { ...c, days };
870
+ }
871
+ var BOOKABLE_STATUSES = ["submitted", "paid_pending_vetting", "call_scheduled"];
872
+ function canBookFrom(status) {
873
+ return BOOKABLE_STATUSES.includes(status);
874
+ }
875
+ function slotWindow(now, windowDays) {
876
+ return { from: now, to: now + windowDays * 864e5 };
877
+ }
878
+ function endForSlot(startAt, slotMinutes) {
879
+ return startAt + slotMinutes * 6e4;
880
+ }
881
+ function isSlotAvailable(slots, startAt) {
882
+ return slots.some((s) => s.startAt === startAt);
883
+ }
884
+ function renderSummary(template, app) {
885
+ return template.replace("{{firstName}}", app.firstName ?? "").replace("{{lastName}}", app.lastName ?? "");
886
+ }
887
+ function bookingDecision(existing) {
888
+ const eventId = existing?.googleEventId ?? null;
889
+ return { reschedule: Boolean(eventId), eventId };
890
+ }
891
+ function introIdempotencyKey(applicationId) {
892
+ return `application:${applicationId}:intro`;
893
+ }
894
+ function meetingCreateRow(i) {
895
+ return {
896
+ id: i.meetingId,
897
+ applicationId: i.applicationId,
898
+ groupId: i.groupId,
899
+ startAt: i.startAt,
900
+ endAt: i.endAt,
901
+ timezone: i.timezone,
902
+ status: "scheduled",
903
+ googleEventId: i.googleEventId,
904
+ ...i.meetUrl ? { meetUrl: i.meetUrl } : {},
905
+ ...i.htmlLink ? { htmlLink: i.htmlLink } : {},
906
+ drift: "none",
907
+ createdAt: i.createdAt
908
+ };
909
+ }
910
+ function meetingRescheduleUpdate(startAt, endAt) {
911
+ return { startAt, endAt, drift: "none" };
912
+ }
913
+ function applicationBookingUpdate(currentStatus, startAt, htmlLink) {
914
+ return {
915
+ meetingAt: startAt,
916
+ ...htmlLink ? { meetingLink: htmlLink } : {},
917
+ ...currentStatus !== "call_scheduled" ? { status: "call_scheduled" } : {}
918
+ };
919
+ }
669
920
  //# sourceMappingURL=index.cjs.map