@odla-ai/chapter 0.23.0 → 0.24.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 +2 -2
- package/dist/{chunk-OYXNI6VR.js → chunk-GMGTJIY3.js} +28 -15
- package/dist/chunk-GMGTJIY3.js.map +1 -0
- package/dist/{chunk-NTZSXAFT.js → chunk-WGQO4WDJ.js} +3 -2
- package/dist/chunk-WGQO4WDJ.js.map +1 -0
- package/dist/{chunk-VCC4S6JU.js → chunk-ZPAJL7KG.js} +28 -21
- package/dist/chunk-ZPAJL7KG.js.map +1 -0
- package/dist/{copy-context-pDogTR4x.d.ts → copy-context-Dp5hvj5W.d.ts} +8 -1
- package/dist/index.cjs +19 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +23 -2
- package/dist/index.d.ts +23 -2
- package/dist/index.js +19 -7
- package/dist/index.js.map +1 -1
- package/dist/ui/admin/index.d.ts +2 -2
- package/dist/ui/admin/index.js +2 -2
- package/dist/ui/index.d.ts +1 -1
- package/dist/ui/index.js +3 -3
- package/dist/ui/member/index.d.ts +2 -2
- package/dist/ui/member/index.js +2 -2
- package/dist/worker/index.cjs +97 -86
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +8 -1
- package/dist/worker/index.d.ts +8 -1
- package/dist/worker/index.js +97 -86
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
- package/runbooks/adopt-existing.md +1 -1
- package/runbooks/greenfield.md +3 -3
- package/dist/chunk-NTZSXAFT.js.map +0 -1
- package/dist/chunk-OYXNI6VR.js.map +0 -1
- package/dist/chunk-VCC4S6JU.js.map +0 -1
package/dist/worker/index.d.cts
CHANGED
|
@@ -15,7 +15,14 @@ interface ChapterCopy {
|
|
|
15
15
|
join: {
|
|
16
16
|
form: TextFields<"submit" | "submitting" | "submitFailed" | "unexpectedFailure">;
|
|
17
17
|
booking: TextFields<"unavailable" | "loadFailed" | "slotTaken" | "failed" | "loading" | "book" | "booking">;
|
|
18
|
-
|
|
18
|
+
/**
|
|
19
|
+
* `consent` is the affirmative statement a member agrees to before the card
|
|
20
|
+
* form appears, rendered beside the checkbox and beneath the policy text.
|
|
21
|
+
* It is what the member is shown at the moment of consent, so it says they
|
|
22
|
+
* agree rather than merely restating the policy. Set it to an empty string
|
|
23
|
+
* to render the checkbox with the policy as its only label.
|
|
24
|
+
*/
|
|
25
|
+
payment: TextFields<"setupFailed" | "preparing" | "pending" | "processing" | "payAndContinue" | "incomplete" | "consent">;
|
|
19
26
|
done: TextFields<"label" | "calendarInvite" | "memberArea">;
|
|
20
27
|
};
|
|
21
28
|
members: {
|
package/dist/worker/index.d.ts
CHANGED
|
@@ -15,7 +15,14 @@ interface ChapterCopy {
|
|
|
15
15
|
join: {
|
|
16
16
|
form: TextFields<"submit" | "submitting" | "submitFailed" | "unexpectedFailure">;
|
|
17
17
|
booking: TextFields<"unavailable" | "loadFailed" | "slotTaken" | "failed" | "loading" | "book" | "booking">;
|
|
18
|
-
|
|
18
|
+
/**
|
|
19
|
+
* `consent` is the affirmative statement a member agrees to before the card
|
|
20
|
+
* form appears, rendered beside the checkbox and beneath the policy text.
|
|
21
|
+
* It is what the member is shown at the moment of consent, so it says they
|
|
22
|
+
* agree rather than merely restating the policy. Set it to an empty string
|
|
23
|
+
* to render the checkbox with the policy as its only label.
|
|
24
|
+
*/
|
|
25
|
+
payment: TextFields<"setupFailed" | "preparing" | "pending" | "processing" | "payAndContinue" | "incomplete" | "consent">;
|
|
19
26
|
done: TextFields<"label" | "calendarInvite" | "memberArea">;
|
|
20
27
|
};
|
|
21
28
|
members: {
|
package/dist/worker/index.js
CHANGED
|
@@ -921,6 +921,93 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
921
921
|
return null;
|
|
922
922
|
};
|
|
923
923
|
|
|
924
|
+
// src/crm-sync.ts
|
|
925
|
+
import { createRecord as createRecord2, updateRecord as updateRecord2, setStage, linkIdentity } from "@odla-ai/crm";
|
|
926
|
+
var str = (v) => typeof v === "string" ? v : v == null ? "" : String(v);
|
|
927
|
+
var crmDeps = (db, ctx) => ({
|
|
928
|
+
crm: ctx.chapter.crm,
|
|
929
|
+
db,
|
|
930
|
+
now: () => Date.now(),
|
|
931
|
+
newId: () => crypto.randomUUID(),
|
|
932
|
+
chapter: ctx.chapter
|
|
933
|
+
});
|
|
934
|
+
function personInputFromApp(chapter, app) {
|
|
935
|
+
const input = sharedPersonInput({
|
|
936
|
+
email: str(app.email),
|
|
937
|
+
firstName: str(app.firstName) || void 0,
|
|
938
|
+
lastName: str(app.lastName) || void 0,
|
|
939
|
+
phone: str(app.phone) || void 0,
|
|
940
|
+
linkedin: str(app.linkedin) || void 0,
|
|
941
|
+
hubRecordId: str(app.id)
|
|
942
|
+
});
|
|
943
|
+
for (const f of chapter.application.crmFields) {
|
|
944
|
+
if (app[f] !== void 0) input[f] = app[f];
|
|
945
|
+
}
|
|
946
|
+
if (app.id !== void 0) input.applicationId = str(app.id);
|
|
947
|
+
return input;
|
|
948
|
+
}
|
|
949
|
+
function billingColumns(app) {
|
|
950
|
+
const status = str(app.status);
|
|
951
|
+
const paid = Boolean(app.stripeSubscriptionId) && status !== "refunded";
|
|
952
|
+
const billingStatus = status === "refunded" ? "refunded" : app.canceled === true ? "canceled" : paid ? "active" : "none";
|
|
953
|
+
const cols = { billingStatus };
|
|
954
|
+
if (app.stripeCustomerId) cols.stripeCustomerId = str(app.stripeCustomerId);
|
|
955
|
+
if (app.stripeSubscriptionId) cols.subscriptionId = str(app.stripeSubscriptionId);
|
|
956
|
+
if (typeof app.renewalAt === "number") cols.renewalAt = app.renewalAt;
|
|
957
|
+
return cols;
|
|
958
|
+
}
|
|
959
|
+
async function syncApplicationToCrm(deps, opts) {
|
|
960
|
+
const emailKey = str(opts.app.email).toLowerCase();
|
|
961
|
+
if (!emailKey) return null;
|
|
962
|
+
const recordDeps = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
|
|
963
|
+
const input = personInputFromApp(deps.chapter, opts.app);
|
|
964
|
+
const { crm_record } = await deps.db.query({
|
|
965
|
+
crm_record: { $: { where: { type: "person", primaryEmail: emailKey }, limit: 1 } }
|
|
966
|
+
});
|
|
967
|
+
const existing = crm_record?.[0] ?? null;
|
|
968
|
+
const stage = opts.stage || void 0;
|
|
969
|
+
let recordId;
|
|
970
|
+
if (existing && typeof existing.id === "string") {
|
|
971
|
+
recordId = existing.id;
|
|
972
|
+
await updateRecord2(recordDeps, { id: recordId, input });
|
|
973
|
+
} else {
|
|
974
|
+
const created = await createRecord2(recordDeps, { type: "person", input, ...stage ? { stage } : {} });
|
|
975
|
+
recordId = created.id;
|
|
976
|
+
}
|
|
977
|
+
if (existing && stage && existing.stage !== stage) {
|
|
978
|
+
await setStage(recordDeps, { id: recordId, to: stage, authorId: "system", mutationId: `crm:stage:${recordId}:${stage}` }).catch(() => void 0);
|
|
979
|
+
}
|
|
980
|
+
await deps.db.transact([{ t: "update", ns: "crm_record", id: recordId, attrs: billingColumns(opts.app) }]);
|
|
981
|
+
await linkIdentity(recordDeps, { recordId, email: emailKey, mutationId: `crm:link:${recordId}:${emailKey}` }).catch(() => void 0);
|
|
982
|
+
return recordId;
|
|
983
|
+
}
|
|
984
|
+
async function backfillCrm(deps) {
|
|
985
|
+
const [appsRes, usersRes] = await Promise.all([
|
|
986
|
+
deps.db.query({ applications: { $: { order: { createdAt: "desc" }, limit: 1e3 } } }),
|
|
987
|
+
deps.db.query({ $users: { $: { limit: 1e3 } } })
|
|
988
|
+
]);
|
|
989
|
+
const seen = /* @__PURE__ */ new Set();
|
|
990
|
+
let synced = 0;
|
|
991
|
+
const errors = [];
|
|
992
|
+
const run = async (app, stage) => {
|
|
993
|
+
const key = str(app.email).toLowerCase();
|
|
994
|
+
if (!key || seen.has(key)) return;
|
|
995
|
+
seen.add(key);
|
|
996
|
+
try {
|
|
997
|
+
await syncApplicationToCrm(deps, { app, stage });
|
|
998
|
+
synced += 1;
|
|
999
|
+
} catch (err) {
|
|
1000
|
+
errors.push({ email: key, error: err instanceof Error ? err.message : String(err) });
|
|
1001
|
+
}
|
|
1002
|
+
};
|
|
1003
|
+
for (const a of appsRes.applications ?? []) await run(a, str(a.status));
|
|
1004
|
+
for (const u of usersRes.$users ?? []) {
|
|
1005
|
+
if (u.deleted === true) continue;
|
|
1006
|
+
await run({ email: u.email, firstName: str(u.name) });
|
|
1007
|
+
}
|
|
1008
|
+
return { synced, errors };
|
|
1009
|
+
}
|
|
1010
|
+
|
|
924
1011
|
// src/worker-routes-schedule.ts
|
|
925
1012
|
import { computeBookableSlots, initCalendar } from "@odla-ai/calendar";
|
|
926
1013
|
function errCode(err) {
|
|
@@ -1024,8 +1111,11 @@ async function bookSlot(req, env, ctx) {
|
|
|
1024
1111
|
if (code === "calendar_slot_unavailable") return json({ error: "slot no longer available", code }, 409);
|
|
1025
1112
|
return json({ error: "booking failed", code }, 502);
|
|
1026
1113
|
}
|
|
1027
|
-
const
|
|
1114
|
+
const bookingAttrs = applicationBookingUpdate(status, startAt, htmlLink);
|
|
1115
|
+
const appOp = { t: "update", ns: "applications", id: applicationId, attrs: bookingAttrs };
|
|
1028
1116
|
await db.transact([meetingOp, appOp]);
|
|
1117
|
+
const booked = { ...app, ...bookingAttrs };
|
|
1118
|
+
await syncApplicationToCrm(crmDeps(db, ctx), { app: booked, stage: String(booked.status ?? "") }).catch(() => void 0);
|
|
1029
1119
|
if (typeof app.email === "string" && app.email) {
|
|
1030
1120
|
await sendTemplated(
|
|
1031
1121
|
{ db, envName: env.ODLA_ENV, sender: env.SEND_EMAIL, from: env.EMAIL_FROM, now: () => Date.now(), newId: () => crypto.randomUUID() },
|
|
@@ -1375,6 +1465,8 @@ async function ingestWebhook(req, env, ctx) {
|
|
|
1375
1465
|
const patch = webhookPatch(event, String(app.status ?? ""));
|
|
1376
1466
|
if (Object.keys(patch).length) {
|
|
1377
1467
|
await db.transact([{ t: "update", ns: "applications", id: String(app.id), attrs: patch }], { mutationId: webhookMutationId(eventId) });
|
|
1468
|
+
const next = { ...app, ...patch };
|
|
1469
|
+
await syncApplicationToCrm(crmDeps(db, ctx), { app: next, stage: String(next.status ?? "") }).catch(() => void 0);
|
|
1378
1470
|
}
|
|
1379
1471
|
if (event.kind === "first_payment") {
|
|
1380
1472
|
await notifyPaymentConfirmed(db, env, eventId, app);
|
|
@@ -1566,86 +1658,6 @@ async function clerkSetRole(secretKey, id, role, fetchImpl = fetch) {
|
|
|
1566
1658
|
return res.ok;
|
|
1567
1659
|
}
|
|
1568
1660
|
|
|
1569
|
-
// src/crm-sync.ts
|
|
1570
|
-
import { createRecord as createRecord2, updateRecord as updateRecord2, setStage, linkIdentity } from "@odla-ai/crm";
|
|
1571
|
-
var str = (v) => typeof v === "string" ? v : v == null ? "" : String(v);
|
|
1572
|
-
function personInputFromApp(chapter, app) {
|
|
1573
|
-
const input = sharedPersonInput({
|
|
1574
|
-
email: str(app.email),
|
|
1575
|
-
firstName: str(app.firstName) || void 0,
|
|
1576
|
-
lastName: str(app.lastName) || void 0,
|
|
1577
|
-
phone: str(app.phone) || void 0,
|
|
1578
|
-
linkedin: str(app.linkedin) || void 0,
|
|
1579
|
-
hubRecordId: str(app.id)
|
|
1580
|
-
});
|
|
1581
|
-
for (const f of chapter.application.crmFields) {
|
|
1582
|
-
if (app[f] !== void 0) input[f] = app[f];
|
|
1583
|
-
}
|
|
1584
|
-
if (app.id !== void 0) input.applicationId = str(app.id);
|
|
1585
|
-
return input;
|
|
1586
|
-
}
|
|
1587
|
-
function billingColumns(app) {
|
|
1588
|
-
const status = str(app.status);
|
|
1589
|
-
const paid = Boolean(app.stripeSubscriptionId) && status !== "refunded";
|
|
1590
|
-
const billingStatus = status === "refunded" ? "refunded" : app.canceled === true ? "canceled" : paid ? "active" : "none";
|
|
1591
|
-
const cols = { billingStatus };
|
|
1592
|
-
if (app.stripeCustomerId) cols.stripeCustomerId = str(app.stripeCustomerId);
|
|
1593
|
-
if (app.stripeSubscriptionId) cols.subscriptionId = str(app.stripeSubscriptionId);
|
|
1594
|
-
if (typeof app.renewalAt === "number") cols.renewalAt = app.renewalAt;
|
|
1595
|
-
return cols;
|
|
1596
|
-
}
|
|
1597
|
-
async function syncApplicationToCrm(deps, opts) {
|
|
1598
|
-
const emailKey = str(opts.app.email).toLowerCase();
|
|
1599
|
-
if (!emailKey) return null;
|
|
1600
|
-
const crmDeps3 = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
|
|
1601
|
-
const input = personInputFromApp(deps.chapter, opts.app);
|
|
1602
|
-
const { crm_record } = await deps.db.query({
|
|
1603
|
-
crm_record: { $: { where: { type: "person", primaryEmail: emailKey }, limit: 1 } }
|
|
1604
|
-
});
|
|
1605
|
-
const existing = crm_record?.[0] ?? null;
|
|
1606
|
-
const stage = opts.stage || void 0;
|
|
1607
|
-
let recordId;
|
|
1608
|
-
if (existing && typeof existing.id === "string") {
|
|
1609
|
-
recordId = existing.id;
|
|
1610
|
-
await updateRecord2(crmDeps3, { id: recordId, input });
|
|
1611
|
-
} else {
|
|
1612
|
-
const created = await createRecord2(crmDeps3, { type: "person", input, ...stage ? { stage } : {} });
|
|
1613
|
-
recordId = created.id;
|
|
1614
|
-
}
|
|
1615
|
-
if (existing && stage && existing.stage !== stage) {
|
|
1616
|
-
await setStage(crmDeps3, { id: recordId, to: stage, authorId: "system", mutationId: `crm:stage:${recordId}:${stage}` }).catch(() => void 0);
|
|
1617
|
-
}
|
|
1618
|
-
await deps.db.transact([{ t: "update", ns: "crm_record", id: recordId, attrs: billingColumns(opts.app) }]);
|
|
1619
|
-
await linkIdentity(crmDeps3, { recordId, email: emailKey, mutationId: `crm:link:${recordId}:${emailKey}` }).catch(() => void 0);
|
|
1620
|
-
return recordId;
|
|
1621
|
-
}
|
|
1622
|
-
async function backfillCrm(deps) {
|
|
1623
|
-
const [appsRes, usersRes] = await Promise.all([
|
|
1624
|
-
deps.db.query({ applications: { $: { order: { createdAt: "desc" }, limit: 1e3 } } }),
|
|
1625
|
-
deps.db.query({ $users: { $: { limit: 1e3 } } })
|
|
1626
|
-
]);
|
|
1627
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1628
|
-
let synced = 0;
|
|
1629
|
-
const errors = [];
|
|
1630
|
-
const run = async (app, stage) => {
|
|
1631
|
-
const key = str(app.email).toLowerCase();
|
|
1632
|
-
if (!key || seen.has(key)) return;
|
|
1633
|
-
seen.add(key);
|
|
1634
|
-
try {
|
|
1635
|
-
await syncApplicationToCrm(deps, { app, stage });
|
|
1636
|
-
synced += 1;
|
|
1637
|
-
} catch (err) {
|
|
1638
|
-
errors.push({ email: key, error: err instanceof Error ? err.message : String(err) });
|
|
1639
|
-
}
|
|
1640
|
-
};
|
|
1641
|
-
for (const a of appsRes.applications ?? []) await run(a, str(a.status));
|
|
1642
|
-
for (const u of usersRes.$users ?? []) {
|
|
1643
|
-
if (u.deleted === true) continue;
|
|
1644
|
-
await run({ email: u.email, firstName: str(u.name) });
|
|
1645
|
-
}
|
|
1646
|
-
return { synced, errors };
|
|
1647
|
-
}
|
|
1648
|
-
|
|
1649
1661
|
// src/worker-routes-admin-people.ts
|
|
1650
1662
|
async function adminGate(req, env, ctx) {
|
|
1651
1663
|
const rawDb = ctx.makeDb(env);
|
|
@@ -1654,7 +1666,7 @@ async function adminGate(req, env, ctx) {
|
|
|
1654
1666
|
if (!await ctx.isAdmin(rawDb, u)) return json({ error: "forbidden" }, 403);
|
|
1655
1667
|
return { db: rawDb, actor: { userId: u.userId, email: u.email ?? void 0 } };
|
|
1656
1668
|
}
|
|
1657
|
-
var
|
|
1669
|
+
var crmDeps2 = (db, ctx) => ({
|
|
1658
1670
|
crm: ctx.chapter.crm,
|
|
1659
1671
|
db,
|
|
1660
1672
|
now: () => Date.now(),
|
|
@@ -1665,7 +1677,7 @@ var handleAdminCrmSync = async (req, url, env, ctx) => {
|
|
|
1665
1677
|
if (req.method !== "POST" || url.pathname !== "/api/admin/crm/sync") return null;
|
|
1666
1678
|
const gate5 = await adminGate(req, env, ctx);
|
|
1667
1679
|
if (gate5 instanceof Response) return gate5;
|
|
1668
|
-
const result = await backfillCrm(
|
|
1680
|
+
const result = await backfillCrm(crmDeps2(gate5.db, ctx));
|
|
1669
1681
|
return json({ ok: true, ...result });
|
|
1670
1682
|
};
|
|
1671
1683
|
var handleAdminPeople = async (req, url, env, ctx) => {
|
|
@@ -1945,7 +1957,6 @@ async function gate2(req, env, ctx) {
|
|
|
1945
1957
|
return rawDb;
|
|
1946
1958
|
}
|
|
1947
1959
|
var calFor = (env) => initCalendar3({ appId: env.ODLA_APP_ID, env: env.ODLA_ENV, adminToken: env.ODLA_API_KEY, endpoint: env.ODLA_PLATFORM });
|
|
1948
|
-
var crmDeps2 = (db, ctx) => ({ crm: ctx.chapter.crm, db, now: () => Date.now(), newId: () => crypto.randomUUID(), chapter: ctx.chapter });
|
|
1949
1960
|
var readJson = async (req) => {
|
|
1950
1961
|
try {
|
|
1951
1962
|
return await req.json();
|
|
@@ -2031,7 +2042,7 @@ var handleAdminApprove = async (req, url, env, ctx) => {
|
|
|
2031
2042
|
if (!canApprove(String(app.status), ctx.chapter.pipeline)) return json({ error: `cannot approve from status "${String(app.status)}"` }, 409);
|
|
2032
2043
|
const target = "approved";
|
|
2033
2044
|
await db.transact([{ t: "update", ns: "applications", id, attrs: { status: target } }]);
|
|
2034
|
-
await syncApplicationToCrm(
|
|
2045
|
+
await syncApplicationToCrm(crmDeps(db, ctx), { app: { ...app, status: target }, stage: target }).catch(() => void 0);
|
|
2035
2046
|
const { promoteTo, send } = ctx.chapter.operations.onApprove;
|
|
2036
2047
|
let rolePromoted = false;
|
|
2037
2048
|
const sk = await getVaultSecret(db, "clerk_secret_key");
|
|
@@ -2112,7 +2123,7 @@ var handleAdminApplicationPatch = async (req, url, env, ctx) => {
|
|
|
2112
2123
|
if (Object.keys(attrs).length === 0) return json({ error: "nothing to update" }, 400);
|
|
2113
2124
|
await db.transact([{ t: "update", ns: "applications", id, attrs }]);
|
|
2114
2125
|
if (attrs.status !== void 0) {
|
|
2115
|
-
await syncApplicationToCrm(
|
|
2126
|
+
await syncApplicationToCrm(crmDeps(db, ctx), { app: { ...app, ...attrs }, stage: String(attrs.status) }).catch(() => void 0);
|
|
2116
2127
|
}
|
|
2117
2128
|
return json({ ok: true });
|
|
2118
2129
|
};
|