@odla-ai/chapter 0.50.0 → 0.52.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 +109 -8
- package/dist/{chunk-4EA5V4JE.js → chunk-7N4VDWM5.js} +239 -136
- package/dist/chunk-7N4VDWM5.js.map +1 -0
- package/dist/{chunk-OS46ZE46.js → chunk-7RIT2UN5.js} +3 -7
- package/dist/chunk-7RIT2UN5.js.map +1 -0
- package/dist/{chunk-JXX4BFDP.js → chunk-A6W53GVM.js} +5 -1
- package/dist/chunk-A6W53GVM.js.map +1 -0
- package/dist/{copy-context-Ddat__H5.d.ts → copy-context-CYLlPzAc.d.ts} +11 -5
- package/dist/index.cjs +111 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +77 -13
- package/dist/index.d.ts +77 -13
- package/dist/index.js +111 -18
- package/dist/index.js.map +1 -1
- package/dist/ui/admin/index.d.ts +14 -3
- package/dist/ui/admin/index.js +4 -2
- package/dist/ui/index.d.ts +2 -2
- package/dist/ui/index.js +5 -3
- package/dist/ui/member/index.d.ts +2 -2
- package/dist/ui/member/index.js +2 -2
- package/dist/worker/index.cjs +983 -368
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +52 -10
- package/dist/worker/index.d.ts +52 -10
- package/dist/worker/index.js +983 -368
- package/dist/worker/index.js.map +1 -1
- package/package.json +2 -1
- package/dist/chunk-4EA5V4JE.js.map +0 -1
- package/dist/chunk-JXX4BFDP.js.map +0 -1
- package/dist/chunk-OS46ZE46.js.map +0 -1
package/dist/worker/index.js
CHANGED
|
@@ -143,11 +143,11 @@ async function clerkVerifiedPrimaryEmail(db, userId) {
|
|
|
143
143
|
|
|
144
144
|
// src/crm-person-projection.ts
|
|
145
145
|
import { createRecord } from "@odla-ai/crm";
|
|
146
|
-
async function ensureProjectedPerson(
|
|
146
|
+
async function ensureProjectedPerson(deps, input, options = {}) {
|
|
147
147
|
const email = String(input.email ?? "").trim().toLowerCase();
|
|
148
148
|
if (!email) throw new Error("crm_projection_email_required");
|
|
149
149
|
const find = async () => {
|
|
150
|
-
const records = (await
|
|
150
|
+
const records = (await deps.db.query({ crm_record: { $: {
|
|
151
151
|
where: { type: "person", primaryEmail: email },
|
|
152
152
|
limit: 2
|
|
153
153
|
} } })).crm_record ?? [];
|
|
@@ -160,7 +160,7 @@ async function ensureProjectedPerson(deps2, input, options = {}) {
|
|
|
160
160
|
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(`chapter:person:v1:${email}`)));
|
|
161
161
|
const id6 = `person-${Array.from(digest, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
162
162
|
try {
|
|
163
|
-
const created = await createRecord({ ...
|
|
163
|
+
const created = await createRecord({ ...deps, db: deps.db }, {
|
|
164
164
|
id: id6,
|
|
165
165
|
type: "person",
|
|
166
166
|
input: { ...input, email },
|
|
@@ -176,14 +176,14 @@ async function ensureProjectedPerson(deps2, input, options = {}) {
|
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
// src/crm-account-projection.ts
|
|
179
|
-
async function syncAccountToCrm(
|
|
180
|
-
const user = (await
|
|
179
|
+
async function syncAccountToCrm(deps, userId) {
|
|
180
|
+
const user = (await deps.db.query({ $users: { $: { where: { id: userId }, limit: 1 } } })).$users?.[0];
|
|
181
181
|
if (!user || user.deleted === true || typeof user.email !== "string" || !user.email.trim()) return null;
|
|
182
|
-
const linked = (await
|
|
182
|
+
const linked = (await deps.db.query({ crm_record: { $: { where: { type: "person", clerkUserId: userId }, limit: 2 } } })).crm_record ?? [];
|
|
183
183
|
if (linked.length > 1) throw new Error("crm_projection_identity_ambiguous");
|
|
184
184
|
if (linked.length === 1) return String(linked[0].id);
|
|
185
185
|
const email = user.email.trim().toLowerCase();
|
|
186
|
-
const peers = (await
|
|
186
|
+
const peers = (await deps.db.query({ $users: { $: { where: { email }, limit: 2 } } })).$users ?? [];
|
|
187
187
|
if (peers.length !== 1 || peers[0]?.id !== userId) throw new Error("crm_projection_identity_ambiguous");
|
|
188
188
|
const userGuard = {
|
|
189
189
|
ns: "$users",
|
|
@@ -192,14 +192,14 @@ async function syncAccountToCrm(deps2, userId) {
|
|
|
192
192
|
equals: { email: user.email, ...user.deleted !== void 0 ? { deleted: user.deleted } : {} },
|
|
193
193
|
...user.deleted === void 0 ? { absent: ["deleted"] } : {}
|
|
194
194
|
};
|
|
195
|
-
const { record: record4 } = await ensureProjectedPerson(
|
|
195
|
+
const { record: record4 } = await ensureProjectedPerson(deps, {
|
|
196
196
|
name: typeof user.name === "string" && user.name.trim() ? user.name : email,
|
|
197
197
|
email
|
|
198
198
|
}, { stage: null, guards: [userGuard], emailConsent: { state: "unsubscribed", source: "account-no-marketing-consent" } });
|
|
199
199
|
if (record4.clerkUserId === userId) return String(record4.id);
|
|
200
200
|
if (record4.clerkUserId != null) throw new Error("crm_projection_identity_conflict");
|
|
201
201
|
try {
|
|
202
|
-
await
|
|
202
|
+
await deps.db.transact([{ t: "update", ns: "crm_record", id: String(record4.id), attrs: { clerkUserId: userId } }], {
|
|
203
203
|
guards: [userGuard, {
|
|
204
204
|
ns: "crm_record",
|
|
205
205
|
id: String(record4.id),
|
|
@@ -215,12 +215,37 @@ async function syncAccountToCrm(deps2, userId) {
|
|
|
215
215
|
});
|
|
216
216
|
} catch (error) {
|
|
217
217
|
if (error.code !== "transact_guard_failed") throw error;
|
|
218
|
-
const current = (await
|
|
218
|
+
const current = (await deps.db.query({ crm_record: { $: { where: { id: record4.id }, limit: 1 } } })).crm_record?.[0];
|
|
219
219
|
if (current?.clerkUserId !== userId) throw error;
|
|
220
220
|
}
|
|
221
221
|
return String(record4.id);
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
+
// src/worker-alerts.ts
|
|
225
|
+
var consoleErrorRecorder = (error, report) => {
|
|
226
|
+
console.error("chapter_alert", JSON.stringify({ message: error.message, ...report }));
|
|
227
|
+
};
|
|
228
|
+
function reportChapterAlert(ctx, error, report) {
|
|
229
|
+
(ctx?.recordError ?? consoleErrorRecorder)(error, report);
|
|
230
|
+
}
|
|
231
|
+
var ACTIVATION_LIMIT = 50;
|
|
232
|
+
function networkCommercialAlerts(ctx, route) {
|
|
233
|
+
return {
|
|
234
|
+
exceptionsActivated(activated, activeCount) {
|
|
235
|
+
if (activated.length === 0) return;
|
|
236
|
+
reportChapterAlert(ctx, new Error("network commercial exception activated"), {
|
|
237
|
+
code: "network_commercial_exception_activated",
|
|
238
|
+
route,
|
|
239
|
+
attributes: {
|
|
240
|
+
"odla.commercial.active_exceptions": activeCount,
|
|
241
|
+
"odla.commercial.activated_exceptions": activated.length
|
|
242
|
+
},
|
|
243
|
+
artifacts: { exceptions: activated.slice(0, ACTIVATION_LIMIT).map(({ eventId, kind }) => ({ eventId, kind })) }
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
224
249
|
// src/worker-context.ts
|
|
225
250
|
var json = (body, status2 = 200) => new Response(JSON.stringify(body), { status: status2, headers: { "content-type": "application/json" } });
|
|
226
251
|
function createWorkerContext(options) {
|
|
@@ -330,6 +355,7 @@ function createWorkerContext(options) {
|
|
|
330
355
|
requirePaymentQuote: options.requirePaymentQuote === true,
|
|
331
356
|
membershipAuthority: options.membershipAuthority,
|
|
332
357
|
membershipAuthorityOperator: options.membershipAuthorityOperator,
|
|
358
|
+
recordError: options.recordError ?? consoleErrorRecorder,
|
|
333
359
|
getPublicConfig,
|
|
334
360
|
verifyUser,
|
|
335
361
|
makeDb,
|
|
@@ -823,6 +849,17 @@ function projectNetworkCommercialEvents(input, sources) {
|
|
|
823
849
|
};
|
|
824
850
|
}
|
|
825
851
|
|
|
852
|
+
// src/network-commercial-fact-identity.ts
|
|
853
|
+
var stable = (value) => JSON.stringify(value, function(_key, item) {
|
|
854
|
+
return item && typeof item === "object" && !Array.isArray(item) ? Object.fromEntries(Object.entries(item).filter(([key3]) => key3 !== "digest").sort(([a], [b]) => a.localeCompare(b))) : item;
|
|
855
|
+
});
|
|
856
|
+
function sameNetworkCommercialFact(stored, incoming) {
|
|
857
|
+
if (!stored || typeof stored !== "object" || Array.isArray(stored)) return false;
|
|
858
|
+
const comparable = { ...incoming };
|
|
859
|
+
if (!stored.seatSubscriptionItems) delete comparable.seatSubscriptionItems;
|
|
860
|
+
return stable(comparable) === stable(stored);
|
|
861
|
+
}
|
|
862
|
+
|
|
826
863
|
// src/network-commercial-store.ts
|
|
827
864
|
var eventRowId = (eventId) => `network-commercial-event:${eventId}`;
|
|
828
865
|
var projectionId = (kind, sourceKey, providerId3) => `network-commercial:${kind}:${sourceKey}:${providerId3}`;
|
|
@@ -834,6 +871,7 @@ var materializedNamespaces = [
|
|
|
834
871
|
"networkCommercialRefunds",
|
|
835
872
|
"networkCommercialSeats"
|
|
836
873
|
];
|
|
874
|
+
var RESOLUTION_ATTRS = ["resolvedDigest", "reviewedDigest", "resolvedBy", "reviewId"];
|
|
837
875
|
async function rows(db, ns) {
|
|
838
876
|
const all = [];
|
|
839
877
|
const pageSize = 500;
|
|
@@ -883,13 +921,14 @@ function projectionOps(projection, now) {
|
|
|
883
921
|
}
|
|
884
922
|
})));
|
|
885
923
|
}
|
|
886
|
-
async function rebuildNetworkCommercialProjection(db, sources, mutationId, now = Date.now()) {
|
|
924
|
+
async function rebuildNetworkCommercialProjection(db, sources, mutationId, now = Date.now(), alerts) {
|
|
887
925
|
const projection = await loadNetworkCommercialProjection(db, sources);
|
|
888
926
|
const [existing, ...materialized] = await Promise.all([
|
|
889
927
|
rows(db, "networkCommercialExceptions"),
|
|
890
928
|
...materializedNamespaces.map((ns) => rows(db, ns))
|
|
891
929
|
]);
|
|
892
930
|
const activeIds = new Set(projection.exceptions.map((item) => exceptionId(item.eventId, item.kind)));
|
|
931
|
+
const wasActive = new Set(existing.filter((row2) => row2.status === "active").map((row2) => String(row2.id)));
|
|
893
932
|
const projectedIds = new Set(projectionOps(projection, now).map((op) => String(op.id)));
|
|
894
933
|
const ops = [
|
|
895
934
|
...projectionOps(projection, now),
|
|
@@ -915,27 +954,80 @@ async function rebuildNetworkCommercialProjection(db, sources, mutationId, now =
|
|
|
915
954
|
}
|
|
916
955
|
}))
|
|
917
956
|
];
|
|
918
|
-
if (ops.length
|
|
957
|
+
if (ops.length === 0) return projection;
|
|
958
|
+
const write = await db.transact(ops, { mutationId });
|
|
959
|
+
if (!write.duplicate) {
|
|
960
|
+
alerts?.exceptionsActivated(
|
|
961
|
+
projection.exceptions.filter((item) => !wasActive.has(exceptionId(item.eventId, item.kind))),
|
|
962
|
+
projection.exceptions.length
|
|
963
|
+
);
|
|
964
|
+
}
|
|
919
965
|
return projection;
|
|
920
966
|
}
|
|
921
|
-
async function
|
|
922
|
-
const
|
|
923
|
-
const
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
const
|
|
928
|
-
|
|
929
|
-
|
|
967
|
+
async function collide(db, sources, stored, fact, now, legacyDigest, alerts) {
|
|
968
|
+
const id6 = exceptionId(fact.eventId, "collision");
|
|
969
|
+
const exception = (await db.query({ networkCommercialExceptions: { $: { where: { id: id6 }, limit: 1 } } })).networkCommercialExceptions?.[0];
|
|
970
|
+
const exact = stored.digest === fact.digest || stored.digest === legacyDigest;
|
|
971
|
+
const canonical3 = !exact && sameNetworkCommercialFact(stored.fact, fact);
|
|
972
|
+
if (exact || canonical3) {
|
|
973
|
+
const recorded = exception?.incomingFact;
|
|
974
|
+
const resolvable = canonical3 && exception?.status === "active" && (recorded === void 0 || sameNetworkCommercialFact(stored.fact, recorded));
|
|
975
|
+
if (!resolvable) return { status: "replayed", projection: await loadNetworkCommercialProjection(db, sources) };
|
|
976
|
+
const reviewId = crypto.randomUUID();
|
|
977
|
+
await db.transact([
|
|
978
|
+
{ t: "update", ns: "paymentRecoveryReviews", id: reviewId, attrs: {
|
|
979
|
+
id: reviewId,
|
|
980
|
+
eventId: fact.eventId,
|
|
981
|
+
actorUserId: "system",
|
|
982
|
+
reviewedAt: now,
|
|
983
|
+
previousDigest: String(stored.digest),
|
|
984
|
+
providerDigest: fact.digest,
|
|
985
|
+
scope: "network",
|
|
986
|
+
previousState: exception,
|
|
987
|
+
action: "canonical_replay",
|
|
988
|
+
reason: "a resend matched the retained canonical fact"
|
|
989
|
+
} },
|
|
990
|
+
{ t: "update", ns: "networkCommercialExceptions", id: id6, attrs: {
|
|
991
|
+
status: "resolved",
|
|
992
|
+
resolvedDigest: stored.digest,
|
|
993
|
+
reviewedDigest: fact.digest,
|
|
994
|
+
resolvedBy: "system",
|
|
995
|
+
reviewId,
|
|
996
|
+
updatedAt: now
|
|
997
|
+
} }
|
|
998
|
+
], { mutationId: `network-commercial-resolve:${fact.eventId}:${fact.digest}`, guards: [
|
|
999
|
+
{ ns: "networkCommercialExceptions", id: id6, exists: true, equals: { status: "active", updatedAt: exception.updatedAt } }
|
|
1000
|
+
] });
|
|
1001
|
+
const projection2 = await rebuildNetworkCommercialProjection(db, sources, `network-commercial-resolve-rebuild:${fact.eventId}:${fact.digest}`, now, alerts);
|
|
1002
|
+
return { status: "replayed", resolvedCollision: true, projection: projection2 };
|
|
1003
|
+
}
|
|
1004
|
+
if (exception?.status === "resolved" && exception.reviewedDigest === fact.digest) {
|
|
1005
|
+
return { status: "replayed", projection: await loadNetworkCommercialProjection(db, sources) };
|
|
1006
|
+
}
|
|
1007
|
+
const stale = RESOLUTION_ATTRS.filter((attr3) => exception !== void 0 && Object.hasOwn(exception, attr3));
|
|
1008
|
+
await db.transact([
|
|
1009
|
+
...stale.length ? [{ t: "retract", ns: "networkCommercialExceptions", id: id6, attrs: stale }] : [],
|
|
1010
|
+
{ t: "update", ns: "networkCommercialExceptions", id: id6, attrs: {
|
|
1011
|
+
id: id6,
|
|
930
1012
|
eventId: fact.eventId,
|
|
931
1013
|
kind: "collision",
|
|
932
1014
|
status: "active",
|
|
933
1015
|
incomingDigest: fact.digest,
|
|
1016
|
+
incomingFact: fact,
|
|
934
1017
|
detail: { eventType: fact.eventType, objectId: fact.objectId },
|
|
935
1018
|
updatedAt: now
|
|
936
|
-
} }
|
|
937
|
-
|
|
1019
|
+
} }
|
|
1020
|
+
]);
|
|
1021
|
+
const projection = await loadNetworkCommercialProjection(db, sources);
|
|
1022
|
+
if (exception?.status !== "active") {
|
|
1023
|
+
alerts?.exceptionsActivated([{ eventId: fact.eventId, kind: "collision", detail: { eventType: fact.eventType, objectId: fact.objectId } }], projection.exceptions.length);
|
|
938
1024
|
}
|
|
1025
|
+
return { status: "collision", projection };
|
|
1026
|
+
}
|
|
1027
|
+
async function ingestNetworkCommercialFact(db, sources, fact, now = Date.now(), legacyDigest, alerts) {
|
|
1028
|
+
const found = await db.query({ networkCommercialEvents: { $: { where: { eventId: fact.eventId }, limit: 1 } } });
|
|
1029
|
+
const existing = found.networkCommercialEvents?.[0];
|
|
1030
|
+
if (existing) return collide(db, sources, existing, fact, now, legacyDigest, alerts);
|
|
939
1031
|
const id6 = eventRowId(fact.eventId);
|
|
940
1032
|
let lostRace = false;
|
|
941
1033
|
let duplicate = false;
|
|
@@ -961,25 +1053,15 @@ async function ingestNetworkCommercialFact(db, sources, fact, now = Date.now(),
|
|
|
961
1053
|
if (duplicate || lostRace) {
|
|
962
1054
|
const raced = await db.query({ networkCommercialEvents: { $: { where: { eventId: fact.eventId }, limit: 1 } } });
|
|
963
1055
|
const row2 = raced.networkCommercialEvents?.[0];
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
const id7 = exceptionId(fact.eventId, "collision");
|
|
967
|
-
await db.transact([{ t: "update", ns: "networkCommercialExceptions", id: id7, attrs: {
|
|
968
|
-
id: id7,
|
|
969
|
-
eventId: fact.eventId,
|
|
970
|
-
kind: "collision",
|
|
971
|
-
status: "active",
|
|
972
|
-
incomingDigest: fact.digest,
|
|
973
|
-
detail: { eventType: fact.eventType, objectId: fact.objectId },
|
|
974
|
-
updatedAt: now
|
|
975
|
-
} }]);
|
|
976
|
-
return { status: "collision", projection: await loadNetworkCommercialProjection(db, sources) };
|
|
1056
|
+
if (!row2) throw new Error("network commercial event vanished after a lost race");
|
|
1057
|
+
return collide(db, sources, row2, fact, now, legacyDigest, alerts);
|
|
977
1058
|
}
|
|
978
1059
|
const projection = await rebuildNetworkCommercialProjection(
|
|
979
1060
|
db,
|
|
980
1061
|
sources,
|
|
981
1062
|
`network-commercial-project:${fact.eventId}:${fact.digest}`,
|
|
982
|
-
now
|
|
1063
|
+
now,
|
|
1064
|
+
alerts
|
|
983
1065
|
);
|
|
984
1066
|
return { status: "stored", projection };
|
|
985
1067
|
}
|
|
@@ -994,10 +1076,7 @@ async function readStripeRecoveryEvent(db, eventId) {
|
|
|
994
1076
|
const rawBody = JSON.stringify(response.body);
|
|
995
1077
|
return { event: response.body, rawBody, digest: await stripeEventDigest(rawBody) };
|
|
996
1078
|
}
|
|
997
|
-
|
|
998
|
-
return item && typeof item === "object" && !Array.isArray(item) ? Object.fromEntries(Object.entries(item).filter(([key3]) => key3 !== "digest").sort(([a], [b]) => a.localeCompare(b))) : item;
|
|
999
|
-
});
|
|
1000
|
-
async function recoverNetworkCommercialEvent(db, sources, eventId, actorUserId, now = Date.now()) {
|
|
1079
|
+
async function recoverNetworkCommercialEvent(db, sources, eventId, actorUserId, now = Date.now(), alerts) {
|
|
1001
1080
|
const eventRowId2 = `network-commercial-event:${eventId}`, exceptionId2 = `network-commercial-exception:${eventId}:collision`;
|
|
1002
1081
|
const rows5 = await db.query({
|
|
1003
1082
|
networkCommercialEvents: { $: { where: { id: eventRowId2 }, limit: 1 } },
|
|
@@ -1007,9 +1086,7 @@ async function recoverNetworkCommercialEvent(db, sources, eventId, actorUserId,
|
|
|
1007
1086
|
if (!stored) throw new Error("stored event not found");
|
|
1008
1087
|
const provider = await readStripeRecoveryEvent(db, eventId);
|
|
1009
1088
|
const fact = normalizeNetworkCommercialStripeEvent(provider.event, provider.digest);
|
|
1010
|
-
|
|
1011
|
-
if (!stored.fact?.seatSubscriptionItems) delete comparable.seatSubscriptionItems;
|
|
1012
|
-
if (stable(comparable) !== stable(stored.fact)) throw new Error("Stripe facts differ; collision requires investigation");
|
|
1089
|
+
if (!sameNetworkCommercialFact(stored.fact, fact)) throw new Error("Stripe facts differ; collision requires investigation");
|
|
1013
1090
|
const id6 = crypto.randomUUID();
|
|
1014
1091
|
await db.transact([
|
|
1015
1092
|
{ t: "update", ns: "paymentRecoveryReviews", id: id6, attrs: {
|
|
@@ -1017,6 +1094,7 @@ async function recoverNetworkCommercialEvent(db, sources, eventId, actorUserId,
|
|
|
1017
1094
|
eventId,
|
|
1018
1095
|
actorUserId,
|
|
1019
1096
|
reviewedAt: now,
|
|
1097
|
+
action: "recover",
|
|
1020
1098
|
previousDigest: stored.digest,
|
|
1021
1099
|
providerDigest: provider.digest,
|
|
1022
1100
|
scope: "network",
|
|
@@ -1027,7 +1105,7 @@ async function recoverNetworkCommercialEvent(db, sources, eventId, actorUserId,
|
|
|
1027
1105
|
t: "update",
|
|
1028
1106
|
ns: "networkCommercialExceptions",
|
|
1029
1107
|
id: exceptionId2,
|
|
1030
|
-
attrs: { status: "resolved", resolvedDigest: provider.digest, updatedAt: now }
|
|
1108
|
+
attrs: { status: "resolved", resolvedDigest: provider.digest, resolvedBy: actorUserId, reviewId: id6, updatedAt: now }
|
|
1031
1109
|
}] : []
|
|
1032
1110
|
], { mutationId: `payment-recovery:${id6}`, guards: [
|
|
1033
1111
|
{ ns: "networkCommercialEvents", id: eventRowId2, exists: true, equals: { digest: stored.digest } },
|
|
@@ -1038,7 +1116,7 @@ async function recoverNetworkCommercialEvent(db, sources, eventId, actorUserId,
|
|
|
1038
1116
|
...exception ? { equals: { status: exception.status, updatedAt: exception.updatedAt } } : {}
|
|
1039
1117
|
}
|
|
1040
1118
|
] });
|
|
1041
|
-
await rebuildNetworkCommercialProjection(db, sources, `payment-recovery-rebuild:${id6}`, now);
|
|
1119
|
+
await rebuildNetworkCommercialProjection(db, sources, `payment-recovery-rebuild:${id6}`, now, alerts);
|
|
1042
1120
|
return { recovered: true, eventId, reviewId: id6 };
|
|
1043
1121
|
}
|
|
1044
1122
|
|
|
@@ -1200,12 +1278,12 @@ function applicationCrmFields(crm, app) {
|
|
|
1200
1278
|
}
|
|
1201
1279
|
|
|
1202
1280
|
// src/applicant-projection.ts
|
|
1203
|
-
async function upsertPerson(
|
|
1281
|
+
async function upsertPerson(deps, opts) {
|
|
1204
1282
|
const email = opts.email.toLowerCase();
|
|
1205
|
-
const crmDeps3 = { crm:
|
|
1206
|
-
const { record: record4, created } = await ensureProjectedPerson(
|
|
1283
|
+
const crmDeps3 = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
|
|
1284
|
+
const { record: record4, created } = await ensureProjectedPerson(deps, { ...opts.input, email });
|
|
1207
1285
|
if (!created) await updateRecord(crmDeps3, { id: String(record4.id), input: opts.input, mutationId: opts.mutationId });
|
|
1208
|
-
const initial =
|
|
1286
|
+
const initial = deps.crm.initialStage("person");
|
|
1209
1287
|
if (!created && record4.stage == null && initial) {
|
|
1210
1288
|
await setStage(crmDeps3, {
|
|
1211
1289
|
id: String(record4.id),
|
|
@@ -1217,7 +1295,7 @@ async function upsertPerson(deps2, opts) {
|
|
|
1217
1295
|
}
|
|
1218
1296
|
return { recordId: String(record4.id) };
|
|
1219
1297
|
}
|
|
1220
|
-
async function projectApplicant(
|
|
1298
|
+
async function projectApplicant(deps, applicant) {
|
|
1221
1299
|
const base = sharedPersonInput({
|
|
1222
1300
|
email: applicant.email,
|
|
1223
1301
|
firstName: applicant.firstName,
|
|
@@ -1226,18 +1304,18 @@ async function projectApplicant(deps2, applicant) {
|
|
|
1226
1304
|
linkedin: applicant.linkedin,
|
|
1227
1305
|
hubRecordId: applicant.applicationId
|
|
1228
1306
|
});
|
|
1229
|
-
Object.assign(base, applicationCrmFields(
|
|
1307
|
+
Object.assign(base, applicationCrmFields(deps.crm, applicant));
|
|
1230
1308
|
const mutationId = `apply:${applicant.applicationId}`;
|
|
1231
1309
|
const extra = applicant.extra ?? {};
|
|
1232
|
-
if (Object.keys(extra).length === 0) return upsertPerson(
|
|
1310
|
+
if (Object.keys(extra).length === 0) return upsertPerson(deps, { email: applicant.email, input: base, mutationId });
|
|
1233
1311
|
try {
|
|
1234
|
-
return await upsertPerson(
|
|
1312
|
+
return await upsertPerson(deps, { email: applicant.email, input: {
|
|
1235
1313
|
...base,
|
|
1236
1314
|
...extra,
|
|
1237
|
-
...applicationCrmFields(
|
|
1315
|
+
...applicationCrmFields(deps.crm, applicant)
|
|
1238
1316
|
}, mutationId });
|
|
1239
1317
|
} catch {
|
|
1240
|
-
return upsertPerson(
|
|
1318
|
+
return upsertPerson(deps, { email: applicant.email, input: base, mutationId });
|
|
1241
1319
|
}
|
|
1242
1320
|
}
|
|
1243
1321
|
|
|
@@ -1335,24 +1413,24 @@ function sharedRecordFromCrm(crm, record4, target, sourceId) {
|
|
|
1335
1413
|
if (input[nameField] === void 0) input[nameField] = record4.name;
|
|
1336
1414
|
return sourceId ? { version: 2, source: { siteId: sourceId, recordId: record4.id }, type: record4.type, input } : { version: 1, type: record4.type, hubRecordId: record4.id, input };
|
|
1337
1415
|
}
|
|
1338
|
-
async function findSharedRecord(
|
|
1416
|
+
async function findSharedRecord(deps, record4, tag) {
|
|
1339
1417
|
const structured = await getRecordByOrigin(
|
|
1340
|
-
{ crm:
|
|
1418
|
+
{ crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId },
|
|
1341
1419
|
record4.sourceId,
|
|
1342
1420
|
record4.sourceRecordId
|
|
1343
1421
|
);
|
|
1344
1422
|
if (structured) return structured;
|
|
1345
|
-
const mapped = await
|
|
1423
|
+
const mapped = await deps.db.query({ crm_tag: { $: { where: { tag }, limit: 1 } } });
|
|
1346
1424
|
const mappedId = mapped.crm_tag?.[0]?.recordId;
|
|
1347
1425
|
if (typeof mappedId === "string") {
|
|
1348
|
-
const found = await
|
|
1426
|
+
const found = await deps.db.query({ crm_record: { $: { where: { id: mappedId }, limit: 1 } } });
|
|
1349
1427
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
1350
1428
|
}
|
|
1351
|
-
const def =
|
|
1429
|
+
const def = deps.crm.type(record4.type);
|
|
1352
1430
|
const emailField = def.emailField;
|
|
1353
1431
|
if (emailField && typeof record4.input[emailField] === "string") {
|
|
1354
1432
|
const primaryEmail = record4.input[emailField].toLowerCase();
|
|
1355
|
-
const found = await
|
|
1433
|
+
const found = await deps.db.query({
|
|
1356
1434
|
crm_record: { $: { where: { type: record4.type, primaryEmail }, limit: 1 } }
|
|
1357
1435
|
});
|
|
1358
1436
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
@@ -1360,7 +1438,7 @@ async function findSharedRecord(deps2, record4, tag) {
|
|
|
1360
1438
|
const domain = record4.input.domain;
|
|
1361
1439
|
const domainSlot = def.fields.domain?.slot;
|
|
1362
1440
|
if (typeof domain === "string" && domainSlot) {
|
|
1363
|
-
const found = await
|
|
1441
|
+
const found = await deps.db.query({
|
|
1364
1442
|
crm_record: { $: { where: { type: record4.type, [domainSlot]: domain }, limit: 1 } }
|
|
1365
1443
|
});
|
|
1366
1444
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
@@ -1368,14 +1446,14 @@ async function findSharedRecord(deps2, record4, tag) {
|
|
|
1368
1446
|
const nameField = def.nameField ?? "name";
|
|
1369
1447
|
const name = record4.input[nameField];
|
|
1370
1448
|
if (record4.type === "company" && typeof name === "string" && name.trim()) {
|
|
1371
|
-
const found = await
|
|
1449
|
+
const found = await deps.db.query({
|
|
1372
1450
|
crm_record: { $: { where: { type: record4.type, name: name.trim() }, limit: 1 } }
|
|
1373
1451
|
});
|
|
1374
1452
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
1375
1453
|
}
|
|
1376
1454
|
return void 0;
|
|
1377
1455
|
}
|
|
1378
|
-
async function projectSharedRecord(
|
|
1456
|
+
async function projectSharedRecord(deps, shared, options = {}) {
|
|
1379
1457
|
const record4 = normalizeSharedRecord(shared, options.sourceId);
|
|
1380
1458
|
if (!record4.type.trim() || !record4.sourceId.trim() || !record4.sourceRecordId.trim()) {
|
|
1381
1459
|
throw new Error("type, source site id, and source record id must be non-empty");
|
|
@@ -1384,14 +1462,14 @@ async function projectSharedRecord(deps2, shared, options = {}) {
|
|
|
1384
1462
|
throw new Error("signed sender does not match payload source");
|
|
1385
1463
|
}
|
|
1386
1464
|
const tag = networkSourceTag(record4.type, record4.sourceRecordId, record4.version === 2 ? record4.sourceId : void 0);
|
|
1387
|
-
const crmDeps3 = { crm:
|
|
1388
|
-
const existing = await findSharedRecord(
|
|
1465
|
+
const crmDeps3 = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
|
|
1466
|
+
const existing = await findSharedRecord(deps, record4, tag);
|
|
1389
1467
|
let recordId;
|
|
1390
1468
|
if (existing && typeof existing.id === "string") {
|
|
1391
1469
|
await updateRecord2(crmDeps3, { id: existing.id, input: record4.input });
|
|
1392
1470
|
recordId = existing.id;
|
|
1393
1471
|
} else if (record4.type === "person" && typeof record4.input.email === "string") {
|
|
1394
|
-
const projected = await ensureProjectedPerson(
|
|
1472
|
+
const projected = await ensureProjectedPerson(deps, record4.input);
|
|
1395
1473
|
recordId = String(projected.record.id);
|
|
1396
1474
|
if (!projected.created) await updateRecord2(crmDeps3, { id: recordId, input: record4.input });
|
|
1397
1475
|
} else {
|
|
@@ -1402,8 +1480,8 @@ async function projectSharedRecord(deps2, shared, options = {}) {
|
|
|
1402
1480
|
mutationId: `share-create:${tag}`
|
|
1403
1481
|
});
|
|
1404
1482
|
}
|
|
1405
|
-
await
|
|
1406
|
-
[{ t: "update", ns: "crm_tag", id: tag, attrs: { key: `${recordId}:${tag}`, recordId, tag, createdAt:
|
|
1483
|
+
await deps.db.transact(
|
|
1484
|
+
[{ t: "update", ns: "crm_tag", id: tag, attrs: { key: `${recordId}:${tag}`, recordId, tag, createdAt: deps.now() } }],
|
|
1407
1485
|
{ mutationId: `share-map:${tag}:${recordId}` }
|
|
1408
1486
|
);
|
|
1409
1487
|
await upsertRecordOrigin(crmDeps3, {
|
|
@@ -1451,21 +1529,21 @@ function billingColumns(app) {
|
|
|
1451
1529
|
if (billingStatus !== "canceled" && billingStatus !== "refunded" && Number.isFinite(renewal) && renewal > 0) cols.renewalAt = renewal;
|
|
1452
1530
|
return cols;
|
|
1453
1531
|
}
|
|
1454
|
-
async function syncApplicationToCrm(
|
|
1532
|
+
async function syncApplicationToCrm(deps, opts) {
|
|
1455
1533
|
if (!opts.app.id) throw new Error("crm_application_id_required");
|
|
1456
1534
|
const emailKey = str(opts.app.email).toLowerCase();
|
|
1457
1535
|
if (!emailKey) return null;
|
|
1458
|
-
const recordDeps = { crm:
|
|
1459
|
-
const input = personInputFromApp(
|
|
1536
|
+
const recordDeps = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
|
|
1537
|
+
const input = personInputFromApp(deps.chapter, opts.app);
|
|
1460
1538
|
const stage = opts.stage || void 0;
|
|
1461
|
-
const { record: existing, created } = await ensureProjectedPerson(
|
|
1539
|
+
const { record: existing, created } = await ensureProjectedPerson(deps, input, { stage });
|
|
1462
1540
|
const recordId = String(existing.id);
|
|
1463
1541
|
if (!created) await updateRecord3(recordDeps, { id: recordId, input });
|
|
1464
1542
|
if (!created && stage && existing.stage !== stage) {
|
|
1465
1543
|
await setStage2(recordDeps, { id: recordId, to: stage, authorId: "system", mutationId: `crm:stage:${recordId}:${existing.updatedAt}:${stage}` });
|
|
1466
1544
|
}
|
|
1467
1545
|
const billing = billingColumns(opts.app);
|
|
1468
|
-
await
|
|
1546
|
+
await deps.db.transact([
|
|
1469
1547
|
{ t: "update", ns: "crm_record", id: recordId, attrs: billing },
|
|
1470
1548
|
// Optional typed dates are removed with a retract, never an invalid null.
|
|
1471
1549
|
...!("renewalAt" in billing) ? [{ t: "retract", ns: "crm_record", id: recordId, attrs: ["renewalAt"] }] : []
|
|
@@ -1473,8 +1551,8 @@ async function syncApplicationToCrm(deps2, opts) {
|
|
|
1473
1551
|
await linkIdentity(recordDeps, { recordId, email: emailKey, mutationId: `crm:link:${recordId}:${emailKey}` }).catch(() => void 0);
|
|
1474
1552
|
return recordId;
|
|
1475
1553
|
}
|
|
1476
|
-
async function backfillCrm(
|
|
1477
|
-
const data = await
|
|
1554
|
+
async function backfillCrm(deps) {
|
|
1555
|
+
const data = await deps.db.query({
|
|
1478
1556
|
applications: { $: { order: { createdAt: "desc" }, limit: 1e3 } }
|
|
1479
1557
|
});
|
|
1480
1558
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -1485,7 +1563,7 @@ async function backfillCrm(deps2) {
|
|
|
1485
1563
|
if (!key3 || seen.has(key3)) return;
|
|
1486
1564
|
seen.add(key3);
|
|
1487
1565
|
try {
|
|
1488
|
-
await syncApplicationToCrm(
|
|
1566
|
+
await syncApplicationToCrm(deps, { app, stage });
|
|
1489
1567
|
synced += 1;
|
|
1490
1568
|
} catch (err) {
|
|
1491
1569
|
errors.push({ email: key3, error: err instanceof Error ? err.message : String(err) });
|
|
@@ -1493,10 +1571,10 @@ async function backfillCrm(deps2) {
|
|
|
1493
1571
|
};
|
|
1494
1572
|
for (const a of data.applications ?? []) await run(a, str(a.status));
|
|
1495
1573
|
for (let offset = 0; ; offset += 200) {
|
|
1496
|
-
const users = (await
|
|
1574
|
+
const users = (await deps.db.query({ $users: { $: { order: { id: "asc" }, limit: 200, offset } } })).$users ?? [];
|
|
1497
1575
|
for (const u of users) {
|
|
1498
1576
|
try {
|
|
1499
|
-
if (await syncAccountToCrm(
|
|
1577
|
+
if (await syncAccountToCrm(deps, String(u.id))) synced += 1;
|
|
1500
1578
|
} catch (err) {
|
|
1501
1579
|
errors.push({ email: str(u.email), error: err instanceof Error ? err.message : String(err) });
|
|
1502
1580
|
}
|
|
@@ -1536,26 +1614,32 @@ function planDelivery(input) {
|
|
|
1536
1614
|
const transport = !isProd && !redirect ? "log-only" : input.cloudflareReady ? "cloudflare" : "log-only";
|
|
1537
1615
|
const to = redirect ? input.group.debugEmail : input.to;
|
|
1538
1616
|
const subject = (redirect ? "[dev] " : "") + render(tpl.subject, vars);
|
|
1539
|
-
const
|
|
1617
|
+
const text8 = redirect ? `(dev redirect; original recipient: ${input.to})
|
|
1540
1618
|
|
|
1541
1619
|
` + render(tpl.text, vars) : render(tpl.text, vars);
|
|
1542
|
-
return { deliver: true, transport, to, subject, text:
|
|
1620
|
+
return { deliver: true, transport, to, subject, text: text8, redirected: redirect };
|
|
1543
1621
|
}
|
|
1544
1622
|
|
|
1545
1623
|
// src/notify.ts
|
|
1546
|
-
var EMAIL_TEMPLATE_NAMES = ["adminNotification", "paymentConfirmation", "prepEmail", "onboardingInvite"];
|
|
1547
|
-
|
|
1548
|
-
const
|
|
1624
|
+
var EMAIL_TEMPLATE_NAMES = ["adminNotification", "submitConfirmation", "paymentConfirmation", "prepEmail", "onboardingInvite"];
|
|
1625
|
+
function fromAddress(configured, name) {
|
|
1626
|
+
const display = name?.trim();
|
|
1627
|
+
if (!display) return configured;
|
|
1628
|
+
const angled = configured.match(/<([^>]+)>/);
|
|
1629
|
+
return { name: display, email: (angled ? angled[1] : configured).trim() };
|
|
1630
|
+
}
|
|
1631
|
+
async function sendTemplated(deps, input) {
|
|
1632
|
+
const { emailLog } = await deps.db.query({ emailLog: { $: { where: { dedupeKey: input.dedupeKey } } } });
|
|
1549
1633
|
const prior = Array.isArray(emailLog) ? emailLog : [];
|
|
1550
1634
|
if (isAlreadySent(prior)) return { sent: true, reason: "already-sent" };
|
|
1551
1635
|
if (prior.length > 1 || prior.length === 1 && prior[0].deliveryState !== "retry_ready") {
|
|
1552
1636
|
return { sent: false, reason: "delivery-uncertain" };
|
|
1553
1637
|
}
|
|
1554
|
-
const cloudflareReady = Boolean(
|
|
1555
|
-
const override =
|
|
1556
|
-
const group =
|
|
1638
|
+
const cloudflareReady = Boolean(deps.sender && deps.from);
|
|
1639
|
+
const override = deps.debugEmail?.trim();
|
|
1640
|
+
const group = deps.envName !== "prod" && deps.debugEmail !== void 0 ? { ...input.group, debugEmail: override && /^[^\s@,;<>]+@[^\s@,;<>]+\.[^\s@,;<>]+$/.test(override) ? override : void 0 } : input.group;
|
|
1557
1641
|
const decision = planDelivery({
|
|
1558
|
-
envName:
|
|
1642
|
+
envName: deps.envName,
|
|
1559
1643
|
group,
|
|
1560
1644
|
template: input.template,
|
|
1561
1645
|
to: input.to,
|
|
@@ -1580,14 +1664,14 @@ async function sendTemplated(deps2, input) {
|
|
|
1580
1664
|
transport: decision.transport,
|
|
1581
1665
|
redirected: decision.redirected,
|
|
1582
1666
|
dedupeKey: input.dedupeKey,
|
|
1583
|
-
sentAt:
|
|
1667
|
+
sentAt: deps.now(),
|
|
1584
1668
|
deliveryState: decision.transport === "log-only" ? "sent" : "sending",
|
|
1585
1669
|
deliveryAttempt: attempt,
|
|
1586
1670
|
error: decision.transport === "log-only" ? "" : "delivery-pending",
|
|
1587
1671
|
...input.applicationId ? { applicationId: input.applicationId } : {}
|
|
1588
1672
|
};
|
|
1589
1673
|
try {
|
|
1590
|
-
const claim = await
|
|
1674
|
+
const claim = await deps.db.transact([{ t: "update", ns: "emailLog", id: id6, attrs: row2 }], {
|
|
1591
1675
|
mutationId: `email-claim:${id6}:${attempt}`,
|
|
1592
1676
|
guards: [previous ? {
|
|
1593
1677
|
ns: "emailLog",
|
|
@@ -1605,8 +1689,8 @@ async function sendTemplated(deps2, input) {
|
|
|
1605
1689
|
if (decision.transport === "log-only") return { sent: true };
|
|
1606
1690
|
let messageId;
|
|
1607
1691
|
try {
|
|
1608
|
-
const response = await
|
|
1609
|
-
from:
|
|
1692
|
+
const response = await deps.sender.send({
|
|
1693
|
+
from: fromAddress(deps.from, input.group.fromName),
|
|
1610
1694
|
to: [decision.to],
|
|
1611
1695
|
subject: String(row2.subject),
|
|
1612
1696
|
text: String(row2.body),
|
|
@@ -1615,12 +1699,12 @@ async function sendTemplated(deps2, input) {
|
|
|
1615
1699
|
if (!response || typeof response.messageId !== "string" || !response.messageId) throw new Error("missing delivery receipt");
|
|
1616
1700
|
messageId = response.messageId;
|
|
1617
1701
|
} catch {
|
|
1618
|
-
await
|
|
1702
|
+
await deps.db.transact([{ t: "update", ns: "emailLog", id: id6, attrs: { deliveryState: "uncertain", error: "delivery-uncertain" } }], {
|
|
1619
1703
|
guards: [{ ns: "emailLog", id: id6, exists: true, equals: { deliveryState: "sending", deliveryAttempt: attempt } }]
|
|
1620
1704
|
});
|
|
1621
1705
|
return { sent: false, reason: "delivery-uncertain" };
|
|
1622
1706
|
}
|
|
1623
|
-
await
|
|
1707
|
+
await deps.db.transact([{ t: "update", ns: "emailLog", id: id6, attrs: { deliveryState: "sent", error: "", messageId, sentAt: deps.now() } }], {
|
|
1624
1708
|
mutationId: `email-confirmed:${id6}:${attempt}`,
|
|
1625
1709
|
guards: [{
|
|
1626
1710
|
ns: "emailLog",
|
|
@@ -1639,6 +1723,7 @@ function emailGroupFrom(row2) {
|
|
|
1639
1723
|
name: String(row2.name ?? ""),
|
|
1640
1724
|
replyTo: str3(row2.replyTo) ?? "",
|
|
1641
1725
|
debugEmail: str3(row2.debugEmail),
|
|
1726
|
+
fromName: str3(row2.fromName),
|
|
1642
1727
|
refundPolicyText: str3(row2.refundPolicyText),
|
|
1643
1728
|
commitmentText: str3(row2.commitmentText),
|
|
1644
1729
|
normsText: str3(row2.normsText),
|
|
@@ -1660,6 +1745,11 @@ function canApprove(status2, p) {
|
|
|
1660
1745
|
}
|
|
1661
1746
|
|
|
1662
1747
|
// src/membership-policy.ts
|
|
1748
|
+
function seatPurchaseDeadline(offering, member) {
|
|
1749
|
+
if (offering.seatPurchaseWindowCalendarMonths === void 0) return Number(member.termEndsAt);
|
|
1750
|
+
if (Number.isSafeInteger(member.seatPurchaseEndsAt)) return Number(member.seatPurchaseEndsAt);
|
|
1751
|
+
return addMembershipCalendarMonths(Number(member.initialTermStartedAt ?? member.termStartedAt), offering.seatPurchaseWindowCalendarMonths);
|
|
1752
|
+
}
|
|
1663
1753
|
var fail2 = (field) => {
|
|
1664
1754
|
throw new TypeError(`invalid membership policy: ${field}`);
|
|
1665
1755
|
};
|
|
@@ -1716,7 +1806,7 @@ function membershipOfferingVersion(value) {
|
|
|
1716
1806
|
autoRenew: true,
|
|
1717
1807
|
paymentGraceDays: integer2(input.paymentGraceDays, 0, 60),
|
|
1718
1808
|
seatPriceCents: integer2(input.seatPriceCents, 1, Number.MAX_SAFE_INTEGER),
|
|
1719
|
-
seatPurchaseWindowCalendarMonths: integer2(input.seatPurchaseWindowCalendarMonths, 1, 120),
|
|
1809
|
+
...input.seatPurchaseWindowCalendarMonths === void 0 || input.seatPurchaseWindowCalendarMonths === null ? {} : { seatPurchaseWindowCalendarMonths: integer2(input.seatPurchaseWindowCalendarMonths, 1, 120) },
|
|
1720
1810
|
requiredDisclosures: ids(input.requiredDisclosures)
|
|
1721
1811
|
};
|
|
1722
1812
|
}
|
|
@@ -2107,7 +2197,7 @@ async function confirmMembershipPayment(db, input, now) {
|
|
|
2107
2197
|
initialTermEndsAt: endsAt,
|
|
2108
2198
|
termStartedAt: paidAt,
|
|
2109
2199
|
termEndsAt: endsAt,
|
|
2110
|
-
seatPurchaseEndsAt:
|
|
2200
|
+
seatPurchaseEndsAt: seatPurchaseDeadline(offering, { initialTermStartedAt: paidAt, termEndsAt: endsAt }),
|
|
2111
2201
|
autoRenew: true,
|
|
2112
2202
|
refundedCents: 0,
|
|
2113
2203
|
revision: 1,
|
|
@@ -2764,24 +2854,22 @@ async function revokeRefundedApplicationRole(db, env, ctx, applicationId) {
|
|
|
2764
2854
|
}
|
|
2765
2855
|
}
|
|
2766
2856
|
|
|
2767
|
-
// src/
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
});
|
|
2781
|
-
async function paymentConfirmationVariables(db, app) {
|
|
2857
|
+
// src/notification-variables.ts
|
|
2858
|
+
function lifecycleNotifyDeps(db, env) {
|
|
2859
|
+
return {
|
|
2860
|
+
db,
|
|
2861
|
+
envName: env.ODLA_ENV,
|
|
2862
|
+
sender: env.SEND_EMAIL,
|
|
2863
|
+
from: env.EMAIL_FROM,
|
|
2864
|
+
debugEmail: env.ODLA_DEBUG_EMAIL,
|
|
2865
|
+
now: () => Date.now(),
|
|
2866
|
+
newId: () => crypto.randomUUID()
|
|
2867
|
+
};
|
|
2868
|
+
}
|
|
2869
|
+
async function frozenCheckoutVariables(db, app) {
|
|
2782
2870
|
const intent = (await db.query({ subscriptionCheckoutIntents: { $: { where: { id: String(app.id) }, limit: 1 } } })).subscriptionCheckoutIntents?.[0];
|
|
2783
2871
|
const offer = intent?.membershipOffer, contract = offer?.contract;
|
|
2784
|
-
const offering = contract?.offering;
|
|
2872
|
+
const offering = contract?.offering, quote = intent?.paymentQuote;
|
|
2785
2873
|
const amount = contract?.dueTodayCents ?? intent?.priceCents;
|
|
2786
2874
|
const currency = offering?.currency ?? intent?.currency;
|
|
2787
2875
|
let formatted = "";
|
|
@@ -2789,9 +2877,75 @@ async function paymentConfirmationVariables(db, app) {
|
|
|
2789
2877
|
formatted = new Intl.NumberFormat("en-US", { style: "currency", currency: currency.toUpperCase() }).format(Number(amount) / 100);
|
|
2790
2878
|
}
|
|
2791
2879
|
return {
|
|
2792
|
-
firstName: typeof app.firstName === "string" ? app.firstName : "",
|
|
2793
2880
|
paymentAmount: formatted,
|
|
2794
2881
|
paymentAmountSummary: formatted ? ` of ${formatted}` : "",
|
|
2882
|
+
tierName: typeof quote?.tierName === "string" ? quote.tierName : ""
|
|
2883
|
+
};
|
|
2884
|
+
}
|
|
2885
|
+
async function catalogTierName(db, group, app) {
|
|
2886
|
+
try {
|
|
2887
|
+
return findTier(await loadTiers(db, group), typeof app.tierId === "string" ? app.tierId : null)?.name ?? "";
|
|
2888
|
+
} catch {
|
|
2889
|
+
return "";
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
async function checkoutVariables(db, group, app) {
|
|
2893
|
+
const frozen = await frozenCheckoutVariables(db, app);
|
|
2894
|
+
return frozen.tierName ? frozen : { ...frozen, tierName: await catalogTierName(db, group, app) };
|
|
2895
|
+
}
|
|
2896
|
+
function formatInstant(at, timeZone) {
|
|
2897
|
+
return new Intl.DateTimeFormat("en-US", {
|
|
2898
|
+
weekday: "long",
|
|
2899
|
+
month: "long",
|
|
2900
|
+
day: "numeric",
|
|
2901
|
+
year: "numeric",
|
|
2902
|
+
hour: "numeric",
|
|
2903
|
+
minute: "2-digit",
|
|
2904
|
+
timeZoneName: "short",
|
|
2905
|
+
timeZone
|
|
2906
|
+
}).format(new Date(at));
|
|
2907
|
+
}
|
|
2908
|
+
var adminUrlFor = (origin) => `${origin}/admin/`;
|
|
2909
|
+
var membersUrlFor = (origin) => `${origin}/members/`;
|
|
2910
|
+
function applicantVariables(app) {
|
|
2911
|
+
const text8 = (v) => typeof v === "string" ? v : "";
|
|
2912
|
+
return { firstName: text8(app.firstName), lastName: text8(app.lastName), email: text8(app.email), phone: text8(app.phone), state: text8(app.state) };
|
|
2913
|
+
}
|
|
2914
|
+
function sampleVariables(origin, timeZone, now = Date.now()) {
|
|
2915
|
+
const startAt = formatInstant(now + 864e5, timeZone);
|
|
2916
|
+
return {
|
|
2917
|
+
firstName: "Sample",
|
|
2918
|
+
lastName: "Person",
|
|
2919
|
+
email: "sample@example.com",
|
|
2920
|
+
phone: "(555) 010-0100",
|
|
2921
|
+
state: "CA",
|
|
2922
|
+
tier: "Membership",
|
|
2923
|
+
tierName: "Membership",
|
|
2924
|
+
paymentAmount: "$100.00",
|
|
2925
|
+
paymentAmountSummary: " of $100.00",
|
|
2926
|
+
paymentNextStep: "Next, schedule your introduction call.",
|
|
2927
|
+
submitNextStep: "Next, schedule your introduction call.",
|
|
2928
|
+
startAt,
|
|
2929
|
+
endAt: formatInstant(now + 864e5 + 45 * 6e4, timeZone),
|
|
2930
|
+
timezone: timeZone,
|
|
2931
|
+
meetUrl: "https://meet.google.com/sample",
|
|
2932
|
+
htmlLink: "https://calendar.google.com/event?eid=sample",
|
|
2933
|
+
meetingTime: startAt,
|
|
2934
|
+
meetingLink: "https://meet.google.com/sample",
|
|
2935
|
+
adminUrl: adminUrlFor(origin),
|
|
2936
|
+
membersUrl: membersUrlFor(origin)
|
|
2937
|
+
};
|
|
2938
|
+
}
|
|
2939
|
+
|
|
2940
|
+
// src/payment-notifications.ts
|
|
2941
|
+
var groupFor = async (db, app) => {
|
|
2942
|
+
const result = await db.query({ groups: { $: { where: { id: String(app.groupId ?? "") }, limit: 1 } } });
|
|
2943
|
+
return result.groups?.[0];
|
|
2944
|
+
};
|
|
2945
|
+
async function paymentConfirmationVariables(db, app) {
|
|
2946
|
+
return {
|
|
2947
|
+
firstName: typeof app.firstName === "string" ? app.firstName : "",
|
|
2948
|
+
...await frozenCheckoutVariables(db, app),
|
|
2795
2949
|
paymentNextStep: app.status === "approved" ? "Your membership is approved." : typeof app.interviewWaivedAt === "number" ? "Your interview is waived. We are completing your membership approval." : "Next, schedule your introduction call."
|
|
2796
2950
|
};
|
|
2797
2951
|
}
|
|
@@ -2799,7 +2953,7 @@ async function notifyPaymentConfirmed(db, env, eventId, app) {
|
|
|
2799
2953
|
if (typeof app.email !== "string" || !app.email) throw new Error("payment notice recipient missing");
|
|
2800
2954
|
const group = await groupFor(db, app);
|
|
2801
2955
|
if (!group) throw new Error("payment notice configuration missing");
|
|
2802
|
-
const result = await sendTemplated(
|
|
2956
|
+
const result = await sendTemplated(lifecycleNotifyDeps(db, env), {
|
|
2803
2957
|
group: emailGroupFrom(group),
|
|
2804
2958
|
template: "paymentConfirmation",
|
|
2805
2959
|
to: app.email,
|
|
@@ -2809,21 +2963,20 @@ async function notifyPaymentConfirmed(db, env, eventId, app) {
|
|
|
2809
2963
|
});
|
|
2810
2964
|
if (!result.sent && !["disabled", "template-missing"].includes(result.reason ?? "")) throw new Error("payment notice reconciliation pending");
|
|
2811
2965
|
}
|
|
2812
|
-
async function notifyAdminOfPayment(db, env, eventId, app) {
|
|
2966
|
+
async function notifyAdminOfPayment(db, env, eventId, app, origin = "") {
|
|
2813
2967
|
try {
|
|
2814
2968
|
const group = await groupFor(db, app);
|
|
2815
2969
|
if (!group || typeof group.notificationEmail !== "string" || !group.notificationEmail) return;
|
|
2816
|
-
const
|
|
2817
|
-
await sendTemplated(
|
|
2970
|
+
const { tierName: tierName2 } = await frozenCheckoutVariables(db, app);
|
|
2971
|
+
await sendTemplated(lifecycleNotifyDeps(db, env), {
|
|
2818
2972
|
group: emailGroupFrom(group),
|
|
2819
2973
|
template: "adminNotification",
|
|
2820
2974
|
to: group.notificationEmail,
|
|
2821
2975
|
vars: {
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
state: text6(app.state)
|
|
2976
|
+
...applicantVariables(app),
|
|
2977
|
+
tier: tierName2,
|
|
2978
|
+
tierName: tierName2,
|
|
2979
|
+
...origin ? { adminUrl: adminUrlFor(origin), membersUrl: membersUrlFor(origin) } : {}
|
|
2827
2980
|
},
|
|
2828
2981
|
dedupeKey: `${eventId}:admin`,
|
|
2829
2982
|
applicationId: String(app.id)
|
|
@@ -3143,6 +3296,26 @@ function reportStripePartition(level, eventId, event, disposition) {
|
|
|
3143
3296
|
...stripeEventIdentity(event)
|
|
3144
3297
|
}));
|
|
3145
3298
|
}
|
|
3299
|
+
var QUARANTINE_DISPOSITIONS = ["quarantine_unmatched", "quarantine_collision"];
|
|
3300
|
+
var QUEUE_LIMIT = 500;
|
|
3301
|
+
var QUEUE_IDS = 50;
|
|
3302
|
+
async function alertStripeQuarantine(db, ctx, eventId, disposition) {
|
|
3303
|
+
const pages = await Promise.all(QUARANTINE_DISPOSITIONS.map(async (value) => (await db.query({ stripeEventReceipts: { $: { where: { disposition: value }, limit: QUEUE_LIMIT } } })).stripeEventReceipts ?? []));
|
|
3304
|
+
const queued = pages.flat();
|
|
3305
|
+
reportChapterAlert(ctx, new Error("stripe webhook quarantined"), {
|
|
3306
|
+
code: "stripe_webhook_quarantined",
|
|
3307
|
+
attributes: {
|
|
3308
|
+
"odla.stripe.disposition": disposition,
|
|
3309
|
+
"odla.stripe.quarantined_count": queued.length,
|
|
3310
|
+
"odla.stripe.queue_truncated": pages.some((page) => page.length >= QUEUE_LIMIT)
|
|
3311
|
+
},
|
|
3312
|
+
artifacts: { eventId, eventIds: queued.slice(0, QUEUE_IDS).map((row2) => String(row2.eventId)) }
|
|
3313
|
+
});
|
|
3314
|
+
}
|
|
3315
|
+
async function reportStripeCollision(db, ctx, eventId, event) {
|
|
3316
|
+
reportStripePartition("error", eventId, event, "quarantine_collision");
|
|
3317
|
+
await alertStripeQuarantine(db, ctx, eventId, "quarantine_collision");
|
|
3318
|
+
}
|
|
3146
3319
|
async function storeStripeReceipt(db, input) {
|
|
3147
3320
|
const existing = await receiptFor(db, input.eventId);
|
|
3148
3321
|
if (existing?.retryReady === true && existing.reviewedDigest === input.digest) {
|
|
@@ -3175,17 +3348,18 @@ async function storeStripeReceipt(db, input) {
|
|
|
3175
3348
|
const raced = await receiptFor(db, input.eventId);
|
|
3176
3349
|
return raced?.digest === input.digest || raced?.digest === input.legacyDigest ? "replayed" : "collision";
|
|
3177
3350
|
}
|
|
3178
|
-
async function quarantineStripeEvent(db, eventId, digest, event) {
|
|
3351
|
+
async function quarantineStripeEvent(db, eventId, digest, event, ctx) {
|
|
3179
3352
|
const disposition = "quarantine_unmatched";
|
|
3180
3353
|
const stored = await storeStripeReceipt(db, { eventId, digest, event, disposition, matched: false });
|
|
3181
3354
|
if (stored === "collision") {
|
|
3182
|
-
|
|
3355
|
+
await reportStripeCollision(db, ctx, eventId, event);
|
|
3183
3356
|
return json({ ok: true, matched: false, collision: true });
|
|
3184
3357
|
}
|
|
3185
3358
|
reportStripePartition("warn", eventId, event, disposition);
|
|
3359
|
+
if (stored === "stored") await alertStripeQuarantine(db, ctx, eventId, disposition);
|
|
3186
3360
|
return json({ ok: true, matched: false, disposition, ...stored === "replayed" ? { replayed: true } : {} });
|
|
3187
3361
|
}
|
|
3188
|
-
async function replayedStripeReceipt(db, eventId, digest, event, legacyDigest) {
|
|
3362
|
+
async function replayedStripeReceipt(db, eventId, digest, event, legacyDigest, ctx) {
|
|
3189
3363
|
const existing = await receiptFor(db, eventId);
|
|
3190
3364
|
if (!existing) return null;
|
|
3191
3365
|
if (existing.retryReady === true && existing.reviewedDigest === digest) return null;
|
|
@@ -3195,7 +3369,7 @@ async function replayedStripeReceipt(db, eventId, digest, event, legacyDigest) {
|
|
|
3195
3369
|
collisionDigest: digest,
|
|
3196
3370
|
retryReady: false
|
|
3197
3371
|
} }], { guards: [{ ns: "stripeEventReceipts", id: String(existing.id), exists: true, equals: { digest: existing.digest } }] });
|
|
3198
|
-
|
|
3372
|
+
await reportStripeCollision(db, ctx, eventId, event);
|
|
3199
3373
|
return json({ ok: true, matched: false, collision: true });
|
|
3200
3374
|
}
|
|
3201
3375
|
return json({ ok: true, matched: existing.matched === true, replayed: true });
|
|
@@ -3226,16 +3400,16 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
|
|
|
3226
3400
|
reportStripePartition("warn", eventId, event, "ignore_sibling");
|
|
3227
3401
|
return json({ ok: true, matched: false, disposition: "ignore_sibling" });
|
|
3228
3402
|
}
|
|
3229
|
-
const replay2 = await replayedStripeReceipt(db, eventId, digest, event, legacyDigest);
|
|
3403
|
+
const replay2 = await replayedStripeReceipt(db, eventId, digest, event, legacyDigest, ctx);
|
|
3230
3404
|
if (replay2) return replay2;
|
|
3231
3405
|
const localIntent = Boolean(intent && intent.chapterId === ctx.chapter.id && intent.environment === env.ODLA_ENV && (typeof intent.runtime !== "string" || intent.runtime === target.runtime));
|
|
3232
3406
|
const disposition2 = classifyMemberStripeEvent(identity, target, localIntent);
|
|
3233
3407
|
if (!localIntent || disposition2 === "quarantine_unmatched") {
|
|
3234
|
-
return quarantineStripeEvent(db, eventId, digest, event);
|
|
3408
|
+
return quarantineStripeEvent(db, eventId, digest, event, ctx);
|
|
3235
3409
|
}
|
|
3236
3410
|
if (event.kind === "gift_paid") {
|
|
3237
3411
|
if (intent.providerObjectId !== event.providerObjectId || intent.status === "refunded" || intent.status === "canceled") {
|
|
3238
|
-
return quarantineStripeEvent(db, eventId, digest, event);
|
|
3412
|
+
return quarantineStripeEvent(db, eventId, digest, event, ctx);
|
|
3239
3413
|
}
|
|
3240
3414
|
try {
|
|
3241
3415
|
const issued = await issueGiftClaim(db, intent, {
|
|
@@ -3244,7 +3418,7 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
|
|
|
3244
3418
|
}, Date.now());
|
|
3245
3419
|
const stored3 = await storeStripeReceipt(db, { eventId, digest, legacyDigest, event, disposition: disposition2, matched: true });
|
|
3246
3420
|
if (stored3 === "collision") {
|
|
3247
|
-
|
|
3421
|
+
await reportStripeCollision(db, ctx, eventId, event);
|
|
3248
3422
|
return json({ ok: true, matched: false, collision: true });
|
|
3249
3423
|
}
|
|
3250
3424
|
return json({ ok: true, matched: true, claimId: issued.claimId });
|
|
@@ -3260,7 +3434,7 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
|
|
|
3260
3434
|
if (claim) await revokeGiftClaim(db, claim, event.reason, Date.now());
|
|
3261
3435
|
const stored2 = await storeStripeReceipt(db, { eventId, digest, legacyDigest, event, disposition: disposition2, matched: Boolean(claim) });
|
|
3262
3436
|
if (stored2 === "collision") {
|
|
3263
|
-
|
|
3437
|
+
await reportStripeCollision(db, ctx, eventId, event);
|
|
3264
3438
|
return json({ ok: true, matched: false, collision: true });
|
|
3265
3439
|
}
|
|
3266
3440
|
return json({ ok: true, matched: Boolean(claim) });
|
|
@@ -3270,12 +3444,12 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
|
|
|
3270
3444
|
reportStripePartition("warn", eventId, event, "ignore_sibling");
|
|
3271
3445
|
return json({ ok: true, matched: false, disposition: "ignore_sibling" });
|
|
3272
3446
|
}
|
|
3273
|
-
const replay = await replayedStripeReceipt(db, eventId, digest, event, legacyDigest);
|
|
3447
|
+
const replay = await replayedStripeReceipt(db, eventId, digest, event, legacyDigest, ctx);
|
|
3274
3448
|
if (replay) return replay;
|
|
3275
3449
|
const localApplication = Boolean(app && String(app.groupId ?? ctx.chapter.id) === ctx.chapter.id && (typeof app.stripeRuntime !== "string" || app.stripeRuntime === target.runtime));
|
|
3276
3450
|
const disposition = classifyMemberStripeEvent(identity, target, localApplication);
|
|
3277
3451
|
if (!localApplication || disposition === "quarantine_unmatched") {
|
|
3278
|
-
return quarantineStripeEvent(db, eventId, digest, event);
|
|
3452
|
+
return quarantineStripeEvent(db, eventId, digest, event, ctx);
|
|
3279
3453
|
}
|
|
3280
3454
|
if (await archivedChapterMembershipEvent(db, rawBody, app, env, ctx)) {
|
|
3281
3455
|
await storeStripeReceipt(db, { eventId, digest, legacyDigest, event, disposition, matched: true });
|
|
@@ -3292,7 +3466,7 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
|
|
|
3292
3466
|
if (receipt.seatId !== event.seatId || receipt.reconciled !== true) throw new Error("invalid seat receipt");
|
|
3293
3467
|
const stored2 = await storeStripeReceipt(db, { eventId, digest, legacyDigest, event, disposition, matched: true });
|
|
3294
3468
|
if (stored2 === "collision") {
|
|
3295
|
-
|
|
3469
|
+
await reportStripeCollision(db, ctx, eventId, event);
|
|
3296
3470
|
return json({ ok: true, matched: false, collision: true });
|
|
3297
3471
|
}
|
|
3298
3472
|
return json({ ok: true, seatId: event.seatId });
|
|
@@ -3316,7 +3490,7 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
|
|
|
3316
3490
|
if (intent) {
|
|
3317
3491
|
const evidence = verifyInitialMembershipInvoice(intent, event.invoice);
|
|
3318
3492
|
if (evidence.status === "retry") return json({ error: "checkout reconciliation pending" }, 503);
|
|
3319
|
-
if (evidence.status === "mismatch") return quarantineStripeEvent(db, eventId, digest, event);
|
|
3493
|
+
if (evidence.status === "mismatch") return quarantineStripeEvent(db, eventId, digest, event, ctx);
|
|
3320
3494
|
if (evidence.renewalAt !== void 0) authoritativeEvent = { ...event, renewalAt: evidence.renewalAt };
|
|
3321
3495
|
}
|
|
3322
3496
|
if (app.canceled !== true && !["refunded", "declined", "canceled"].includes(String(app.status))) {
|
|
@@ -3367,7 +3541,7 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
|
|
|
3367
3541
|
} catch {
|
|
3368
3542
|
return json({ error: "payment notice reconciliation pending" }, 503);
|
|
3369
3543
|
}
|
|
3370
|
-
if (ctx.chapter.sends.adminNotification === "payment") await notifyAdminOfPayment(db, env, eventId, app);
|
|
3544
|
+
if (ctx.chapter.sends.adminNotification === "payment") await notifyAdminOfPayment(db, env, eventId, app, input.origin);
|
|
3371
3545
|
if (typeof next.interviewWaivedAt === "number") {
|
|
3372
3546
|
const approval = await approveApplication({
|
|
3373
3547
|
db,
|
|
@@ -3382,15 +3556,132 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
|
|
|
3382
3556
|
}
|
|
3383
3557
|
const stored = await storeStripeReceipt(db, { eventId, digest, legacyDigest, event, disposition, matched: true });
|
|
3384
3558
|
if (stored === "collision") {
|
|
3385
|
-
|
|
3559
|
+
await reportStripeCollision(db, ctx, eventId, event);
|
|
3386
3560
|
return json({ ok: true, matched: false, collision: true });
|
|
3387
3561
|
}
|
|
3388
3562
|
return json({ ok: true });
|
|
3389
3563
|
}
|
|
3390
3564
|
|
|
3565
|
+
// src/network-commercial-resolve.ts
|
|
3566
|
+
var EVENT_ID = /^evt_[A-Za-z0-9_]{1,150}$/;
|
|
3567
|
+
var fail3 = (message, code) => Object.assign(new Error(message), { code });
|
|
3568
|
+
async function resolveNetworkCommercialCollision(db, sources, input, now = Date.now(), alerts) {
|
|
3569
|
+
if (!EVENT_ID.test(input.eventId)) throw fail3("invalid event reference", "invalid_event");
|
|
3570
|
+
const id6 = exceptionId(input.eventId, "collision"), reviewId = `resolve:${input.mutationId}`;
|
|
3571
|
+
const found = await db.query({
|
|
3572
|
+
networkCommercialExceptions: { $: { where: { id: id6 }, limit: 1 } },
|
|
3573
|
+
networkCommercialEvents: { $: { where: { eventId: input.eventId }, limit: 1 } }
|
|
3574
|
+
});
|
|
3575
|
+
const exception = found.networkCommercialExceptions?.[0], stored = found.networkCommercialEvents?.[0];
|
|
3576
|
+
if (exception?.status === "resolved" && exception.reviewId === reviewId) {
|
|
3577
|
+
return { eventId: input.eventId, reviewId, projection: await loadNetworkCommercialProjection(db, sources) };
|
|
3578
|
+
}
|
|
3579
|
+
if (!exception || exception.status !== "active") throw fail3("no active collision for this event", "collision_not_active");
|
|
3580
|
+
if (!stored) throw fail3("stored event not found", "event_not_found");
|
|
3581
|
+
const dismissed = typeof exception.incomingDigest === "string" ? exception.incomingDigest : void 0;
|
|
3582
|
+
const write = await db.transact([
|
|
3583
|
+
{ t: "update", ns: "paymentRecoveryReviews", id: reviewId, attrs: {
|
|
3584
|
+
id: reviewId,
|
|
3585
|
+
eventId: input.eventId,
|
|
3586
|
+
actorUserId: input.actorUserId,
|
|
3587
|
+
reviewedAt: now,
|
|
3588
|
+
previousDigest: String(stored.digest),
|
|
3589
|
+
providerDigest: dismissed ?? "",
|
|
3590
|
+
scope: "network",
|
|
3591
|
+
previousState: exception,
|
|
3592
|
+
action: "resolve",
|
|
3593
|
+
reason: input.reason
|
|
3594
|
+
} },
|
|
3595
|
+
{ t: "update", ns: "networkCommercialExceptions", id: id6, attrs: {
|
|
3596
|
+
status: "resolved",
|
|
3597
|
+
resolvedDigest: stored.digest,
|
|
3598
|
+
...dismissed ? { reviewedDigest: dismissed } : {},
|
|
3599
|
+
resolvedBy: input.actorUserId,
|
|
3600
|
+
reviewId,
|
|
3601
|
+
updatedAt: now
|
|
3602
|
+
} }
|
|
3603
|
+
], { mutationId: input.mutationId, guards: [
|
|
3604
|
+
{ ns: "networkCommercialExceptions", id: id6, exists: true, equals: { status: "active", updatedAt: exception.updatedAt } }
|
|
3605
|
+
] });
|
|
3606
|
+
if (write.duplicate) throw fail3("mutationId was already used for another change", "mutation_reused");
|
|
3607
|
+
const projection = await rebuildNetworkCommercialProjection(db, sources, `${input.mutationId}:rebuild`, now, alerts);
|
|
3608
|
+
return { eventId: input.eventId, reviewId, projection };
|
|
3609
|
+
}
|
|
3610
|
+
async function retryNetworkCommercialEvent(db, sources, eventId, actorUserId, now = Date.now(), alerts) {
|
|
3611
|
+
if (!EVENT_ID.test(eventId)) throw fail3("invalid event reference", "invalid_event");
|
|
3612
|
+
const active = (await loadNetworkCommercialProjection(db, sources)).exceptions.filter((item) => item.eventId === eventId);
|
|
3613
|
+
if (active.length === 0) throw fail3("event has no active exception", "exception_not_active");
|
|
3614
|
+
const provider = await readStripeRecoveryEvent(db, eventId);
|
|
3615
|
+
const stored = (await db.query({ networkCommercialEvents: { $: { where: { eventId }, limit: 1 } } })).networkCommercialEvents?.[0];
|
|
3616
|
+
const reviewId = crypto.randomUUID();
|
|
3617
|
+
await db.transact([{ t: "update", ns: "paymentRecoveryReviews", id: reviewId, attrs: {
|
|
3618
|
+
id: reviewId,
|
|
3619
|
+
eventId,
|
|
3620
|
+
actorUserId,
|
|
3621
|
+
reviewedAt: now,
|
|
3622
|
+
previousDigest: String(stored?.digest ?? ""),
|
|
3623
|
+
providerDigest: provider.digest,
|
|
3624
|
+
scope: "network",
|
|
3625
|
+
previousState: { exceptions: active },
|
|
3626
|
+
action: "retry"
|
|
3627
|
+
} }], { mutationId: `payment-recovery:${reviewId}` });
|
|
3628
|
+
const fact = normalizeNetworkCommercialStripeEvent(provider.event, provider.digest);
|
|
3629
|
+
const result = await ingestNetworkCommercialFact(db, sources, fact, now, await stripePayloadDigest(provider.rawBody), alerts);
|
|
3630
|
+
return {
|
|
3631
|
+
eventId,
|
|
3632
|
+
reviewId,
|
|
3633
|
+
status: result.status,
|
|
3634
|
+
resolvedCollision: result.status === "replayed" && result.resolvedCollision === true,
|
|
3635
|
+
exceptions: result.projection.exceptions.length
|
|
3636
|
+
};
|
|
3637
|
+
}
|
|
3638
|
+
|
|
3639
|
+
// src/worker-routes-network-commercial-recovery.ts
|
|
3640
|
+
var PATH = "/api/admin/payment-recovery";
|
|
3641
|
+
var handleNetworkCommercialRecovery = async (req, url, env, ctx) => {
|
|
3642
|
+
const model = ctx.chapter.networkCommercialReadModel;
|
|
3643
|
+
if (url.pathname !== PATH || !model) return null;
|
|
3644
|
+
if (req.method !== "GET" && req.method !== "POST") return json({ error: "method not allowed" }, 405);
|
|
3645
|
+
const raw2 = ctx.makeDb(env), user = await ctx.verifyUser(req, env);
|
|
3646
|
+
if (!user) return json({ error: "unauthorized" }, 401);
|
|
3647
|
+
if (!await ctx.isSuperAdminEmail(raw2, user.email, user.userId)) return json({ error: "forbidden" }, 403);
|
|
3648
|
+
const db = raw2;
|
|
3649
|
+
if (req.method === "GET") {
|
|
3650
|
+
const { exceptions } = await loadNetworkCommercialProjection(db, model.sources);
|
|
3651
|
+
return json({
|
|
3652
|
+
scope: "network",
|
|
3653
|
+
count: exceptions.length,
|
|
3654
|
+
truncated: false,
|
|
3655
|
+
rows: exceptions.map((item) => ({
|
|
3656
|
+
eventId: item.eventId,
|
|
3657
|
+
kind: item.detail.eventType,
|
|
3658
|
+
disposition: item.kind,
|
|
3659
|
+
sourceKey: item.sourceKey ?? null,
|
|
3660
|
+
retryReady: /^evt_/.test(item.eventId)
|
|
3661
|
+
}))
|
|
3662
|
+
});
|
|
3663
|
+
}
|
|
3664
|
+
let eventId;
|
|
3665
|
+
try {
|
|
3666
|
+
eventId = String((await req.json()).eventId ?? "");
|
|
3667
|
+
} catch {
|
|
3668
|
+
return json({ error: "eventId required" }, 400);
|
|
3669
|
+
}
|
|
3670
|
+
try {
|
|
3671
|
+
const retried = await retryNetworkCommercialEvent(db, model.sources, eventId, user.userId, Date.now(), networkCommercialAlerts(ctx, PATH));
|
|
3672
|
+
return json({ ok: true, ...retried });
|
|
3673
|
+
} catch (error) {
|
|
3674
|
+
const code = error.code;
|
|
3675
|
+
if (code === "invalid_event") return json({ error: "invalid event reference" }, 400);
|
|
3676
|
+
if (code === "exception_not_active") return json({ error: "event has no active exception" }, 409);
|
|
3677
|
+
return json({ error: "Event recovery is pending. Check its provider evidence and retry." }, 503);
|
|
3678
|
+
}
|
|
3679
|
+
};
|
|
3680
|
+
|
|
3391
3681
|
// src/worker-routes-payment-recovery.ts
|
|
3392
3682
|
var handlePaymentRecovery = async (req, url, env, ctx) => {
|
|
3393
|
-
if (url.pathname !== "/api/admin/payment-recovery"
|
|
3683
|
+
if (url.pathname !== "/api/admin/payment-recovery") return null;
|
|
3684
|
+
if (ctx.chapter.mode !== "chapter") return handleNetworkCommercialRecovery(req, url, env, ctx);
|
|
3394
3685
|
if (!["GET", "POST"].includes(req.method)) return json({ error: "method not allowed" }, 405);
|
|
3395
3686
|
const raw2 = ctx.makeDb(env), user = await ctx.verifyUser(req, env);
|
|
3396
3687
|
if (!user) return json({ error: "unauthorized" }, 401);
|
|
@@ -3452,10 +3743,10 @@ var handlePaymentRecovery = async (req, url, env, ctx) => {
|
|
|
3452
3743
|
};
|
|
3453
3744
|
|
|
3454
3745
|
// src/worker-routes-admin-named-seat.ts
|
|
3455
|
-
var
|
|
3746
|
+
var PATH2 = /^\/api\/admin\/applications\/([0-9a-f-]+)\/named-seat(\/refund)?$/;
|
|
3456
3747
|
var REFUNDABLE = /* @__PURE__ */ new Set(["renewal_pending", "pending_acceptance", "pending_approval", "pending_primary", "active"]);
|
|
3457
3748
|
var handleAdminNamedSeat = async (req, url, env, ctx) => {
|
|
3458
|
-
const match = url.pathname.match(
|
|
3749
|
+
const match = url.pathname.match(PATH2);
|
|
3459
3750
|
if (!match) return null;
|
|
3460
3751
|
const seats = ctx.membershipAuthority?.namedSeats;
|
|
3461
3752
|
if (ctx.chapter.mode !== "chapter" || !seats) return json({ error: "named seats are not configured" }, 404);
|
|
@@ -3508,42 +3799,42 @@ async function closeRecipientApplication(db, env, ctx, seatId) {
|
|
|
3508
3799
|
|
|
3509
3800
|
// src/membership-seat-refund-stripe.ts
|
|
3510
3801
|
import { stripeRequest as stripeRequest4 } from "@odla-ai/stripe";
|
|
3511
|
-
var
|
|
3802
|
+
var fail4 = (code = "named_seat_refund_mismatch") => {
|
|
3512
3803
|
throw Object.assign(new Error(code), { code });
|
|
3513
3804
|
};
|
|
3514
3805
|
async function refundNamedSeatStripePayment(secretKey, seat) {
|
|
3515
|
-
if (!seat.providerPaymentIntentId || seat.status !== "refund_pending")
|
|
3806
|
+
if (!seat.providerPaymentIntentId || seat.status !== "refund_pending") fail4();
|
|
3516
3807
|
const call = async (method, path, params, idempotencyKey) => {
|
|
3517
3808
|
const result = await stripeRequest4({ secretKey }, method, path, params, idempotencyKey ? { idempotencyKey } : {});
|
|
3518
|
-
if (!result.ok || !result.body) return
|
|
3809
|
+
if (!result.ok || !result.body) return fail4("named_seat_refund_provider_unavailable");
|
|
3519
3810
|
return result.body;
|
|
3520
3811
|
};
|
|
3521
3812
|
const metadata = { ...stripePartitionMetadata(seat), applicationId: seat.applicationId, membershipId: seat.primaryMembershipId, seatId: seat.id };
|
|
3522
3813
|
const payment = await call("GET", `/v1/payment_intents/${encodeURIComponent(seat.providerPaymentIntentId)}`);
|
|
3523
|
-
if (payment.id !== seat.providerPaymentIntentId || payment.customer !== seat.providerCustomerId || payment.amount !== seat.amountCents || payment.amount_received !== seat.amountCents || payment.currency !== seat.currency || payment.livemode !== (seat.environment === "prod") || payment.status !== "succeeded" || !Object.entries(metadata).every(([key3, value]) => payment.metadata?.[key3] === value))
|
|
3814
|
+
if (payment.id !== seat.providerPaymentIntentId || payment.customer !== seat.providerCustomerId || payment.amount !== seat.amountCents || payment.amount_received !== seat.amountCents || payment.currency !== seat.currency || payment.livemode !== (seat.environment === "prod") || payment.status !== "succeeded" || !Object.entries(metadata).every(([key3, value]) => payment.metadata?.[key3] === value)) fail4();
|
|
3524
3815
|
const sub = await call("GET", `/v1/subscriptions/${encodeURIComponent(seat.providerSubscriptionId)}`);
|
|
3525
|
-
if (sub.id !== seat.providerSubscriptionId || sub.customer !== seat.providerCustomerId || sub.livemode !== (seat.environment === "prod") || !Object.entries({ ...stripePartitionMetadata(seat), applicationId: seat.applicationId }).every(([key3, value]) => sub.metadata?.[key3] === value) || sub.items?.has_more !== false || !Array.isArray(sub.items?.data))
|
|
3816
|
+
if (sub.id !== seat.providerSubscriptionId || sub.customer !== seat.providerCustomerId || sub.livemode !== (seat.environment === "prod") || !Object.entries({ ...stripePartitionMetadata(seat), applicationId: seat.applicationId }).every(([key3, value]) => sub.metadata?.[key3] === value) || sub.items?.has_more !== false || !Array.isArray(sub.items?.data)) fail4();
|
|
3526
3817
|
const items = sub.items.data.filter((item) => item.metadata?.seatId === seat.id);
|
|
3527
|
-
if (items.length > 1)
|
|
3818
|
+
if (items.length > 1) fail4();
|
|
3528
3819
|
if (items.length && sub.status !== "canceled") {
|
|
3529
3820
|
const item = items[0];
|
|
3530
|
-
if (!/^si_[A-Za-z0-9_]+$/.test(item.id) || item.price?.id !== seat.priceId || item.quantity !== 1)
|
|
3821
|
+
if (!/^si_[A-Za-z0-9_]+$/.test(item.id) || item.price?.id !== seat.priceId || item.quantity !== 1) fail4();
|
|
3531
3822
|
const removed = await call("DELETE", `/v1/subscription_items/${encodeURIComponent(item.id)}`, { proration_behavior: "none" });
|
|
3532
|
-
if (removed.id !== item.id || removed.deleted !== true)
|
|
3823
|
+
if (removed.id !== item.id || removed.deleted !== true) fail4("named_seat_renewal_removal_unconfirmed");
|
|
3533
3824
|
}
|
|
3534
3825
|
const list = await call("GET", "/v1/refunds", { payment_intent: seat.providerPaymentIntentId, limit: 100 });
|
|
3535
|
-
if (!Array.isArray(list.data) || list.has_more !== false || list.data.length >= 100)
|
|
3826
|
+
if (!Array.isArray(list.data) || list.has_more !== false || list.data.length >= 100) fail4("named_seat_refund_history_incomplete");
|
|
3536
3827
|
let refundedCents = 0, pendingCents = 0;
|
|
3537
3828
|
const refundIds = [];
|
|
3538
3829
|
for (const refund of list.data) {
|
|
3539
|
-
if (!/^re_[A-Za-z0-9_]+$/.test(refund.id) || refund.payment_intent !== seat.providerPaymentIntentId || refund.currency !== seat.currency || !Number.isSafeInteger(refund.amount) || refund.amount <= 0 || !["succeeded", "pending", "requires_action", "failed", "canceled"].includes(refund.status))
|
|
3830
|
+
if (!/^re_[A-Za-z0-9_]+$/.test(refund.id) || refund.payment_intent !== seat.providerPaymentIntentId || refund.currency !== seat.currency || !Number.isSafeInteger(refund.amount) || refund.amount <= 0 || !["succeeded", "pending", "requires_action", "failed", "canceled"].includes(refund.status)) fail4();
|
|
3540
3831
|
if (refund.status === "succeeded") {
|
|
3541
3832
|
refundedCents += refund.amount;
|
|
3542
3833
|
refundIds.push(refund.id);
|
|
3543
3834
|
} else if (refund.status === "pending" || refund.status === "requires_action") pendingCents += refund.amount;
|
|
3544
3835
|
}
|
|
3545
|
-
if (refundedCents + pendingCents > seat.amountCents)
|
|
3546
|
-
if (pendingCents)
|
|
3836
|
+
if (refundedCents + pendingCents > seat.amountCents) fail4();
|
|
3837
|
+
if (pendingCents) fail4("named_seat_refund_pending");
|
|
3547
3838
|
if (refundedCents < seat.amountCents) {
|
|
3548
3839
|
const remaining = seat.amountCents - refundedCents;
|
|
3549
3840
|
const refund = await call("POST", "/v1/refunds", {
|
|
@@ -3552,8 +3843,8 @@ async function refundNamedSeatStripePayment(secretKey, seat) {
|
|
|
3552
3843
|
reason: "requested_by_customer",
|
|
3553
3844
|
metadata
|
|
3554
3845
|
}, `named-seat-refund:${seat.id}:${remaining}`);
|
|
3555
|
-
if (refund.payment_intent !== seat.providerPaymentIntentId || refund.amount !== remaining || refund.currency !== seat.currency || !/^re_[A-Za-z0-9_]+$/.test(refund.id))
|
|
3556
|
-
if (refund.status !== "succeeded")
|
|
3846
|
+
if (refund.payment_intent !== seat.providerPaymentIntentId || refund.amount !== remaining || refund.currency !== seat.currency || !/^re_[A-Za-z0-9_]+$/.test(refund.id)) fail4();
|
|
3847
|
+
if (refund.status !== "succeeded") fail4("named_seat_refund_pending");
|
|
3557
3848
|
refundedCents += remaining;
|
|
3558
3849
|
refundIds.push(refund.id);
|
|
3559
3850
|
}
|
|
@@ -3642,9 +3933,37 @@ async function refundNamedSeatByOperator(db, input, stripeSecretKey, now) {
|
|
|
3642
3933
|
return { seatId: String(seat.id), status: "refunded", refundedCents: Number(refunded.refundedCents), subscriptionCanceled: false };
|
|
3643
3934
|
}
|
|
3644
3935
|
|
|
3936
|
+
// src/membership-seat-window.ts
|
|
3937
|
+
async function liftSeatPurchaseWindow(db, scope2, now, limit = 100) {
|
|
3938
|
+
membershipClock(now);
|
|
3939
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200) throw new TypeError("invalid lift limit");
|
|
3940
|
+
const state = await loadMembershipAuthorityState(db, scope2);
|
|
3941
|
+
const rows5 = (await db.query({ networkMembership: { $: { where: { ...scope2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkMembership ?? [];
|
|
3942
|
+
let lifted = 0;
|
|
3943
|
+
for (const member of rows5.slice(0, limit)) {
|
|
3944
|
+
if (Object.entries(scope2).some(([key3, value]) => member[key3] !== value)) throw new Error("membership lift scope mismatch");
|
|
3945
|
+
const offering = membershipOfferingVersion(member.offeringSnapshot);
|
|
3946
|
+
if (offering.seatPurchaseWindowCalendarMonths === void 0) continue;
|
|
3947
|
+
const { seatPurchaseWindowCalendarMonths: _window, ...windowless } = offering;
|
|
3948
|
+
await db.transact([membershipUpdate("networkMembership", String(member.id), {
|
|
3949
|
+
offeringSnapshot: windowless,
|
|
3950
|
+
seatPurchaseEndsAt: Number(member.termEndsAt),
|
|
3951
|
+
revision: Number(member.revision) + 1,
|
|
3952
|
+
updatedAt: now
|
|
3953
|
+
})], { mutationId: `seat-window-lift:${member.id}:${member.revision}`, guards: [
|
|
3954
|
+
{ ns: "networkMembership", id: String(member.id), exists: true, equals: { revision: member.revision } },
|
|
3955
|
+
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative" } }
|
|
3956
|
+
] });
|
|
3957
|
+
lifted++;
|
|
3958
|
+
}
|
|
3959
|
+
return { lifted, more: rows5.length > limit };
|
|
3960
|
+
}
|
|
3961
|
+
|
|
3645
3962
|
// src/worker-routes-admin-membership-seats.ts
|
|
3646
3963
|
var LIST = "/api/admin/membership-seats";
|
|
3964
|
+
var LIFT = "/api/admin/membership-seats/lift-purchase-window";
|
|
3647
3965
|
var ONE = /^\/api\/admin\/membership-seats\/([A-Za-z0-9][A-Za-z0-9_.:-]{2,159})\/refund$/;
|
|
3966
|
+
var SCOPE = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
3648
3967
|
var PUBLIC = [
|
|
3649
3968
|
"id",
|
|
3650
3969
|
"appId",
|
|
@@ -3669,11 +3988,11 @@ var PUBLIC = [
|
|
|
3669
3988
|
];
|
|
3670
3989
|
var project = (row2) => Object.fromEntries(PUBLIC.filter((key3) => row2[key3] !== void 0).map((key3) => [key3, row2[key3]]));
|
|
3671
3990
|
var handleAdminMembershipSeats = async (req, url, env, ctx) => {
|
|
3672
|
-
const one = url.pathname.match(ONE);
|
|
3673
|
-
if (url.pathname !== LIST && !one) return null;
|
|
3991
|
+
const one = url.pathname.match(ONE), lift = url.pathname === LIFT;
|
|
3992
|
+
if (url.pathname !== LIST && !one && !lift) return null;
|
|
3674
3993
|
const operator = ctx.membershipAuthorityOperator;
|
|
3675
3994
|
if (!operator) return json({ error: "not found" }, 404);
|
|
3676
|
-
if (req.method !== (one ? "POST" : "GET")) return json({ error: "method not allowed" }, 405);
|
|
3995
|
+
if (req.method !== (one || lift ? "POST" : "GET")) return json({ error: "method not allowed" }, 405);
|
|
3677
3996
|
const raw2 = ctx.makeDb(env), user = await ctx.verifyUser(req, env);
|
|
3678
3997
|
if (!user) return json({ error: "unauthorized" }, 401);
|
|
3679
3998
|
if (!await ctx.isSuperAdminEmail(raw2, user.email, user.userId)) return json({ error: "super-admin required", code: "super_admin_required" }, 403);
|
|
@@ -3683,6 +4002,24 @@ var handleAdminMembershipSeats = async (req, url, env, ctx) => {
|
|
|
3683
4002
|
} catch {
|
|
3684
4003
|
return json({ error: "the membership authority is not hosted by this deployment" }, 404);
|
|
3685
4004
|
}
|
|
4005
|
+
if (lift) {
|
|
4006
|
+
let body;
|
|
4007
|
+
try {
|
|
4008
|
+
body = await req.json();
|
|
4009
|
+
} catch {
|
|
4010
|
+
return json({ error: "chapterId and runtime are required" }, 400);
|
|
4011
|
+
}
|
|
4012
|
+
const appId = body.appId ?? body.chapterId;
|
|
4013
|
+
if (typeof body.chapterId !== "string" || !SCOPE.test(body.chapterId) || typeof appId !== "string" || !SCOPE.test(appId) || typeof body.runtime !== "string" || !/^[a-z0-9]{2,12}$/.test(body.runtime)) {
|
|
4014
|
+
return json({ error: "chapterId and runtime are required" }, 400);
|
|
4015
|
+
}
|
|
4016
|
+
try {
|
|
4017
|
+
return json({ ok: true, ...await liftSeatPurchaseWindow(db, { appId, chapterId: body.chapterId, environment: env.ODLA_ENV, runtime: body.runtime }, Date.now()) });
|
|
4018
|
+
} catch (error) {
|
|
4019
|
+
const code = error.code;
|
|
4020
|
+
return json({ error: "purchase window lift is pending; retry to confirm", ...code ? { code } : {} }, code ? 409 : 503);
|
|
4021
|
+
}
|
|
4022
|
+
}
|
|
3686
4023
|
if (!one) {
|
|
3687
4024
|
const rows5 = (await db.query({ networkSeat: { $: { where: { environment: env.ODLA_ENV }, order: { updatedAt: "desc" }, limit: 201 } } })).networkSeat ?? [];
|
|
3688
4025
|
return json({ seats: rows5.slice(0, 200).map(project), truncated: rows5.length > 200 });
|
|
@@ -3712,6 +4049,107 @@ var handleAdminMembershipSeats = async (req, url, env, ctx) => {
|
|
|
3712
4049
|
}
|
|
3713
4050
|
};
|
|
3714
4051
|
|
|
4052
|
+
// src/membership-policy-versions.ts
|
|
4053
|
+
var LIMIT = 200;
|
|
4054
|
+
var text2 = (value) => typeof value === "string" && value.length > 0 && value.length <= 200 ? value : void 0;
|
|
4055
|
+
var instant = (value) => {
|
|
4056
|
+
const at = typeof value === "number" ? value : typeof value === "string" ? Date.parse(value) : NaN;
|
|
4057
|
+
return Number.isSafeInteger(at) && at >= 0 ? at : void 0;
|
|
4058
|
+
};
|
|
4059
|
+
function provenance(row2) {
|
|
4060
|
+
const source = text2(row2.source), actor = text2(row2.actor ?? row2.$createdBy), recordedAt = instant(row2.recordedAt ?? row2.createdAt ?? row2.$createdAt);
|
|
4061
|
+
return { ...source ? { source } : {}, ...actor ? { actor } : {}, ...recordedAt !== void 0 ? { recordedAt } : {} };
|
|
4062
|
+
}
|
|
4063
|
+
var live = (w, now) => w.from <= now && (w.until === void 0 || now < w.until);
|
|
4064
|
+
function stateOf(item, all, now, window, rivals) {
|
|
4065
|
+
if (item.status !== "published") return { state: item.status };
|
|
4066
|
+
const mine = window(item);
|
|
4067
|
+
if (now < mine.from) return { state: "scheduled" };
|
|
4068
|
+
const closed = mine.until !== void 0 && mine.until <= now;
|
|
4069
|
+
const higher = all.filter((other) => other.id !== item.id && other.status === "published" && other.version > item.version && rivals(item, other)).map(window).filter((w) => w.from <= now && (w.until === void 0 || w.until > mine.from));
|
|
4070
|
+
if (!closed && !higher.some((w) => live(w, now))) return { state: "active" };
|
|
4071
|
+
const dates = higher.map((w) => Math.max(w.from, mine.from));
|
|
4072
|
+
if (closed) dates.push(mine.until);
|
|
4073
|
+
return { state: "superseded", supersededAt: Math.min(...dates) };
|
|
4074
|
+
}
|
|
4075
|
+
var mechanism = (policy) => policy.percentOffBps === 0 || policy.discountEligibleSkuIds.length === 0 ? "none" : policy.benefitDuration.kind === "while_active" ? "stripe_coupon_forever" : policy.benefitDuration.kind === "first_term" ? "stripe_coupon_once" : "stripe_coupon_term_bound";
|
|
4076
|
+
var sharesSku = (a, b) => a.cohortEligibleSkuIds.some((sku) => b.cohortEligibleSkuIds.includes(sku));
|
|
4077
|
+
var byChapter = (a, b) => a.chapterId.localeCompare(b.chapterId);
|
|
4078
|
+
function normalize(rows5, collection, parse, invalid2) {
|
|
4079
|
+
const out = [];
|
|
4080
|
+
for (const row2 of rows5) {
|
|
4081
|
+
try {
|
|
4082
|
+
out.push({ value: parse(row2.snapshot), row: row2 });
|
|
4083
|
+
} catch (error) {
|
|
4084
|
+
invalid2.push({ collection, id: String(row2.id ?? ""), error: error instanceof Error ? error.message : String(error) });
|
|
4085
|
+
}
|
|
4086
|
+
}
|
|
4087
|
+
return out;
|
|
4088
|
+
}
|
|
4089
|
+
async function listMembershipPolicyVersions(db, input) {
|
|
4090
|
+
const where = input.chapterId ? { chapterId: input.chapterId } : {};
|
|
4091
|
+
const rows5 = await db.query({
|
|
4092
|
+
membershipOfferingVersion: { $: { where, limit: LIMIT } },
|
|
4093
|
+
discountPolicyVersion: { $: { where, limit: LIMIT } }
|
|
4094
|
+
});
|
|
4095
|
+
const offeringRows = rows5.membershipOfferingVersion ?? [], policyRows = rows5.discountPolicyVersion ?? [];
|
|
4096
|
+
const invalid2 = [];
|
|
4097
|
+
const offerings = normalize(offeringRows, "membershipOfferingVersion", membershipOfferingVersion, invalid2);
|
|
4098
|
+
const policies = normalize(policyRows, "discountPolicyVersion", membershipDiscountPolicyVersion, invalid2);
|
|
4099
|
+
const offeringValues = offerings.map((item) => item.value), policyValues = policies.map((item) => item.value);
|
|
4100
|
+
return {
|
|
4101
|
+
now: input.now,
|
|
4102
|
+
chapterId: input.chapterId ?? null,
|
|
4103
|
+
offerings: offerings.map(({ value, row: row2 }) => ({
|
|
4104
|
+
...value,
|
|
4105
|
+
...provenance(row2),
|
|
4106
|
+
...stateOf(
|
|
4107
|
+
value,
|
|
4108
|
+
offeringValues,
|
|
4109
|
+
input.now,
|
|
4110
|
+
(v) => ({ from: v.effectiveFrom, ...v.effectiveUntil !== void 0 ? { until: v.effectiveUntil } : {} }),
|
|
4111
|
+
(a, b) => a.chapterId === b.chapterId && a.baseSkuId === b.baseSkuId
|
|
4112
|
+
)
|
|
4113
|
+
})).sort((a, b) => byChapter(a, b) || a.baseSkuId.localeCompare(b.baseSkuId) || b.version - a.version),
|
|
4114
|
+
discountPolicies: policies.map(({ value, row: row2 }) => ({
|
|
4115
|
+
...value,
|
|
4116
|
+
...provenance(row2),
|
|
4117
|
+
mechanism: mechanism(value),
|
|
4118
|
+
...stateOf(
|
|
4119
|
+
value,
|
|
4120
|
+
policyValues,
|
|
4121
|
+
input.now,
|
|
4122
|
+
(v) => ({ from: v.qualifyingStartsAt, until: v.qualifyingEndsAt }),
|
|
4123
|
+
(a, b) => a.chapterId === b.chapterId && sharesSku(a, b)
|
|
4124
|
+
)
|
|
4125
|
+
})).sort((a, b) => byChapter(a, b) || b.version - a.version),
|
|
4126
|
+
invalid: invalid2,
|
|
4127
|
+
truncated: offeringRows.length >= LIMIT || policyRows.length >= LIMIT
|
|
4128
|
+
};
|
|
4129
|
+
}
|
|
4130
|
+
|
|
4131
|
+
// src/worker-routes-admin-membership-policy.ts
|
|
4132
|
+
var PATH3 = "/api/admin/membership-policy-versions";
|
|
4133
|
+
var SCOPE2 = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
4134
|
+
var handleAdminMembershipPolicyVersions = async (req, url, env, ctx) => {
|
|
4135
|
+
if (url.pathname !== PATH3) return null;
|
|
4136
|
+
const operator = ctx.membershipAuthorityOperator;
|
|
4137
|
+
if (!operator) return json({ error: "not found" }, 404);
|
|
4138
|
+
if (req.method !== "GET") return json({ error: "method not allowed" }, 405);
|
|
4139
|
+
const raw2 = ctx.makeDb(env), user = await ctx.verifyUser(req, env);
|
|
4140
|
+
if (!user) return json({ error: "unauthorized" }, 401);
|
|
4141
|
+
if (!await ctx.isSuperAdminEmail(raw2, user.email, user.userId)) return json({ error: "super-admin required", code: "super_admin_required" }, 403);
|
|
4142
|
+
const chapterId = url.searchParams.get("chapterId");
|
|
4143
|
+
if (chapterId !== null && !SCOPE2.test(chapterId)) return json({ error: "chapterId is invalid" }, 400);
|
|
4144
|
+
let db;
|
|
4145
|
+
try {
|
|
4146
|
+
db = operator.makeDb(env);
|
|
4147
|
+
} catch {
|
|
4148
|
+
return json({ error: "the membership authority is not hosted by this deployment" }, 404);
|
|
4149
|
+
}
|
|
4150
|
+
return json(await listMembershipPolicyVersions(db, { chapterId, now: Date.now() }));
|
|
4151
|
+
};
|
|
4152
|
+
|
|
3715
4153
|
// src/worker-routes-membership-cancel.ts
|
|
3716
4154
|
var handleMembershipCancellation = async (req, url, env, ctx) => {
|
|
3717
4155
|
const match = url.pathname.match(/^\/api\/admin\/applications\/([0-9a-f-]+)\/cancel-renewal$/);
|
|
@@ -4564,8 +5002,8 @@ var APPLICATION_LABELS = {
|
|
|
4564
5002
|
state: "State"
|
|
4565
5003
|
};
|
|
4566
5004
|
function humanize(id6) {
|
|
4567
|
-
const
|
|
4568
|
-
return
|
|
5005
|
+
const text8 = id6.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
|
|
5006
|
+
return text8 ? `${text8[0].toUpperCase()}${text8.slice(1)}` : id6;
|
|
4569
5007
|
}
|
|
4570
5008
|
function applicationDefs(chapter) {
|
|
4571
5009
|
try {
|
|
@@ -4632,7 +5070,7 @@ var ID2 = /^[a-z][a-z0-9-]{1,79}$/;
|
|
|
4632
5070
|
var PRICE = /^price_[A-Za-z0-9]{12,}$/;
|
|
4633
5071
|
var CURRENCY = /^[a-z]{3}$/;
|
|
4634
5072
|
var record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
4635
|
-
var
|
|
5073
|
+
var text3 = (value, path, errors, max) => {
|
|
4636
5074
|
if (typeof value !== "string" || !value.trim()) {
|
|
4637
5075
|
errors.push(`${path} is required`);
|
|
4638
5076
|
return "";
|
|
@@ -4660,23 +5098,23 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
4660
5098
|
}
|
|
4661
5099
|
const allowed2 = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
|
|
4662
5100
|
for (const key3 of Object.keys(item)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
|
|
4663
|
-
const id6 =
|
|
5101
|
+
const id6 = text3(item.id, `${path}.id`, errors, 80);
|
|
4664
5102
|
if (id6 && !ID2.test(id6)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
4665
5103
|
if (ids2.has(id6)) errors.push(`${path}.id duplicates "${id6}"`);
|
|
4666
5104
|
ids2.add(id6);
|
|
4667
5105
|
const targetRaw = record(item.target);
|
|
4668
5106
|
let target = { type: "entitlement", entitlementKey: "invalid" };
|
|
4669
5107
|
if (targetRaw?.type === "tier") {
|
|
4670
|
-
const tierId =
|
|
5108
|
+
const tierId = text3(targetRaw.tierId, `${path}.target.tierId`, errors, 80);
|
|
4671
5109
|
if (!tierIds.has(tierId)) errors.push(`${path}.target.tierId must reference a declared tier`);
|
|
4672
5110
|
target = { type: "tier", tierId };
|
|
4673
5111
|
} else if (targetRaw?.type === "entitlement") {
|
|
4674
|
-
const entitlementKey =
|
|
5112
|
+
const entitlementKey = text3(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
|
|
4675
5113
|
if (entitlementKey && !ID2.test(entitlementKey)) errors.push(`${path}.target.entitlementKey must be a lowercase kebab-case id`);
|
|
4676
5114
|
target = { type: "entitlement", entitlementKey };
|
|
4677
5115
|
} else errors.push(`${path}.target.type must be tier or entitlement`);
|
|
4678
5116
|
if (item.billingMode !== "one_time" && item.billingMode !== "subscription") errors.push(`${path}.billingMode must be one_time or subscription`);
|
|
4679
|
-
const stripePriceId =
|
|
5117
|
+
const stripePriceId = text3(item.stripePriceId, `${path}.stripePriceId`, errors, 128);
|
|
4680
5118
|
if (stripePriceId && !PRICE.test(stripePriceId)) errors.push(`${path}.stripePriceId must be a real Stripe Price id`);
|
|
4681
5119
|
if (!Number.isSafeInteger(item.priceCents) || Number(item.priceCents) < 1 || Number(item.priceCents) > 1e8) errors.push(`${path}.priceCents must be an integer from 1 to 100,000,000`);
|
|
4682
5120
|
const currency = typeof item.currency === "string" ? item.currency.trim().toLowerCase() : "";
|
|
@@ -4698,9 +5136,9 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
4698
5136
|
redemptionLifetimeHours: lifetime,
|
|
4699
5137
|
...activeFrom !== void 0 ? { activeFrom } : {},
|
|
4700
5138
|
...activeUntil !== void 0 ? { activeUntil } : {},
|
|
4701
|
-
title:
|
|
4702
|
-
description:
|
|
4703
|
-
ctaLabel:
|
|
5139
|
+
title: text3(item.title, `${path}.title`, errors, 160),
|
|
5140
|
+
description: text3(item.description, `${path}.description`, errors, 2e3),
|
|
5141
|
+
ctaLabel: text3(item.ctaLabel, `${path}.ctaLabel`, errors, 120)
|
|
4704
5142
|
});
|
|
4705
5143
|
}
|
|
4706
5144
|
return errors.length ? { ok: false, errors } : { ok: true, value: { schemaVersion: 1, offers } };
|
|
@@ -4918,22 +5356,22 @@ async function createPaymentQuote(input) {
|
|
|
4918
5356
|
// src/membership-quote-projection.ts
|
|
4919
5357
|
var MEMBERSHIP_QUOTE_NAMESPACE = "membershipQuoteProjections";
|
|
4920
5358
|
var keys2 = ["appId", "chapterId", "environment", "runtime", "applicationId", "skuId"];
|
|
4921
|
-
var
|
|
5359
|
+
var fail5 = (code) => {
|
|
4922
5360
|
throw Object.assign(new Error(code), { code });
|
|
4923
5361
|
};
|
|
4924
5362
|
var load3 = async (db, ns, id6) => (await db.query({ [ns]: { $: { where: { id: id6 }, limit: 1 } } }))[ns]?.[0];
|
|
4925
5363
|
async function restore2(row2, target, now, allowExpired = false) {
|
|
4926
|
-
if (!row2 || keys2.some((key3) => row2[key3] !== target[key3]) || !Number.isSafeInteger(row2.revision) || Number(row2.revision) < 1 || typeof row2.requestId !== "string") return
|
|
4927
|
-
if (row2.status !== "ready") return
|
|
5364
|
+
if (!row2 || keys2.some((key3) => row2[key3] !== target[key3]) || !Number.isSafeInteger(row2.revision) || Number(row2.revision) < 1 || typeof row2.requestId !== "string") return fail5("membership_quote_unavailable");
|
|
5365
|
+
if (row2.status !== "ready") return fail5("membership_quote_pending");
|
|
4928
5366
|
const offer = await verifyIssuedMembershipOffer(row2.offer, { ...target, offerId: await membershipOfferIdForRequest({
|
|
4929
5367
|
...target,
|
|
4930
5368
|
requestId: row2.requestId
|
|
4931
5369
|
}), now, lifetimeMs: 6e4 });
|
|
4932
|
-
if (offer.issuedAt > now + 3e4 || !allowExpired && offer.expiresAt <= now) return
|
|
5370
|
+
if (offer.issuedAt > now + 3e4 || !allowExpired && offer.expiresAt <= now) return fail5("membership_quote_expired");
|
|
4933
5371
|
const tier = row2.tier, quote = row2.quote;
|
|
4934
5372
|
const offering = offer.contract.offering;
|
|
4935
5373
|
if (!tier || tier.id !== target.skuId || tier.groupId !== target.chapterId || !tier.stripePriceId || tier.priceCents !== offering.basePriceCents || tier.currency !== offering.currency || tier.interval !== offering.interval || !quote) {
|
|
4936
|
-
return
|
|
5374
|
+
return fail5("membership_catalog_mismatch");
|
|
4937
5375
|
}
|
|
4938
5376
|
const expected = await createPaymentQuote({
|
|
4939
5377
|
...target,
|
|
@@ -4944,7 +5382,7 @@ async function restore2(row2, target, now, allowExpired = false) {
|
|
|
4944
5382
|
presentation: quote.presentation,
|
|
4945
5383
|
founding: foundingOf(offer)
|
|
4946
5384
|
});
|
|
4947
|
-
if (quote.digest !== expected.digest) return
|
|
5385
|
+
if (quote.digest !== expected.digest) return fail5("membership_quote_conflict");
|
|
4948
5386
|
return { tier, quote: expected, billingTerms: offer.contract.billingTerms, membershipOffer: offer, projectionRevision: Number(row2.revision) };
|
|
4949
5387
|
}
|
|
4950
5388
|
async function readMembershipQuote(db, target, now) {
|
|
@@ -4952,9 +5390,9 @@ async function readMembershipQuote(db, target, now) {
|
|
|
4952
5390
|
}
|
|
4953
5391
|
async function prepareMembershipQuote(db, input, issue) {
|
|
4954
5392
|
const { applicationId: id6, now, app } = input;
|
|
4955
|
-
if (app.id !== id6 || app.groupId !== input.chapterId || app.tierId !== input.skuId || app.stripeRuntime !== input.runtime || app.status !== "submitted" || app.canceled === true || app.giftOfferId || app.giftClaimId || app.namedSeatId || app.namedSeatClaimPending) return
|
|
5393
|
+
if (app.id !== id6 || app.groupId !== input.chapterId || app.tierId !== input.skuId || app.stripeRuntime !== input.runtime || app.status !== "submitted" || app.canceled === true || app.giftOfferId || app.giftClaimId || app.namedSeatId || app.namedSeatClaimPending) return fail5("membership_quote_unavailable");
|
|
4956
5394
|
let row2 = await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, id6);
|
|
4957
|
-
if (row2 && keys2.some((key3) => row2[key3] !== input[key3])) return
|
|
5395
|
+
if (row2 && keys2.some((key3) => row2[key3] !== input[key3])) return fail5("membership_quote_conflict");
|
|
4958
5396
|
if (row2?.status === "ready") {
|
|
4959
5397
|
try {
|
|
4960
5398
|
return await restore2(row2, input, now);
|
|
@@ -4999,7 +5437,7 @@ async function prepareMembershipQuote(db, input, issue) {
|
|
|
4999
5437
|
}
|
|
5000
5438
|
if (row2?.status === "ready") return restore2(row2, input, now);
|
|
5001
5439
|
if (!row2 || row2.status !== "pending" || keys2.some((key3) => row2[key3] !== input[key3]) || typeof row2.requestId !== "string") {
|
|
5002
|
-
return
|
|
5440
|
+
return fail5("membership_quote_pending");
|
|
5003
5441
|
}
|
|
5004
5442
|
const target = Object.fromEntries(keys2.map((key3) => [key3, input[key3]]));
|
|
5005
5443
|
const offer = await verifyIssuedMembershipOffer(await issue({ ...target, requestId: row2.requestId }), {
|
|
@@ -5263,17 +5701,17 @@ async function readBoundedRequestText(req, maxBytes) {
|
|
|
5263
5701
|
if (!reader) return "";
|
|
5264
5702
|
const decoder = new TextDecoder();
|
|
5265
5703
|
let bytes = 0;
|
|
5266
|
-
let
|
|
5704
|
+
let text8 = "";
|
|
5267
5705
|
for (; ; ) {
|
|
5268
5706
|
const { done, value } = await reader.read();
|
|
5269
5707
|
if (req.signal.aborted) return invalid2();
|
|
5270
5708
|
if (done) {
|
|
5271
5709
|
complete = true;
|
|
5272
|
-
return
|
|
5710
|
+
return text8 + decoder.decode();
|
|
5273
5711
|
}
|
|
5274
5712
|
bytes += value.byteLength;
|
|
5275
5713
|
if (bytes > maxBytes) return json({ error: "request body too large" }, 413);
|
|
5276
|
-
|
|
5714
|
+
text8 += decoder.decode(value, { stream: true });
|
|
5277
5715
|
}
|
|
5278
5716
|
} catch {
|
|
5279
5717
|
return invalid2();
|
|
@@ -5626,37 +6064,61 @@ async function provisionApplicant(db, chapter, applicationId, fields) {
|
|
|
5626
6064
|
} catch {
|
|
5627
6065
|
}
|
|
5628
6066
|
}
|
|
5629
|
-
|
|
6067
|
+
|
|
6068
|
+
// src/submit-notifications.ts
|
|
6069
|
+
async function notifyAdminOfApplication(db, env, notice) {
|
|
5630
6070
|
try {
|
|
5631
|
-
const
|
|
5632
|
-
|
|
5633
|
-
|
|
5634
|
-
|
|
5635
|
-
|
|
5636
|
-
|
|
5637
|
-
|
|
5638
|
-
|
|
5639
|
-
|
|
5640
|
-
|
|
5641
|
-
|
|
5642
|
-
|
|
5643
|
-
|
|
6071
|
+
const { group, application: app } = notice;
|
|
6072
|
+
if (typeof group.notificationEmail !== "string" || !group.notificationEmail) return;
|
|
6073
|
+
const { tierName: tierName2 } = await checkoutVariables(db, group, app);
|
|
6074
|
+
await sendTemplated(lifecycleNotifyDeps(db, env), {
|
|
6075
|
+
group: emailGroupFrom(group),
|
|
6076
|
+
template: "adminNotification",
|
|
6077
|
+
to: group.notificationEmail,
|
|
6078
|
+
vars: { ...applicantVariables(app), tier: tierName2, tierName: tierName2, adminUrl: adminUrlFor(notice.origin), membersUrl: membersUrlFor(notice.origin) },
|
|
6079
|
+
dedupeKey: `apply:${String(app.id)}:admin`,
|
|
6080
|
+
applicationId: String(app.id)
|
|
6081
|
+
});
|
|
6082
|
+
} catch {
|
|
6083
|
+
}
|
|
6084
|
+
}
|
|
6085
|
+
var FREE_CONTINUATIONS = /* @__PURE__ */ new Set(["booking", "accepted", "approve"]);
|
|
6086
|
+
function shouldConfirmSubmit(state, group) {
|
|
6087
|
+
if (state !== null && FREE_CONTINUATIONS.has(state)) return true;
|
|
6088
|
+
const templates = group.emailTemplates;
|
|
6089
|
+
return templates?.paymentConfirmation?.enabled === false;
|
|
6090
|
+
}
|
|
6091
|
+
function submitNextStep(state) {
|
|
6092
|
+
switch (state) {
|
|
6093
|
+
case "accepted":
|
|
6094
|
+
return "Your membership is approved.";
|
|
6095
|
+
case "booking":
|
|
6096
|
+
return "Next, schedule your introduction call.";
|
|
6097
|
+
case "payment":
|
|
6098
|
+
case "paymentPending":
|
|
6099
|
+
case "gift":
|
|
6100
|
+
return "Next, complete your membership payment.";
|
|
6101
|
+
default:
|
|
6102
|
+
return "";
|
|
6103
|
+
}
|
|
6104
|
+
}
|
|
6105
|
+
async function notifySubmitConfirmed(db, env, notice) {
|
|
6106
|
+
try {
|
|
6107
|
+
const { group, application: app } = notice;
|
|
6108
|
+
if (typeof app.email !== "string" || !app.email || !shouldConfirmSubmit(notice.state, group)) return;
|
|
6109
|
+
await sendTemplated(lifecycleNotifyDeps(db, env), {
|
|
6110
|
+
group: emailGroupFrom(group),
|
|
6111
|
+
template: "submitConfirmation",
|
|
6112
|
+
to: app.email,
|
|
6113
|
+
vars: {
|
|
6114
|
+
...applicantVariables(app),
|
|
6115
|
+
...await checkoutVariables(db, group, app),
|
|
6116
|
+
submitNextStep: submitNextStep(notice.state),
|
|
6117
|
+
membersUrl: membersUrlFor(notice.origin)
|
|
5644
6118
|
},
|
|
5645
|
-
{
|
|
5646
|
-
|
|
5647
|
-
|
|
5648
|
-
to: group.notificationEmail,
|
|
5649
|
-
vars: {
|
|
5650
|
-
firstName: stringValue(fields.firstName),
|
|
5651
|
-
lastName: stringValue(fields.lastName),
|
|
5652
|
-
email: stringValue(fields.email),
|
|
5653
|
-
phone: stringValue(fields.phone),
|
|
5654
|
-
state: stringValue(fields.state)
|
|
5655
|
-
},
|
|
5656
|
-
dedupeKey: `apply:${applicationId}:admin`,
|
|
5657
|
-
applicationId
|
|
5658
|
-
}
|
|
5659
|
-
);
|
|
6119
|
+
dedupeKey: `apply:${String(app.id)}:confirm`,
|
|
6120
|
+
applicationId: String(app.id)
|
|
6121
|
+
});
|
|
5660
6122
|
} catch {
|
|
5661
6123
|
}
|
|
5662
6124
|
}
|
|
@@ -5854,17 +6316,17 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
5854
6316
|
if (provisioningFailure.status === 409) await rejectUnprovisionedSignup(db, result.id, canonical3.status);
|
|
5855
6317
|
return json(provisioningFailure.body, provisioningFailure.status);
|
|
5856
6318
|
}
|
|
5857
|
-
if (!result.duplicate && effectiveChapter.sends.adminNotification === "submit") {
|
|
5858
|
-
await notifyAdminOfApplication(db, env, chapter.id, result.id, canonical3);
|
|
5859
|
-
}
|
|
5860
6319
|
const groups = (await db.query({ groups: { $: { where: { id: chapter.id }, limit: 1 } } })).groups;
|
|
5861
6320
|
const group = Array.isArray(groups) ? groups[0] : void 0;
|
|
5862
|
-
const
|
|
6321
|
+
const notice = group ? { group, application: canonical3, origin: url.origin } : null;
|
|
6322
|
+
if (notice && !result.duplicate && effectiveChapter.sends.adminNotification === "submit") await notifyAdminOfApplication(db, env, notice);
|
|
6323
|
+
const state = notice ? await continueJoin(db, ctx, env, url.origin, canonical3, notice.group) : null;
|
|
5863
6324
|
if (!state) return json({
|
|
5864
6325
|
error: "Application saved; its next step is temporarily unavailable. Retry this submission.",
|
|
5865
6326
|
code: "application_continuation_unavailable",
|
|
5866
6327
|
id: result.id
|
|
5867
6328
|
}, 503);
|
|
6329
|
+
if (notice && !result.duplicate) await notifySubmitConfirmed(db, env, { ...notice, state });
|
|
5868
6330
|
const final = (await db.query({ applications: { $: { where: { id: result.id }, limit: 1 } } })).applications?.[0];
|
|
5869
6331
|
if (!final || final.groupId !== chapter.id || state === "accepted" && final.status !== "approved") {
|
|
5870
6332
|
return json({ error: "application changed; reload its current state" }, 409);
|
|
@@ -5984,10 +6446,10 @@ function boundedString(value, path, errors, max, required = false) {
|
|
|
5984
6446
|
errors.push(`${path} must be a string`);
|
|
5985
6447
|
return void 0;
|
|
5986
6448
|
}
|
|
5987
|
-
const
|
|
5988
|
-
if (required && !
|
|
5989
|
-
if (
|
|
5990
|
-
return
|
|
6449
|
+
const text8 = value.trim();
|
|
6450
|
+
if (required && !text8) errors.push(`${path} is required`);
|
|
6451
|
+
if (text8.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
|
|
6452
|
+
return text8;
|
|
5991
6453
|
}
|
|
5992
6454
|
function validateMembershipPageDocument(input, options = {}) {
|
|
5993
6455
|
const errors = [];
|
|
@@ -6036,8 +6498,8 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
6036
6498
|
else {
|
|
6037
6499
|
if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
|
|
6038
6500
|
benefits = item.benefits.slice(0, MAX_BENEFITS).flatMap((benefit, benefitIndex) => {
|
|
6039
|
-
const
|
|
6040
|
-
return
|
|
6501
|
+
const text8 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
|
|
6502
|
+
return text8 ? [text8] : [];
|
|
6041
6503
|
});
|
|
6042
6504
|
}
|
|
6043
6505
|
}
|
|
@@ -6131,7 +6593,7 @@ var recordOf = (value) => value !== null && typeof value === "object" && !Array.
|
|
|
6131
6593
|
function unknownKeys(value, allowed2, path, errors) {
|
|
6132
6594
|
for (const key3 of Object.keys(value)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
|
|
6133
6595
|
}
|
|
6134
|
-
function
|
|
6596
|
+
function text4(value, path, errors, max = 65536) {
|
|
6135
6597
|
if (typeof value !== "string" || !value.trim()) {
|
|
6136
6598
|
errors.push(`${path} is required`);
|
|
6137
6599
|
return "";
|
|
@@ -6154,11 +6616,11 @@ function tiersOf(value, errors) {
|
|
|
6154
6616
|
return [];
|
|
6155
6617
|
}
|
|
6156
6618
|
unknownKeys(item, TIER_KEYS2, path, errors);
|
|
6157
|
-
const id6 =
|
|
6619
|
+
const id6 = text4(item.id, `${path}.id`, errors, 80);
|
|
6158
6620
|
if (id6 && !TIER.test(id6)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
6159
6621
|
if (seen.has(id6)) errors.push(`${path}.id duplicates "${id6}"`);
|
|
6160
6622
|
seen.add(id6);
|
|
6161
|
-
const name =
|
|
6623
|
+
const name = text4(item.name, `${path}.name`, errors, 160);
|
|
6162
6624
|
if (typeof item.available !== "boolean") errors.push(`${path}.available must be boolean`);
|
|
6163
6625
|
if (!Number.isInteger(item.sortOrder) || Number(item.sortOrder) < 0) errors.push(`${path}.sortOrder must be a non-negative integer`);
|
|
6164
6626
|
if (!Number.isInteger(item.priceCents) || Number(item.priceCents) < 0 || Number(item.priceCents) > 1e8) {
|
|
@@ -6176,7 +6638,7 @@ function tiersOf(value, errors) {
|
|
|
6176
6638
|
if (item.interval !== void 0 && item.interval !== "year" && item.interval !== "month") {
|
|
6177
6639
|
errors.push(`${path}.interval must be year or month`);
|
|
6178
6640
|
}
|
|
6179
|
-
const blurb = item.blurb === void 0 ? void 0 :
|
|
6641
|
+
const blurb = item.blurb === void 0 ? void 0 : text4(item.blurb, `${path}.blurb`, errors, 4e3);
|
|
6180
6642
|
return [{
|
|
6181
6643
|
id: id6,
|
|
6182
6644
|
name,
|
|
@@ -6234,7 +6696,7 @@ function policyOf(value, errors, tiers) {
|
|
|
6234
6696
|
const result = {};
|
|
6235
6697
|
for (const key3 of allowed2) {
|
|
6236
6698
|
if (input[key3] === void 0 && key3 !== "disclaimerText" && key3 !== "refundPolicyText") continue;
|
|
6237
|
-
result[key3] =
|
|
6699
|
+
result[key3] = text4(input[key3], `document.policy.${key3}`, errors);
|
|
6238
6700
|
}
|
|
6239
6701
|
const tierPolicies = {};
|
|
6240
6702
|
if (input.tierPolicies !== void 0) {
|
|
@@ -6249,8 +6711,8 @@ function policyOf(value, errors, tiers) {
|
|
|
6249
6711
|
}
|
|
6250
6712
|
unknownKeys(policy, /* @__PURE__ */ new Set(["refundPolicyText", "trustCopy"]), path, errors);
|
|
6251
6713
|
tierPolicies[id6] = {
|
|
6252
|
-
refundPolicyText:
|
|
6253
|
-
...policy.trustCopy !== void 0 ? { trustCopy:
|
|
6714
|
+
refundPolicyText: text4(policy.refundPolicyText, `${path}.refundPolicyText`, errors),
|
|
6715
|
+
...policy.trustCopy !== void 0 ? { trustCopy: text4(policy.trustCopy, `${path}.trustCopy`, errors) } : {}
|
|
6254
6716
|
};
|
|
6255
6717
|
}
|
|
6256
6718
|
}
|
|
@@ -6290,9 +6752,9 @@ function validateSignupControlDocument(input) {
|
|
|
6290
6752
|
if (!Number.isSafeInteger(root.generatedAt) || Number(root.generatedAt) < 1) errors.push("document.generatedAt must be epoch milliseconds");
|
|
6291
6753
|
const source = recordOf(root.source);
|
|
6292
6754
|
const target = recordOf(root.target);
|
|
6293
|
-
const appId =
|
|
6294
|
-
const chapterId =
|
|
6295
|
-
const environment =
|
|
6755
|
+
const appId = text4(source?.appId, "document.source.appId", errors, 63);
|
|
6756
|
+
const chapterId = text4(target?.chapterId, "document.target.chapterId", errors, 63);
|
|
6757
|
+
const environment = text4(target?.environment, "document.target.environment", errors, 63);
|
|
6296
6758
|
if (appId && !SLUG.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
6297
6759
|
if (chapterId && !SLUG.test(chapterId)) errors.push("document.target.chapterId must be a lowercase app slug");
|
|
6298
6760
|
if (environment && !ENVIRONMENT.test(environment)) errors.push("document.target.environment is invalid");
|
|
@@ -7471,7 +7933,7 @@ async function deliverAdmissionGrants(options) {
|
|
|
7471
7933
|
}
|
|
7472
7934
|
|
|
7473
7935
|
// src/worker-routes-network-admission.ts
|
|
7474
|
-
var
|
|
7936
|
+
var PATH4 = "/api/admin/network/admissions";
|
|
7475
7937
|
var RETRY_PATH = "/api/admin/network/admissions/retry";
|
|
7476
7938
|
var REVOKE_PATH = "/api/admin/network/admissions/revoke";
|
|
7477
7939
|
var MUTATION_ID2 = /^[A-Za-z0-9_-]{16,128}$/;
|
|
@@ -7551,11 +8013,11 @@ async function parseBody(req) {
|
|
|
7551
8013
|
}
|
|
7552
8014
|
}
|
|
7553
8015
|
var handleAdminNetworkAdmissions = async (req, url, env, ctx) => {
|
|
7554
|
-
if (![
|
|
8016
|
+
if (![PATH4, RETRY_PATH, REVOKE_PATH].includes(url.pathname)) return null;
|
|
7555
8017
|
const auth = await gate2(req, env, ctx);
|
|
7556
8018
|
if ("response" in auth) return auth.response;
|
|
7557
8019
|
const db = auth.db;
|
|
7558
|
-
if (url.pathname ===
|
|
8020
|
+
if (url.pathname === PATH4 && req.method === "GET") {
|
|
7559
8021
|
const targetId2 = url.searchParams.get("targetId") ?? "";
|
|
7560
8022
|
const target2 = targetFor(ctx, targetId2);
|
|
7561
8023
|
if (!target2) return reject6(404, "unknown_target", "admission target is not configured");
|
|
@@ -7656,6 +8118,41 @@ async function retryLeaderAdmissionDeliveries(env, ctx, limit = 100) {
|
|
|
7656
8118
|
|
|
7657
8119
|
// src/worker-routes-schedule.ts
|
|
7658
8120
|
import { computeBookableSlots, initCalendar } from "@odla-ai/calendar";
|
|
8121
|
+
|
|
8122
|
+
// src/booking-notifications.ts
|
|
8123
|
+
async function bookingConfirmationVariables(db, group, app, meeting) {
|
|
8124
|
+
const startAt = formatInstant(meeting.startAt, meeting.timezone);
|
|
8125
|
+
const meetUrl = meeting.meetUrl ?? "", htmlLink = meeting.htmlLink ?? "";
|
|
8126
|
+
return {
|
|
8127
|
+
...applicantVariables(app),
|
|
8128
|
+
...await checkoutVariables(db, group, app),
|
|
8129
|
+
startAt,
|
|
8130
|
+
endAt: formatInstant(meeting.endAt, meeting.timezone),
|
|
8131
|
+
timezone: meeting.timezone,
|
|
8132
|
+
meetUrl,
|
|
8133
|
+
htmlLink,
|
|
8134
|
+
// The placeholders the seeded template used before these variables existed.
|
|
8135
|
+
meetingTime: startAt,
|
|
8136
|
+
meetingLink: meetUrl || htmlLink
|
|
8137
|
+
};
|
|
8138
|
+
}
|
|
8139
|
+
async function notifyBookingConfirmed(db, env, input) {
|
|
8140
|
+
const app = input.application;
|
|
8141
|
+
if (typeof app.email !== "string" || !app.email) return;
|
|
8142
|
+
try {
|
|
8143
|
+
await sendTemplated(lifecycleNotifyDeps(db, env), {
|
|
8144
|
+
group: emailGroupFrom(input.group),
|
|
8145
|
+
template: "prepEmail",
|
|
8146
|
+
to: app.email,
|
|
8147
|
+
vars: await bookingConfirmationVariables(db, input.group, app, input.meeting),
|
|
8148
|
+
dedupeKey: `prep:${String(app.id)}`,
|
|
8149
|
+
applicationId: String(app.id)
|
|
8150
|
+
});
|
|
8151
|
+
} catch {
|
|
8152
|
+
}
|
|
8153
|
+
}
|
|
8154
|
+
|
|
8155
|
+
// src/worker-routes-schedule.ts
|
|
7659
8156
|
function errCode(err) {
|
|
7660
8157
|
if (err && typeof err === "object") {
|
|
7661
8158
|
const code = err.code;
|
|
@@ -7770,12 +8267,7 @@ async function bookSlot(req, env, ctx) {
|
|
|
7770
8267
|
await db.transact([meetingOp, appOp]);
|
|
7771
8268
|
const booked = { ...app, ...bookingAttrs };
|
|
7772
8269
|
await syncApplicationToCrm(crmDeps(db, ctx), { app: booked, stage: String(booked.status ?? "") }).catch(() => void 0);
|
|
7773
|
-
|
|
7774
|
-
await sendTemplated(
|
|
7775
|
-
{ db, envName: env.ODLA_ENV, sender: env.SEND_EMAIL, from: env.EMAIL_FROM, debugEmail: env.ODLA_DEBUG_EMAIL, now: () => Date.now(), newId: () => crypto.randomUUID() },
|
|
7776
|
-
{ group: emailGroupFrom(group), template: "prepEmail", to: app.email, vars: { firstName: String(app.firstName ?? "") }, dedupeKey: `prep:${applicationId}`, applicationId }
|
|
7777
|
-
).catch(() => void 0);
|
|
7778
|
-
}
|
|
8270
|
+
await notifyBookingConfirmed(db, env, { group, application: app, meeting: { startAt, endAt, timezone: cfg.timezone, meetUrl, htmlLink } });
|
|
7779
8271
|
return json({ ok: true, startAt, endAt, meetUrl, rescheduled: decision.reschedule });
|
|
7780
8272
|
}
|
|
7781
8273
|
var handleSchedule = async (req, url, env, ctx) => {
|
|
@@ -7806,21 +8298,21 @@ import { stripeRequest as stripeRequest5 } from "@odla-ai/stripe";
|
|
|
7806
8298
|
var row = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
7807
8299
|
var id2 = (value) => typeof value === "string" ? value : String(row(value).id ?? "");
|
|
7808
8300
|
var providerId2 = (value, prefix) => new RegExp(`^${prefix}_[A-Za-z0-9]+$`).test(value);
|
|
7809
|
-
function
|
|
8301
|
+
function fail6(code = "refund_target_mismatch") {
|
|
7810
8302
|
throw Object.assign(new Error(code), { code });
|
|
7811
8303
|
}
|
|
7812
8304
|
async function refundSubscriptionPayment(secretKey, input) {
|
|
7813
|
-
if (!providerId2(input.customerId, "cus") || !providerId2(input.subscriptionId, "sub"))
|
|
8305
|
+
if (!providerId2(input.customerId, "cus") || !providerId2(input.subscriptionId, "sub")) fail6();
|
|
7814
8306
|
const call = async (method, path, params, key3) => {
|
|
7815
8307
|
const result = await stripeRequest5({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
|
|
7816
|
-
if (!result.ok)
|
|
7817
|
-
if (!result.body || typeof result.body !== "object" || Array.isArray(result.body))
|
|
8308
|
+
if (!result.ok) fail6("stripe_error");
|
|
8309
|
+
if (!result.body || typeof result.body !== "object" || Array.isArray(result.body)) fail6("stripe_invalid_response");
|
|
7818
8310
|
return result.body;
|
|
7819
8311
|
};
|
|
7820
8312
|
const sub = await call("GET", `/v1/subscriptions/${input.subscriptionId}`);
|
|
7821
|
-
if (id2(sub) !== input.subscriptionId || id2(sub.customer) !== input.customerId)
|
|
8313
|
+
if (id2(sub) !== input.subscriptionId || id2(sub.customer) !== input.customerId) fail6();
|
|
7822
8314
|
for (const [key3, value] of Object.entries(input.metadata ?? {})) {
|
|
7823
|
-
if (row(sub.metadata)[key3] !== value)
|
|
8315
|
+
if (row(sub.metadata)[key3] !== value) fail6();
|
|
7824
8316
|
}
|
|
7825
8317
|
let cursor;
|
|
7826
8318
|
let earliest;
|
|
@@ -7833,55 +8325,55 @@ async function refundSubscriptionPayment(secretKey, input) {
|
|
|
7833
8325
|
limit: 100,
|
|
7834
8326
|
...cursor ? { starting_after: cursor } : {}
|
|
7835
8327
|
});
|
|
7836
|
-
if (!Array.isArray(list.data) || typeof list.has_more !== "boolean")
|
|
8328
|
+
if (!Array.isArray(list.data) || typeof list.has_more !== "boolean") fail6("stripe_invalid_response");
|
|
7837
8329
|
for (const value of list.data) {
|
|
7838
8330
|
const invoice2 = row(value);
|
|
7839
8331
|
const subscription = invoice2.subscription ?? row(row(invoice2.parent).subscription_details).subscription;
|
|
7840
|
-
if (!providerId2(id2(invoice2), "in") || seen.has(id2(invoice2)) || id2(subscription) !== input.subscriptionId || id2(invoice2.customer) !== input.customerId || invoice2.status !== "paid" || !Number.isSafeInteger(invoice2.created) || !Number.isSafeInteger(invoice2.amount_paid) || Number(invoice2.amount_paid) < 0)
|
|
8332
|
+
if (!providerId2(id2(invoice2), "in") || seen.has(id2(invoice2)) || id2(subscription) !== input.subscriptionId || id2(invoice2.customer) !== input.customerId || invoice2.status !== "paid" || !Number.isSafeInteger(invoice2.created) || !Number.isSafeInteger(invoice2.amount_paid) || Number(invoice2.amount_paid) < 0) fail6();
|
|
7841
8333
|
seen.add(id2(invoice2));
|
|
7842
8334
|
if (Number(invoice2.amount_paid) > 0 && (!earliest || Number(invoice2.created) < Number(earliest.created))) earliest = invoice2;
|
|
7843
8335
|
}
|
|
7844
8336
|
if (!list.has_more) break;
|
|
7845
|
-
if (!list.data.length || page === 9)
|
|
8337
|
+
if (!list.data.length || page === 9) fail6("refund_history_incomplete");
|
|
7846
8338
|
cursor = id2(list.data[list.data.length - 1]);
|
|
7847
8339
|
}
|
|
7848
|
-
if (!earliest)
|
|
8340
|
+
if (!earliest) fail6("no_charge");
|
|
7849
8341
|
const invoice = earliest;
|
|
7850
|
-
if (input.expectedInvoiceId !== void 0 && id2(invoice) !== input.expectedInvoiceId || input.expectedAmountCents !== void 0 && invoice.amount_paid !== input.expectedAmountCents)
|
|
8342
|
+
if (input.expectedInvoiceId !== void 0 && id2(invoice) !== input.expectedInvoiceId || input.expectedAmountCents !== void 0 && invoice.amount_paid !== input.expectedAmountCents) fail6();
|
|
7851
8343
|
let chargeId = id2(invoice.charge);
|
|
7852
8344
|
let paymentIntentId = id2(invoice.payment_intent);
|
|
7853
8345
|
if (!chargeId && !paymentIntentId) {
|
|
7854
8346
|
const payments = await call("GET", "/v1/invoice_payments", { invoice: id2(invoice), status: "paid", limit: 100 });
|
|
7855
|
-
if (payments.has_more !== false || !Array.isArray(payments.data) || payments.data.length !== 1)
|
|
8347
|
+
if (payments.has_more !== false || !Array.isArray(payments.data) || payments.data.length !== 1) fail6("refund_payment_ambiguous");
|
|
7856
8348
|
const payment = row(payments.data[0]);
|
|
7857
|
-
if (id2(payment.invoice) !== id2(invoice) || payment.status !== "paid" || row(payment.payment).type !== "payment_intent")
|
|
8349
|
+
if (id2(payment.invoice) !== id2(invoice) || payment.status !== "paid" || row(payment.payment).type !== "payment_intent") fail6();
|
|
7858
8350
|
paymentIntentId = id2(row(payment.payment).payment_intent);
|
|
7859
8351
|
}
|
|
7860
8352
|
if (!chargeId) {
|
|
7861
|
-
if (!providerId2(paymentIntentId, "pi"))
|
|
8353
|
+
if (!providerId2(paymentIntentId, "pi")) fail6();
|
|
7862
8354
|
const payment = await call("GET", `/v1/payment_intents/${paymentIntentId}`);
|
|
7863
|
-
if (id2(payment) !== paymentIntentId || id2(payment.customer) !== input.customerId || payment.status !== "succeeded")
|
|
8355
|
+
if (id2(payment) !== paymentIntentId || id2(payment.customer) !== input.customerId || payment.status !== "succeeded") fail6();
|
|
7864
8356
|
chargeId = id2(payment.latest_charge);
|
|
7865
8357
|
}
|
|
7866
|
-
if (!providerId2(chargeId, "ch"))
|
|
8358
|
+
if (!providerId2(chargeId, "ch")) fail6();
|
|
7867
8359
|
const charge = await call("GET", `/v1/charges/${chargeId}`);
|
|
7868
|
-
if (id2(charge) !== chargeId || id2(charge.customer) !== input.customerId || charge.status !== "succeeded" || charge.invoice != null && id2(charge.invoice) !== id2(invoice) || paymentIntentId && id2(charge.payment_intent) !== paymentIntentId || !Number.isSafeInteger(charge.amount) || Number(charge.amount) <= 0 || !Number.isSafeInteger(charge.amount_refunded) || Number(charge.amount_refunded) < 0 || Number(charge.amount_refunded) > Number(charge.amount) || charge.currency !== invoice.currency || Number(charge.amount) !== Number(invoice.amount_paid))
|
|
7869
|
-
if (input.alreadyRefundedChargeId !== void 0 && (chargeId !== input.alreadyRefundedChargeId || charge.amount_refunded !== charge.amount || charge.refunded !== true))
|
|
8360
|
+
if (id2(charge) !== chargeId || id2(charge.customer) !== input.customerId || charge.status !== "succeeded" || charge.invoice != null && id2(charge.invoice) !== id2(invoice) || paymentIntentId && id2(charge.payment_intent) !== paymentIntentId || !Number.isSafeInteger(charge.amount) || Number(charge.amount) <= 0 || !Number.isSafeInteger(charge.amount_refunded) || Number(charge.amount_refunded) < 0 || Number(charge.amount_refunded) > Number(charge.amount) || charge.currency !== invoice.currency || Number(charge.amount) !== Number(invoice.amount_paid)) fail6();
|
|
8361
|
+
if (input.alreadyRefundedChargeId !== void 0 && (chargeId !== input.alreadyRefundedChargeId || charge.amount_refunded !== charge.amount || charge.refunded !== true)) fail6();
|
|
7870
8362
|
if (charge.amount_refunded !== charge.amount) {
|
|
7871
8363
|
const refund = await call("POST", "/v1/refunds", {
|
|
7872
8364
|
charge: chargeId,
|
|
7873
8365
|
...input.metadata ? { metadata: input.metadata } : {}
|
|
7874
8366
|
}, `chapter-initial-refund:${input.subscriptionId}:${chargeId}`);
|
|
7875
|
-
if (id2(refund.charge) !== chargeId || !["succeeded", "pending"].includes(String(refund.status)) || !Number.isSafeInteger(refund.amount) || Number(refund.amount) !== Number(charge.amount) - Number(charge.amount_refunded))
|
|
8367
|
+
if (id2(refund.charge) !== chargeId || !["succeeded", "pending"].includes(String(refund.status)) || !Number.isSafeInteger(refund.amount) || Number(refund.amount) !== Number(charge.amount) - Number(charge.amount_refunded)) fail6("refund_not_confirmed");
|
|
7876
8368
|
}
|
|
7877
8369
|
let subscriptionCanceled = sub.status === "canceled";
|
|
7878
8370
|
if (input.cancelSubscription !== false && !subscriptionCanceled) {
|
|
7879
8371
|
const canceled = await call("DELETE", `/v1/subscriptions/${input.subscriptionId}`);
|
|
7880
|
-
if (id2(canceled) !== input.subscriptionId || canceled.status !== "canceled")
|
|
8372
|
+
if (id2(canceled) !== input.subscriptionId || canceled.status !== "canceled") fail6("refund_cancel_incomplete");
|
|
7881
8373
|
subscriptionCanceled = true;
|
|
7882
8374
|
}
|
|
7883
8375
|
const confirmed = await call("GET", `/v1/charges/${chargeId}`);
|
|
7884
|
-
if (id2(confirmed) !== chargeId || id2(confirmed.customer) !== input.customerId || confirmed.amount !== charge.amount || confirmed.currency !== charge.currency || confirmed.amount_refunded !== charge.amount || confirmed.refunded !== true)
|
|
8376
|
+
if (id2(confirmed) !== chargeId || id2(confirmed.customer) !== input.customerId || confirmed.amount !== charge.amount || confirmed.currency !== charge.currency || confirmed.amount_refunded !== charge.amount || confirmed.refunded !== true) fail6("refund_not_confirmed");
|
|
7885
8377
|
return { refundedCents: Number(charge.amount), subscriptionCanceled };
|
|
7886
8378
|
}
|
|
7887
8379
|
|
|
@@ -7947,7 +8439,7 @@ async function stripeCall(sk, method, path, params, idempotencyKey) {
|
|
|
7947
8439
|
idempotencyKey ? { idempotencyKey } : {}
|
|
7948
8440
|
);
|
|
7949
8441
|
}
|
|
7950
|
-
function
|
|
8442
|
+
function fail7(op, r) {
|
|
7951
8443
|
const err = new Error(`stripe ${op} failed: ${r.status}`);
|
|
7952
8444
|
err.code = "stripe_error";
|
|
7953
8445
|
throw err;
|
|
@@ -7997,7 +8489,7 @@ function createStripeProvider(config) {
|
|
|
7997
8489
|
{ email: input.email, name: input.name, metadata: meta },
|
|
7998
8490
|
`cus:${input.applicationId}`
|
|
7999
8491
|
);
|
|
8000
|
-
if (!cust.ok)
|
|
8492
|
+
if (!cust.ok) fail7("customer create", cust);
|
|
8001
8493
|
if (typeof cust.body.id !== "string" || !/^cus_[A-Za-z0-9]+$/.test(cust.body.id)) subscriptionContractMismatch();
|
|
8002
8494
|
customerId = cust.body.id;
|
|
8003
8495
|
}
|
|
@@ -8029,7 +8521,7 @@ function createStripeProvider(config) {
|
|
|
8029
8521
|
// retry between create and the db write can't orphan a second one.
|
|
8030
8522
|
`sub:${input.applicationId}${input.purchaseAttemptId ? `:${input.purchaseAttemptId}` : ""}`
|
|
8031
8523
|
);
|
|
8032
|
-
if (!sub.ok)
|
|
8524
|
+
if (!sub.ok) fail7("subscription create", sub);
|
|
8033
8525
|
if (input.billingTerms?.discount?.duration === "once" && Array.isArray(sub.body.discounts) && sub.body.discounts.length === 0) {
|
|
8034
8526
|
const subscriptionId = sub.body.id;
|
|
8035
8527
|
if (typeof subscriptionId !== "string" || !/^sub_[A-Za-z0-9]+$/.test(subscriptionId)) subscriptionContractMismatch();
|
|
@@ -8039,14 +8531,14 @@ function createStripeProvider(config) {
|
|
|
8039
8531
|
"expand[2]": "discounts",
|
|
8040
8532
|
"expand[3]": "latest_invoice.discounts"
|
|
8041
8533
|
});
|
|
8042
|
-
if (!expanded.ok)
|
|
8534
|
+
if (!expanded.ok) fail7("subscription invoice evidence", expanded);
|
|
8043
8535
|
if (expanded.body.id !== subscriptionId) subscriptionContractMismatch();
|
|
8044
8536
|
sub = expanded;
|
|
8045
8537
|
}
|
|
8046
8538
|
verifySubscriptionResult(sub.body, input, customerId, meta);
|
|
8047
8539
|
const invoice = sub.body.latest_invoice;
|
|
8048
8540
|
const clientSecret2 = clientSecretOf(sub.body);
|
|
8049
|
-
if (!clientSecret2)
|
|
8541
|
+
if (!clientSecret2) fail7("subscription confirmation-secret missing", sub);
|
|
8050
8542
|
await tagSubscriptionPayment(
|
|
8051
8543
|
sk,
|
|
8052
8544
|
invoice,
|
|
@@ -8730,15 +9222,16 @@ async function readNamedSeatOffer(db, input, now) {
|
|
|
8730
9222
|
const offering = membershipOfferingVersion(member.offeringSnapshot);
|
|
8731
9223
|
const seat = member.namedSeatId ? await membershipLoad(db, "networkSeat", String(member.namedSeatId)) : void 0;
|
|
8732
9224
|
if (seat) requireMembershipScope(seat, input);
|
|
9225
|
+
const purchaseEndsAt = seatPurchaseDeadline(offering, member);
|
|
8733
9226
|
const offer = {
|
|
8734
9227
|
membershipId: id6,
|
|
8735
9228
|
amountCents: offering.seatPriceCents,
|
|
8736
9229
|
currency: offering.currency,
|
|
8737
9230
|
interval: offering.interval,
|
|
8738
9231
|
termEndsAt: Number(member.termEndsAt),
|
|
8739
|
-
purchaseEndsAt
|
|
9232
|
+
purchaseEndsAt,
|
|
8740
9233
|
autoRenew: member.autoRenew === true,
|
|
8741
|
-
eligible: ["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) && member.refundedCents === 0 && now >= Number(member.initialTermStartedAt) && now < Number(member.termEndsAt) && now <=
|
|
9234
|
+
eligible: ["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) && member.refundedCents === 0 && now >= Number(member.initialTermStartedAt) && now < Number(member.termEndsAt) && now <= purchaseEndsAt && (!seat || ["payment_pending", "payment_processing"].includes(String(seat.status))),
|
|
8742
9235
|
...seat ? { seat: {
|
|
8743
9236
|
seatId: String(seat.id),
|
|
8744
9237
|
status: String(seat.status),
|
|
@@ -8746,7 +9239,7 @@ async function readNamedSeatOffer(db, input, now) {
|
|
|
8746
9239
|
recipientEmail: String(seat.recipientEmailNormalized)
|
|
8747
9240
|
} } : {}
|
|
8748
9241
|
};
|
|
8749
|
-
return { ...offer, digest: await namedSeatOfferDigest(offer) };
|
|
9242
|
+
return { ...offer, digest: await namedSeatOfferDigest(offer), purchaseWindowed: offering.seatPurchaseWindowCalendarMonths !== void 0 };
|
|
8750
9243
|
}
|
|
8751
9244
|
|
|
8752
9245
|
// src/named-seat-request.ts
|
|
@@ -8870,6 +9363,58 @@ var handleNamedSeat = async (req, url, env, ctx) => {
|
|
|
8870
9363
|
}
|
|
8871
9364
|
};
|
|
8872
9365
|
|
|
9366
|
+
// src/named-seat-accepted-template.ts
|
|
9367
|
+
var SEAT_ACCEPTED_TEMPLATE = {
|
|
9368
|
+
enabled: true,
|
|
9369
|
+
subject: "{{recipientName}} accepted your {{chapterName}} seat",
|
|
9370
|
+
text: "Hi {{firstName}},\n\n{{recipientName}} has accepted the {{tierName}} seat you purchased at {{chapterName}}. They will complete their own application and any membership requirements from here; nothing more is needed from you.\n\nWarmly,\n{{chapterName}}"
|
|
9371
|
+
};
|
|
9372
|
+
|
|
9373
|
+
// src/named-seat-accepted-notice.ts
|
|
9374
|
+
var text5 = (value) => typeof value === "string" ? value : "";
|
|
9375
|
+
async function notifyNamedSeatAccepted(db, env, ctx, input) {
|
|
9376
|
+
const purchaser = await membershipLoad(db, "applications", input.primaryApplicationId);
|
|
9377
|
+
if (!purchaser || !chapterBillingApplication(purchaser, ctx.chapter.id, env.ODLA_RUNTIME) || !text5(purchaser.email)) {
|
|
9378
|
+
return { sent: false, reason: "purchaser-missing" };
|
|
9379
|
+
}
|
|
9380
|
+
const group = await membershipLoad(db, "groups", ctx.chapter.id);
|
|
9381
|
+
if (!group) return { sent: false, reason: "group-missing" };
|
|
9382
|
+
const config = emailGroupFrom(group);
|
|
9383
|
+
config.emailTemplates = { seatAccepted: SEAT_ACCEPTED_TEMPLATE, ...config.emailTemplates };
|
|
9384
|
+
const purchaserName = [purchaser.firstName, purchaser.lastName].map(text5).join(" ").replace(/\s+/g, " ").trim();
|
|
9385
|
+
return sendTemplated({
|
|
9386
|
+
db,
|
|
9387
|
+
envName: env.ODLA_ENV,
|
|
9388
|
+
sender: env.SEND_EMAIL,
|
|
9389
|
+
from: env.EMAIL_FROM,
|
|
9390
|
+
debugEmail: env.ODLA_DEBUG_EMAIL,
|
|
9391
|
+
now: () => Date.now(),
|
|
9392
|
+
newId: () => crypto.randomUUID()
|
|
9393
|
+
}, {
|
|
9394
|
+
group: config,
|
|
9395
|
+
template: "seatAccepted",
|
|
9396
|
+
to: text5(purchaser.email),
|
|
9397
|
+
dedupeKey: `named-seat-accepted:${input.seatId}`,
|
|
9398
|
+
applicationId: input.primaryApplicationId,
|
|
9399
|
+
vars: {
|
|
9400
|
+
firstName: text5(purchaser.firstName),
|
|
9401
|
+
purchaserName,
|
|
9402
|
+
recipientName: input.recipientName,
|
|
9403
|
+
tierName: await tierName(db, group, input.skuId),
|
|
9404
|
+
chapterName: ctx.chapter.name,
|
|
9405
|
+
membersUrl: `${input.origin}/members/`
|
|
9406
|
+
}
|
|
9407
|
+
});
|
|
9408
|
+
}
|
|
9409
|
+
async function tierName(db, group, skuId) {
|
|
9410
|
+
try {
|
|
9411
|
+
const tier = (await loadTiers(db, group)).find((candidate) => candidate.id === skuId);
|
|
9412
|
+
if (tier) return tier.name;
|
|
9413
|
+
} catch {
|
|
9414
|
+
}
|
|
9415
|
+
return skuId;
|
|
9416
|
+
}
|
|
9417
|
+
|
|
8873
9418
|
// src/worker-routes-named-seat-recipient.ts
|
|
8874
9419
|
var handleNamedSeatRecipient = async (req, url, env, ctx) => {
|
|
8875
9420
|
if (url.pathname === "/api/named-seat/invitation") {
|
|
@@ -8987,6 +9532,13 @@ var handleNamedSeatRecipient = async (req, url, env, ctx) => {
|
|
|
8987
9532
|
const current = await membershipLoad(db, "applications", body.applicationId), group = await membershipLoad(db, "groups", ctx.chapter.id);
|
|
8988
9533
|
if (!current || current.namedSeatClaimPending !== false || !current.namedSeatAcceptedAt || !group) throw new Error("seat application reconciliation pending");
|
|
8989
9534
|
if (current.clerkUserId !== user.userId || current.groupId !== ctx.chapter.id || current.tierId !== invitation.skuId || stripeRuntimeIdentity(current.stripeRuntime) !== runtime) throw new Error("seat application identity changed");
|
|
9535
|
+
await notifyNamedSeatAccepted(db, env, ctx, {
|
|
9536
|
+
seatId: body.seatId,
|
|
9537
|
+
primaryApplicationId: invitation.primaryApplicationId,
|
|
9538
|
+
recipientName: invitation.recipientName,
|
|
9539
|
+
skuId: invitation.skuId,
|
|
9540
|
+
origin: url.origin
|
|
9541
|
+
}).catch(() => console.warn("named_seat_accepted_notice_incomplete", { seatId: body.seatId }));
|
|
8990
9542
|
await syncApplicationToCrm(crmDeps(db, ctx), { app: current, stage: String(current.status) }).catch(() => void 0);
|
|
8991
9543
|
const state = await continueJoin(db, ctx, env, url.origin, current, group);
|
|
8992
9544
|
if (!state) throw new Error("seat Journey reconciliation pending");
|
|
@@ -9347,8 +9899,8 @@ var handleAdminMeetings = async (req, url, env, ctx) => {
|
|
|
9347
9899
|
};
|
|
9348
9900
|
|
|
9349
9901
|
// src/crm-account-repair.ts
|
|
9350
|
-
async function repairAccountPeoplePage(
|
|
9351
|
-
const users = (await
|
|
9902
|
+
async function repairAccountPeoplePage(deps, after) {
|
|
9903
|
+
const users = (await deps.db.query({ $users: { $: {
|
|
9352
9904
|
...after ? { where: { id: { $gt: after } } } : {},
|
|
9353
9905
|
order: { id: "asc" },
|
|
9354
9906
|
limit: 50
|
|
@@ -9357,7 +9909,7 @@ async function repairAccountPeoplePage(deps2, after) {
|
|
|
9357
9909
|
const errors = [];
|
|
9358
9910
|
for (const user of users) {
|
|
9359
9911
|
try {
|
|
9360
|
-
if (await syncAccountToCrm(
|
|
9912
|
+
if (await syncAccountToCrm(deps, String(user.id))) synced++;
|
|
9361
9913
|
} catch (error) {
|
|
9362
9914
|
const message = error instanceof Error ? error.message : "";
|
|
9363
9915
|
errors.push({ userId: String(user.id), code: /^crm_projection_identity_(ambiguous|archived|conflict)$/.test(message) ? message : "crm_projection_unavailable" });
|
|
@@ -9369,8 +9921,8 @@ async function repairAccountPeoplePage(deps2, after) {
|
|
|
9369
9921
|
// src/crm-application-repair.ts
|
|
9370
9922
|
import { getRecord as getRecord4, updateRecord as updateRecord5 } from "@odla-ai/crm";
|
|
9371
9923
|
async function repairApplicationLink(db, chapter, recordId, actorId) {
|
|
9372
|
-
const
|
|
9373
|
-
const record4 = await getRecord4(
|
|
9924
|
+
const deps = { crm: chapter.crm, db };
|
|
9925
|
+
const record4 = await getRecord4(deps, recordId);
|
|
9374
9926
|
if (!record4 || record4.type !== "person" || record4.archived === true) return { status: 404, error: "person_not_found" };
|
|
9375
9927
|
if (!chapter.crm.type("person").fields.applicationId) return { status: 409, error: "application_link_field_not_configured" };
|
|
9376
9928
|
const fields = record4.fields;
|
|
@@ -9388,7 +9940,7 @@ async function repairApplicationLink(db, chapter, recordId, actorId) {
|
|
|
9388
9940
|
}
|
|
9389
9941
|
const app = [...applications.values()][0];
|
|
9390
9942
|
if (!app || typeof app.id !== "string" || typeof record4.updatedAt !== "number") return { status: 409, error: "application_link_evidence_incomplete" };
|
|
9391
|
-
await updateRecord5(
|
|
9943
|
+
await updateRecord5(deps, {
|
|
9392
9944
|
id: recordId,
|
|
9393
9945
|
input: applicationCrmFields(chapter.crm, { applicationId: app.id, tierId: app.tierId }),
|
|
9394
9946
|
expectedUpdatedAt: record4.updatedAt,
|
|
@@ -10086,15 +10638,6 @@ async function loadGroup2(db, id6) {
|
|
|
10086
10638
|
return groups?.[0] ?? null;
|
|
10087
10639
|
}
|
|
10088
10640
|
var str2 = (v, d = "") => typeof v === "string" ? v : d;
|
|
10089
|
-
var notifyDeps = (db, env) => ({
|
|
10090
|
-
db,
|
|
10091
|
-
debugEmail: env.ODLA_DEBUG_EMAIL,
|
|
10092
|
-
envName: env.ODLA_ENV,
|
|
10093
|
-
sender: env.SEND_EMAIL,
|
|
10094
|
-
from: env.EMAIL_FROM,
|
|
10095
|
-
now: () => Date.now(),
|
|
10096
|
-
newId: () => crypto.randomUUID()
|
|
10097
|
-
});
|
|
10098
10641
|
var handleAdminGroupEmail = async (req, url, env, ctx) => {
|
|
10099
10642
|
if (url.pathname !== "/api/admin/group/email" || req.method !== "GET" && req.method !== "PUT") return null;
|
|
10100
10643
|
const got = await gate5(req, env, ctx);
|
|
@@ -10113,6 +10656,7 @@ var handleAdminGroupEmail = async (req, url, env, ctx) => {
|
|
|
10113
10656
|
groupId: group.id,
|
|
10114
10657
|
name: group.name,
|
|
10115
10658
|
replyTo: str2(group.replyTo),
|
|
10659
|
+
fromName: str2(group.fromName),
|
|
10116
10660
|
notificationEmail: str2(group.notificationEmail),
|
|
10117
10661
|
debugEmail: str2(group.debugEmail),
|
|
10118
10662
|
emailTemplates,
|
|
@@ -10139,11 +10683,11 @@ var handleAdminGroupEmail = async (req, url, env, ctx) => {
|
|
|
10139
10683
|
for (const key3 of EMAIL_TEMPLATE_NAMES) {
|
|
10140
10684
|
const t = templates[key3];
|
|
10141
10685
|
const subject = typeof t?.subject === "string" ? t.subject.trim() : "";
|
|
10142
|
-
const
|
|
10143
|
-
if (!subject || !
|
|
10686
|
+
const text8 = typeof t?.text === "string" ? t.text : "";
|
|
10687
|
+
if (!subject || !text8.trim()) return json({ error: `template "${key3}" needs a subject and a body` }, 400);
|
|
10144
10688
|
if (/[\r\n]/.test(subject) || subject.length > 200) return json({ error: `template "${key3}" subject must be a single line under 200 characters` }, 400);
|
|
10145
|
-
if (
|
|
10146
|
-
clean[key3] = { subject, text:
|
|
10689
|
+
if (text8.length > 1e4) return json({ error: `template "${key3}" body is too long` }, 400);
|
|
10690
|
+
clean[key3] = { subject, text: text8, enabled: t?.enabled !== false };
|
|
10147
10691
|
}
|
|
10148
10692
|
const emailish = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
10149
10693
|
const notificationEmail = str2(body.notificationEmail).trim();
|
|
@@ -10152,13 +10696,15 @@ var handleAdminGroupEmail = async (req, url, env, ctx) => {
|
|
|
10152
10696
|
if (!emailish.test(notificationEmail)) return json({ error: "notification address must be a valid email" }, 400);
|
|
10153
10697
|
if (!emailish.test(replyTo)) return json({ error: "reply-to address must be a valid email" }, 400);
|
|
10154
10698
|
if (debugEmail && !emailish.test(debugEmail)) return json({ error: "debug address must be a valid email" }, 400);
|
|
10699
|
+
const fromName = str2(body.fromName).trim();
|
|
10700
|
+
if (/[\r\n<>]/.test(fromName) || fromName.length > 100) return json({ error: "from name must be one line of at most 100 characters with no angle brackets" }, 400);
|
|
10155
10701
|
const commitmentText = str2(body.commitmentText);
|
|
10156
10702
|
const normsText = str2(body.normsText);
|
|
10157
10703
|
if (commitmentText.length > 5e3 || normsText.length > 5e3) return json({ error: "commitment/norms text is too long" }, 400);
|
|
10158
10704
|
const id6 = { ns: "groups", attr: "id", value: ctx.chapter.id };
|
|
10159
10705
|
await db.transact([
|
|
10160
10706
|
{ t: "merge", ns: "groups", id: id6, attrs: { emailTemplates: clean } },
|
|
10161
|
-
{ t: "update", ns: "groups", id: id6, attrs: { commitmentText, normsText, notificationEmail, replyTo, debugEmail } }
|
|
10707
|
+
{ t: "update", ns: "groups", id: id6, attrs: { commitmentText, normsText, notificationEmail, replyTo, debugEmail, fromName } }
|
|
10162
10708
|
]);
|
|
10163
10709
|
return json({ ok: true });
|
|
10164
10710
|
};
|
|
@@ -10207,11 +10753,11 @@ var handleAdminEmailTest = async (req, url, env, ctx) => {
|
|
|
10207
10753
|
}
|
|
10208
10754
|
const group = await loadGroup2(db, ctx.chapter.id);
|
|
10209
10755
|
if (!group) return json({ error: "not found" }, 404);
|
|
10210
|
-
const res = await sendTemplated(
|
|
10756
|
+
const res = await sendTemplated(lifecycleNotifyDeps(db, env), {
|
|
10211
10757
|
group: emailGroupFrom(group),
|
|
10212
10758
|
template,
|
|
10213
10759
|
to: str2(group.notificationEmail),
|
|
10214
|
-
vars:
|
|
10760
|
+
vars: sampleVariables(url.origin, resolveScheduling(group.schedulingJson).timezone),
|
|
10215
10761
|
dedupeKey: `test:${template}:${Date.now()}`,
|
|
10216
10762
|
force: true
|
|
10217
10763
|
});
|
|
@@ -10238,7 +10784,7 @@ var handleAdminComms = async (req, url, env, ctx) => {
|
|
|
10238
10784
|
const app = data.applications?.[0] ?? null;
|
|
10239
10785
|
if (!app || !chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME)) return json({ error: "not found" }, 404);
|
|
10240
10786
|
const group = await loadGroup2(db, str2(app?.groupId, ctx.chapter.id));
|
|
10241
|
-
const vars = app ? { firstName: str2(app.firstName), lastName: str2(app.lastName), email: str2(app.email), phone: str2(app.phone), state: str2(app.state), adminUrl:
|
|
10787
|
+
const vars = app ? { firstName: str2(app.firstName), lastName: str2(app.lastName), email: str2(app.email), phone: str2(app.phone), state: str2(app.state), adminUrl: adminUrlFor(url.origin), membersUrl: membersUrlFor(url.origin) } : null;
|
|
10242
10788
|
const emails = (data.emailLog ?? []).filter((r) => r.template !== "adminNotification").map((r) => {
|
|
10243
10789
|
let mailBody = typeof r.body === "string" ? r.body : null;
|
|
10244
10790
|
if (!mailBody && group && vars) mailBody = renderTemplateBody(emailGroupFrom(group), String(r.template), vars);
|
|
@@ -10262,6 +10808,28 @@ var handleAdminComms = async (req, url, env, ctx) => {
|
|
|
10262
10808
|
import { getRecord as getRecord5, typeSummary } from "@odla-ai/crm";
|
|
10263
10809
|
import { signFederatedRequest as signFederatedRequest4, verifyFederatedRequest as verifyFederatedRequest5 } from "@odla-ai/db";
|
|
10264
10810
|
|
|
10811
|
+
// src/network-edge.ts
|
|
10812
|
+
var EDGE_UNPAIRED = "edge_unpaired";
|
|
10813
|
+
function pairingCommand(leaderId, targetId, env) {
|
|
10814
|
+
const scope2 = env ? ` --env ${env}` : "";
|
|
10815
|
+
return `odla-ai chapter edge pair --leader ${leaderId} --follower ${targetId}${scope2}`;
|
|
10816
|
+
}
|
|
10817
|
+
function edgeUnpaired(leaderId, target, env) {
|
|
10818
|
+
return {
|
|
10819
|
+
code: EDGE_UNPAIRED,
|
|
10820
|
+
target: target.id,
|
|
10821
|
+
secretName: target.secretName,
|
|
10822
|
+
message: `edge to "${target.id}" is unpaired: leader vault slot "${target.secretName}" is empty`,
|
|
10823
|
+
hint: pairingCommand(leaderId, target.id, env)
|
|
10824
|
+
};
|
|
10825
|
+
}
|
|
10826
|
+
function unpairedResponse(unpaired) {
|
|
10827
|
+
return new Response(JSON.stringify({ error: unpaired.message, ...unpaired }), {
|
|
10828
|
+
status: 502,
|
|
10829
|
+
headers: { "content-type": "application/json" }
|
|
10830
|
+
});
|
|
10831
|
+
}
|
|
10832
|
+
|
|
10265
10833
|
// src/worker-network-push.ts
|
|
10266
10834
|
import { recordDeliveryAttempt } from "@odla-ai/crm";
|
|
10267
10835
|
import { signFederatedRequest as signFederatedRequest3 } from "@odla-ai/db";
|
|
@@ -10274,15 +10842,15 @@ async function pushNetworkRecord(db, env, ctx, target, record4) {
|
|
|
10274
10842
|
newId: () => crypto.randomUUID()
|
|
10275
10843
|
};
|
|
10276
10844
|
if (!secret) {
|
|
10277
|
-
const
|
|
10845
|
+
const unpaired = edgeUnpaired(ctx.chapter.id, target, env.ODLA_ENV);
|
|
10278
10846
|
await recordDeliveryAttempt(crmDeps3, {
|
|
10279
10847
|
recordId: record4.id,
|
|
10280
10848
|
targetId: target.id,
|
|
10281
10849
|
status: "failed",
|
|
10282
10850
|
payloadVersion: 2,
|
|
10283
|
-
error
|
|
10851
|
+
error: unpaired.message
|
|
10284
10852
|
});
|
|
10285
|
-
return { id: target.id, name: target.name, ok: false, error };
|
|
10853
|
+
return { id: target.id, name: target.name, ok: false, error: unpaired.message, code: unpaired.code, hint: unpaired.hint };
|
|
10286
10854
|
}
|
|
10287
10855
|
let payload;
|
|
10288
10856
|
try {
|
|
@@ -10423,17 +10991,20 @@ async function snapshotOne(db, env, ctx, target) {
|
|
|
10423
10991
|
publicSite: { available: null }
|
|
10424
10992
|
};
|
|
10425
10993
|
const secret = await getVaultSecret(db, target.secretName);
|
|
10426
|
-
if (!secret)
|
|
10427
|
-
|
|
10428
|
-
|
|
10429
|
-
|
|
10430
|
-
|
|
10431
|
-
|
|
10432
|
-
|
|
10433
|
-
|
|
10434
|
-
|
|
10435
|
-
|
|
10436
|
-
|
|
10994
|
+
if (!secret) {
|
|
10995
|
+
const unpaired = edgeUnpaired(ctx.chapter.id, target, env.ODLA_ENV);
|
|
10996
|
+
return {
|
|
10997
|
+
...base,
|
|
10998
|
+
available: false,
|
|
10999
|
+
transport: {
|
|
11000
|
+
kind: transportKind,
|
|
11001
|
+
...target.binding ? { binding: target.binding } : {},
|
|
11002
|
+
reachable: null,
|
|
11003
|
+
error: unpaired
|
|
11004
|
+
},
|
|
11005
|
+
error: unpaired.message
|
|
11006
|
+
};
|
|
11007
|
+
}
|
|
10437
11008
|
const destination = new URL("/api/network/snapshot", target.url);
|
|
10438
11009
|
try {
|
|
10439
11010
|
const transport = resolveNetworkTargetTransport(env, target);
|
|
@@ -10913,7 +11484,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
|
10913
11484
|
if (hasFields && Object.keys(fields).some((name) => !capabilities.edit?.fields.some((field) => field.id === name))) {
|
|
10914
11485
|
return json({ error: "use the verified identity or application workflow for these fields", code: "field_forbidden" }, 403);
|
|
10915
11486
|
}
|
|
10916
|
-
const
|
|
11487
|
+
const deps = { crm: ctx.chapter.crm, db };
|
|
10917
11488
|
if (hasFields && ctx.chapter.mode === "chapter" && record4.type === "person") {
|
|
10918
11489
|
const updated2 = await updateChapterPerson({
|
|
10919
11490
|
db,
|
|
@@ -10932,7 +11503,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
|
10932
11503
|
record: await operationalReaderRecord(updated2.record, reader, db, env, ctx)
|
|
10933
11504
|
});
|
|
10934
11505
|
}
|
|
10935
|
-
const updated = hasFields ? await updateRecord6(
|
|
11506
|
+
const updated = hasFields ? await updateRecord6(deps, {
|
|
10936
11507
|
id: record4.id,
|
|
10937
11508
|
input: fields,
|
|
10938
11509
|
mutationId: activityId,
|
|
@@ -10943,7 +11514,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
|
10943
11514
|
authorId: `network:${verified.sender}`,
|
|
10944
11515
|
meta: auditMeta
|
|
10945
11516
|
}
|
|
10946
|
-
}) : await setStage3(
|
|
11517
|
+
}) : await setStage3(deps, {
|
|
10947
11518
|
id: record4.id,
|
|
10948
11519
|
to: input.stage,
|
|
10949
11520
|
authorId: `network:${verified.sender}`,
|
|
@@ -11043,7 +11614,10 @@ function targetFor2(ctx, id6) {
|
|
|
11043
11614
|
}
|
|
11044
11615
|
async function readTargetRecords(db, env, ctx, target, query) {
|
|
11045
11616
|
const secret = await getVaultSecret(db, target.secretName);
|
|
11046
|
-
if (!secret)
|
|
11617
|
+
if (!secret) {
|
|
11618
|
+
const unpaired = edgeUnpaired(ctx.chapter.id, target, env.ODLA_ENV);
|
|
11619
|
+
return { ok: false, error: unpaired.message, unpaired };
|
|
11620
|
+
}
|
|
11047
11621
|
const destination = new URL("/api/network/records", target.url);
|
|
11048
11622
|
destination.search = query.toString();
|
|
11049
11623
|
try {
|
|
@@ -11080,7 +11654,7 @@ async function mutateTargetRecord(db, env, ctx, target, input) {
|
|
|
11080
11654
|
return json({ error: "stage transitions are not enabled for this target", code: "stage_forbidden" }, 403);
|
|
11081
11655
|
}
|
|
11082
11656
|
const secret = await getVaultSecret(db, target.secretName);
|
|
11083
|
-
if (!secret) return
|
|
11657
|
+
if (!secret) return unpairedResponse(edgeUnpaired(ctx.chapter.id, target, env.ODLA_ENV));
|
|
11084
11658
|
const destination = new URL("/api/network/records", target.url);
|
|
11085
11659
|
const body = JSON.stringify(input);
|
|
11086
11660
|
try {
|
|
@@ -11162,7 +11736,9 @@ var handleAdminNetworkRecords = async (req, url, env, ctx) => {
|
|
|
11162
11736
|
if (window.search) query.set("search", window.search);
|
|
11163
11737
|
if (window.stage) query.set("stage", window.stage);
|
|
11164
11738
|
const result = await readTargetRecords(got.db, env, ctx, target, query);
|
|
11165
|
-
if (!result.ok)
|
|
11739
|
+
if (!result.ok) {
|
|
11740
|
+
return result.unpaired ? unpairedResponse(result.unpaired) : json({ error: result.error }, result.status ?? 502);
|
|
11741
|
+
}
|
|
11166
11742
|
if ((window.stage || url.searchParams.has("sort")) && (!result.page.appliedQuery || result.page.appliedQuery.sort !== window.sortId || result.page.appliedQuery.stage !== window.stage)) {
|
|
11167
11743
|
return json({ error: "follower did not apply the requested record query" }, 502);
|
|
11168
11744
|
}
|
|
@@ -11418,7 +11994,7 @@ var handleAdminNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
|
11418
11994
|
body = JSON.stringify({ body: note, mutationId: crypto.randomUUID() });
|
|
11419
11995
|
}
|
|
11420
11996
|
const secret = await getVaultSecret(got.db, target.secretName);
|
|
11421
|
-
if (!secret) return
|
|
11997
|
+
if (!secret) return unpairedResponse(edgeUnpaired(ctx.chapter.id, target, env.ODLA_ENV));
|
|
11422
11998
|
const destination = new URL("/api/network/shared-notes", target.url);
|
|
11423
11999
|
destination.search = new URLSearchParams({ type: recordType, recordId }).toString();
|
|
11424
12000
|
try {
|
|
@@ -11472,11 +12048,11 @@ function validateNetworkPersonImport(input, allowedChapterIds) {
|
|
|
11472
12048
|
if (raw2.approved !== true || raw2.isRealPerson !== true) errors.push("import must be approved and identify a real person");
|
|
11473
12049
|
const sourceSystem = typeof raw2.sourceSystem === "string" ? raw2.sourceSystem.trim().toLowerCase() : "";
|
|
11474
12050
|
const sourceRecordId = typeof raw2.sourceRecordId === "string" ? raw2.sourceRecordId.trim() : "";
|
|
11475
|
-
const
|
|
12051
|
+
const provenance2 = typeof raw2.provenance === "string" ? raw2.provenance.trim() : "";
|
|
11476
12052
|
const displayName = typeof raw2.displayName === "string" ? raw2.displayName.trim() : "";
|
|
11477
12053
|
if (!TOKEN.test(sourceSystem) || /(?:^|[._:-])(fixture|test|development|dev)(?:$|[._:-])/.test(sourceSystem)) errors.push("sourceSystem must name an approved non-development external system");
|
|
11478
12054
|
if (!sourceRecordId || sourceRecordId.length > 200) errors.push("sourceRecordId is required and bounded");
|
|
11479
|
-
if (!
|
|
12055
|
+
if (!provenance2 || provenance2.length > 500) errors.push("provenance is required and bounded");
|
|
11480
12056
|
if (!displayName || displayName.length > 200) errors.push("displayName is required and bounded");
|
|
11481
12057
|
if (FIXTURE_MARKER.test(sourceRecordId) || FIXTURE_MARKER.test(displayName)) {
|
|
11482
12058
|
errors.push("fixture, test, sample, and dummy rows cannot become network Persons");
|
|
@@ -11544,7 +12120,7 @@ function validateNetworkPersonImport(input, allowedChapterIds) {
|
|
|
11544
12120
|
sourceKind: "external_reimport",
|
|
11545
12121
|
sourceSystem,
|
|
11546
12122
|
sourceRecordId,
|
|
11547
|
-
provenance,
|
|
12123
|
+
provenance: provenance2,
|
|
11548
12124
|
approved: true,
|
|
11549
12125
|
isRealPerson: true,
|
|
11550
12126
|
displayName,
|
|
@@ -12249,7 +12825,7 @@ async function revealNetworkPersonRegistryRecord(db, input, mutationId, actor, n
|
|
|
12249
12825
|
}
|
|
12250
12826
|
|
|
12251
12827
|
// src/worker-routes-network-person-registry.ts
|
|
12252
|
-
var
|
|
12828
|
+
var PATH5 = "/api/admin/network-person-registry";
|
|
12253
12829
|
var MAX_BODY2 = 8 * 1024 * 1024;
|
|
12254
12830
|
var MUTATION_ID4 = /^[A-Za-z0-9_-]{16,128}$/;
|
|
12255
12831
|
var positiveRevision = (value) => {
|
|
@@ -12274,7 +12850,7 @@ function registryError(error) {
|
|
|
12274
12850
|
return null;
|
|
12275
12851
|
}
|
|
12276
12852
|
var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
12277
|
-
if (url.pathname !==
|
|
12853
|
+
if (url.pathname !== PATH5) return null;
|
|
12278
12854
|
if (!ctx.chapter.networkPersonRegistry) return json({ error: "not found" }, 404);
|
|
12279
12855
|
if (req.method !== "GET" && req.method !== "POST") return json({ error: "method not allowed" }, 405);
|
|
12280
12856
|
if (req.method === "POST" && Number(req.headers.get("content-length") ?? 0) > MAX_BODY2) {
|
|
@@ -12286,9 +12862,9 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
|
12286
12862
|
if (req.method === "POST") {
|
|
12287
12863
|
let body;
|
|
12288
12864
|
try {
|
|
12289
|
-
const
|
|
12290
|
-
if (new TextEncoder().encode(
|
|
12291
|
-
const parsed = JSON.parse(
|
|
12865
|
+
const text8 = await req.text();
|
|
12866
|
+
if (new TextEncoder().encode(text8).byteLength > MAX_BODY2) return json({ error: "request is too large" }, 413);
|
|
12867
|
+
const parsed = JSON.parse(text8);
|
|
12292
12868
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("invalid");
|
|
12293
12869
|
body = parsed;
|
|
12294
12870
|
} catch {
|
|
@@ -12457,7 +13033,7 @@ import { signFederatedRequest as signFederatedRequest7, verifyFederatedRequest a
|
|
|
12457
13033
|
// src/network-commercial-parity.ts
|
|
12458
13034
|
var DIGEST = /^[0-9a-f]{64}$/;
|
|
12459
13035
|
var MAX_RECORDS = 500;
|
|
12460
|
-
var
|
|
13036
|
+
var text6 = (value, max = 255) => typeof value === "string" && value.length > 0 && value.length <= max ? value : null;
|
|
12461
13037
|
function parityMessage(partition, kind, value) {
|
|
12462
13038
|
return [
|
|
12463
13039
|
"chapter-commercial-parity",
|
|
@@ -12489,18 +13065,18 @@ async function digestWithKey(key3, partition, kind, value) {
|
|
|
12489
13065
|
async function buildChapterCommercialParitySnapshot(input) {
|
|
12490
13066
|
const requestedMax = input.maxRecords ?? MAX_RECORDS;
|
|
12491
13067
|
const maxRecords = Number.isSafeInteger(requestedMax) ? Math.max(0, Math.min(requestedMax, MAX_RECORDS)) : MAX_RECORDS;
|
|
12492
|
-
const eligible = input.rows.filter((row2) =>
|
|
13068
|
+
const eligible = input.rows.filter((row2) => text6(row2.id) && (text6(row2.stripeCustomerId) || text6(row2.stripeSubscriptionId)));
|
|
12493
13069
|
const selected2 = eligible.slice(0, maxRecords);
|
|
12494
13070
|
const key3 = await parityKey(input.secret);
|
|
12495
13071
|
const records = await Promise.all(selected2.map(async (row2) => {
|
|
12496
|
-
const applicationId =
|
|
12497
|
-
const customerId =
|
|
12498
|
-
const subscriptionId =
|
|
13072
|
+
const applicationId = text6(row2.id);
|
|
13073
|
+
const customerId = text6(row2.stripeCustomerId);
|
|
13074
|
+
const subscriptionId = text6(row2.stripeSubscriptionId);
|
|
12499
13075
|
return {
|
|
12500
13076
|
applicationDigest: await digestWithKey(key3, input.partition, "application", applicationId),
|
|
12501
13077
|
customerDigest: customerId ? await digestWithKey(key3, input.partition, "customer", customerId) : null,
|
|
12502
13078
|
subscriptionDigest: subscriptionId ? await digestWithKey(key3, input.partition, "subscription", subscriptionId) : null,
|
|
12503
|
-
status:
|
|
13079
|
+
status: text6(row2.status, 64),
|
|
12504
13080
|
canceled: row2.canceled === true
|
|
12505
13081
|
};
|
|
12506
13082
|
}));
|
|
@@ -12526,7 +13102,7 @@ function validSnapshot(snapshot, source) {
|
|
|
12526
13102
|
if (snapshot?.version !== 1 || snapshot.site?.id !== source.chapterId || !partition || !Array.isArray(records) || records.length > MAX_RECORDS || partition.appId !== source.appId || partition.chapterId !== source.chapterId || partition.environment !== source.environment || partition.dataEnvironment !== source.dataEnvironment || partition.runtime !== source.runtime || typeof snapshot.complete !== "boolean" || typeof snapshot.truncated !== "boolean" || !snapshot.counts) return false;
|
|
12527
13103
|
const seen = /* @__PURE__ */ new Set();
|
|
12528
13104
|
for (const record4 of records) {
|
|
12529
|
-
if (!DIGEST.test(record4.applicationDigest) || record4.customerDigest !== null && !DIGEST.test(record4.customerDigest) || record4.subscriptionDigest !== null && !DIGEST.test(record4.subscriptionDigest) || record4.status !== null &&
|
|
13105
|
+
if (!DIGEST.test(record4.applicationDigest) || record4.customerDigest !== null && !DIGEST.test(record4.customerDigest) || record4.subscriptionDigest !== null && !DIGEST.test(record4.subscriptionDigest) || record4.status !== null && text6(record4.status, 64) === null || typeof record4.canceled !== "boolean") return false;
|
|
12530
13106
|
const key3 = recordKey(record4);
|
|
12531
13107
|
if (seen.has(key3)) return false;
|
|
12532
13108
|
seen.add(key3);
|
|
@@ -12537,16 +13113,16 @@ async function reconcileCommercialParitySnapshot(input) {
|
|
|
12537
13113
|
if (!validSnapshot(input.snapshot, input.source)) throw new Error("invalid chapter commercial parity snapshot");
|
|
12538
13114
|
const sourceKey = networkCommercialSourceKey(input.source);
|
|
12539
13115
|
const key3 = await parityKey(input.secret);
|
|
12540
|
-
const shadow = await Promise.all(input.projection.subscriptions.filter((row2) => row2.sourceKey === sourceKey &&
|
|
12541
|
-
const customerId =
|
|
12542
|
-
const subscriptionId =
|
|
13116
|
+
const shadow = await Promise.all(input.projection.subscriptions.filter((row2) => row2.sourceKey === sourceKey && text6(row2.applicationId) && text6(row2.objectId)).map(async (row2) => {
|
|
13117
|
+
const customerId = text6(row2.customerId);
|
|
13118
|
+
const subscriptionId = text6(row2.subscriptionId) ?? text6(row2.objectId);
|
|
12543
13119
|
return {
|
|
12544
13120
|
applicationDigest: await digestWithKey(key3, input.snapshot.partition, "application", row2.applicationId),
|
|
12545
13121
|
customerDigest: customerId ? await digestWithKey(key3, input.snapshot.partition, "customer", customerId) : null,
|
|
12546
13122
|
subscriptionDigest: subscriptionId ? await digestWithKey(key3, input.snapshot.partition, "subscription", subscriptionId) : null,
|
|
12547
13123
|
status: null,
|
|
12548
13124
|
canceled: false,
|
|
12549
|
-
stripeStatus:
|
|
13125
|
+
stripeStatus: text6(row2.status, 64),
|
|
12550
13126
|
stripeCanceled: row2.status === "canceled"
|
|
12551
13127
|
};
|
|
12552
13128
|
}));
|
|
@@ -12672,7 +13248,7 @@ var handleNetworkCommercialParity = async (req, url, env, ctx) => {
|
|
|
12672
13248
|
async function reconcileTarget(db, env, ctx, target, projection) {
|
|
12673
13249
|
const base = { targetId: target.id, name: target.name, url: target.url };
|
|
12674
13250
|
const secret = await getVaultSecret(db, target.secretName);
|
|
12675
|
-
if (!secret) return { ...base, available: false, ok: false, error:
|
|
13251
|
+
if (!secret) return { ...base, available: false, ok: false, error: edgeUnpaired(ctx.chapter.id, target, env.ODLA_ENV) };
|
|
12676
13252
|
const destination = new URL(PARITY_PATH, target.url);
|
|
12677
13253
|
try {
|
|
12678
13254
|
const transport = resolveNetworkTargetTransport(env, target);
|
|
@@ -12828,11 +13404,23 @@ var handleNetworkCommercialStripeWebhook = async (req, url, env, ctx) => {
|
|
|
12828
13404
|
const event = await constructStripeEvent(body, signature, secret);
|
|
12829
13405
|
const fact = normalizeNetworkCommercialStripeEvent(event, await stripeEventDigest(body));
|
|
12830
13406
|
if (!fact.eventId || !fact.eventType || !fact.objectId) return json({ error: "Stripe event identity is invalid" }, 400);
|
|
12831
|
-
const result = await ingestNetworkCommercialFact(
|
|
13407
|
+
const result = await ingestNetworkCommercialFact(
|
|
13408
|
+
db,
|
|
13409
|
+
ctx.chapter.networkCommercialReadModel.sources,
|
|
13410
|
+
fact,
|
|
13411
|
+
Date.now(),
|
|
13412
|
+
await stripePayloadDigest(body),
|
|
13413
|
+
networkCommercialAlerts(ctx, WEBHOOK_PATH)
|
|
13414
|
+
);
|
|
12832
13415
|
if (result.status === "collision") {
|
|
12833
13416
|
console.error("network commercial event collision", JSON.stringify({ eventId: fact.eventId, eventType: fact.eventType }));
|
|
12834
13417
|
}
|
|
12835
|
-
return json({
|
|
13418
|
+
return json({
|
|
13419
|
+
ok: true,
|
|
13420
|
+
status: result.status,
|
|
13421
|
+
eventId: fact.eventId,
|
|
13422
|
+
...result.status === "replayed" && result.resolvedCollision ? { resolvedCollision: true } : {}
|
|
13423
|
+
});
|
|
12836
13424
|
} catch (error) {
|
|
12837
13425
|
if (error instanceof StripeError) return json({ error: error.message, code: error.code }, 400);
|
|
12838
13426
|
throw error;
|
|
@@ -12856,18 +13444,39 @@ var handleAdminNetworkCommercialReadModel = async (req, url, env, ctx) => {
|
|
|
12856
13444
|
} catch {
|
|
12857
13445
|
return json({ error: "invalid JSON body" }, 400);
|
|
12858
13446
|
}
|
|
13447
|
+
const alerts = networkCommercialAlerts(ctx, ADMIN_PATH2);
|
|
12859
13448
|
if (parsed.command === "recover-event") {
|
|
12860
13449
|
try {
|
|
12861
13450
|
return json(await recoverNetworkCommercialEvent(
|
|
12862
13451
|
db,
|
|
12863
13452
|
ctx.chapter.networkCommercialReadModel.sources,
|
|
12864
13453
|
String(parsed.eventId ?? ""),
|
|
12865
|
-
got.user.userId
|
|
13454
|
+
got.user.userId,
|
|
13455
|
+
Date.now(),
|
|
13456
|
+
alerts
|
|
12866
13457
|
));
|
|
12867
13458
|
} catch (error) {
|
|
12868
13459
|
return json({ error: error instanceof Error ? error.message : "event recovery failed" }, 409);
|
|
12869
13460
|
}
|
|
12870
13461
|
}
|
|
13462
|
+
if (parsed.command === "resolve") {
|
|
13463
|
+
if (typeof parsed.mutationId !== "string" || !MUTATION_ID5.test(parsed.mutationId)) return json({ error: "resolve requires a valid mutationId" }, 400);
|
|
13464
|
+
const reason = typeof parsed.reason === "string" ? parsed.reason.trim() : "";
|
|
13465
|
+
if (!reason || reason.length > 500) return json({ error: "resolve requires a reason of at most 500 characters" }, 400);
|
|
13466
|
+
try {
|
|
13467
|
+
const result = await resolveNetworkCommercialCollision(
|
|
13468
|
+
db,
|
|
13469
|
+
ctx.chapter.networkCommercialReadModel.sources,
|
|
13470
|
+
{ eventId: String(parsed.eventId ?? ""), mutationId: parsed.mutationId, actorUserId: got.user.userId, reason },
|
|
13471
|
+
Date.now(),
|
|
13472
|
+
alerts
|
|
13473
|
+
);
|
|
13474
|
+
return json({ resolved: true, eventId: result.eventId, reviewId: result.reviewId, ...safeProjection(result.projection) });
|
|
13475
|
+
} catch (error) {
|
|
13476
|
+
const code = error.code;
|
|
13477
|
+
return json({ error: error instanceof Error ? error.message : "resolve failed", code }, code === "invalid_event" ? 400 : 409);
|
|
13478
|
+
}
|
|
13479
|
+
}
|
|
12871
13480
|
if (parsed.command === "reconcile-chapters") {
|
|
12872
13481
|
const projection2 = await loadNetworkCommercialProjection(db, ctx.chapter.networkCommercialReadModel.sources);
|
|
12873
13482
|
return json(await reconcileChapterCommercialTargets(db, env, ctx, projection2));
|
|
@@ -12895,9 +13504,9 @@ var handleAdminNetworkCommercialReadModel = async (req, url, env, ctx) => {
|
|
|
12895
13504
|
}
|
|
12896
13505
|
}
|
|
12897
13506
|
if (parsed.command !== "rebuild" || typeof parsed.mutationId !== "string" || !MUTATION_ID5.test(parsed.mutationId)) {
|
|
12898
|
-
return json({ error: "rebuild, reconcile-stripe, or reconcile-chapters command is required; rebuild also
|
|
13507
|
+
return json({ error: "rebuild, resolve, recover-event, reconcile-stripe, or reconcile-chapters command is required; rebuild and resolve also require a valid mutationId" }, 400);
|
|
12899
13508
|
}
|
|
12900
|
-
const projection = await rebuildNetworkCommercialProjection(db, ctx.chapter.networkCommercialReadModel.sources, parsed.mutationId);
|
|
13509
|
+
const projection = await rebuildNetworkCommercialProjection(db, ctx.chapter.networkCommercialReadModel.sources, parsed.mutationId, Date.now(), alerts);
|
|
12901
13510
|
return json({ rebuilt: true, ...safeProjection(projection) });
|
|
12902
13511
|
}
|
|
12903
13512
|
return json(safeProjection(await loadNetworkCommercialProjection(db, ctx.chapter.networkCommercialReadModel.sources)));
|
|
@@ -13007,7 +13616,7 @@ var record3 = (value) => value !== null && typeof value === "object" && !Array.i
|
|
|
13007
13616
|
function unknownKeys3(value, allowed2, path, errors) {
|
|
13008
13617
|
for (const key3 of Object.keys(value)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
|
|
13009
13618
|
}
|
|
13010
|
-
function
|
|
13619
|
+
function text7(value, path, errors, max = MAX_TEXT2, required = true) {
|
|
13011
13620
|
if (value === void 0 && !required) return void 0;
|
|
13012
13621
|
if (typeof value !== "string" || required && !value.trim()) {
|
|
13013
13622
|
errors.push(`${path} must be a non-empty string`);
|
|
@@ -13033,7 +13642,7 @@ function scopeValue(value, path, errors) {
|
|
|
13033
13642
|
return { kind: raw2.kind };
|
|
13034
13643
|
}
|
|
13035
13644
|
if (raw2.kind === "chapter") {
|
|
13036
|
-
const chapterId =
|
|
13645
|
+
const chapterId = text7(raw2.chapterId, `${path}.chapterId`, errors, 63) ?? "";
|
|
13037
13646
|
if (chapterId && !APP_ID.test(chapterId)) errors.push(`${path}.chapterId must be a lowercase app slug`);
|
|
13038
13647
|
return { kind: "chapter", chapterId };
|
|
13039
13648
|
}
|
|
@@ -13047,22 +13656,22 @@ function resourceValue(value, path, errors) {
|
|
|
13047
13656
|
return null;
|
|
13048
13657
|
}
|
|
13049
13658
|
unknownKeys3(raw2, RESOURCE_KEYS, path, errors);
|
|
13050
|
-
const id6 =
|
|
13659
|
+
const id6 = text7(raw2.id, `${path}.id`, errors, 80) ?? "";
|
|
13051
13660
|
if (id6 && !ID4.test(id6)) errors.push(`${path}.id must be lowercase kebab-case`);
|
|
13052
|
-
const kind =
|
|
13661
|
+
const kind = text7(raw2.kind, `${path}.kind`, errors) ?? "";
|
|
13053
13662
|
if (!KINDS.has(kind)) errors.push(`${path}.kind is unsupported`);
|
|
13054
|
-
const title =
|
|
13055
|
-
const summary =
|
|
13056
|
-
const status2 =
|
|
13663
|
+
const title = text7(raw2.title, `${path}.title`, errors, 240) ?? "";
|
|
13664
|
+
const summary = text7(raw2.summary, `${path}.summary`, errors, 1e3, false);
|
|
13665
|
+
const status2 = text7(raw2.status, `${path}.status`, errors) ?? "";
|
|
13057
13666
|
if (!STATUSES.has(status2)) errors.push(`${path}.status must be draft, published, or archived`);
|
|
13058
13667
|
const revision = integer4(raw2.revision, `${path}.revision`, errors);
|
|
13059
13668
|
const scope2 = scopeValue(raw2.scope, `${path}.scope`, errors) ?? { kind: "global" };
|
|
13060
|
-
const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) =>
|
|
13669
|
+
const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) => text7(tag, `${path}.tags[${index2}]`, errors, 80) ?? "") : [];
|
|
13061
13670
|
if (!Array.isArray(raw2.tags)) errors.push(`${path}.tags must be an array`);
|
|
13062
13671
|
if (tags.length > MAX_TAGS) errors.push(`${path}.tags may contain at most ${MAX_TAGS} items`);
|
|
13063
13672
|
if (new Set(tags).size !== tags.length) errors.push(`${path}.tags must be unique`);
|
|
13064
|
-
const bodyMarkdown =
|
|
13065
|
-
const url =
|
|
13673
|
+
const bodyMarkdown = text7(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN3, false);
|
|
13674
|
+
const url = text7(raw2.url, `${path}.url`, errors, 2048, false);
|
|
13066
13675
|
if (url) {
|
|
13067
13676
|
try {
|
|
13068
13677
|
if (new URL(url).protocol !== "https:") errors.push(`${path}.url must use https`);
|
|
@@ -13074,9 +13683,9 @@ function resourceValue(value, path, errors) {
|
|
|
13074
13683
|
if (raw2.brandAsset !== void 0 && !asset) errors.push(`${path}.brandAsset must be an object`);
|
|
13075
13684
|
if (asset) unknownKeys3(asset, ASSET_KEYS, `${path}.brandAsset`, errors);
|
|
13076
13685
|
const brandAsset = asset ? {
|
|
13077
|
-
bookId:
|
|
13078
|
-
assetId:
|
|
13079
|
-
...asset.filename === void 0 ? {} : { filename:
|
|
13686
|
+
bookId: text7(asset.bookId, `${path}.brandAsset.bookId`, errors, 160) ?? "",
|
|
13687
|
+
assetId: text7(asset.assetId, `${path}.brandAsset.assetId`, errors, 160) ?? "",
|
|
13688
|
+
...asset.filename === void 0 ? {} : { filename: text7(asset.filename, `${path}.brandAsset.filename`, errors, 240) ?? "" }
|
|
13080
13689
|
} : void 0;
|
|
13081
13690
|
if ((kind === "document" || kind === "note") && !bodyMarkdown) errors.push(`${path}.bodyMarkdown is required for ${kind}`);
|
|
13082
13691
|
if ((kind === "link" || kind === "inspiration") && !url) errors.push(`${path}.url is required for ${kind}`);
|
|
@@ -13091,21 +13700,21 @@ function resourceValue(value, path, errors) {
|
|
|
13091
13700
|
if (provenanceRaw?.promotedFrom !== void 0 && !promotedRaw) errors.push(`${path}.provenance.promotedFrom must be an object`);
|
|
13092
13701
|
if (promotedRaw) unknownKeys3(promotedRaw, PROMOTION_KEYS, `${path}.provenance.promotedFrom`, errors);
|
|
13093
13702
|
const promotedFrom = promotedRaw ? {
|
|
13094
|
-
chapterId:
|
|
13095
|
-
resourceId:
|
|
13703
|
+
chapterId: text7(promotedRaw.chapterId, `${path}.provenance.promotedFrom.chapterId`, errors, 63) ?? "",
|
|
13704
|
+
resourceId: text7(promotedRaw.resourceId, `${path}.provenance.promotedFrom.resourceId`, errors, 80) ?? "",
|
|
13096
13705
|
resourceRevision: integer4(promotedRaw.resourceRevision, `${path}.provenance.promotedFrom.resourceRevision`, errors),
|
|
13097
13706
|
documentRevision: integer4(promotedRaw.documentRevision, `${path}.provenance.promotedFrom.documentRevision`, errors),
|
|
13098
|
-
promotedBy:
|
|
13707
|
+
promotedBy: text7(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
|
|
13099
13708
|
promotedAt: integer4(promotedRaw.promotedAt, `${path}.provenance.promotedFrom.promotedAt`, errors, 0)
|
|
13100
13709
|
} : void 0;
|
|
13101
|
-
const
|
|
13102
|
-
createdBy:
|
|
13710
|
+
const provenance2 = {
|
|
13711
|
+
createdBy: text7(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
|
|
13103
13712
|
createdAt: integer4(provenanceRaw?.createdAt, `${path}.provenance.createdAt`, errors, 0),
|
|
13104
|
-
updatedBy:
|
|
13713
|
+
updatedBy: text7(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
|
|
13105
13714
|
updatedAt: integer4(provenanceRaw?.updatedAt, `${path}.provenance.updatedAt`, errors, 0),
|
|
13106
13715
|
...promotedFrom ? { promotedFrom } : {}
|
|
13107
13716
|
};
|
|
13108
|
-
return { id: id6, kind, title, ...summary ? { summary } : {}, tags, scope: scope2, status: status2, revision, ...bodyMarkdown ? { bodyMarkdown } : {}, ...url ? { url } : {}, ...brandAsset ? { brandAsset } : {}, provenance };
|
|
13717
|
+
return { id: id6, kind, title, ...summary ? { summary } : {}, tags, scope: scope2, status: status2, revision, ...bodyMarkdown ? { bodyMarkdown } : {}, ...url ? { url } : {}, ...brandAsset ? { brandAsset } : {}, provenance: provenance2 };
|
|
13109
13718
|
}
|
|
13110
13719
|
function validateBuilderLibraryDocument(value, options = {}) {
|
|
13111
13720
|
const errors = [];
|
|
@@ -13120,7 +13729,7 @@ function validateBuilderLibraryDocument(value, options = {}) {
|
|
|
13120
13729
|
if (sourceRaw) unknownKeys3(sourceRaw, SOURCE_KEYS2, "document.source", errors);
|
|
13121
13730
|
const sourceType = sourceRaw?.type;
|
|
13122
13731
|
if (sourceType !== "chapter" && sourceType !== "built_not_found") errors.push("document.source.type must be chapter or built_not_found");
|
|
13123
|
-
const sourceAppId =
|
|
13732
|
+
const sourceAppId = text7(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
|
|
13124
13733
|
if (sourceAppId && !APP_ID.test(sourceAppId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
13125
13734
|
if (options.sourceType && sourceType !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
|
|
13126
13735
|
if (options.sourceAppId && sourceAppId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
|
|
@@ -13128,8 +13737,8 @@ function validateBuilderLibraryDocument(value, options = {}) {
|
|
|
13128
13737
|
if (raw2.target !== void 0 && !targetRaw) errors.push("document.target must be an object");
|
|
13129
13738
|
if (targetRaw) unknownKeys3(targetRaw, TARGET_KEYS2, "document.target", errors);
|
|
13130
13739
|
const target = targetRaw ? {
|
|
13131
|
-
appId:
|
|
13132
|
-
environment:
|
|
13740
|
+
appId: text7(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
|
|
13741
|
+
environment: text7(targetRaw.environment, "document.target.environment", errors, 80) ?? ""
|
|
13133
13742
|
} : void 0;
|
|
13134
13743
|
if (options.targetAppId && target?.appId !== options.targetAppId) errors.push(`document.target.appId must be ${options.targetAppId}`);
|
|
13135
13744
|
if (!Array.isArray(raw2.resources)) errors.push("document.resources must be an array");
|
|
@@ -13763,7 +14372,11 @@ async function reconcileMembershipRenewal(db, input, now) {
|
|
|
13763
14372
|
membershipUpdate("networkMembership", id6, {
|
|
13764
14373
|
status: status2,
|
|
13765
14374
|
graceEndsAt,
|
|
13766
|
-
...paid ? {
|
|
14375
|
+
...paid ? {
|
|
14376
|
+
termStartedAt: start,
|
|
14377
|
+
termEndsAt: end,
|
|
14378
|
+
...membershipOfferingVersion(member.offeringSnapshot).seatPurchaseWindowCalendarMonths === void 0 ? { seatPurchaseEndsAt: end } : {}
|
|
14379
|
+
} : {},
|
|
13767
14380
|
revision: Number(member.revision) + 1,
|
|
13768
14381
|
updatedAt: now
|
|
13769
14382
|
}),
|
|
@@ -13923,10 +14536,11 @@ async function reserveNamedMembershipSeat(db, input, now) {
|
|
|
13923
14536
|
const id6 = (await membershipOfferIdForRequest({ ...input, requestId: "named_seat" })).replace(/^offer_/, "seat_");
|
|
13924
14537
|
let seat = await membershipLoad(db, "networkSeat", id6);
|
|
13925
14538
|
if (!seat) {
|
|
13926
|
-
|
|
14539
|
+
const offering = membershipOfferingVersion(member.offeringSnapshot), claimId = await recipientKey(input, email);
|
|
14540
|
+
const purchaseEndsAt = seatPurchaseDeadline(offering, member);
|
|
14541
|
+
if (!["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) || Number(member.refundedCents) !== 0 || now < Number(member.initialTermStartedAt) || now >= Number(member.termEndsAt) || now > purchaseEndsAt || !Number.isSafeInteger(purchaseEndsAt) || member.namedSeatId !== void 0) {
|
|
13927
14542
|
membershipFail("named_seat_purchase_unavailable");
|
|
13928
14543
|
}
|
|
13929
|
-
const offering = membershipOfferingVersion(member.offeringSnapshot), claimId = await recipientKey(input, email);
|
|
13930
14544
|
const attrs = {
|
|
13931
14545
|
id: id6,
|
|
13932
14546
|
...membershipScope(input),
|
|
@@ -13945,7 +14559,7 @@ async function reserveNamedMembershipSeat(db, input, now) {
|
|
|
13945
14559
|
status: "payment_pending",
|
|
13946
14560
|
preparedAt: now,
|
|
13947
14561
|
termEndsAt: member.termEndsAt,
|
|
13948
|
-
seatPurchaseEndsAt:
|
|
14562
|
+
seatPurchaseEndsAt: purchaseEndsAt,
|
|
13949
14563
|
offeringSnapshot: offering,
|
|
13950
14564
|
revision: 1,
|
|
13951
14565
|
createdAt: now,
|
|
@@ -14147,7 +14761,7 @@ async function confirmNamedMembershipSeatPayment(db, input, now) {
|
|
|
14147
14761
|
}
|
|
14148
14762
|
if (seat.purchasedAt === void 0) {
|
|
14149
14763
|
if (!["payment_processing", "payment_canceling"].includes(String(seat.status))) membershipFail("named_seat_payment_not_expected");
|
|
14150
|
-
const eligible = seat.status !== "payment_canceling" && ["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) && Number(member.refundedCents) === 0 && paidAt <=
|
|
14764
|
+
const eligible = seat.status !== "payment_canceling" && ["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) && Number(member.refundedCents) === 0 && paidAt <= seatPurchaseDeadline(membershipOfferingVersion(member.offeringSnapshot), member) && now < Number(member.termEndsAt);
|
|
14151
14765
|
const status2 = eligible ? "renewal_pending" : "refund_pending", effectId = `${seat.id}:${eligible ? "attach-renewal" : "refund"}`;
|
|
14152
14766
|
try {
|
|
14153
14767
|
await db.transact([
|
|
@@ -15136,7 +15750,7 @@ async function readBoundedJson(response, signal, maxBytes, label) {
|
|
|
15136
15750
|
};
|
|
15137
15751
|
signal.addEventListener("abort", cancel, { once: true });
|
|
15138
15752
|
let size = 0;
|
|
15139
|
-
let
|
|
15753
|
+
let text8 = "";
|
|
15140
15754
|
const decoder = new TextDecoder();
|
|
15141
15755
|
try {
|
|
15142
15756
|
while (true) {
|
|
@@ -15146,10 +15760,10 @@ async function readBoundedJson(response, signal, maxBytes, label) {
|
|
|
15146
15760
|
if (done) break;
|
|
15147
15761
|
size += value.byteLength;
|
|
15148
15762
|
if (size > maxBytes) throw new TypeError(`${label} exceeds limit`);
|
|
15149
|
-
|
|
15763
|
+
text8 += decoder.decode(value, { stream: true });
|
|
15150
15764
|
}
|
|
15151
15765
|
try {
|
|
15152
|
-
return JSON.parse(
|
|
15766
|
+
return JSON.parse(text8 + decoder.decode());
|
|
15153
15767
|
} catch {
|
|
15154
15768
|
throw new TypeError(`invalid ${label}`);
|
|
15155
15769
|
}
|
|
@@ -15366,7 +15980,7 @@ async function requestNamedSeatReservation(options) {
|
|
|
15366
15980
|
async function requestNamedSeatOffer(options) {
|
|
15367
15981
|
const result = await requestAuthority(options, "seat-offer", { applicationId: options.applicationId, skuId: options.skuId });
|
|
15368
15982
|
const offer = result.seat;
|
|
15369
|
-
if (!offer || offer.membershipId !== await networkMembershipId(options) || !Number.isSafeInteger(offer.amountCents) || offer.amountCents <= 0 || !/^[a-z]{3}$/.test(offer.currency) || !["month", "year"].includes(offer.interval) || typeof offer.eligible !== "boolean" || typeof offer.autoRenew !== "boolean" || !Number.isSafeInteger(offer.termEndsAt) || !Number.isSafeInteger(offer.purchaseEndsAt) || offer.digest !== await namedSeatOfferDigest(offer)) throw new Error("invalid named seat offer");
|
|
15983
|
+
if (!offer || offer.membershipId !== await networkMembershipId(options) || !Number.isSafeInteger(offer.amountCents) || offer.amountCents <= 0 || !/^[a-z]{3}$/.test(offer.currency) || !["month", "year"].includes(offer.interval) || typeof offer.eligible !== "boolean" || typeof offer.autoRenew !== "boolean" || !Number.isSafeInteger(offer.termEndsAt) || !Number.isSafeInteger(offer.purchaseEndsAt) || offer.purchaseWindowed !== void 0 && typeof offer.purchaseWindowed !== "boolean" || offer.digest !== await namedSeatOfferDigest(offer)) throw new Error("invalid named seat offer");
|
|
15370
15984
|
return offer;
|
|
15371
15985
|
}
|
|
15372
15986
|
async function requestNamedSeatInvitation(options) {
|
|
@@ -15811,6 +16425,7 @@ var BUILTIN_ROUTES = [
|
|
|
15811
16425
|
handlePaymentRecovery,
|
|
15812
16426
|
handleAdminNamedSeat,
|
|
15813
16427
|
handleAdminMembershipSeats,
|
|
16428
|
+
handleAdminMembershipPolicyVersions,
|
|
15814
16429
|
handleAdminMeetings,
|
|
15815
16430
|
handleAdminScheduling,
|
|
15816
16431
|
// Roster + identity
|