@odla-ai/chapter 0.51.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 +102 -8
- package/dist/{chunk-4EA5V4JE.js → chunk-7N4VDWM5.js} +239 -136
- package/dist/chunk-7N4VDWM5.js.map +1 -0
- package/dist/{chunk-UAXWA5BL.js → chunk-7RIT2UN5.js} +2 -2
- 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 +88 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +70 -12
- package/dist/index.d.ts +70 -12
- package/dist/index.js +88 -7
- 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 +912 -354
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +47 -9
- package/dist/worker/index.d.ts +47 -9
- package/dist/worker/index.js +912 -354
- 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-UAXWA5BL.js.map → chunk-7RIT2UN5.js.map} +0 -0
package/dist/worker/index.cjs
CHANGED
|
@@ -190,11 +190,11 @@ async function clerkVerifiedPrimaryEmail(db, userId) {
|
|
|
190
190
|
|
|
191
191
|
// src/crm-person-projection.ts
|
|
192
192
|
var import_crm = require("@odla-ai/crm");
|
|
193
|
-
async function ensureProjectedPerson(
|
|
193
|
+
async function ensureProjectedPerson(deps, input, options = {}) {
|
|
194
194
|
const email = String(input.email ?? "").trim().toLowerCase();
|
|
195
195
|
if (!email) throw new Error("crm_projection_email_required");
|
|
196
196
|
const find = async () => {
|
|
197
|
-
const records = (await
|
|
197
|
+
const records = (await deps.db.query({ crm_record: { $: {
|
|
198
198
|
where: { type: "person", primaryEmail: email },
|
|
199
199
|
limit: 2
|
|
200
200
|
} } })).crm_record ?? [];
|
|
@@ -207,7 +207,7 @@ async function ensureProjectedPerson(deps2, input, options = {}) {
|
|
|
207
207
|
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(`chapter:person:v1:${email}`)));
|
|
208
208
|
const id6 = `person-${Array.from(digest, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
209
209
|
try {
|
|
210
|
-
const created = await (0, import_crm.createRecord)({ ...
|
|
210
|
+
const created = await (0, import_crm.createRecord)({ ...deps, db: deps.db }, {
|
|
211
211
|
id: id6,
|
|
212
212
|
type: "person",
|
|
213
213
|
input: { ...input, email },
|
|
@@ -223,14 +223,14 @@ async function ensureProjectedPerson(deps2, input, options = {}) {
|
|
|
223
223
|
}
|
|
224
224
|
|
|
225
225
|
// src/crm-account-projection.ts
|
|
226
|
-
async function syncAccountToCrm(
|
|
227
|
-
const user = (await
|
|
226
|
+
async function syncAccountToCrm(deps, userId) {
|
|
227
|
+
const user = (await deps.db.query({ $users: { $: { where: { id: userId }, limit: 1 } } })).$users?.[0];
|
|
228
228
|
if (!user || user.deleted === true || typeof user.email !== "string" || !user.email.trim()) return null;
|
|
229
|
-
const linked = (await
|
|
229
|
+
const linked = (await deps.db.query({ crm_record: { $: { where: { type: "person", clerkUserId: userId }, limit: 2 } } })).crm_record ?? [];
|
|
230
230
|
if (linked.length > 1) throw new Error("crm_projection_identity_ambiguous");
|
|
231
231
|
if (linked.length === 1) return String(linked[0].id);
|
|
232
232
|
const email = user.email.trim().toLowerCase();
|
|
233
|
-
const peers = (await
|
|
233
|
+
const peers = (await deps.db.query({ $users: { $: { where: { email }, limit: 2 } } })).$users ?? [];
|
|
234
234
|
if (peers.length !== 1 || peers[0]?.id !== userId) throw new Error("crm_projection_identity_ambiguous");
|
|
235
235
|
const userGuard = {
|
|
236
236
|
ns: "$users",
|
|
@@ -239,14 +239,14 @@ async function syncAccountToCrm(deps2, userId) {
|
|
|
239
239
|
equals: { email: user.email, ...user.deleted !== void 0 ? { deleted: user.deleted } : {} },
|
|
240
240
|
...user.deleted === void 0 ? { absent: ["deleted"] } : {}
|
|
241
241
|
};
|
|
242
|
-
const { record: record4 } = await ensureProjectedPerson(
|
|
242
|
+
const { record: record4 } = await ensureProjectedPerson(deps, {
|
|
243
243
|
name: typeof user.name === "string" && user.name.trim() ? user.name : email,
|
|
244
244
|
email
|
|
245
245
|
}, { stage: null, guards: [userGuard], emailConsent: { state: "unsubscribed", source: "account-no-marketing-consent" } });
|
|
246
246
|
if (record4.clerkUserId === userId) return String(record4.id);
|
|
247
247
|
if (record4.clerkUserId != null) throw new Error("crm_projection_identity_conflict");
|
|
248
248
|
try {
|
|
249
|
-
await
|
|
249
|
+
await deps.db.transact([{ t: "update", ns: "crm_record", id: String(record4.id), attrs: { clerkUserId: userId } }], {
|
|
250
250
|
guards: [userGuard, {
|
|
251
251
|
ns: "crm_record",
|
|
252
252
|
id: String(record4.id),
|
|
@@ -262,12 +262,37 @@ async function syncAccountToCrm(deps2, userId) {
|
|
|
262
262
|
});
|
|
263
263
|
} catch (error) {
|
|
264
264
|
if (error.code !== "transact_guard_failed") throw error;
|
|
265
|
-
const current = (await
|
|
265
|
+
const current = (await deps.db.query({ crm_record: { $: { where: { id: record4.id }, limit: 1 } } })).crm_record?.[0];
|
|
266
266
|
if (current?.clerkUserId !== userId) throw error;
|
|
267
267
|
}
|
|
268
268
|
return String(record4.id);
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
+
// src/worker-alerts.ts
|
|
272
|
+
var consoleErrorRecorder = (error, report) => {
|
|
273
|
+
console.error("chapter_alert", JSON.stringify({ message: error.message, ...report }));
|
|
274
|
+
};
|
|
275
|
+
function reportChapterAlert(ctx, error, report) {
|
|
276
|
+
(ctx?.recordError ?? consoleErrorRecorder)(error, report);
|
|
277
|
+
}
|
|
278
|
+
var ACTIVATION_LIMIT = 50;
|
|
279
|
+
function networkCommercialAlerts(ctx, route) {
|
|
280
|
+
return {
|
|
281
|
+
exceptionsActivated(activated, activeCount) {
|
|
282
|
+
if (activated.length === 0) return;
|
|
283
|
+
reportChapterAlert(ctx, new Error("network commercial exception activated"), {
|
|
284
|
+
code: "network_commercial_exception_activated",
|
|
285
|
+
route,
|
|
286
|
+
attributes: {
|
|
287
|
+
"odla.commercial.active_exceptions": activeCount,
|
|
288
|
+
"odla.commercial.activated_exceptions": activated.length
|
|
289
|
+
},
|
|
290
|
+
artifacts: { exceptions: activated.slice(0, ACTIVATION_LIMIT).map(({ eventId, kind }) => ({ eventId, kind })) }
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
271
296
|
// src/worker-context.ts
|
|
272
297
|
var json = (body, status2 = 200) => new Response(JSON.stringify(body), { status: status2, headers: { "content-type": "application/json" } });
|
|
273
298
|
function createWorkerContext(options) {
|
|
@@ -377,6 +402,7 @@ function createWorkerContext(options) {
|
|
|
377
402
|
requirePaymentQuote: options.requirePaymentQuote === true,
|
|
378
403
|
membershipAuthority: options.membershipAuthority,
|
|
379
404
|
membershipAuthorityOperator: options.membershipAuthorityOperator,
|
|
405
|
+
recordError: options.recordError ?? consoleErrorRecorder,
|
|
380
406
|
getPublicConfig,
|
|
381
407
|
verifyUser,
|
|
382
408
|
makeDb,
|
|
@@ -870,6 +896,17 @@ function projectNetworkCommercialEvents(input, sources) {
|
|
|
870
896
|
};
|
|
871
897
|
}
|
|
872
898
|
|
|
899
|
+
// src/network-commercial-fact-identity.ts
|
|
900
|
+
var stable = (value) => JSON.stringify(value, function(_key, item) {
|
|
901
|
+
return item && typeof item === "object" && !Array.isArray(item) ? Object.fromEntries(Object.entries(item).filter(([key3]) => key3 !== "digest").sort(([a], [b]) => a.localeCompare(b))) : item;
|
|
902
|
+
});
|
|
903
|
+
function sameNetworkCommercialFact(stored, incoming) {
|
|
904
|
+
if (!stored || typeof stored !== "object" || Array.isArray(stored)) return false;
|
|
905
|
+
const comparable = { ...incoming };
|
|
906
|
+
if (!stored.seatSubscriptionItems) delete comparable.seatSubscriptionItems;
|
|
907
|
+
return stable(comparable) === stable(stored);
|
|
908
|
+
}
|
|
909
|
+
|
|
873
910
|
// src/network-commercial-store.ts
|
|
874
911
|
var eventRowId = (eventId) => `network-commercial-event:${eventId}`;
|
|
875
912
|
var projectionId = (kind, sourceKey, providerId3) => `network-commercial:${kind}:${sourceKey}:${providerId3}`;
|
|
@@ -881,6 +918,7 @@ var materializedNamespaces = [
|
|
|
881
918
|
"networkCommercialRefunds",
|
|
882
919
|
"networkCommercialSeats"
|
|
883
920
|
];
|
|
921
|
+
var RESOLUTION_ATTRS = ["resolvedDigest", "reviewedDigest", "resolvedBy", "reviewId"];
|
|
884
922
|
async function rows(db, ns) {
|
|
885
923
|
const all = [];
|
|
886
924
|
const pageSize = 500;
|
|
@@ -930,13 +968,14 @@ function projectionOps(projection, now) {
|
|
|
930
968
|
}
|
|
931
969
|
})));
|
|
932
970
|
}
|
|
933
|
-
async function rebuildNetworkCommercialProjection(db, sources, mutationId, now = Date.now()) {
|
|
971
|
+
async function rebuildNetworkCommercialProjection(db, sources, mutationId, now = Date.now(), alerts) {
|
|
934
972
|
const projection = await loadNetworkCommercialProjection(db, sources);
|
|
935
973
|
const [existing, ...materialized] = await Promise.all([
|
|
936
974
|
rows(db, "networkCommercialExceptions"),
|
|
937
975
|
...materializedNamespaces.map((ns) => rows(db, ns))
|
|
938
976
|
]);
|
|
939
977
|
const activeIds = new Set(projection.exceptions.map((item) => exceptionId(item.eventId, item.kind)));
|
|
978
|
+
const wasActive = new Set(existing.filter((row2) => row2.status === "active").map((row2) => String(row2.id)));
|
|
940
979
|
const projectedIds = new Set(projectionOps(projection, now).map((op) => String(op.id)));
|
|
941
980
|
const ops = [
|
|
942
981
|
...projectionOps(projection, now),
|
|
@@ -962,27 +1001,80 @@ async function rebuildNetworkCommercialProjection(db, sources, mutationId, now =
|
|
|
962
1001
|
}
|
|
963
1002
|
}))
|
|
964
1003
|
];
|
|
965
|
-
if (ops.length
|
|
1004
|
+
if (ops.length === 0) return projection;
|
|
1005
|
+
const write = await db.transact(ops, { mutationId });
|
|
1006
|
+
if (!write.duplicate) {
|
|
1007
|
+
alerts?.exceptionsActivated(
|
|
1008
|
+
projection.exceptions.filter((item) => !wasActive.has(exceptionId(item.eventId, item.kind))),
|
|
1009
|
+
projection.exceptions.length
|
|
1010
|
+
);
|
|
1011
|
+
}
|
|
966
1012
|
return projection;
|
|
967
1013
|
}
|
|
968
|
-
async function
|
|
969
|
-
const
|
|
970
|
-
const
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
const
|
|
975
|
-
|
|
976
|
-
|
|
1014
|
+
async function collide(db, sources, stored, fact, now, legacyDigest, alerts) {
|
|
1015
|
+
const id6 = exceptionId(fact.eventId, "collision");
|
|
1016
|
+
const exception = (await db.query({ networkCommercialExceptions: { $: { where: { id: id6 }, limit: 1 } } })).networkCommercialExceptions?.[0];
|
|
1017
|
+
const exact = stored.digest === fact.digest || stored.digest === legacyDigest;
|
|
1018
|
+
const canonical3 = !exact && sameNetworkCommercialFact(stored.fact, fact);
|
|
1019
|
+
if (exact || canonical3) {
|
|
1020
|
+
const recorded = exception?.incomingFact;
|
|
1021
|
+
const resolvable = canonical3 && exception?.status === "active" && (recorded === void 0 || sameNetworkCommercialFact(stored.fact, recorded));
|
|
1022
|
+
if (!resolvable) return { status: "replayed", projection: await loadNetworkCommercialProjection(db, sources) };
|
|
1023
|
+
const reviewId = crypto.randomUUID();
|
|
1024
|
+
await db.transact([
|
|
1025
|
+
{ t: "update", ns: "paymentRecoveryReviews", id: reviewId, attrs: {
|
|
1026
|
+
id: reviewId,
|
|
1027
|
+
eventId: fact.eventId,
|
|
1028
|
+
actorUserId: "system",
|
|
1029
|
+
reviewedAt: now,
|
|
1030
|
+
previousDigest: String(stored.digest),
|
|
1031
|
+
providerDigest: fact.digest,
|
|
1032
|
+
scope: "network",
|
|
1033
|
+
previousState: exception,
|
|
1034
|
+
action: "canonical_replay",
|
|
1035
|
+
reason: "a resend matched the retained canonical fact"
|
|
1036
|
+
} },
|
|
1037
|
+
{ t: "update", ns: "networkCommercialExceptions", id: id6, attrs: {
|
|
1038
|
+
status: "resolved",
|
|
1039
|
+
resolvedDigest: stored.digest,
|
|
1040
|
+
reviewedDigest: fact.digest,
|
|
1041
|
+
resolvedBy: "system",
|
|
1042
|
+
reviewId,
|
|
1043
|
+
updatedAt: now
|
|
1044
|
+
} }
|
|
1045
|
+
], { mutationId: `network-commercial-resolve:${fact.eventId}:${fact.digest}`, guards: [
|
|
1046
|
+
{ ns: "networkCommercialExceptions", id: id6, exists: true, equals: { status: "active", updatedAt: exception.updatedAt } }
|
|
1047
|
+
] });
|
|
1048
|
+
const projection2 = await rebuildNetworkCommercialProjection(db, sources, `network-commercial-resolve-rebuild:${fact.eventId}:${fact.digest}`, now, alerts);
|
|
1049
|
+
return { status: "replayed", resolvedCollision: true, projection: projection2 };
|
|
1050
|
+
}
|
|
1051
|
+
if (exception?.status === "resolved" && exception.reviewedDigest === fact.digest) {
|
|
1052
|
+
return { status: "replayed", projection: await loadNetworkCommercialProjection(db, sources) };
|
|
1053
|
+
}
|
|
1054
|
+
const stale = RESOLUTION_ATTRS.filter((attr3) => exception !== void 0 && Object.hasOwn(exception, attr3));
|
|
1055
|
+
await db.transact([
|
|
1056
|
+
...stale.length ? [{ t: "retract", ns: "networkCommercialExceptions", id: id6, attrs: stale }] : [],
|
|
1057
|
+
{ t: "update", ns: "networkCommercialExceptions", id: id6, attrs: {
|
|
1058
|
+
id: id6,
|
|
977
1059
|
eventId: fact.eventId,
|
|
978
1060
|
kind: "collision",
|
|
979
1061
|
status: "active",
|
|
980
1062
|
incomingDigest: fact.digest,
|
|
1063
|
+
incomingFact: fact,
|
|
981
1064
|
detail: { eventType: fact.eventType, objectId: fact.objectId },
|
|
982
1065
|
updatedAt: now
|
|
983
|
-
} }
|
|
984
|
-
|
|
1066
|
+
} }
|
|
1067
|
+
]);
|
|
1068
|
+
const projection = await loadNetworkCommercialProjection(db, sources);
|
|
1069
|
+
if (exception?.status !== "active") {
|
|
1070
|
+
alerts?.exceptionsActivated([{ eventId: fact.eventId, kind: "collision", detail: { eventType: fact.eventType, objectId: fact.objectId } }], projection.exceptions.length);
|
|
985
1071
|
}
|
|
1072
|
+
return { status: "collision", projection };
|
|
1073
|
+
}
|
|
1074
|
+
async function ingestNetworkCommercialFact(db, sources, fact, now = Date.now(), legacyDigest, alerts) {
|
|
1075
|
+
const found = await db.query({ networkCommercialEvents: { $: { where: { eventId: fact.eventId }, limit: 1 } } });
|
|
1076
|
+
const existing = found.networkCommercialEvents?.[0];
|
|
1077
|
+
if (existing) return collide(db, sources, existing, fact, now, legacyDigest, alerts);
|
|
986
1078
|
const id6 = eventRowId(fact.eventId);
|
|
987
1079
|
let lostRace = false;
|
|
988
1080
|
let duplicate = false;
|
|
@@ -1008,25 +1100,15 @@ async function ingestNetworkCommercialFact(db, sources, fact, now = Date.now(),
|
|
|
1008
1100
|
if (duplicate || lostRace) {
|
|
1009
1101
|
const raced = await db.query({ networkCommercialEvents: { $: { where: { eventId: fact.eventId }, limit: 1 } } });
|
|
1010
1102
|
const row2 = raced.networkCommercialEvents?.[0];
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
const id7 = exceptionId(fact.eventId, "collision");
|
|
1014
|
-
await db.transact([{ t: "update", ns: "networkCommercialExceptions", id: id7, attrs: {
|
|
1015
|
-
id: id7,
|
|
1016
|
-
eventId: fact.eventId,
|
|
1017
|
-
kind: "collision",
|
|
1018
|
-
status: "active",
|
|
1019
|
-
incomingDigest: fact.digest,
|
|
1020
|
-
detail: { eventType: fact.eventType, objectId: fact.objectId },
|
|
1021
|
-
updatedAt: now
|
|
1022
|
-
} }]);
|
|
1023
|
-
return { status: "collision", projection: await loadNetworkCommercialProjection(db, sources) };
|
|
1103
|
+
if (!row2) throw new Error("network commercial event vanished after a lost race");
|
|
1104
|
+
return collide(db, sources, row2, fact, now, legacyDigest, alerts);
|
|
1024
1105
|
}
|
|
1025
1106
|
const projection = await rebuildNetworkCommercialProjection(
|
|
1026
1107
|
db,
|
|
1027
1108
|
sources,
|
|
1028
1109
|
`network-commercial-project:${fact.eventId}:${fact.digest}`,
|
|
1029
|
-
now
|
|
1110
|
+
now,
|
|
1111
|
+
alerts
|
|
1030
1112
|
);
|
|
1031
1113
|
return { status: "stored", projection };
|
|
1032
1114
|
}
|
|
@@ -1041,10 +1123,7 @@ async function readStripeRecoveryEvent(db, eventId) {
|
|
|
1041
1123
|
const rawBody = JSON.stringify(response.body);
|
|
1042
1124
|
return { event: response.body, rawBody, digest: await stripeEventDigest(rawBody) };
|
|
1043
1125
|
}
|
|
1044
|
-
|
|
1045
|
-
return item && typeof item === "object" && !Array.isArray(item) ? Object.fromEntries(Object.entries(item).filter(([key3]) => key3 !== "digest").sort(([a], [b]) => a.localeCompare(b))) : item;
|
|
1046
|
-
});
|
|
1047
|
-
async function recoverNetworkCommercialEvent(db, sources, eventId, actorUserId, now = Date.now()) {
|
|
1126
|
+
async function recoverNetworkCommercialEvent(db, sources, eventId, actorUserId, now = Date.now(), alerts) {
|
|
1048
1127
|
const eventRowId2 = `network-commercial-event:${eventId}`, exceptionId2 = `network-commercial-exception:${eventId}:collision`;
|
|
1049
1128
|
const rows5 = await db.query({
|
|
1050
1129
|
networkCommercialEvents: { $: { where: { id: eventRowId2 }, limit: 1 } },
|
|
@@ -1054,9 +1133,7 @@ async function recoverNetworkCommercialEvent(db, sources, eventId, actorUserId,
|
|
|
1054
1133
|
if (!stored) throw new Error("stored event not found");
|
|
1055
1134
|
const provider = await readStripeRecoveryEvent(db, eventId);
|
|
1056
1135
|
const fact = normalizeNetworkCommercialStripeEvent(provider.event, provider.digest);
|
|
1057
|
-
|
|
1058
|
-
if (!stored.fact?.seatSubscriptionItems) delete comparable.seatSubscriptionItems;
|
|
1059
|
-
if (stable(comparable) !== stable(stored.fact)) throw new Error("Stripe facts differ; collision requires investigation");
|
|
1136
|
+
if (!sameNetworkCommercialFact(stored.fact, fact)) throw new Error("Stripe facts differ; collision requires investigation");
|
|
1060
1137
|
const id6 = crypto.randomUUID();
|
|
1061
1138
|
await db.transact([
|
|
1062
1139
|
{ t: "update", ns: "paymentRecoveryReviews", id: id6, attrs: {
|
|
@@ -1064,6 +1141,7 @@ async function recoverNetworkCommercialEvent(db, sources, eventId, actorUserId,
|
|
|
1064
1141
|
eventId,
|
|
1065
1142
|
actorUserId,
|
|
1066
1143
|
reviewedAt: now,
|
|
1144
|
+
action: "recover",
|
|
1067
1145
|
previousDigest: stored.digest,
|
|
1068
1146
|
providerDigest: provider.digest,
|
|
1069
1147
|
scope: "network",
|
|
@@ -1074,7 +1152,7 @@ async function recoverNetworkCommercialEvent(db, sources, eventId, actorUserId,
|
|
|
1074
1152
|
t: "update",
|
|
1075
1153
|
ns: "networkCommercialExceptions",
|
|
1076
1154
|
id: exceptionId2,
|
|
1077
|
-
attrs: { status: "resolved", resolvedDigest: provider.digest, updatedAt: now }
|
|
1155
|
+
attrs: { status: "resolved", resolvedDigest: provider.digest, resolvedBy: actorUserId, reviewId: id6, updatedAt: now }
|
|
1078
1156
|
}] : []
|
|
1079
1157
|
], { mutationId: `payment-recovery:${id6}`, guards: [
|
|
1080
1158
|
{ ns: "networkCommercialEvents", id: eventRowId2, exists: true, equals: { digest: stored.digest } },
|
|
@@ -1085,7 +1163,7 @@ async function recoverNetworkCommercialEvent(db, sources, eventId, actorUserId,
|
|
|
1085
1163
|
...exception ? { equals: { status: exception.status, updatedAt: exception.updatedAt } } : {}
|
|
1086
1164
|
}
|
|
1087
1165
|
] });
|
|
1088
|
-
await rebuildNetworkCommercialProjection(db, sources, `payment-recovery-rebuild:${id6}`, now);
|
|
1166
|
+
await rebuildNetworkCommercialProjection(db, sources, `payment-recovery-rebuild:${id6}`, now, alerts);
|
|
1089
1167
|
return { recovered: true, eventId, reviewId: id6 };
|
|
1090
1168
|
}
|
|
1091
1169
|
|
|
@@ -1242,12 +1320,12 @@ function applicationCrmFields(crm, app) {
|
|
|
1242
1320
|
}
|
|
1243
1321
|
|
|
1244
1322
|
// src/applicant-projection.ts
|
|
1245
|
-
async function upsertPerson(
|
|
1323
|
+
async function upsertPerson(deps, opts) {
|
|
1246
1324
|
const email = opts.email.toLowerCase();
|
|
1247
|
-
const crmDeps3 = { crm:
|
|
1248
|
-
const { record: record4, created } = await ensureProjectedPerson(
|
|
1325
|
+
const crmDeps3 = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
|
|
1326
|
+
const { record: record4, created } = await ensureProjectedPerson(deps, { ...opts.input, email });
|
|
1249
1327
|
if (!created) await (0, import_crm2.updateRecord)(crmDeps3, { id: String(record4.id), input: opts.input, mutationId: opts.mutationId });
|
|
1250
|
-
const initial =
|
|
1328
|
+
const initial = deps.crm.initialStage("person");
|
|
1251
1329
|
if (!created && record4.stage == null && initial) {
|
|
1252
1330
|
await (0, import_crm2.setStage)(crmDeps3, {
|
|
1253
1331
|
id: String(record4.id),
|
|
@@ -1259,7 +1337,7 @@ async function upsertPerson(deps2, opts) {
|
|
|
1259
1337
|
}
|
|
1260
1338
|
return { recordId: String(record4.id) };
|
|
1261
1339
|
}
|
|
1262
|
-
async function projectApplicant(
|
|
1340
|
+
async function projectApplicant(deps, applicant) {
|
|
1263
1341
|
const base = sharedPersonInput({
|
|
1264
1342
|
email: applicant.email,
|
|
1265
1343
|
firstName: applicant.firstName,
|
|
@@ -1268,18 +1346,18 @@ async function projectApplicant(deps2, applicant) {
|
|
|
1268
1346
|
linkedin: applicant.linkedin,
|
|
1269
1347
|
hubRecordId: applicant.applicationId
|
|
1270
1348
|
});
|
|
1271
|
-
Object.assign(base, applicationCrmFields(
|
|
1349
|
+
Object.assign(base, applicationCrmFields(deps.crm, applicant));
|
|
1272
1350
|
const mutationId = `apply:${applicant.applicationId}`;
|
|
1273
1351
|
const extra = applicant.extra ?? {};
|
|
1274
|
-
if (Object.keys(extra).length === 0) return upsertPerson(
|
|
1352
|
+
if (Object.keys(extra).length === 0) return upsertPerson(deps, { email: applicant.email, input: base, mutationId });
|
|
1275
1353
|
try {
|
|
1276
|
-
return await upsertPerson(
|
|
1354
|
+
return await upsertPerson(deps, { email: applicant.email, input: {
|
|
1277
1355
|
...base,
|
|
1278
1356
|
...extra,
|
|
1279
|
-
...applicationCrmFields(
|
|
1357
|
+
...applicationCrmFields(deps.crm, applicant)
|
|
1280
1358
|
}, mutationId });
|
|
1281
1359
|
} catch {
|
|
1282
|
-
return upsertPerson(
|
|
1360
|
+
return upsertPerson(deps, { email: applicant.email, input: base, mutationId });
|
|
1283
1361
|
}
|
|
1284
1362
|
}
|
|
1285
1363
|
|
|
@@ -1377,24 +1455,24 @@ function sharedRecordFromCrm(crm, record4, target, sourceId) {
|
|
|
1377
1455
|
if (input[nameField] === void 0) input[nameField] = record4.name;
|
|
1378
1456
|
return sourceId ? { version: 2, source: { siteId: sourceId, recordId: record4.id }, type: record4.type, input } : { version: 1, type: record4.type, hubRecordId: record4.id, input };
|
|
1379
1457
|
}
|
|
1380
|
-
async function findSharedRecord(
|
|
1458
|
+
async function findSharedRecord(deps, record4, tag) {
|
|
1381
1459
|
const structured = await (0, import_crm3.getRecordByOrigin)(
|
|
1382
|
-
{ crm:
|
|
1460
|
+
{ crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId },
|
|
1383
1461
|
record4.sourceId,
|
|
1384
1462
|
record4.sourceRecordId
|
|
1385
1463
|
);
|
|
1386
1464
|
if (structured) return structured;
|
|
1387
|
-
const mapped = await
|
|
1465
|
+
const mapped = await deps.db.query({ crm_tag: { $: { where: { tag }, limit: 1 } } });
|
|
1388
1466
|
const mappedId = mapped.crm_tag?.[0]?.recordId;
|
|
1389
1467
|
if (typeof mappedId === "string") {
|
|
1390
|
-
const found = await
|
|
1468
|
+
const found = await deps.db.query({ crm_record: { $: { where: { id: mappedId }, limit: 1 } } });
|
|
1391
1469
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
1392
1470
|
}
|
|
1393
|
-
const def =
|
|
1471
|
+
const def = deps.crm.type(record4.type);
|
|
1394
1472
|
const emailField = def.emailField;
|
|
1395
1473
|
if (emailField && typeof record4.input[emailField] === "string") {
|
|
1396
1474
|
const primaryEmail = record4.input[emailField].toLowerCase();
|
|
1397
|
-
const found = await
|
|
1475
|
+
const found = await deps.db.query({
|
|
1398
1476
|
crm_record: { $: { where: { type: record4.type, primaryEmail }, limit: 1 } }
|
|
1399
1477
|
});
|
|
1400
1478
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
@@ -1402,7 +1480,7 @@ async function findSharedRecord(deps2, record4, tag) {
|
|
|
1402
1480
|
const domain = record4.input.domain;
|
|
1403
1481
|
const domainSlot = def.fields.domain?.slot;
|
|
1404
1482
|
if (typeof domain === "string" && domainSlot) {
|
|
1405
|
-
const found = await
|
|
1483
|
+
const found = await deps.db.query({
|
|
1406
1484
|
crm_record: { $: { where: { type: record4.type, [domainSlot]: domain }, limit: 1 } }
|
|
1407
1485
|
});
|
|
1408
1486
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
@@ -1410,14 +1488,14 @@ async function findSharedRecord(deps2, record4, tag) {
|
|
|
1410
1488
|
const nameField = def.nameField ?? "name";
|
|
1411
1489
|
const name = record4.input[nameField];
|
|
1412
1490
|
if (record4.type === "company" && typeof name === "string" && name.trim()) {
|
|
1413
|
-
const found = await
|
|
1491
|
+
const found = await deps.db.query({
|
|
1414
1492
|
crm_record: { $: { where: { type: record4.type, name: name.trim() }, limit: 1 } }
|
|
1415
1493
|
});
|
|
1416
1494
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
1417
1495
|
}
|
|
1418
1496
|
return void 0;
|
|
1419
1497
|
}
|
|
1420
|
-
async function projectSharedRecord(
|
|
1498
|
+
async function projectSharedRecord(deps, shared, options = {}) {
|
|
1421
1499
|
const record4 = normalizeSharedRecord(shared, options.sourceId);
|
|
1422
1500
|
if (!record4.type.trim() || !record4.sourceId.trim() || !record4.sourceRecordId.trim()) {
|
|
1423
1501
|
throw new Error("type, source site id, and source record id must be non-empty");
|
|
@@ -1426,14 +1504,14 @@ async function projectSharedRecord(deps2, shared, options = {}) {
|
|
|
1426
1504
|
throw new Error("signed sender does not match payload source");
|
|
1427
1505
|
}
|
|
1428
1506
|
const tag = networkSourceTag(record4.type, record4.sourceRecordId, record4.version === 2 ? record4.sourceId : void 0);
|
|
1429
|
-
const crmDeps3 = { crm:
|
|
1430
|
-
const existing = await findSharedRecord(
|
|
1507
|
+
const crmDeps3 = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
|
|
1508
|
+
const existing = await findSharedRecord(deps, record4, tag);
|
|
1431
1509
|
let recordId;
|
|
1432
1510
|
if (existing && typeof existing.id === "string") {
|
|
1433
1511
|
await (0, import_crm3.updateRecord)(crmDeps3, { id: existing.id, input: record4.input });
|
|
1434
1512
|
recordId = existing.id;
|
|
1435
1513
|
} else if (record4.type === "person" && typeof record4.input.email === "string") {
|
|
1436
|
-
const projected = await ensureProjectedPerson(
|
|
1514
|
+
const projected = await ensureProjectedPerson(deps, record4.input);
|
|
1437
1515
|
recordId = String(projected.record.id);
|
|
1438
1516
|
if (!projected.created) await (0, import_crm3.updateRecord)(crmDeps3, { id: recordId, input: record4.input });
|
|
1439
1517
|
} else {
|
|
@@ -1444,8 +1522,8 @@ async function projectSharedRecord(deps2, shared, options = {}) {
|
|
|
1444
1522
|
mutationId: `share-create:${tag}`
|
|
1445
1523
|
});
|
|
1446
1524
|
}
|
|
1447
|
-
await
|
|
1448
|
-
[{ t: "update", ns: "crm_tag", id: tag, attrs: { key: `${recordId}:${tag}`, recordId, tag, createdAt:
|
|
1525
|
+
await deps.db.transact(
|
|
1526
|
+
[{ t: "update", ns: "crm_tag", id: tag, attrs: { key: `${recordId}:${tag}`, recordId, tag, createdAt: deps.now() } }],
|
|
1449
1527
|
{ mutationId: `share-map:${tag}:${recordId}` }
|
|
1450
1528
|
);
|
|
1451
1529
|
await (0, import_crm3.upsertRecordOrigin)(crmDeps3, {
|
|
@@ -1493,21 +1571,21 @@ function billingColumns(app) {
|
|
|
1493
1571
|
if (billingStatus !== "canceled" && billingStatus !== "refunded" && Number.isFinite(renewal) && renewal > 0) cols.renewalAt = renewal;
|
|
1494
1572
|
return cols;
|
|
1495
1573
|
}
|
|
1496
|
-
async function syncApplicationToCrm(
|
|
1574
|
+
async function syncApplicationToCrm(deps, opts) {
|
|
1497
1575
|
if (!opts.app.id) throw new Error("crm_application_id_required");
|
|
1498
1576
|
const emailKey = str(opts.app.email).toLowerCase();
|
|
1499
1577
|
if (!emailKey) return null;
|
|
1500
|
-
const recordDeps = { crm:
|
|
1501
|
-
const input = personInputFromApp(
|
|
1578
|
+
const recordDeps = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
|
|
1579
|
+
const input = personInputFromApp(deps.chapter, opts.app);
|
|
1502
1580
|
const stage = opts.stage || void 0;
|
|
1503
|
-
const { record: existing, created } = await ensureProjectedPerson(
|
|
1581
|
+
const { record: existing, created } = await ensureProjectedPerson(deps, input, { stage });
|
|
1504
1582
|
const recordId = String(existing.id);
|
|
1505
1583
|
if (!created) await (0, import_crm4.updateRecord)(recordDeps, { id: recordId, input });
|
|
1506
1584
|
if (!created && stage && existing.stage !== stage) {
|
|
1507
1585
|
await (0, import_crm4.setStage)(recordDeps, { id: recordId, to: stage, authorId: "system", mutationId: `crm:stage:${recordId}:${existing.updatedAt}:${stage}` });
|
|
1508
1586
|
}
|
|
1509
1587
|
const billing = billingColumns(opts.app);
|
|
1510
|
-
await
|
|
1588
|
+
await deps.db.transact([
|
|
1511
1589
|
{ t: "update", ns: "crm_record", id: recordId, attrs: billing },
|
|
1512
1590
|
// Optional typed dates are removed with a retract, never an invalid null.
|
|
1513
1591
|
...!("renewalAt" in billing) ? [{ t: "retract", ns: "crm_record", id: recordId, attrs: ["renewalAt"] }] : []
|
|
@@ -1515,8 +1593,8 @@ async function syncApplicationToCrm(deps2, opts) {
|
|
|
1515
1593
|
await (0, import_crm4.linkIdentity)(recordDeps, { recordId, email: emailKey, mutationId: `crm:link:${recordId}:${emailKey}` }).catch(() => void 0);
|
|
1516
1594
|
return recordId;
|
|
1517
1595
|
}
|
|
1518
|
-
async function backfillCrm(
|
|
1519
|
-
const data = await
|
|
1596
|
+
async function backfillCrm(deps) {
|
|
1597
|
+
const data = await deps.db.query({
|
|
1520
1598
|
applications: { $: { order: { createdAt: "desc" }, limit: 1e3 } }
|
|
1521
1599
|
});
|
|
1522
1600
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -1527,7 +1605,7 @@ async function backfillCrm(deps2) {
|
|
|
1527
1605
|
if (!key3 || seen.has(key3)) return;
|
|
1528
1606
|
seen.add(key3);
|
|
1529
1607
|
try {
|
|
1530
|
-
await syncApplicationToCrm(
|
|
1608
|
+
await syncApplicationToCrm(deps, { app, stage });
|
|
1531
1609
|
synced += 1;
|
|
1532
1610
|
} catch (err) {
|
|
1533
1611
|
errors.push({ email: key3, error: err instanceof Error ? err.message : String(err) });
|
|
@@ -1535,10 +1613,10 @@ async function backfillCrm(deps2) {
|
|
|
1535
1613
|
};
|
|
1536
1614
|
for (const a of data.applications ?? []) await run(a, str(a.status));
|
|
1537
1615
|
for (let offset = 0; ; offset += 200) {
|
|
1538
|
-
const users = (await
|
|
1616
|
+
const users = (await deps.db.query({ $users: { $: { order: { id: "asc" }, limit: 200, offset } } })).$users ?? [];
|
|
1539
1617
|
for (const u of users) {
|
|
1540
1618
|
try {
|
|
1541
|
-
if (await syncAccountToCrm(
|
|
1619
|
+
if (await syncAccountToCrm(deps, String(u.id))) synced += 1;
|
|
1542
1620
|
} catch (err) {
|
|
1543
1621
|
errors.push({ email: str(u.email), error: err instanceof Error ? err.message : String(err) });
|
|
1544
1622
|
}
|
|
@@ -1578,26 +1656,32 @@ function planDelivery(input) {
|
|
|
1578
1656
|
const transport = !isProd && !redirect ? "log-only" : input.cloudflareReady ? "cloudflare" : "log-only";
|
|
1579
1657
|
const to = redirect ? input.group.debugEmail : input.to;
|
|
1580
1658
|
const subject = (redirect ? "[dev] " : "") + render(tpl.subject, vars);
|
|
1581
|
-
const
|
|
1659
|
+
const text8 = redirect ? `(dev redirect; original recipient: ${input.to})
|
|
1582
1660
|
|
|
1583
1661
|
` + render(tpl.text, vars) : render(tpl.text, vars);
|
|
1584
|
-
return { deliver: true, transport, to, subject, text:
|
|
1662
|
+
return { deliver: true, transport, to, subject, text: text8, redirected: redirect };
|
|
1585
1663
|
}
|
|
1586
1664
|
|
|
1587
1665
|
// src/notify.ts
|
|
1588
|
-
var EMAIL_TEMPLATE_NAMES = ["adminNotification", "paymentConfirmation", "prepEmail", "onboardingInvite"];
|
|
1589
|
-
|
|
1590
|
-
const
|
|
1666
|
+
var EMAIL_TEMPLATE_NAMES = ["adminNotification", "submitConfirmation", "paymentConfirmation", "prepEmail", "onboardingInvite"];
|
|
1667
|
+
function fromAddress(configured, name) {
|
|
1668
|
+
const display = name?.trim();
|
|
1669
|
+
if (!display) return configured;
|
|
1670
|
+
const angled = configured.match(/<([^>]+)>/);
|
|
1671
|
+
return { name: display, email: (angled ? angled[1] : configured).trim() };
|
|
1672
|
+
}
|
|
1673
|
+
async function sendTemplated(deps, input) {
|
|
1674
|
+
const { emailLog } = await deps.db.query({ emailLog: { $: { where: { dedupeKey: input.dedupeKey } } } });
|
|
1591
1675
|
const prior = Array.isArray(emailLog) ? emailLog : [];
|
|
1592
1676
|
if (isAlreadySent(prior)) return { sent: true, reason: "already-sent" };
|
|
1593
1677
|
if (prior.length > 1 || prior.length === 1 && prior[0].deliveryState !== "retry_ready") {
|
|
1594
1678
|
return { sent: false, reason: "delivery-uncertain" };
|
|
1595
1679
|
}
|
|
1596
|
-
const cloudflareReady = Boolean(
|
|
1597
|
-
const override =
|
|
1598
|
-
const group =
|
|
1680
|
+
const cloudflareReady = Boolean(deps.sender && deps.from);
|
|
1681
|
+
const override = deps.debugEmail?.trim();
|
|
1682
|
+
const group = deps.envName !== "prod" && deps.debugEmail !== void 0 ? { ...input.group, debugEmail: override && /^[^\s@,;<>]+@[^\s@,;<>]+\.[^\s@,;<>]+$/.test(override) ? override : void 0 } : input.group;
|
|
1599
1683
|
const decision = planDelivery({
|
|
1600
|
-
envName:
|
|
1684
|
+
envName: deps.envName,
|
|
1601
1685
|
group,
|
|
1602
1686
|
template: input.template,
|
|
1603
1687
|
to: input.to,
|
|
@@ -1622,14 +1706,14 @@ async function sendTemplated(deps2, input) {
|
|
|
1622
1706
|
transport: decision.transport,
|
|
1623
1707
|
redirected: decision.redirected,
|
|
1624
1708
|
dedupeKey: input.dedupeKey,
|
|
1625
|
-
sentAt:
|
|
1709
|
+
sentAt: deps.now(),
|
|
1626
1710
|
deliveryState: decision.transport === "log-only" ? "sent" : "sending",
|
|
1627
1711
|
deliveryAttempt: attempt,
|
|
1628
1712
|
error: decision.transport === "log-only" ? "" : "delivery-pending",
|
|
1629
1713
|
...input.applicationId ? { applicationId: input.applicationId } : {}
|
|
1630
1714
|
};
|
|
1631
1715
|
try {
|
|
1632
|
-
const claim = await
|
|
1716
|
+
const claim = await deps.db.transact([{ t: "update", ns: "emailLog", id: id6, attrs: row2 }], {
|
|
1633
1717
|
mutationId: `email-claim:${id6}:${attempt}`,
|
|
1634
1718
|
guards: [previous ? {
|
|
1635
1719
|
ns: "emailLog",
|
|
@@ -1647,8 +1731,8 @@ async function sendTemplated(deps2, input) {
|
|
|
1647
1731
|
if (decision.transport === "log-only") return { sent: true };
|
|
1648
1732
|
let messageId;
|
|
1649
1733
|
try {
|
|
1650
|
-
const response = await
|
|
1651
|
-
from:
|
|
1734
|
+
const response = await deps.sender.send({
|
|
1735
|
+
from: fromAddress(deps.from, input.group.fromName),
|
|
1652
1736
|
to: [decision.to],
|
|
1653
1737
|
subject: String(row2.subject),
|
|
1654
1738
|
text: String(row2.body),
|
|
@@ -1657,12 +1741,12 @@ async function sendTemplated(deps2, input) {
|
|
|
1657
1741
|
if (!response || typeof response.messageId !== "string" || !response.messageId) throw new Error("missing delivery receipt");
|
|
1658
1742
|
messageId = response.messageId;
|
|
1659
1743
|
} catch {
|
|
1660
|
-
await
|
|
1744
|
+
await deps.db.transact([{ t: "update", ns: "emailLog", id: id6, attrs: { deliveryState: "uncertain", error: "delivery-uncertain" } }], {
|
|
1661
1745
|
guards: [{ ns: "emailLog", id: id6, exists: true, equals: { deliveryState: "sending", deliveryAttempt: attempt } }]
|
|
1662
1746
|
});
|
|
1663
1747
|
return { sent: false, reason: "delivery-uncertain" };
|
|
1664
1748
|
}
|
|
1665
|
-
await
|
|
1749
|
+
await deps.db.transact([{ t: "update", ns: "emailLog", id: id6, attrs: { deliveryState: "sent", error: "", messageId, sentAt: deps.now() } }], {
|
|
1666
1750
|
mutationId: `email-confirmed:${id6}:${attempt}`,
|
|
1667
1751
|
guards: [{
|
|
1668
1752
|
ns: "emailLog",
|
|
@@ -1681,6 +1765,7 @@ function emailGroupFrom(row2) {
|
|
|
1681
1765
|
name: String(row2.name ?? ""),
|
|
1682
1766
|
replyTo: str3(row2.replyTo) ?? "",
|
|
1683
1767
|
debugEmail: str3(row2.debugEmail),
|
|
1768
|
+
fromName: str3(row2.fromName),
|
|
1684
1769
|
refundPolicyText: str3(row2.refundPolicyText),
|
|
1685
1770
|
commitmentText: str3(row2.commitmentText),
|
|
1686
1771
|
normsText: str3(row2.normsText),
|
|
@@ -2811,24 +2896,22 @@ async function revokeRefundedApplicationRole(db, env, ctx, applicationId) {
|
|
|
2811
2896
|
}
|
|
2812
2897
|
}
|
|
2813
2898
|
|
|
2814
|
-
// src/
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
});
|
|
2828
|
-
async function paymentConfirmationVariables(db, app) {
|
|
2899
|
+
// src/notification-variables.ts
|
|
2900
|
+
function lifecycleNotifyDeps(db, env) {
|
|
2901
|
+
return {
|
|
2902
|
+
db,
|
|
2903
|
+
envName: env.ODLA_ENV,
|
|
2904
|
+
sender: env.SEND_EMAIL,
|
|
2905
|
+
from: env.EMAIL_FROM,
|
|
2906
|
+
debugEmail: env.ODLA_DEBUG_EMAIL,
|
|
2907
|
+
now: () => Date.now(),
|
|
2908
|
+
newId: () => crypto.randomUUID()
|
|
2909
|
+
};
|
|
2910
|
+
}
|
|
2911
|
+
async function frozenCheckoutVariables(db, app) {
|
|
2829
2912
|
const intent = (await db.query({ subscriptionCheckoutIntents: { $: { where: { id: String(app.id) }, limit: 1 } } })).subscriptionCheckoutIntents?.[0];
|
|
2830
2913
|
const offer = intent?.membershipOffer, contract = offer?.contract;
|
|
2831
|
-
const offering = contract?.offering;
|
|
2914
|
+
const offering = contract?.offering, quote = intent?.paymentQuote;
|
|
2832
2915
|
const amount = contract?.dueTodayCents ?? intent?.priceCents;
|
|
2833
2916
|
const currency = offering?.currency ?? intent?.currency;
|
|
2834
2917
|
let formatted = "";
|
|
@@ -2836,9 +2919,75 @@ async function paymentConfirmationVariables(db, app) {
|
|
|
2836
2919
|
formatted = new Intl.NumberFormat("en-US", { style: "currency", currency: currency.toUpperCase() }).format(Number(amount) / 100);
|
|
2837
2920
|
}
|
|
2838
2921
|
return {
|
|
2839
|
-
firstName: typeof app.firstName === "string" ? app.firstName : "",
|
|
2840
2922
|
paymentAmount: formatted,
|
|
2841
2923
|
paymentAmountSummary: formatted ? ` of ${formatted}` : "",
|
|
2924
|
+
tierName: typeof quote?.tierName === "string" ? quote.tierName : ""
|
|
2925
|
+
};
|
|
2926
|
+
}
|
|
2927
|
+
async function catalogTierName(db, group, app) {
|
|
2928
|
+
try {
|
|
2929
|
+
return findTier(await loadTiers(db, group), typeof app.tierId === "string" ? app.tierId : null)?.name ?? "";
|
|
2930
|
+
} catch {
|
|
2931
|
+
return "";
|
|
2932
|
+
}
|
|
2933
|
+
}
|
|
2934
|
+
async function checkoutVariables(db, group, app) {
|
|
2935
|
+
const frozen = await frozenCheckoutVariables(db, app);
|
|
2936
|
+
return frozen.tierName ? frozen : { ...frozen, tierName: await catalogTierName(db, group, app) };
|
|
2937
|
+
}
|
|
2938
|
+
function formatInstant(at, timeZone) {
|
|
2939
|
+
return new Intl.DateTimeFormat("en-US", {
|
|
2940
|
+
weekday: "long",
|
|
2941
|
+
month: "long",
|
|
2942
|
+
day: "numeric",
|
|
2943
|
+
year: "numeric",
|
|
2944
|
+
hour: "numeric",
|
|
2945
|
+
minute: "2-digit",
|
|
2946
|
+
timeZoneName: "short",
|
|
2947
|
+
timeZone
|
|
2948
|
+
}).format(new Date(at));
|
|
2949
|
+
}
|
|
2950
|
+
var adminUrlFor = (origin) => `${origin}/admin/`;
|
|
2951
|
+
var membersUrlFor = (origin) => `${origin}/members/`;
|
|
2952
|
+
function applicantVariables(app) {
|
|
2953
|
+
const text8 = (v) => typeof v === "string" ? v : "";
|
|
2954
|
+
return { firstName: text8(app.firstName), lastName: text8(app.lastName), email: text8(app.email), phone: text8(app.phone), state: text8(app.state) };
|
|
2955
|
+
}
|
|
2956
|
+
function sampleVariables(origin, timeZone, now = Date.now()) {
|
|
2957
|
+
const startAt = formatInstant(now + 864e5, timeZone);
|
|
2958
|
+
return {
|
|
2959
|
+
firstName: "Sample",
|
|
2960
|
+
lastName: "Person",
|
|
2961
|
+
email: "sample@example.com",
|
|
2962
|
+
phone: "(555) 010-0100",
|
|
2963
|
+
state: "CA",
|
|
2964
|
+
tier: "Membership",
|
|
2965
|
+
tierName: "Membership",
|
|
2966
|
+
paymentAmount: "$100.00",
|
|
2967
|
+
paymentAmountSummary: " of $100.00",
|
|
2968
|
+
paymentNextStep: "Next, schedule your introduction call.",
|
|
2969
|
+
submitNextStep: "Next, schedule your introduction call.",
|
|
2970
|
+
startAt,
|
|
2971
|
+
endAt: formatInstant(now + 864e5 + 45 * 6e4, timeZone),
|
|
2972
|
+
timezone: timeZone,
|
|
2973
|
+
meetUrl: "https://meet.google.com/sample",
|
|
2974
|
+
htmlLink: "https://calendar.google.com/event?eid=sample",
|
|
2975
|
+
meetingTime: startAt,
|
|
2976
|
+
meetingLink: "https://meet.google.com/sample",
|
|
2977
|
+
adminUrl: adminUrlFor(origin),
|
|
2978
|
+
membersUrl: membersUrlFor(origin)
|
|
2979
|
+
};
|
|
2980
|
+
}
|
|
2981
|
+
|
|
2982
|
+
// src/payment-notifications.ts
|
|
2983
|
+
var groupFor = async (db, app) => {
|
|
2984
|
+
const result = await db.query({ groups: { $: { where: { id: String(app.groupId ?? "") }, limit: 1 } } });
|
|
2985
|
+
return result.groups?.[0];
|
|
2986
|
+
};
|
|
2987
|
+
async function paymentConfirmationVariables(db, app) {
|
|
2988
|
+
return {
|
|
2989
|
+
firstName: typeof app.firstName === "string" ? app.firstName : "",
|
|
2990
|
+
...await frozenCheckoutVariables(db, app),
|
|
2842
2991
|
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."
|
|
2843
2992
|
};
|
|
2844
2993
|
}
|
|
@@ -2846,7 +2995,7 @@ async function notifyPaymentConfirmed(db, env, eventId, app) {
|
|
|
2846
2995
|
if (typeof app.email !== "string" || !app.email) throw new Error("payment notice recipient missing");
|
|
2847
2996
|
const group = await groupFor(db, app);
|
|
2848
2997
|
if (!group) throw new Error("payment notice configuration missing");
|
|
2849
|
-
const result = await sendTemplated(
|
|
2998
|
+
const result = await sendTemplated(lifecycleNotifyDeps(db, env), {
|
|
2850
2999
|
group: emailGroupFrom(group),
|
|
2851
3000
|
template: "paymentConfirmation",
|
|
2852
3001
|
to: app.email,
|
|
@@ -2856,21 +3005,20 @@ async function notifyPaymentConfirmed(db, env, eventId, app) {
|
|
|
2856
3005
|
});
|
|
2857
3006
|
if (!result.sent && !["disabled", "template-missing"].includes(result.reason ?? "")) throw new Error("payment notice reconciliation pending");
|
|
2858
3007
|
}
|
|
2859
|
-
async function notifyAdminOfPayment(db, env, eventId, app) {
|
|
3008
|
+
async function notifyAdminOfPayment(db, env, eventId, app, origin = "") {
|
|
2860
3009
|
try {
|
|
2861
3010
|
const group = await groupFor(db, app);
|
|
2862
3011
|
if (!group || typeof group.notificationEmail !== "string" || !group.notificationEmail) return;
|
|
2863
|
-
const
|
|
2864
|
-
await sendTemplated(
|
|
3012
|
+
const { tierName: tierName2 } = await frozenCheckoutVariables(db, app);
|
|
3013
|
+
await sendTemplated(lifecycleNotifyDeps(db, env), {
|
|
2865
3014
|
group: emailGroupFrom(group),
|
|
2866
3015
|
template: "adminNotification",
|
|
2867
3016
|
to: group.notificationEmail,
|
|
2868
3017
|
vars: {
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
state: text6(app.state)
|
|
3018
|
+
...applicantVariables(app),
|
|
3019
|
+
tier: tierName2,
|
|
3020
|
+
tierName: tierName2,
|
|
3021
|
+
...origin ? { adminUrl: adminUrlFor(origin), membersUrl: membersUrlFor(origin) } : {}
|
|
2874
3022
|
},
|
|
2875
3023
|
dedupeKey: `${eventId}:admin`,
|
|
2876
3024
|
applicationId: String(app.id)
|
|
@@ -3190,6 +3338,26 @@ function reportStripePartition(level, eventId, event, disposition) {
|
|
|
3190
3338
|
...stripeEventIdentity(event)
|
|
3191
3339
|
}));
|
|
3192
3340
|
}
|
|
3341
|
+
var QUARANTINE_DISPOSITIONS = ["quarantine_unmatched", "quarantine_collision"];
|
|
3342
|
+
var QUEUE_LIMIT = 500;
|
|
3343
|
+
var QUEUE_IDS = 50;
|
|
3344
|
+
async function alertStripeQuarantine(db, ctx, eventId, disposition) {
|
|
3345
|
+
const pages = await Promise.all(QUARANTINE_DISPOSITIONS.map(async (value) => (await db.query({ stripeEventReceipts: { $: { where: { disposition: value }, limit: QUEUE_LIMIT } } })).stripeEventReceipts ?? []));
|
|
3346
|
+
const queued = pages.flat();
|
|
3347
|
+
reportChapterAlert(ctx, new Error("stripe webhook quarantined"), {
|
|
3348
|
+
code: "stripe_webhook_quarantined",
|
|
3349
|
+
attributes: {
|
|
3350
|
+
"odla.stripe.disposition": disposition,
|
|
3351
|
+
"odla.stripe.quarantined_count": queued.length,
|
|
3352
|
+
"odla.stripe.queue_truncated": pages.some((page) => page.length >= QUEUE_LIMIT)
|
|
3353
|
+
},
|
|
3354
|
+
artifacts: { eventId, eventIds: queued.slice(0, QUEUE_IDS).map((row2) => String(row2.eventId)) }
|
|
3355
|
+
});
|
|
3356
|
+
}
|
|
3357
|
+
async function reportStripeCollision(db, ctx, eventId, event) {
|
|
3358
|
+
reportStripePartition("error", eventId, event, "quarantine_collision");
|
|
3359
|
+
await alertStripeQuarantine(db, ctx, eventId, "quarantine_collision");
|
|
3360
|
+
}
|
|
3193
3361
|
async function storeStripeReceipt(db, input) {
|
|
3194
3362
|
const existing = await receiptFor(db, input.eventId);
|
|
3195
3363
|
if (existing?.retryReady === true && existing.reviewedDigest === input.digest) {
|
|
@@ -3222,17 +3390,18 @@ async function storeStripeReceipt(db, input) {
|
|
|
3222
3390
|
const raced = await receiptFor(db, input.eventId);
|
|
3223
3391
|
return raced?.digest === input.digest || raced?.digest === input.legacyDigest ? "replayed" : "collision";
|
|
3224
3392
|
}
|
|
3225
|
-
async function quarantineStripeEvent(db, eventId, digest, event) {
|
|
3393
|
+
async function quarantineStripeEvent(db, eventId, digest, event, ctx) {
|
|
3226
3394
|
const disposition = "quarantine_unmatched";
|
|
3227
3395
|
const stored = await storeStripeReceipt(db, { eventId, digest, event, disposition, matched: false });
|
|
3228
3396
|
if (stored === "collision") {
|
|
3229
|
-
|
|
3397
|
+
await reportStripeCollision(db, ctx, eventId, event);
|
|
3230
3398
|
return json({ ok: true, matched: false, collision: true });
|
|
3231
3399
|
}
|
|
3232
3400
|
reportStripePartition("warn", eventId, event, disposition);
|
|
3401
|
+
if (stored === "stored") await alertStripeQuarantine(db, ctx, eventId, disposition);
|
|
3233
3402
|
return json({ ok: true, matched: false, disposition, ...stored === "replayed" ? { replayed: true } : {} });
|
|
3234
3403
|
}
|
|
3235
|
-
async function replayedStripeReceipt(db, eventId, digest, event, legacyDigest) {
|
|
3404
|
+
async function replayedStripeReceipt(db, eventId, digest, event, legacyDigest, ctx) {
|
|
3236
3405
|
const existing = await receiptFor(db, eventId);
|
|
3237
3406
|
if (!existing) return null;
|
|
3238
3407
|
if (existing.retryReady === true && existing.reviewedDigest === digest) return null;
|
|
@@ -3242,7 +3411,7 @@ async function replayedStripeReceipt(db, eventId, digest, event, legacyDigest) {
|
|
|
3242
3411
|
collisionDigest: digest,
|
|
3243
3412
|
retryReady: false
|
|
3244
3413
|
} }], { guards: [{ ns: "stripeEventReceipts", id: String(existing.id), exists: true, equals: { digest: existing.digest } }] });
|
|
3245
|
-
|
|
3414
|
+
await reportStripeCollision(db, ctx, eventId, event);
|
|
3246
3415
|
return json({ ok: true, matched: false, collision: true });
|
|
3247
3416
|
}
|
|
3248
3417
|
return json({ ok: true, matched: existing.matched === true, replayed: true });
|
|
@@ -3273,16 +3442,16 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
|
|
|
3273
3442
|
reportStripePartition("warn", eventId, event, "ignore_sibling");
|
|
3274
3443
|
return json({ ok: true, matched: false, disposition: "ignore_sibling" });
|
|
3275
3444
|
}
|
|
3276
|
-
const replay2 = await replayedStripeReceipt(db, eventId, digest, event, legacyDigest);
|
|
3445
|
+
const replay2 = await replayedStripeReceipt(db, eventId, digest, event, legacyDigest, ctx);
|
|
3277
3446
|
if (replay2) return replay2;
|
|
3278
3447
|
const localIntent = Boolean(intent && intent.chapterId === ctx.chapter.id && intent.environment === env.ODLA_ENV && (typeof intent.runtime !== "string" || intent.runtime === target.runtime));
|
|
3279
3448
|
const disposition2 = classifyMemberStripeEvent(identity, target, localIntent);
|
|
3280
3449
|
if (!localIntent || disposition2 === "quarantine_unmatched") {
|
|
3281
|
-
return quarantineStripeEvent(db, eventId, digest, event);
|
|
3450
|
+
return quarantineStripeEvent(db, eventId, digest, event, ctx);
|
|
3282
3451
|
}
|
|
3283
3452
|
if (event.kind === "gift_paid") {
|
|
3284
3453
|
if (intent.providerObjectId !== event.providerObjectId || intent.status === "refunded" || intent.status === "canceled") {
|
|
3285
|
-
return quarantineStripeEvent(db, eventId, digest, event);
|
|
3454
|
+
return quarantineStripeEvent(db, eventId, digest, event, ctx);
|
|
3286
3455
|
}
|
|
3287
3456
|
try {
|
|
3288
3457
|
const issued = await issueGiftClaim(db, intent, {
|
|
@@ -3291,7 +3460,7 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
|
|
|
3291
3460
|
}, Date.now());
|
|
3292
3461
|
const stored3 = await storeStripeReceipt(db, { eventId, digest, legacyDigest, event, disposition: disposition2, matched: true });
|
|
3293
3462
|
if (stored3 === "collision") {
|
|
3294
|
-
|
|
3463
|
+
await reportStripeCollision(db, ctx, eventId, event);
|
|
3295
3464
|
return json({ ok: true, matched: false, collision: true });
|
|
3296
3465
|
}
|
|
3297
3466
|
return json({ ok: true, matched: true, claimId: issued.claimId });
|
|
@@ -3307,7 +3476,7 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
|
|
|
3307
3476
|
if (claim) await revokeGiftClaim(db, claim, event.reason, Date.now());
|
|
3308
3477
|
const stored2 = await storeStripeReceipt(db, { eventId, digest, legacyDigest, event, disposition: disposition2, matched: Boolean(claim) });
|
|
3309
3478
|
if (stored2 === "collision") {
|
|
3310
|
-
|
|
3479
|
+
await reportStripeCollision(db, ctx, eventId, event);
|
|
3311
3480
|
return json({ ok: true, matched: false, collision: true });
|
|
3312
3481
|
}
|
|
3313
3482
|
return json({ ok: true, matched: Boolean(claim) });
|
|
@@ -3317,12 +3486,12 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
|
|
|
3317
3486
|
reportStripePartition("warn", eventId, event, "ignore_sibling");
|
|
3318
3487
|
return json({ ok: true, matched: false, disposition: "ignore_sibling" });
|
|
3319
3488
|
}
|
|
3320
|
-
const replay = await replayedStripeReceipt(db, eventId, digest, event, legacyDigest);
|
|
3489
|
+
const replay = await replayedStripeReceipt(db, eventId, digest, event, legacyDigest, ctx);
|
|
3321
3490
|
if (replay) return replay;
|
|
3322
3491
|
const localApplication = Boolean(app && String(app.groupId ?? ctx.chapter.id) === ctx.chapter.id && (typeof app.stripeRuntime !== "string" || app.stripeRuntime === target.runtime));
|
|
3323
3492
|
const disposition = classifyMemberStripeEvent(identity, target, localApplication);
|
|
3324
3493
|
if (!localApplication || disposition === "quarantine_unmatched") {
|
|
3325
|
-
return quarantineStripeEvent(db, eventId, digest, event);
|
|
3494
|
+
return quarantineStripeEvent(db, eventId, digest, event, ctx);
|
|
3326
3495
|
}
|
|
3327
3496
|
if (await archivedChapterMembershipEvent(db, rawBody, app, env, ctx)) {
|
|
3328
3497
|
await storeStripeReceipt(db, { eventId, digest, legacyDigest, event, disposition, matched: true });
|
|
@@ -3339,7 +3508,7 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
|
|
|
3339
3508
|
if (receipt.seatId !== event.seatId || receipt.reconciled !== true) throw new Error("invalid seat receipt");
|
|
3340
3509
|
const stored2 = await storeStripeReceipt(db, { eventId, digest, legacyDigest, event, disposition, matched: true });
|
|
3341
3510
|
if (stored2 === "collision") {
|
|
3342
|
-
|
|
3511
|
+
await reportStripeCollision(db, ctx, eventId, event);
|
|
3343
3512
|
return json({ ok: true, matched: false, collision: true });
|
|
3344
3513
|
}
|
|
3345
3514
|
return json({ ok: true, seatId: event.seatId });
|
|
@@ -3363,7 +3532,7 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
|
|
|
3363
3532
|
if (intent) {
|
|
3364
3533
|
const evidence = verifyInitialMembershipInvoice(intent, event.invoice);
|
|
3365
3534
|
if (evidence.status === "retry") return json({ error: "checkout reconciliation pending" }, 503);
|
|
3366
|
-
if (evidence.status === "mismatch") return quarantineStripeEvent(db, eventId, digest, event);
|
|
3535
|
+
if (evidence.status === "mismatch") return quarantineStripeEvent(db, eventId, digest, event, ctx);
|
|
3367
3536
|
if (evidence.renewalAt !== void 0) authoritativeEvent = { ...event, renewalAt: evidence.renewalAt };
|
|
3368
3537
|
}
|
|
3369
3538
|
if (app.canceled !== true && !["refunded", "declined", "canceled"].includes(String(app.status))) {
|
|
@@ -3414,7 +3583,7 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
|
|
|
3414
3583
|
} catch {
|
|
3415
3584
|
return json({ error: "payment notice reconciliation pending" }, 503);
|
|
3416
3585
|
}
|
|
3417
|
-
if (ctx.chapter.sends.adminNotification === "payment") await notifyAdminOfPayment(db, env, eventId, app);
|
|
3586
|
+
if (ctx.chapter.sends.adminNotification === "payment") await notifyAdminOfPayment(db, env, eventId, app, input.origin);
|
|
3418
3587
|
if (typeof next.interviewWaivedAt === "number") {
|
|
3419
3588
|
const approval = await approveApplication({
|
|
3420
3589
|
db,
|
|
@@ -3429,15 +3598,132 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
|
|
|
3429
3598
|
}
|
|
3430
3599
|
const stored = await storeStripeReceipt(db, { eventId, digest, legacyDigest, event, disposition, matched: true });
|
|
3431
3600
|
if (stored === "collision") {
|
|
3432
|
-
|
|
3601
|
+
await reportStripeCollision(db, ctx, eventId, event);
|
|
3433
3602
|
return json({ ok: true, matched: false, collision: true });
|
|
3434
3603
|
}
|
|
3435
3604
|
return json({ ok: true });
|
|
3436
3605
|
}
|
|
3437
3606
|
|
|
3607
|
+
// src/network-commercial-resolve.ts
|
|
3608
|
+
var EVENT_ID = /^evt_[A-Za-z0-9_]{1,150}$/;
|
|
3609
|
+
var fail3 = (message, code) => Object.assign(new Error(message), { code });
|
|
3610
|
+
async function resolveNetworkCommercialCollision(db, sources, input, now = Date.now(), alerts) {
|
|
3611
|
+
if (!EVENT_ID.test(input.eventId)) throw fail3("invalid event reference", "invalid_event");
|
|
3612
|
+
const id6 = exceptionId(input.eventId, "collision"), reviewId = `resolve:${input.mutationId}`;
|
|
3613
|
+
const found = await db.query({
|
|
3614
|
+
networkCommercialExceptions: { $: { where: { id: id6 }, limit: 1 } },
|
|
3615
|
+
networkCommercialEvents: { $: { where: { eventId: input.eventId }, limit: 1 } }
|
|
3616
|
+
});
|
|
3617
|
+
const exception = found.networkCommercialExceptions?.[0], stored = found.networkCommercialEvents?.[0];
|
|
3618
|
+
if (exception?.status === "resolved" && exception.reviewId === reviewId) {
|
|
3619
|
+
return { eventId: input.eventId, reviewId, projection: await loadNetworkCommercialProjection(db, sources) };
|
|
3620
|
+
}
|
|
3621
|
+
if (!exception || exception.status !== "active") throw fail3("no active collision for this event", "collision_not_active");
|
|
3622
|
+
if (!stored) throw fail3("stored event not found", "event_not_found");
|
|
3623
|
+
const dismissed = typeof exception.incomingDigest === "string" ? exception.incomingDigest : void 0;
|
|
3624
|
+
const write = await db.transact([
|
|
3625
|
+
{ t: "update", ns: "paymentRecoveryReviews", id: reviewId, attrs: {
|
|
3626
|
+
id: reviewId,
|
|
3627
|
+
eventId: input.eventId,
|
|
3628
|
+
actorUserId: input.actorUserId,
|
|
3629
|
+
reviewedAt: now,
|
|
3630
|
+
previousDigest: String(stored.digest),
|
|
3631
|
+
providerDigest: dismissed ?? "",
|
|
3632
|
+
scope: "network",
|
|
3633
|
+
previousState: exception,
|
|
3634
|
+
action: "resolve",
|
|
3635
|
+
reason: input.reason
|
|
3636
|
+
} },
|
|
3637
|
+
{ t: "update", ns: "networkCommercialExceptions", id: id6, attrs: {
|
|
3638
|
+
status: "resolved",
|
|
3639
|
+
resolvedDigest: stored.digest,
|
|
3640
|
+
...dismissed ? { reviewedDigest: dismissed } : {},
|
|
3641
|
+
resolvedBy: input.actorUserId,
|
|
3642
|
+
reviewId,
|
|
3643
|
+
updatedAt: now
|
|
3644
|
+
} }
|
|
3645
|
+
], { mutationId: input.mutationId, guards: [
|
|
3646
|
+
{ ns: "networkCommercialExceptions", id: id6, exists: true, equals: { status: "active", updatedAt: exception.updatedAt } }
|
|
3647
|
+
] });
|
|
3648
|
+
if (write.duplicate) throw fail3("mutationId was already used for another change", "mutation_reused");
|
|
3649
|
+
const projection = await rebuildNetworkCommercialProjection(db, sources, `${input.mutationId}:rebuild`, now, alerts);
|
|
3650
|
+
return { eventId: input.eventId, reviewId, projection };
|
|
3651
|
+
}
|
|
3652
|
+
async function retryNetworkCommercialEvent(db, sources, eventId, actorUserId, now = Date.now(), alerts) {
|
|
3653
|
+
if (!EVENT_ID.test(eventId)) throw fail3("invalid event reference", "invalid_event");
|
|
3654
|
+
const active = (await loadNetworkCommercialProjection(db, sources)).exceptions.filter((item) => item.eventId === eventId);
|
|
3655
|
+
if (active.length === 0) throw fail3("event has no active exception", "exception_not_active");
|
|
3656
|
+
const provider = await readStripeRecoveryEvent(db, eventId);
|
|
3657
|
+
const stored = (await db.query({ networkCommercialEvents: { $: { where: { eventId }, limit: 1 } } })).networkCommercialEvents?.[0];
|
|
3658
|
+
const reviewId = crypto.randomUUID();
|
|
3659
|
+
await db.transact([{ t: "update", ns: "paymentRecoveryReviews", id: reviewId, attrs: {
|
|
3660
|
+
id: reviewId,
|
|
3661
|
+
eventId,
|
|
3662
|
+
actorUserId,
|
|
3663
|
+
reviewedAt: now,
|
|
3664
|
+
previousDigest: String(stored?.digest ?? ""),
|
|
3665
|
+
providerDigest: provider.digest,
|
|
3666
|
+
scope: "network",
|
|
3667
|
+
previousState: { exceptions: active },
|
|
3668
|
+
action: "retry"
|
|
3669
|
+
} }], { mutationId: `payment-recovery:${reviewId}` });
|
|
3670
|
+
const fact = normalizeNetworkCommercialStripeEvent(provider.event, provider.digest);
|
|
3671
|
+
const result = await ingestNetworkCommercialFact(db, sources, fact, now, await stripePayloadDigest(provider.rawBody), alerts);
|
|
3672
|
+
return {
|
|
3673
|
+
eventId,
|
|
3674
|
+
reviewId,
|
|
3675
|
+
status: result.status,
|
|
3676
|
+
resolvedCollision: result.status === "replayed" && result.resolvedCollision === true,
|
|
3677
|
+
exceptions: result.projection.exceptions.length
|
|
3678
|
+
};
|
|
3679
|
+
}
|
|
3680
|
+
|
|
3681
|
+
// src/worker-routes-network-commercial-recovery.ts
|
|
3682
|
+
var PATH = "/api/admin/payment-recovery";
|
|
3683
|
+
var handleNetworkCommercialRecovery = async (req, url, env, ctx) => {
|
|
3684
|
+
const model = ctx.chapter.networkCommercialReadModel;
|
|
3685
|
+
if (url.pathname !== PATH || !model) return null;
|
|
3686
|
+
if (req.method !== "GET" && req.method !== "POST") return json({ error: "method not allowed" }, 405);
|
|
3687
|
+
const raw2 = ctx.makeDb(env), user = await ctx.verifyUser(req, env);
|
|
3688
|
+
if (!user) return json({ error: "unauthorized" }, 401);
|
|
3689
|
+
if (!await ctx.isSuperAdminEmail(raw2, user.email, user.userId)) return json({ error: "forbidden" }, 403);
|
|
3690
|
+
const db = raw2;
|
|
3691
|
+
if (req.method === "GET") {
|
|
3692
|
+
const { exceptions } = await loadNetworkCommercialProjection(db, model.sources);
|
|
3693
|
+
return json({
|
|
3694
|
+
scope: "network",
|
|
3695
|
+
count: exceptions.length,
|
|
3696
|
+
truncated: false,
|
|
3697
|
+
rows: exceptions.map((item) => ({
|
|
3698
|
+
eventId: item.eventId,
|
|
3699
|
+
kind: item.detail.eventType,
|
|
3700
|
+
disposition: item.kind,
|
|
3701
|
+
sourceKey: item.sourceKey ?? null,
|
|
3702
|
+
retryReady: /^evt_/.test(item.eventId)
|
|
3703
|
+
}))
|
|
3704
|
+
});
|
|
3705
|
+
}
|
|
3706
|
+
let eventId;
|
|
3707
|
+
try {
|
|
3708
|
+
eventId = String((await req.json()).eventId ?? "");
|
|
3709
|
+
} catch {
|
|
3710
|
+
return json({ error: "eventId required" }, 400);
|
|
3711
|
+
}
|
|
3712
|
+
try {
|
|
3713
|
+
const retried = await retryNetworkCommercialEvent(db, model.sources, eventId, user.userId, Date.now(), networkCommercialAlerts(ctx, PATH));
|
|
3714
|
+
return json({ ok: true, ...retried });
|
|
3715
|
+
} catch (error) {
|
|
3716
|
+
const code = error.code;
|
|
3717
|
+
if (code === "invalid_event") return json({ error: "invalid event reference" }, 400);
|
|
3718
|
+
if (code === "exception_not_active") return json({ error: "event has no active exception" }, 409);
|
|
3719
|
+
return json({ error: "Event recovery is pending. Check its provider evidence and retry." }, 503);
|
|
3720
|
+
}
|
|
3721
|
+
};
|
|
3722
|
+
|
|
3438
3723
|
// src/worker-routes-payment-recovery.ts
|
|
3439
3724
|
var handlePaymentRecovery = async (req, url, env, ctx) => {
|
|
3440
|
-
if (url.pathname !== "/api/admin/payment-recovery"
|
|
3725
|
+
if (url.pathname !== "/api/admin/payment-recovery") return null;
|
|
3726
|
+
if (ctx.chapter.mode !== "chapter") return handleNetworkCommercialRecovery(req, url, env, ctx);
|
|
3441
3727
|
if (!["GET", "POST"].includes(req.method)) return json({ error: "method not allowed" }, 405);
|
|
3442
3728
|
const raw2 = ctx.makeDb(env), user = await ctx.verifyUser(req, env);
|
|
3443
3729
|
if (!user) return json({ error: "unauthorized" }, 401);
|
|
@@ -3499,10 +3785,10 @@ var handlePaymentRecovery = async (req, url, env, ctx) => {
|
|
|
3499
3785
|
};
|
|
3500
3786
|
|
|
3501
3787
|
// src/worker-routes-admin-named-seat.ts
|
|
3502
|
-
var
|
|
3788
|
+
var PATH2 = /^\/api\/admin\/applications\/([0-9a-f-]+)\/named-seat(\/refund)?$/;
|
|
3503
3789
|
var REFUNDABLE = /* @__PURE__ */ new Set(["renewal_pending", "pending_acceptance", "pending_approval", "pending_primary", "active"]);
|
|
3504
3790
|
var handleAdminNamedSeat = async (req, url, env, ctx) => {
|
|
3505
|
-
const match = url.pathname.match(
|
|
3791
|
+
const match = url.pathname.match(PATH2);
|
|
3506
3792
|
if (!match) return null;
|
|
3507
3793
|
const seats = ctx.membershipAuthority?.namedSeats;
|
|
3508
3794
|
if (ctx.chapter.mode !== "chapter" || !seats) return json({ error: "named seats are not configured" }, 404);
|
|
@@ -3555,42 +3841,42 @@ async function closeRecipientApplication(db, env, ctx, seatId) {
|
|
|
3555
3841
|
|
|
3556
3842
|
// src/membership-seat-refund-stripe.ts
|
|
3557
3843
|
var import_stripe6 = require("@odla-ai/stripe");
|
|
3558
|
-
var
|
|
3844
|
+
var fail4 = (code = "named_seat_refund_mismatch") => {
|
|
3559
3845
|
throw Object.assign(new Error(code), { code });
|
|
3560
3846
|
};
|
|
3561
3847
|
async function refundNamedSeatStripePayment(secretKey, seat) {
|
|
3562
|
-
if (!seat.providerPaymentIntentId || seat.status !== "refund_pending")
|
|
3848
|
+
if (!seat.providerPaymentIntentId || seat.status !== "refund_pending") fail4();
|
|
3563
3849
|
const call = async (method, path, params, idempotencyKey) => {
|
|
3564
3850
|
const result = await (0, import_stripe6.stripeRequest)({ secretKey }, method, path, params, idempotencyKey ? { idempotencyKey } : {});
|
|
3565
|
-
if (!result.ok || !result.body) return
|
|
3851
|
+
if (!result.ok || !result.body) return fail4("named_seat_refund_provider_unavailable");
|
|
3566
3852
|
return result.body;
|
|
3567
3853
|
};
|
|
3568
3854
|
const metadata = { ...stripePartitionMetadata(seat), applicationId: seat.applicationId, membershipId: seat.primaryMembershipId, seatId: seat.id };
|
|
3569
3855
|
const payment = await call("GET", `/v1/payment_intents/${encodeURIComponent(seat.providerPaymentIntentId)}`);
|
|
3570
|
-
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))
|
|
3856
|
+
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();
|
|
3571
3857
|
const sub = await call("GET", `/v1/subscriptions/${encodeURIComponent(seat.providerSubscriptionId)}`);
|
|
3572
|
-
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))
|
|
3858
|
+
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();
|
|
3573
3859
|
const items = sub.items.data.filter((item) => item.metadata?.seatId === seat.id);
|
|
3574
|
-
if (items.length > 1)
|
|
3860
|
+
if (items.length > 1) fail4();
|
|
3575
3861
|
if (items.length && sub.status !== "canceled") {
|
|
3576
3862
|
const item = items[0];
|
|
3577
|
-
if (!/^si_[A-Za-z0-9_]+$/.test(item.id) || item.price?.id !== seat.priceId || item.quantity !== 1)
|
|
3863
|
+
if (!/^si_[A-Za-z0-9_]+$/.test(item.id) || item.price?.id !== seat.priceId || item.quantity !== 1) fail4();
|
|
3578
3864
|
const removed = await call("DELETE", `/v1/subscription_items/${encodeURIComponent(item.id)}`, { proration_behavior: "none" });
|
|
3579
|
-
if (removed.id !== item.id || removed.deleted !== true)
|
|
3865
|
+
if (removed.id !== item.id || removed.deleted !== true) fail4("named_seat_renewal_removal_unconfirmed");
|
|
3580
3866
|
}
|
|
3581
3867
|
const list = await call("GET", "/v1/refunds", { payment_intent: seat.providerPaymentIntentId, limit: 100 });
|
|
3582
|
-
if (!Array.isArray(list.data) || list.has_more !== false || list.data.length >= 100)
|
|
3868
|
+
if (!Array.isArray(list.data) || list.has_more !== false || list.data.length >= 100) fail4("named_seat_refund_history_incomplete");
|
|
3583
3869
|
let refundedCents = 0, pendingCents = 0;
|
|
3584
3870
|
const refundIds = [];
|
|
3585
3871
|
for (const refund of list.data) {
|
|
3586
|
-
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))
|
|
3872
|
+
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();
|
|
3587
3873
|
if (refund.status === "succeeded") {
|
|
3588
3874
|
refundedCents += refund.amount;
|
|
3589
3875
|
refundIds.push(refund.id);
|
|
3590
3876
|
} else if (refund.status === "pending" || refund.status === "requires_action") pendingCents += refund.amount;
|
|
3591
3877
|
}
|
|
3592
|
-
if (refundedCents + pendingCents > seat.amountCents)
|
|
3593
|
-
if (pendingCents)
|
|
3878
|
+
if (refundedCents + pendingCents > seat.amountCents) fail4();
|
|
3879
|
+
if (pendingCents) fail4("named_seat_refund_pending");
|
|
3594
3880
|
if (refundedCents < seat.amountCents) {
|
|
3595
3881
|
const remaining = seat.amountCents - refundedCents;
|
|
3596
3882
|
const refund = await call("POST", "/v1/refunds", {
|
|
@@ -3599,8 +3885,8 @@ async function refundNamedSeatStripePayment(secretKey, seat) {
|
|
|
3599
3885
|
reason: "requested_by_customer",
|
|
3600
3886
|
metadata
|
|
3601
3887
|
}, `named-seat-refund:${seat.id}:${remaining}`);
|
|
3602
|
-
if (refund.payment_intent !== seat.providerPaymentIntentId || refund.amount !== remaining || refund.currency !== seat.currency || !/^re_[A-Za-z0-9_]+$/.test(refund.id))
|
|
3603
|
-
if (refund.status !== "succeeded")
|
|
3888
|
+
if (refund.payment_intent !== seat.providerPaymentIntentId || refund.amount !== remaining || refund.currency !== seat.currency || !/^re_[A-Za-z0-9_]+$/.test(refund.id)) fail4();
|
|
3889
|
+
if (refund.status !== "succeeded") fail4("named_seat_refund_pending");
|
|
3604
3890
|
refundedCents += remaining;
|
|
3605
3891
|
refundIds.push(refund.id);
|
|
3606
3892
|
}
|
|
@@ -3805,6 +4091,107 @@ var handleAdminMembershipSeats = async (req, url, env, ctx) => {
|
|
|
3805
4091
|
}
|
|
3806
4092
|
};
|
|
3807
4093
|
|
|
4094
|
+
// src/membership-policy-versions.ts
|
|
4095
|
+
var LIMIT = 200;
|
|
4096
|
+
var text2 = (value) => typeof value === "string" && value.length > 0 && value.length <= 200 ? value : void 0;
|
|
4097
|
+
var instant = (value) => {
|
|
4098
|
+
const at = typeof value === "number" ? value : typeof value === "string" ? Date.parse(value) : NaN;
|
|
4099
|
+
return Number.isSafeInteger(at) && at >= 0 ? at : void 0;
|
|
4100
|
+
};
|
|
4101
|
+
function provenance(row2) {
|
|
4102
|
+
const source = text2(row2.source), actor = text2(row2.actor ?? row2.$createdBy), recordedAt = instant(row2.recordedAt ?? row2.createdAt ?? row2.$createdAt);
|
|
4103
|
+
return { ...source ? { source } : {}, ...actor ? { actor } : {}, ...recordedAt !== void 0 ? { recordedAt } : {} };
|
|
4104
|
+
}
|
|
4105
|
+
var live = (w, now) => w.from <= now && (w.until === void 0 || now < w.until);
|
|
4106
|
+
function stateOf(item, all, now, window, rivals) {
|
|
4107
|
+
if (item.status !== "published") return { state: item.status };
|
|
4108
|
+
const mine = window(item);
|
|
4109
|
+
if (now < mine.from) return { state: "scheduled" };
|
|
4110
|
+
const closed = mine.until !== void 0 && mine.until <= now;
|
|
4111
|
+
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));
|
|
4112
|
+
if (!closed && !higher.some((w) => live(w, now))) return { state: "active" };
|
|
4113
|
+
const dates = higher.map((w) => Math.max(w.from, mine.from));
|
|
4114
|
+
if (closed) dates.push(mine.until);
|
|
4115
|
+
return { state: "superseded", supersededAt: Math.min(...dates) };
|
|
4116
|
+
}
|
|
4117
|
+
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";
|
|
4118
|
+
var sharesSku = (a, b) => a.cohortEligibleSkuIds.some((sku) => b.cohortEligibleSkuIds.includes(sku));
|
|
4119
|
+
var byChapter = (a, b) => a.chapterId.localeCompare(b.chapterId);
|
|
4120
|
+
function normalize(rows5, collection, parse, invalid2) {
|
|
4121
|
+
const out = [];
|
|
4122
|
+
for (const row2 of rows5) {
|
|
4123
|
+
try {
|
|
4124
|
+
out.push({ value: parse(row2.snapshot), row: row2 });
|
|
4125
|
+
} catch (error) {
|
|
4126
|
+
invalid2.push({ collection, id: String(row2.id ?? ""), error: error instanceof Error ? error.message : String(error) });
|
|
4127
|
+
}
|
|
4128
|
+
}
|
|
4129
|
+
return out;
|
|
4130
|
+
}
|
|
4131
|
+
async function listMembershipPolicyVersions(db, input) {
|
|
4132
|
+
const where = input.chapterId ? { chapterId: input.chapterId } : {};
|
|
4133
|
+
const rows5 = await db.query({
|
|
4134
|
+
membershipOfferingVersion: { $: { where, limit: LIMIT } },
|
|
4135
|
+
discountPolicyVersion: { $: { where, limit: LIMIT } }
|
|
4136
|
+
});
|
|
4137
|
+
const offeringRows = rows5.membershipOfferingVersion ?? [], policyRows = rows5.discountPolicyVersion ?? [];
|
|
4138
|
+
const invalid2 = [];
|
|
4139
|
+
const offerings = normalize(offeringRows, "membershipOfferingVersion", membershipOfferingVersion, invalid2);
|
|
4140
|
+
const policies = normalize(policyRows, "discountPolicyVersion", membershipDiscountPolicyVersion, invalid2);
|
|
4141
|
+
const offeringValues = offerings.map((item) => item.value), policyValues = policies.map((item) => item.value);
|
|
4142
|
+
return {
|
|
4143
|
+
now: input.now,
|
|
4144
|
+
chapterId: input.chapterId ?? null,
|
|
4145
|
+
offerings: offerings.map(({ value, row: row2 }) => ({
|
|
4146
|
+
...value,
|
|
4147
|
+
...provenance(row2),
|
|
4148
|
+
...stateOf(
|
|
4149
|
+
value,
|
|
4150
|
+
offeringValues,
|
|
4151
|
+
input.now,
|
|
4152
|
+
(v) => ({ from: v.effectiveFrom, ...v.effectiveUntil !== void 0 ? { until: v.effectiveUntil } : {} }),
|
|
4153
|
+
(a, b) => a.chapterId === b.chapterId && a.baseSkuId === b.baseSkuId
|
|
4154
|
+
)
|
|
4155
|
+
})).sort((a, b) => byChapter(a, b) || a.baseSkuId.localeCompare(b.baseSkuId) || b.version - a.version),
|
|
4156
|
+
discountPolicies: policies.map(({ value, row: row2 }) => ({
|
|
4157
|
+
...value,
|
|
4158
|
+
...provenance(row2),
|
|
4159
|
+
mechanism: mechanism(value),
|
|
4160
|
+
...stateOf(
|
|
4161
|
+
value,
|
|
4162
|
+
policyValues,
|
|
4163
|
+
input.now,
|
|
4164
|
+
(v) => ({ from: v.qualifyingStartsAt, until: v.qualifyingEndsAt }),
|
|
4165
|
+
(a, b) => a.chapterId === b.chapterId && sharesSku(a, b)
|
|
4166
|
+
)
|
|
4167
|
+
})).sort((a, b) => byChapter(a, b) || b.version - a.version),
|
|
4168
|
+
invalid: invalid2,
|
|
4169
|
+
truncated: offeringRows.length >= LIMIT || policyRows.length >= LIMIT
|
|
4170
|
+
};
|
|
4171
|
+
}
|
|
4172
|
+
|
|
4173
|
+
// src/worker-routes-admin-membership-policy.ts
|
|
4174
|
+
var PATH3 = "/api/admin/membership-policy-versions";
|
|
4175
|
+
var SCOPE2 = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
4176
|
+
var handleAdminMembershipPolicyVersions = async (req, url, env, ctx) => {
|
|
4177
|
+
if (url.pathname !== PATH3) return null;
|
|
4178
|
+
const operator = ctx.membershipAuthorityOperator;
|
|
4179
|
+
if (!operator) return json({ error: "not found" }, 404);
|
|
4180
|
+
if (req.method !== "GET") return json({ error: "method not allowed" }, 405);
|
|
4181
|
+
const raw2 = ctx.makeDb(env), user = await ctx.verifyUser(req, env);
|
|
4182
|
+
if (!user) return json({ error: "unauthorized" }, 401);
|
|
4183
|
+
if (!await ctx.isSuperAdminEmail(raw2, user.email, user.userId)) return json({ error: "super-admin required", code: "super_admin_required" }, 403);
|
|
4184
|
+
const chapterId = url.searchParams.get("chapterId");
|
|
4185
|
+
if (chapterId !== null && !SCOPE2.test(chapterId)) return json({ error: "chapterId is invalid" }, 400);
|
|
4186
|
+
let db;
|
|
4187
|
+
try {
|
|
4188
|
+
db = operator.makeDb(env);
|
|
4189
|
+
} catch {
|
|
4190
|
+
return json({ error: "the membership authority is not hosted by this deployment" }, 404);
|
|
4191
|
+
}
|
|
4192
|
+
return json(await listMembershipPolicyVersions(db, { chapterId, now: Date.now() }));
|
|
4193
|
+
};
|
|
4194
|
+
|
|
3808
4195
|
// src/worker-routes-membership-cancel.ts
|
|
3809
4196
|
var handleMembershipCancellation = async (req, url, env, ctx) => {
|
|
3810
4197
|
const match = url.pathname.match(/^\/api\/admin\/applications\/([0-9a-f-]+)\/cancel-renewal$/);
|
|
@@ -4657,8 +5044,8 @@ var APPLICATION_LABELS = {
|
|
|
4657
5044
|
state: "State"
|
|
4658
5045
|
};
|
|
4659
5046
|
function humanize(id6) {
|
|
4660
|
-
const
|
|
4661
|
-
return
|
|
5047
|
+
const text8 = id6.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
|
|
5048
|
+
return text8 ? `${text8[0].toUpperCase()}${text8.slice(1)}` : id6;
|
|
4662
5049
|
}
|
|
4663
5050
|
function applicationDefs(chapter) {
|
|
4664
5051
|
try {
|
|
@@ -4725,7 +5112,7 @@ var ID2 = /^[a-z][a-z0-9-]{1,79}$/;
|
|
|
4725
5112
|
var PRICE = /^price_[A-Za-z0-9]{12,}$/;
|
|
4726
5113
|
var CURRENCY = /^[a-z]{3}$/;
|
|
4727
5114
|
var record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
4728
|
-
var
|
|
5115
|
+
var text3 = (value, path, errors, max) => {
|
|
4729
5116
|
if (typeof value !== "string" || !value.trim()) {
|
|
4730
5117
|
errors.push(`${path} is required`);
|
|
4731
5118
|
return "";
|
|
@@ -4753,23 +5140,23 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
4753
5140
|
}
|
|
4754
5141
|
const allowed2 = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
|
|
4755
5142
|
for (const key3 of Object.keys(item)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
|
|
4756
|
-
const id6 =
|
|
5143
|
+
const id6 = text3(item.id, `${path}.id`, errors, 80);
|
|
4757
5144
|
if (id6 && !ID2.test(id6)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
4758
5145
|
if (ids2.has(id6)) errors.push(`${path}.id duplicates "${id6}"`);
|
|
4759
5146
|
ids2.add(id6);
|
|
4760
5147
|
const targetRaw = record(item.target);
|
|
4761
5148
|
let target = { type: "entitlement", entitlementKey: "invalid" };
|
|
4762
5149
|
if (targetRaw?.type === "tier") {
|
|
4763
|
-
const tierId =
|
|
5150
|
+
const tierId = text3(targetRaw.tierId, `${path}.target.tierId`, errors, 80);
|
|
4764
5151
|
if (!tierIds.has(tierId)) errors.push(`${path}.target.tierId must reference a declared tier`);
|
|
4765
5152
|
target = { type: "tier", tierId };
|
|
4766
5153
|
} else if (targetRaw?.type === "entitlement") {
|
|
4767
|
-
const entitlementKey =
|
|
5154
|
+
const entitlementKey = text3(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
|
|
4768
5155
|
if (entitlementKey && !ID2.test(entitlementKey)) errors.push(`${path}.target.entitlementKey must be a lowercase kebab-case id`);
|
|
4769
5156
|
target = { type: "entitlement", entitlementKey };
|
|
4770
5157
|
} else errors.push(`${path}.target.type must be tier or entitlement`);
|
|
4771
5158
|
if (item.billingMode !== "one_time" && item.billingMode !== "subscription") errors.push(`${path}.billingMode must be one_time or subscription`);
|
|
4772
|
-
const stripePriceId =
|
|
5159
|
+
const stripePriceId = text3(item.stripePriceId, `${path}.stripePriceId`, errors, 128);
|
|
4773
5160
|
if (stripePriceId && !PRICE.test(stripePriceId)) errors.push(`${path}.stripePriceId must be a real Stripe Price id`);
|
|
4774
5161
|
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`);
|
|
4775
5162
|
const currency = typeof item.currency === "string" ? item.currency.trim().toLowerCase() : "";
|
|
@@ -4791,9 +5178,9 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
4791
5178
|
redemptionLifetimeHours: lifetime,
|
|
4792
5179
|
...activeFrom !== void 0 ? { activeFrom } : {},
|
|
4793
5180
|
...activeUntil !== void 0 ? { activeUntil } : {},
|
|
4794
|
-
title:
|
|
4795
|
-
description:
|
|
4796
|
-
ctaLabel:
|
|
5181
|
+
title: text3(item.title, `${path}.title`, errors, 160),
|
|
5182
|
+
description: text3(item.description, `${path}.description`, errors, 2e3),
|
|
5183
|
+
ctaLabel: text3(item.ctaLabel, `${path}.ctaLabel`, errors, 120)
|
|
4797
5184
|
});
|
|
4798
5185
|
}
|
|
4799
5186
|
return errors.length ? { ok: false, errors } : { ok: true, value: { schemaVersion: 1, offers } };
|
|
@@ -5011,22 +5398,22 @@ async function createPaymentQuote(input) {
|
|
|
5011
5398
|
// src/membership-quote-projection.ts
|
|
5012
5399
|
var MEMBERSHIP_QUOTE_NAMESPACE = "membershipQuoteProjections";
|
|
5013
5400
|
var keys2 = ["appId", "chapterId", "environment", "runtime", "applicationId", "skuId"];
|
|
5014
|
-
var
|
|
5401
|
+
var fail5 = (code) => {
|
|
5015
5402
|
throw Object.assign(new Error(code), { code });
|
|
5016
5403
|
};
|
|
5017
5404
|
var load3 = async (db, ns, id6) => (await db.query({ [ns]: { $: { where: { id: id6 }, limit: 1 } } }))[ns]?.[0];
|
|
5018
5405
|
async function restore2(row2, target, now, allowExpired = false) {
|
|
5019
|
-
if (!row2 || keys2.some((key3) => row2[key3] !== target[key3]) || !Number.isSafeInteger(row2.revision) || Number(row2.revision) < 1 || typeof row2.requestId !== "string") return
|
|
5020
|
-
if (row2.status !== "ready") return
|
|
5406
|
+
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");
|
|
5407
|
+
if (row2.status !== "ready") return fail5("membership_quote_pending");
|
|
5021
5408
|
const offer = await verifyIssuedMembershipOffer(row2.offer, { ...target, offerId: await membershipOfferIdForRequest({
|
|
5022
5409
|
...target,
|
|
5023
5410
|
requestId: row2.requestId
|
|
5024
5411
|
}), now, lifetimeMs: 6e4 });
|
|
5025
|
-
if (offer.issuedAt > now + 3e4 || !allowExpired && offer.expiresAt <= now) return
|
|
5412
|
+
if (offer.issuedAt > now + 3e4 || !allowExpired && offer.expiresAt <= now) return fail5("membership_quote_expired");
|
|
5026
5413
|
const tier = row2.tier, quote = row2.quote;
|
|
5027
5414
|
const offering = offer.contract.offering;
|
|
5028
5415
|
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) {
|
|
5029
|
-
return
|
|
5416
|
+
return fail5("membership_catalog_mismatch");
|
|
5030
5417
|
}
|
|
5031
5418
|
const expected = await createPaymentQuote({
|
|
5032
5419
|
...target,
|
|
@@ -5037,7 +5424,7 @@ async function restore2(row2, target, now, allowExpired = false) {
|
|
|
5037
5424
|
presentation: quote.presentation,
|
|
5038
5425
|
founding: foundingOf(offer)
|
|
5039
5426
|
});
|
|
5040
|
-
if (quote.digest !== expected.digest) return
|
|
5427
|
+
if (quote.digest !== expected.digest) return fail5("membership_quote_conflict");
|
|
5041
5428
|
return { tier, quote: expected, billingTerms: offer.contract.billingTerms, membershipOffer: offer, projectionRevision: Number(row2.revision) };
|
|
5042
5429
|
}
|
|
5043
5430
|
async function readMembershipQuote(db, target, now) {
|
|
@@ -5045,9 +5432,9 @@ async function readMembershipQuote(db, target, now) {
|
|
|
5045
5432
|
}
|
|
5046
5433
|
async function prepareMembershipQuote(db, input, issue) {
|
|
5047
5434
|
const { applicationId: id6, now, app } = input;
|
|
5048
|
-
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
|
|
5435
|
+
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");
|
|
5049
5436
|
let row2 = await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, id6);
|
|
5050
|
-
if (row2 && keys2.some((key3) => row2[key3] !== input[key3])) return
|
|
5437
|
+
if (row2 && keys2.some((key3) => row2[key3] !== input[key3])) return fail5("membership_quote_conflict");
|
|
5051
5438
|
if (row2?.status === "ready") {
|
|
5052
5439
|
try {
|
|
5053
5440
|
return await restore2(row2, input, now);
|
|
@@ -5092,7 +5479,7 @@ async function prepareMembershipQuote(db, input, issue) {
|
|
|
5092
5479
|
}
|
|
5093
5480
|
if (row2?.status === "ready") return restore2(row2, input, now);
|
|
5094
5481
|
if (!row2 || row2.status !== "pending" || keys2.some((key3) => row2[key3] !== input[key3]) || typeof row2.requestId !== "string") {
|
|
5095
|
-
return
|
|
5482
|
+
return fail5("membership_quote_pending");
|
|
5096
5483
|
}
|
|
5097
5484
|
const target = Object.fromEntries(keys2.map((key3) => [key3, input[key3]]));
|
|
5098
5485
|
const offer = await verifyIssuedMembershipOffer(await issue({ ...target, requestId: row2.requestId }), {
|
|
@@ -5356,17 +5743,17 @@ async function readBoundedRequestText(req, maxBytes) {
|
|
|
5356
5743
|
if (!reader) return "";
|
|
5357
5744
|
const decoder = new TextDecoder();
|
|
5358
5745
|
let bytes = 0;
|
|
5359
|
-
let
|
|
5746
|
+
let text8 = "";
|
|
5360
5747
|
for (; ; ) {
|
|
5361
5748
|
const { done, value } = await reader.read();
|
|
5362
5749
|
if (req.signal.aborted) return invalid2();
|
|
5363
5750
|
if (done) {
|
|
5364
5751
|
complete = true;
|
|
5365
|
-
return
|
|
5752
|
+
return text8 + decoder.decode();
|
|
5366
5753
|
}
|
|
5367
5754
|
bytes += value.byteLength;
|
|
5368
5755
|
if (bytes > maxBytes) return json({ error: "request body too large" }, 413);
|
|
5369
|
-
|
|
5756
|
+
text8 += decoder.decode(value, { stream: true });
|
|
5370
5757
|
}
|
|
5371
5758
|
} catch {
|
|
5372
5759
|
return invalid2();
|
|
@@ -5719,37 +6106,61 @@ async function provisionApplicant(db, chapter, applicationId, fields) {
|
|
|
5719
6106
|
} catch {
|
|
5720
6107
|
}
|
|
5721
6108
|
}
|
|
5722
|
-
|
|
6109
|
+
|
|
6110
|
+
// src/submit-notifications.ts
|
|
6111
|
+
async function notifyAdminOfApplication(db, env, notice) {
|
|
5723
6112
|
try {
|
|
5724
|
-
const
|
|
5725
|
-
|
|
5726
|
-
|
|
5727
|
-
|
|
5728
|
-
|
|
5729
|
-
|
|
5730
|
-
|
|
5731
|
-
|
|
5732
|
-
|
|
5733
|
-
|
|
5734
|
-
|
|
5735
|
-
|
|
5736
|
-
|
|
6113
|
+
const { group, application: app } = notice;
|
|
6114
|
+
if (typeof group.notificationEmail !== "string" || !group.notificationEmail) return;
|
|
6115
|
+
const { tierName: tierName2 } = await checkoutVariables(db, group, app);
|
|
6116
|
+
await sendTemplated(lifecycleNotifyDeps(db, env), {
|
|
6117
|
+
group: emailGroupFrom(group),
|
|
6118
|
+
template: "adminNotification",
|
|
6119
|
+
to: group.notificationEmail,
|
|
6120
|
+
vars: { ...applicantVariables(app), tier: tierName2, tierName: tierName2, adminUrl: adminUrlFor(notice.origin), membersUrl: membersUrlFor(notice.origin) },
|
|
6121
|
+
dedupeKey: `apply:${String(app.id)}:admin`,
|
|
6122
|
+
applicationId: String(app.id)
|
|
6123
|
+
});
|
|
6124
|
+
} catch {
|
|
6125
|
+
}
|
|
6126
|
+
}
|
|
6127
|
+
var FREE_CONTINUATIONS = /* @__PURE__ */ new Set(["booking", "accepted", "approve"]);
|
|
6128
|
+
function shouldConfirmSubmit(state, group) {
|
|
6129
|
+
if (state !== null && FREE_CONTINUATIONS.has(state)) return true;
|
|
6130
|
+
const templates = group.emailTemplates;
|
|
6131
|
+
return templates?.paymentConfirmation?.enabled === false;
|
|
6132
|
+
}
|
|
6133
|
+
function submitNextStep(state) {
|
|
6134
|
+
switch (state) {
|
|
6135
|
+
case "accepted":
|
|
6136
|
+
return "Your membership is approved.";
|
|
6137
|
+
case "booking":
|
|
6138
|
+
return "Next, schedule your introduction call.";
|
|
6139
|
+
case "payment":
|
|
6140
|
+
case "paymentPending":
|
|
6141
|
+
case "gift":
|
|
6142
|
+
return "Next, complete your membership payment.";
|
|
6143
|
+
default:
|
|
6144
|
+
return "";
|
|
6145
|
+
}
|
|
6146
|
+
}
|
|
6147
|
+
async function notifySubmitConfirmed(db, env, notice) {
|
|
6148
|
+
try {
|
|
6149
|
+
const { group, application: app } = notice;
|
|
6150
|
+
if (typeof app.email !== "string" || !app.email || !shouldConfirmSubmit(notice.state, group)) return;
|
|
6151
|
+
await sendTemplated(lifecycleNotifyDeps(db, env), {
|
|
6152
|
+
group: emailGroupFrom(group),
|
|
6153
|
+
template: "submitConfirmation",
|
|
6154
|
+
to: app.email,
|
|
6155
|
+
vars: {
|
|
6156
|
+
...applicantVariables(app),
|
|
6157
|
+
...await checkoutVariables(db, group, app),
|
|
6158
|
+
submitNextStep: submitNextStep(notice.state),
|
|
6159
|
+
membersUrl: membersUrlFor(notice.origin)
|
|
5737
6160
|
},
|
|
5738
|
-
{
|
|
5739
|
-
|
|
5740
|
-
|
|
5741
|
-
to: group.notificationEmail,
|
|
5742
|
-
vars: {
|
|
5743
|
-
firstName: stringValue(fields.firstName),
|
|
5744
|
-
lastName: stringValue(fields.lastName),
|
|
5745
|
-
email: stringValue(fields.email),
|
|
5746
|
-
phone: stringValue(fields.phone),
|
|
5747
|
-
state: stringValue(fields.state)
|
|
5748
|
-
},
|
|
5749
|
-
dedupeKey: `apply:${applicationId}:admin`,
|
|
5750
|
-
applicationId
|
|
5751
|
-
}
|
|
5752
|
-
);
|
|
6161
|
+
dedupeKey: `apply:${String(app.id)}:confirm`,
|
|
6162
|
+
applicationId: String(app.id)
|
|
6163
|
+
});
|
|
5753
6164
|
} catch {
|
|
5754
6165
|
}
|
|
5755
6166
|
}
|
|
@@ -5947,17 +6358,17 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
5947
6358
|
if (provisioningFailure.status === 409) await rejectUnprovisionedSignup(db, result.id, canonical3.status);
|
|
5948
6359
|
return json(provisioningFailure.body, provisioningFailure.status);
|
|
5949
6360
|
}
|
|
5950
|
-
if (!result.duplicate && effectiveChapter.sends.adminNotification === "submit") {
|
|
5951
|
-
await notifyAdminOfApplication(db, env, chapter.id, result.id, canonical3);
|
|
5952
|
-
}
|
|
5953
6361
|
const groups = (await db.query({ groups: { $: { where: { id: chapter.id }, limit: 1 } } })).groups;
|
|
5954
6362
|
const group = Array.isArray(groups) ? groups[0] : void 0;
|
|
5955
|
-
const
|
|
6363
|
+
const notice = group ? { group, application: canonical3, origin: url.origin } : null;
|
|
6364
|
+
if (notice && !result.duplicate && effectiveChapter.sends.adminNotification === "submit") await notifyAdminOfApplication(db, env, notice);
|
|
6365
|
+
const state = notice ? await continueJoin(db, ctx, env, url.origin, canonical3, notice.group) : null;
|
|
5956
6366
|
if (!state) return json({
|
|
5957
6367
|
error: "Application saved; its next step is temporarily unavailable. Retry this submission.",
|
|
5958
6368
|
code: "application_continuation_unavailable",
|
|
5959
6369
|
id: result.id
|
|
5960
6370
|
}, 503);
|
|
6371
|
+
if (notice && !result.duplicate) await notifySubmitConfirmed(db, env, { ...notice, state });
|
|
5961
6372
|
const final = (await db.query({ applications: { $: { where: { id: result.id }, limit: 1 } } })).applications?.[0];
|
|
5962
6373
|
if (!final || final.groupId !== chapter.id || state === "accepted" && final.status !== "approved") {
|
|
5963
6374
|
return json({ error: "application changed; reload its current state" }, 409);
|
|
@@ -6077,10 +6488,10 @@ function boundedString(value, path, errors, max, required = false) {
|
|
|
6077
6488
|
errors.push(`${path} must be a string`);
|
|
6078
6489
|
return void 0;
|
|
6079
6490
|
}
|
|
6080
|
-
const
|
|
6081
|
-
if (required && !
|
|
6082
|
-
if (
|
|
6083
|
-
return
|
|
6491
|
+
const text8 = value.trim();
|
|
6492
|
+
if (required && !text8) errors.push(`${path} is required`);
|
|
6493
|
+
if (text8.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
|
|
6494
|
+
return text8;
|
|
6084
6495
|
}
|
|
6085
6496
|
function validateMembershipPageDocument(input, options = {}) {
|
|
6086
6497
|
const errors = [];
|
|
@@ -6129,8 +6540,8 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
6129
6540
|
else {
|
|
6130
6541
|
if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
|
|
6131
6542
|
benefits = item.benefits.slice(0, MAX_BENEFITS).flatMap((benefit, benefitIndex) => {
|
|
6132
|
-
const
|
|
6133
|
-
return
|
|
6543
|
+
const text8 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
|
|
6544
|
+
return text8 ? [text8] : [];
|
|
6134
6545
|
});
|
|
6135
6546
|
}
|
|
6136
6547
|
}
|
|
@@ -6224,7 +6635,7 @@ var recordOf = (value) => value !== null && typeof value === "object" && !Array.
|
|
|
6224
6635
|
function unknownKeys(value, allowed2, path, errors) {
|
|
6225
6636
|
for (const key3 of Object.keys(value)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
|
|
6226
6637
|
}
|
|
6227
|
-
function
|
|
6638
|
+
function text4(value, path, errors, max = 65536) {
|
|
6228
6639
|
if (typeof value !== "string" || !value.trim()) {
|
|
6229
6640
|
errors.push(`${path} is required`);
|
|
6230
6641
|
return "";
|
|
@@ -6247,11 +6658,11 @@ function tiersOf(value, errors) {
|
|
|
6247
6658
|
return [];
|
|
6248
6659
|
}
|
|
6249
6660
|
unknownKeys(item, TIER_KEYS2, path, errors);
|
|
6250
|
-
const id6 =
|
|
6661
|
+
const id6 = text4(item.id, `${path}.id`, errors, 80);
|
|
6251
6662
|
if (id6 && !TIER.test(id6)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
6252
6663
|
if (seen.has(id6)) errors.push(`${path}.id duplicates "${id6}"`);
|
|
6253
6664
|
seen.add(id6);
|
|
6254
|
-
const name =
|
|
6665
|
+
const name = text4(item.name, `${path}.name`, errors, 160);
|
|
6255
6666
|
if (typeof item.available !== "boolean") errors.push(`${path}.available must be boolean`);
|
|
6256
6667
|
if (!Number.isInteger(item.sortOrder) || Number(item.sortOrder) < 0) errors.push(`${path}.sortOrder must be a non-negative integer`);
|
|
6257
6668
|
if (!Number.isInteger(item.priceCents) || Number(item.priceCents) < 0 || Number(item.priceCents) > 1e8) {
|
|
@@ -6269,7 +6680,7 @@ function tiersOf(value, errors) {
|
|
|
6269
6680
|
if (item.interval !== void 0 && item.interval !== "year" && item.interval !== "month") {
|
|
6270
6681
|
errors.push(`${path}.interval must be year or month`);
|
|
6271
6682
|
}
|
|
6272
|
-
const blurb = item.blurb === void 0 ? void 0 :
|
|
6683
|
+
const blurb = item.blurb === void 0 ? void 0 : text4(item.blurb, `${path}.blurb`, errors, 4e3);
|
|
6273
6684
|
return [{
|
|
6274
6685
|
id: id6,
|
|
6275
6686
|
name,
|
|
@@ -6327,7 +6738,7 @@ function policyOf(value, errors, tiers) {
|
|
|
6327
6738
|
const result = {};
|
|
6328
6739
|
for (const key3 of allowed2) {
|
|
6329
6740
|
if (input[key3] === void 0 && key3 !== "disclaimerText" && key3 !== "refundPolicyText") continue;
|
|
6330
|
-
result[key3] =
|
|
6741
|
+
result[key3] = text4(input[key3], `document.policy.${key3}`, errors);
|
|
6331
6742
|
}
|
|
6332
6743
|
const tierPolicies = {};
|
|
6333
6744
|
if (input.tierPolicies !== void 0) {
|
|
@@ -6342,8 +6753,8 @@ function policyOf(value, errors, tiers) {
|
|
|
6342
6753
|
}
|
|
6343
6754
|
unknownKeys(policy, /* @__PURE__ */ new Set(["refundPolicyText", "trustCopy"]), path, errors);
|
|
6344
6755
|
tierPolicies[id6] = {
|
|
6345
|
-
refundPolicyText:
|
|
6346
|
-
...policy.trustCopy !== void 0 ? { trustCopy:
|
|
6756
|
+
refundPolicyText: text4(policy.refundPolicyText, `${path}.refundPolicyText`, errors),
|
|
6757
|
+
...policy.trustCopy !== void 0 ? { trustCopy: text4(policy.trustCopy, `${path}.trustCopy`, errors) } : {}
|
|
6347
6758
|
};
|
|
6348
6759
|
}
|
|
6349
6760
|
}
|
|
@@ -6383,9 +6794,9 @@ function validateSignupControlDocument(input) {
|
|
|
6383
6794
|
if (!Number.isSafeInteger(root.generatedAt) || Number(root.generatedAt) < 1) errors.push("document.generatedAt must be epoch milliseconds");
|
|
6384
6795
|
const source = recordOf(root.source);
|
|
6385
6796
|
const target = recordOf(root.target);
|
|
6386
|
-
const appId =
|
|
6387
|
-
const chapterId =
|
|
6388
|
-
const environment =
|
|
6797
|
+
const appId = text4(source?.appId, "document.source.appId", errors, 63);
|
|
6798
|
+
const chapterId = text4(target?.chapterId, "document.target.chapterId", errors, 63);
|
|
6799
|
+
const environment = text4(target?.environment, "document.target.environment", errors, 63);
|
|
6389
6800
|
if (appId && !SLUG.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
6390
6801
|
if (chapterId && !SLUG.test(chapterId)) errors.push("document.target.chapterId must be a lowercase app slug");
|
|
6391
6802
|
if (environment && !ENVIRONMENT.test(environment)) errors.push("document.target.environment is invalid");
|
|
@@ -7564,7 +7975,7 @@ async function deliverAdmissionGrants(options) {
|
|
|
7564
7975
|
}
|
|
7565
7976
|
|
|
7566
7977
|
// src/worker-routes-network-admission.ts
|
|
7567
|
-
var
|
|
7978
|
+
var PATH4 = "/api/admin/network/admissions";
|
|
7568
7979
|
var RETRY_PATH = "/api/admin/network/admissions/retry";
|
|
7569
7980
|
var REVOKE_PATH = "/api/admin/network/admissions/revoke";
|
|
7570
7981
|
var MUTATION_ID2 = /^[A-Za-z0-9_-]{16,128}$/;
|
|
@@ -7644,11 +8055,11 @@ async function parseBody(req) {
|
|
|
7644
8055
|
}
|
|
7645
8056
|
}
|
|
7646
8057
|
var handleAdminNetworkAdmissions = async (req, url, env, ctx) => {
|
|
7647
|
-
if (![
|
|
8058
|
+
if (![PATH4, RETRY_PATH, REVOKE_PATH].includes(url.pathname)) return null;
|
|
7648
8059
|
const auth = await gate2(req, env, ctx);
|
|
7649
8060
|
if ("response" in auth) return auth.response;
|
|
7650
8061
|
const db = auth.db;
|
|
7651
|
-
if (url.pathname ===
|
|
8062
|
+
if (url.pathname === PATH4 && req.method === "GET") {
|
|
7652
8063
|
const targetId2 = url.searchParams.get("targetId") ?? "";
|
|
7653
8064
|
const target2 = targetFor(ctx, targetId2);
|
|
7654
8065
|
if (!target2) return reject6(404, "unknown_target", "admission target is not configured");
|
|
@@ -7749,6 +8160,41 @@ async function retryLeaderAdmissionDeliveries(env, ctx, limit = 100) {
|
|
|
7749
8160
|
|
|
7750
8161
|
// src/worker-routes-schedule.ts
|
|
7751
8162
|
var import_calendar = require("@odla-ai/calendar");
|
|
8163
|
+
|
|
8164
|
+
// src/booking-notifications.ts
|
|
8165
|
+
async function bookingConfirmationVariables(db, group, app, meeting) {
|
|
8166
|
+
const startAt = formatInstant(meeting.startAt, meeting.timezone);
|
|
8167
|
+
const meetUrl = meeting.meetUrl ?? "", htmlLink = meeting.htmlLink ?? "";
|
|
8168
|
+
return {
|
|
8169
|
+
...applicantVariables(app),
|
|
8170
|
+
...await checkoutVariables(db, group, app),
|
|
8171
|
+
startAt,
|
|
8172
|
+
endAt: formatInstant(meeting.endAt, meeting.timezone),
|
|
8173
|
+
timezone: meeting.timezone,
|
|
8174
|
+
meetUrl,
|
|
8175
|
+
htmlLink,
|
|
8176
|
+
// The placeholders the seeded template used before these variables existed.
|
|
8177
|
+
meetingTime: startAt,
|
|
8178
|
+
meetingLink: meetUrl || htmlLink
|
|
8179
|
+
};
|
|
8180
|
+
}
|
|
8181
|
+
async function notifyBookingConfirmed(db, env, input) {
|
|
8182
|
+
const app = input.application;
|
|
8183
|
+
if (typeof app.email !== "string" || !app.email) return;
|
|
8184
|
+
try {
|
|
8185
|
+
await sendTemplated(lifecycleNotifyDeps(db, env), {
|
|
8186
|
+
group: emailGroupFrom(input.group),
|
|
8187
|
+
template: "prepEmail",
|
|
8188
|
+
to: app.email,
|
|
8189
|
+
vars: await bookingConfirmationVariables(db, input.group, app, input.meeting),
|
|
8190
|
+
dedupeKey: `prep:${String(app.id)}`,
|
|
8191
|
+
applicationId: String(app.id)
|
|
8192
|
+
});
|
|
8193
|
+
} catch {
|
|
8194
|
+
}
|
|
8195
|
+
}
|
|
8196
|
+
|
|
8197
|
+
// src/worker-routes-schedule.ts
|
|
7752
8198
|
function errCode(err) {
|
|
7753
8199
|
if (err && typeof err === "object") {
|
|
7754
8200
|
const code = err.code;
|
|
@@ -7863,12 +8309,7 @@ async function bookSlot(req, env, ctx) {
|
|
|
7863
8309
|
await db.transact([meetingOp, appOp]);
|
|
7864
8310
|
const booked = { ...app, ...bookingAttrs };
|
|
7865
8311
|
await syncApplicationToCrm(crmDeps(db, ctx), { app: booked, stage: String(booked.status ?? "") }).catch(() => void 0);
|
|
7866
|
-
|
|
7867
|
-
await sendTemplated(
|
|
7868
|
-
{ db, envName: env.ODLA_ENV, sender: env.SEND_EMAIL, from: env.EMAIL_FROM, debugEmail: env.ODLA_DEBUG_EMAIL, now: () => Date.now(), newId: () => crypto.randomUUID() },
|
|
7869
|
-
{ group: emailGroupFrom(group), template: "prepEmail", to: app.email, vars: { firstName: String(app.firstName ?? "") }, dedupeKey: `prep:${applicationId}`, applicationId }
|
|
7870
|
-
).catch(() => void 0);
|
|
7871
|
-
}
|
|
8312
|
+
await notifyBookingConfirmed(db, env, { group, application: app, meeting: { startAt, endAt, timezone: cfg.timezone, meetUrl, htmlLink } });
|
|
7872
8313
|
return json({ ok: true, startAt, endAt, meetUrl, rescheduled: decision.reschedule });
|
|
7873
8314
|
}
|
|
7874
8315
|
var handleSchedule = async (req, url, env, ctx) => {
|
|
@@ -7899,21 +8340,21 @@ var import_stripe7 = require("@odla-ai/stripe");
|
|
|
7899
8340
|
var row = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
7900
8341
|
var id2 = (value) => typeof value === "string" ? value : String(row(value).id ?? "");
|
|
7901
8342
|
var providerId2 = (value, prefix) => new RegExp(`^${prefix}_[A-Za-z0-9]+$`).test(value);
|
|
7902
|
-
function
|
|
8343
|
+
function fail6(code = "refund_target_mismatch") {
|
|
7903
8344
|
throw Object.assign(new Error(code), { code });
|
|
7904
8345
|
}
|
|
7905
8346
|
async function refundSubscriptionPayment(secretKey, input) {
|
|
7906
|
-
if (!providerId2(input.customerId, "cus") || !providerId2(input.subscriptionId, "sub"))
|
|
8347
|
+
if (!providerId2(input.customerId, "cus") || !providerId2(input.subscriptionId, "sub")) fail6();
|
|
7907
8348
|
const call = async (method, path, params, key3) => {
|
|
7908
8349
|
const result = await (0, import_stripe7.stripeRequest)({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
|
|
7909
|
-
if (!result.ok)
|
|
7910
|
-
if (!result.body || typeof result.body !== "object" || Array.isArray(result.body))
|
|
8350
|
+
if (!result.ok) fail6("stripe_error");
|
|
8351
|
+
if (!result.body || typeof result.body !== "object" || Array.isArray(result.body)) fail6("stripe_invalid_response");
|
|
7911
8352
|
return result.body;
|
|
7912
8353
|
};
|
|
7913
8354
|
const sub = await call("GET", `/v1/subscriptions/${input.subscriptionId}`);
|
|
7914
|
-
if (id2(sub) !== input.subscriptionId || id2(sub.customer) !== input.customerId)
|
|
8355
|
+
if (id2(sub) !== input.subscriptionId || id2(sub.customer) !== input.customerId) fail6();
|
|
7915
8356
|
for (const [key3, value] of Object.entries(input.metadata ?? {})) {
|
|
7916
|
-
if (row(sub.metadata)[key3] !== value)
|
|
8357
|
+
if (row(sub.metadata)[key3] !== value) fail6();
|
|
7917
8358
|
}
|
|
7918
8359
|
let cursor;
|
|
7919
8360
|
let earliest;
|
|
@@ -7926,55 +8367,55 @@ async function refundSubscriptionPayment(secretKey, input) {
|
|
|
7926
8367
|
limit: 100,
|
|
7927
8368
|
...cursor ? { starting_after: cursor } : {}
|
|
7928
8369
|
});
|
|
7929
|
-
if (!Array.isArray(list.data) || typeof list.has_more !== "boolean")
|
|
8370
|
+
if (!Array.isArray(list.data) || typeof list.has_more !== "boolean") fail6("stripe_invalid_response");
|
|
7930
8371
|
for (const value of list.data) {
|
|
7931
8372
|
const invoice2 = row(value);
|
|
7932
8373
|
const subscription = invoice2.subscription ?? row(row(invoice2.parent).subscription_details).subscription;
|
|
7933
|
-
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)
|
|
8374
|
+
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();
|
|
7934
8375
|
seen.add(id2(invoice2));
|
|
7935
8376
|
if (Number(invoice2.amount_paid) > 0 && (!earliest || Number(invoice2.created) < Number(earliest.created))) earliest = invoice2;
|
|
7936
8377
|
}
|
|
7937
8378
|
if (!list.has_more) break;
|
|
7938
|
-
if (!list.data.length || page === 9)
|
|
8379
|
+
if (!list.data.length || page === 9) fail6("refund_history_incomplete");
|
|
7939
8380
|
cursor = id2(list.data[list.data.length - 1]);
|
|
7940
8381
|
}
|
|
7941
|
-
if (!earliest)
|
|
8382
|
+
if (!earliest) fail6("no_charge");
|
|
7942
8383
|
const invoice = earliest;
|
|
7943
|
-
if (input.expectedInvoiceId !== void 0 && id2(invoice) !== input.expectedInvoiceId || input.expectedAmountCents !== void 0 && invoice.amount_paid !== input.expectedAmountCents)
|
|
8384
|
+
if (input.expectedInvoiceId !== void 0 && id2(invoice) !== input.expectedInvoiceId || input.expectedAmountCents !== void 0 && invoice.amount_paid !== input.expectedAmountCents) fail6();
|
|
7944
8385
|
let chargeId = id2(invoice.charge);
|
|
7945
8386
|
let paymentIntentId = id2(invoice.payment_intent);
|
|
7946
8387
|
if (!chargeId && !paymentIntentId) {
|
|
7947
8388
|
const payments = await call("GET", "/v1/invoice_payments", { invoice: id2(invoice), status: "paid", limit: 100 });
|
|
7948
|
-
if (payments.has_more !== false || !Array.isArray(payments.data) || payments.data.length !== 1)
|
|
8389
|
+
if (payments.has_more !== false || !Array.isArray(payments.data) || payments.data.length !== 1) fail6("refund_payment_ambiguous");
|
|
7949
8390
|
const payment = row(payments.data[0]);
|
|
7950
|
-
if (id2(payment.invoice) !== id2(invoice) || payment.status !== "paid" || row(payment.payment).type !== "payment_intent")
|
|
8391
|
+
if (id2(payment.invoice) !== id2(invoice) || payment.status !== "paid" || row(payment.payment).type !== "payment_intent") fail6();
|
|
7951
8392
|
paymentIntentId = id2(row(payment.payment).payment_intent);
|
|
7952
8393
|
}
|
|
7953
8394
|
if (!chargeId) {
|
|
7954
|
-
if (!providerId2(paymentIntentId, "pi"))
|
|
8395
|
+
if (!providerId2(paymentIntentId, "pi")) fail6();
|
|
7955
8396
|
const payment = await call("GET", `/v1/payment_intents/${paymentIntentId}`);
|
|
7956
|
-
if (id2(payment) !== paymentIntentId || id2(payment.customer) !== input.customerId || payment.status !== "succeeded")
|
|
8397
|
+
if (id2(payment) !== paymentIntentId || id2(payment.customer) !== input.customerId || payment.status !== "succeeded") fail6();
|
|
7957
8398
|
chargeId = id2(payment.latest_charge);
|
|
7958
8399
|
}
|
|
7959
|
-
if (!providerId2(chargeId, "ch"))
|
|
8400
|
+
if (!providerId2(chargeId, "ch")) fail6();
|
|
7960
8401
|
const charge = await call("GET", `/v1/charges/${chargeId}`);
|
|
7961
|
-
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))
|
|
7962
|
-
if (input.alreadyRefundedChargeId !== void 0 && (chargeId !== input.alreadyRefundedChargeId || charge.amount_refunded !== charge.amount || charge.refunded !== true))
|
|
8402
|
+
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();
|
|
8403
|
+
if (input.alreadyRefundedChargeId !== void 0 && (chargeId !== input.alreadyRefundedChargeId || charge.amount_refunded !== charge.amount || charge.refunded !== true)) fail6();
|
|
7963
8404
|
if (charge.amount_refunded !== charge.amount) {
|
|
7964
8405
|
const refund = await call("POST", "/v1/refunds", {
|
|
7965
8406
|
charge: chargeId,
|
|
7966
8407
|
...input.metadata ? { metadata: input.metadata } : {}
|
|
7967
8408
|
}, `chapter-initial-refund:${input.subscriptionId}:${chargeId}`);
|
|
7968
|
-
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))
|
|
8409
|
+
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");
|
|
7969
8410
|
}
|
|
7970
8411
|
let subscriptionCanceled = sub.status === "canceled";
|
|
7971
8412
|
if (input.cancelSubscription !== false && !subscriptionCanceled) {
|
|
7972
8413
|
const canceled = await call("DELETE", `/v1/subscriptions/${input.subscriptionId}`);
|
|
7973
|
-
if (id2(canceled) !== input.subscriptionId || canceled.status !== "canceled")
|
|
8414
|
+
if (id2(canceled) !== input.subscriptionId || canceled.status !== "canceled") fail6("refund_cancel_incomplete");
|
|
7974
8415
|
subscriptionCanceled = true;
|
|
7975
8416
|
}
|
|
7976
8417
|
const confirmed = await call("GET", `/v1/charges/${chargeId}`);
|
|
7977
|
-
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)
|
|
8418
|
+
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");
|
|
7978
8419
|
return { refundedCents: Number(charge.amount), subscriptionCanceled };
|
|
7979
8420
|
}
|
|
7980
8421
|
|
|
@@ -8040,7 +8481,7 @@ async function stripeCall(sk, method, path, params, idempotencyKey) {
|
|
|
8040
8481
|
idempotencyKey ? { idempotencyKey } : {}
|
|
8041
8482
|
);
|
|
8042
8483
|
}
|
|
8043
|
-
function
|
|
8484
|
+
function fail7(op, r) {
|
|
8044
8485
|
const err = new Error(`stripe ${op} failed: ${r.status}`);
|
|
8045
8486
|
err.code = "stripe_error";
|
|
8046
8487
|
throw err;
|
|
@@ -8090,7 +8531,7 @@ function createStripeProvider(config) {
|
|
|
8090
8531
|
{ email: input.email, name: input.name, metadata: meta },
|
|
8091
8532
|
`cus:${input.applicationId}`
|
|
8092
8533
|
);
|
|
8093
|
-
if (!cust.ok)
|
|
8534
|
+
if (!cust.ok) fail7("customer create", cust);
|
|
8094
8535
|
if (typeof cust.body.id !== "string" || !/^cus_[A-Za-z0-9]+$/.test(cust.body.id)) subscriptionContractMismatch();
|
|
8095
8536
|
customerId = cust.body.id;
|
|
8096
8537
|
}
|
|
@@ -8122,7 +8563,7 @@ function createStripeProvider(config) {
|
|
|
8122
8563
|
// retry between create and the db write can't orphan a second one.
|
|
8123
8564
|
`sub:${input.applicationId}${input.purchaseAttemptId ? `:${input.purchaseAttemptId}` : ""}`
|
|
8124
8565
|
);
|
|
8125
|
-
if (!sub.ok)
|
|
8566
|
+
if (!sub.ok) fail7("subscription create", sub);
|
|
8126
8567
|
if (input.billingTerms?.discount?.duration === "once" && Array.isArray(sub.body.discounts) && sub.body.discounts.length === 0) {
|
|
8127
8568
|
const subscriptionId = sub.body.id;
|
|
8128
8569
|
if (typeof subscriptionId !== "string" || !/^sub_[A-Za-z0-9]+$/.test(subscriptionId)) subscriptionContractMismatch();
|
|
@@ -8132,14 +8573,14 @@ function createStripeProvider(config) {
|
|
|
8132
8573
|
"expand[2]": "discounts",
|
|
8133
8574
|
"expand[3]": "latest_invoice.discounts"
|
|
8134
8575
|
});
|
|
8135
|
-
if (!expanded.ok)
|
|
8576
|
+
if (!expanded.ok) fail7("subscription invoice evidence", expanded);
|
|
8136
8577
|
if (expanded.body.id !== subscriptionId) subscriptionContractMismatch();
|
|
8137
8578
|
sub = expanded;
|
|
8138
8579
|
}
|
|
8139
8580
|
verifySubscriptionResult(sub.body, input, customerId, meta);
|
|
8140
8581
|
const invoice = sub.body.latest_invoice;
|
|
8141
8582
|
const clientSecret2 = clientSecretOf(sub.body);
|
|
8142
|
-
if (!clientSecret2)
|
|
8583
|
+
if (!clientSecret2) fail7("subscription confirmation-secret missing", sub);
|
|
8143
8584
|
await tagSubscriptionPayment(
|
|
8144
8585
|
sk,
|
|
8145
8586
|
invoice,
|
|
@@ -8964,6 +9405,58 @@ var handleNamedSeat = async (req, url, env, ctx) => {
|
|
|
8964
9405
|
}
|
|
8965
9406
|
};
|
|
8966
9407
|
|
|
9408
|
+
// src/named-seat-accepted-template.ts
|
|
9409
|
+
var SEAT_ACCEPTED_TEMPLATE = {
|
|
9410
|
+
enabled: true,
|
|
9411
|
+
subject: "{{recipientName}} accepted your {{chapterName}} seat",
|
|
9412
|
+
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}}"
|
|
9413
|
+
};
|
|
9414
|
+
|
|
9415
|
+
// src/named-seat-accepted-notice.ts
|
|
9416
|
+
var text5 = (value) => typeof value === "string" ? value : "";
|
|
9417
|
+
async function notifyNamedSeatAccepted(db, env, ctx, input) {
|
|
9418
|
+
const purchaser = await membershipLoad(db, "applications", input.primaryApplicationId);
|
|
9419
|
+
if (!purchaser || !chapterBillingApplication(purchaser, ctx.chapter.id, env.ODLA_RUNTIME) || !text5(purchaser.email)) {
|
|
9420
|
+
return { sent: false, reason: "purchaser-missing" };
|
|
9421
|
+
}
|
|
9422
|
+
const group = await membershipLoad(db, "groups", ctx.chapter.id);
|
|
9423
|
+
if (!group) return { sent: false, reason: "group-missing" };
|
|
9424
|
+
const config = emailGroupFrom(group);
|
|
9425
|
+
config.emailTemplates = { seatAccepted: SEAT_ACCEPTED_TEMPLATE, ...config.emailTemplates };
|
|
9426
|
+
const purchaserName = [purchaser.firstName, purchaser.lastName].map(text5).join(" ").replace(/\s+/g, " ").trim();
|
|
9427
|
+
return sendTemplated({
|
|
9428
|
+
db,
|
|
9429
|
+
envName: env.ODLA_ENV,
|
|
9430
|
+
sender: env.SEND_EMAIL,
|
|
9431
|
+
from: env.EMAIL_FROM,
|
|
9432
|
+
debugEmail: env.ODLA_DEBUG_EMAIL,
|
|
9433
|
+
now: () => Date.now(),
|
|
9434
|
+
newId: () => crypto.randomUUID()
|
|
9435
|
+
}, {
|
|
9436
|
+
group: config,
|
|
9437
|
+
template: "seatAccepted",
|
|
9438
|
+
to: text5(purchaser.email),
|
|
9439
|
+
dedupeKey: `named-seat-accepted:${input.seatId}`,
|
|
9440
|
+
applicationId: input.primaryApplicationId,
|
|
9441
|
+
vars: {
|
|
9442
|
+
firstName: text5(purchaser.firstName),
|
|
9443
|
+
purchaserName,
|
|
9444
|
+
recipientName: input.recipientName,
|
|
9445
|
+
tierName: await tierName(db, group, input.skuId),
|
|
9446
|
+
chapterName: ctx.chapter.name,
|
|
9447
|
+
membersUrl: `${input.origin}/members/`
|
|
9448
|
+
}
|
|
9449
|
+
});
|
|
9450
|
+
}
|
|
9451
|
+
async function tierName(db, group, skuId) {
|
|
9452
|
+
try {
|
|
9453
|
+
const tier = (await loadTiers(db, group)).find((candidate) => candidate.id === skuId);
|
|
9454
|
+
if (tier) return tier.name;
|
|
9455
|
+
} catch {
|
|
9456
|
+
}
|
|
9457
|
+
return skuId;
|
|
9458
|
+
}
|
|
9459
|
+
|
|
8967
9460
|
// src/worker-routes-named-seat-recipient.ts
|
|
8968
9461
|
var handleNamedSeatRecipient = async (req, url, env, ctx) => {
|
|
8969
9462
|
if (url.pathname === "/api/named-seat/invitation") {
|
|
@@ -9081,6 +9574,13 @@ var handleNamedSeatRecipient = async (req, url, env, ctx) => {
|
|
|
9081
9574
|
const current = await membershipLoad(db, "applications", body.applicationId), group = await membershipLoad(db, "groups", ctx.chapter.id);
|
|
9082
9575
|
if (!current || current.namedSeatClaimPending !== false || !current.namedSeatAcceptedAt || !group) throw new Error("seat application reconciliation pending");
|
|
9083
9576
|
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");
|
|
9577
|
+
await notifyNamedSeatAccepted(db, env, ctx, {
|
|
9578
|
+
seatId: body.seatId,
|
|
9579
|
+
primaryApplicationId: invitation.primaryApplicationId,
|
|
9580
|
+
recipientName: invitation.recipientName,
|
|
9581
|
+
skuId: invitation.skuId,
|
|
9582
|
+
origin: url.origin
|
|
9583
|
+
}).catch(() => console.warn("named_seat_accepted_notice_incomplete", { seatId: body.seatId }));
|
|
9084
9584
|
await syncApplicationToCrm(crmDeps(db, ctx), { app: current, stage: String(current.status) }).catch(() => void 0);
|
|
9085
9585
|
const state = await continueJoin(db, ctx, env, url.origin, current, group);
|
|
9086
9586
|
if (!state) throw new Error("seat Journey reconciliation pending");
|
|
@@ -9441,8 +9941,8 @@ var handleAdminMeetings = async (req, url, env, ctx) => {
|
|
|
9441
9941
|
};
|
|
9442
9942
|
|
|
9443
9943
|
// src/crm-account-repair.ts
|
|
9444
|
-
async function repairAccountPeoplePage(
|
|
9445
|
-
const users = (await
|
|
9944
|
+
async function repairAccountPeoplePage(deps, after) {
|
|
9945
|
+
const users = (await deps.db.query({ $users: { $: {
|
|
9446
9946
|
...after ? { where: { id: { $gt: after } } } : {},
|
|
9447
9947
|
order: { id: "asc" },
|
|
9448
9948
|
limit: 50
|
|
@@ -9451,7 +9951,7 @@ async function repairAccountPeoplePage(deps2, after) {
|
|
|
9451
9951
|
const errors = [];
|
|
9452
9952
|
for (const user of users) {
|
|
9453
9953
|
try {
|
|
9454
|
-
if (await syncAccountToCrm(
|
|
9954
|
+
if (await syncAccountToCrm(deps, String(user.id))) synced++;
|
|
9455
9955
|
} catch (error) {
|
|
9456
9956
|
const message = error instanceof Error ? error.message : "";
|
|
9457
9957
|
errors.push({ userId: String(user.id), code: /^crm_projection_identity_(ambiguous|archived|conflict)$/.test(message) ? message : "crm_projection_unavailable" });
|
|
@@ -9463,8 +9963,8 @@ async function repairAccountPeoplePage(deps2, after) {
|
|
|
9463
9963
|
// src/crm-application-repair.ts
|
|
9464
9964
|
var import_crm11 = require("@odla-ai/crm");
|
|
9465
9965
|
async function repairApplicationLink(db, chapter, recordId, actorId) {
|
|
9466
|
-
const
|
|
9467
|
-
const record4 = await (0, import_crm11.getRecord)(
|
|
9966
|
+
const deps = { crm: chapter.crm, db };
|
|
9967
|
+
const record4 = await (0, import_crm11.getRecord)(deps, recordId);
|
|
9468
9968
|
if (!record4 || record4.type !== "person" || record4.archived === true) return { status: 404, error: "person_not_found" };
|
|
9469
9969
|
if (!chapter.crm.type("person").fields.applicationId) return { status: 409, error: "application_link_field_not_configured" };
|
|
9470
9970
|
const fields = record4.fields;
|
|
@@ -9482,7 +9982,7 @@ async function repairApplicationLink(db, chapter, recordId, actorId) {
|
|
|
9482
9982
|
}
|
|
9483
9983
|
const app = [...applications.values()][0];
|
|
9484
9984
|
if (!app || typeof app.id !== "string" || typeof record4.updatedAt !== "number") return { status: 409, error: "application_link_evidence_incomplete" };
|
|
9485
|
-
await (0, import_crm11.updateRecord)(
|
|
9985
|
+
await (0, import_crm11.updateRecord)(deps, {
|
|
9486
9986
|
id: recordId,
|
|
9487
9987
|
input: applicationCrmFields(chapter.crm, { applicationId: app.id, tierId: app.tierId }),
|
|
9488
9988
|
expectedUpdatedAt: record4.updatedAt,
|
|
@@ -10180,15 +10680,6 @@ async function loadGroup2(db, id6) {
|
|
|
10180
10680
|
return groups?.[0] ?? null;
|
|
10181
10681
|
}
|
|
10182
10682
|
var str2 = (v, d = "") => typeof v === "string" ? v : d;
|
|
10183
|
-
var notifyDeps = (db, env) => ({
|
|
10184
|
-
db,
|
|
10185
|
-
debugEmail: env.ODLA_DEBUG_EMAIL,
|
|
10186
|
-
envName: env.ODLA_ENV,
|
|
10187
|
-
sender: env.SEND_EMAIL,
|
|
10188
|
-
from: env.EMAIL_FROM,
|
|
10189
|
-
now: () => Date.now(),
|
|
10190
|
-
newId: () => crypto.randomUUID()
|
|
10191
|
-
});
|
|
10192
10683
|
var handleAdminGroupEmail = async (req, url, env, ctx) => {
|
|
10193
10684
|
if (url.pathname !== "/api/admin/group/email" || req.method !== "GET" && req.method !== "PUT") return null;
|
|
10194
10685
|
const got = await gate5(req, env, ctx);
|
|
@@ -10207,6 +10698,7 @@ var handleAdminGroupEmail = async (req, url, env, ctx) => {
|
|
|
10207
10698
|
groupId: group.id,
|
|
10208
10699
|
name: group.name,
|
|
10209
10700
|
replyTo: str2(group.replyTo),
|
|
10701
|
+
fromName: str2(group.fromName),
|
|
10210
10702
|
notificationEmail: str2(group.notificationEmail),
|
|
10211
10703
|
debugEmail: str2(group.debugEmail),
|
|
10212
10704
|
emailTemplates,
|
|
@@ -10233,11 +10725,11 @@ var handleAdminGroupEmail = async (req, url, env, ctx) => {
|
|
|
10233
10725
|
for (const key3 of EMAIL_TEMPLATE_NAMES) {
|
|
10234
10726
|
const t = templates[key3];
|
|
10235
10727
|
const subject = typeof t?.subject === "string" ? t.subject.trim() : "";
|
|
10236
|
-
const
|
|
10237
|
-
if (!subject || !
|
|
10728
|
+
const text8 = typeof t?.text === "string" ? t.text : "";
|
|
10729
|
+
if (!subject || !text8.trim()) return json({ error: `template "${key3}" needs a subject and a body` }, 400);
|
|
10238
10730
|
if (/[\r\n]/.test(subject) || subject.length > 200) return json({ error: `template "${key3}" subject must be a single line under 200 characters` }, 400);
|
|
10239
|
-
if (
|
|
10240
|
-
clean[key3] = { subject, text:
|
|
10731
|
+
if (text8.length > 1e4) return json({ error: `template "${key3}" body is too long` }, 400);
|
|
10732
|
+
clean[key3] = { subject, text: text8, enabled: t?.enabled !== false };
|
|
10241
10733
|
}
|
|
10242
10734
|
const emailish = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
10243
10735
|
const notificationEmail = str2(body.notificationEmail).trim();
|
|
@@ -10246,13 +10738,15 @@ var handleAdminGroupEmail = async (req, url, env, ctx) => {
|
|
|
10246
10738
|
if (!emailish.test(notificationEmail)) return json({ error: "notification address must be a valid email" }, 400);
|
|
10247
10739
|
if (!emailish.test(replyTo)) return json({ error: "reply-to address must be a valid email" }, 400);
|
|
10248
10740
|
if (debugEmail && !emailish.test(debugEmail)) return json({ error: "debug address must be a valid email" }, 400);
|
|
10741
|
+
const fromName = str2(body.fromName).trim();
|
|
10742
|
+
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);
|
|
10249
10743
|
const commitmentText = str2(body.commitmentText);
|
|
10250
10744
|
const normsText = str2(body.normsText);
|
|
10251
10745
|
if (commitmentText.length > 5e3 || normsText.length > 5e3) return json({ error: "commitment/norms text is too long" }, 400);
|
|
10252
10746
|
const id6 = { ns: "groups", attr: "id", value: ctx.chapter.id };
|
|
10253
10747
|
await db.transact([
|
|
10254
10748
|
{ t: "merge", ns: "groups", id: id6, attrs: { emailTemplates: clean } },
|
|
10255
|
-
{ t: "update", ns: "groups", id: id6, attrs: { commitmentText, normsText, notificationEmail, replyTo, debugEmail } }
|
|
10749
|
+
{ t: "update", ns: "groups", id: id6, attrs: { commitmentText, normsText, notificationEmail, replyTo, debugEmail, fromName } }
|
|
10256
10750
|
]);
|
|
10257
10751
|
return json({ ok: true });
|
|
10258
10752
|
};
|
|
@@ -10301,11 +10795,11 @@ var handleAdminEmailTest = async (req, url, env, ctx) => {
|
|
|
10301
10795
|
}
|
|
10302
10796
|
const group = await loadGroup2(db, ctx.chapter.id);
|
|
10303
10797
|
if (!group) return json({ error: "not found" }, 404);
|
|
10304
|
-
const res = await sendTemplated(
|
|
10798
|
+
const res = await sendTemplated(lifecycleNotifyDeps(db, env), {
|
|
10305
10799
|
group: emailGroupFrom(group),
|
|
10306
10800
|
template,
|
|
10307
10801
|
to: str2(group.notificationEmail),
|
|
10308
|
-
vars:
|
|
10802
|
+
vars: sampleVariables(url.origin, resolveScheduling(group.schedulingJson).timezone),
|
|
10309
10803
|
dedupeKey: `test:${template}:${Date.now()}`,
|
|
10310
10804
|
force: true
|
|
10311
10805
|
});
|
|
@@ -10332,7 +10826,7 @@ var handleAdminComms = async (req, url, env, ctx) => {
|
|
|
10332
10826
|
const app = data.applications?.[0] ?? null;
|
|
10333
10827
|
if (!app || !chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME)) return json({ error: "not found" }, 404);
|
|
10334
10828
|
const group = await loadGroup2(db, str2(app?.groupId, ctx.chapter.id));
|
|
10335
|
-
const vars = app ? { firstName: str2(app.firstName), lastName: str2(app.lastName), email: str2(app.email), phone: str2(app.phone), state: str2(app.state), adminUrl:
|
|
10829
|
+
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;
|
|
10336
10830
|
const emails = (data.emailLog ?? []).filter((r) => r.template !== "adminNotification").map((r) => {
|
|
10337
10831
|
let mailBody = typeof r.body === "string" ? r.body : null;
|
|
10338
10832
|
if (!mailBody && group && vars) mailBody = renderTemplateBody(emailGroupFrom(group), String(r.template), vars);
|
|
@@ -10356,6 +10850,28 @@ var handleAdminComms = async (req, url, env, ctx) => {
|
|
|
10356
10850
|
var import_crm13 = require("@odla-ai/crm");
|
|
10357
10851
|
var import_db9 = require("@odla-ai/db");
|
|
10358
10852
|
|
|
10853
|
+
// src/network-edge.ts
|
|
10854
|
+
var EDGE_UNPAIRED = "edge_unpaired";
|
|
10855
|
+
function pairingCommand(leaderId, targetId, env) {
|
|
10856
|
+
const scope2 = env ? ` --env ${env}` : "";
|
|
10857
|
+
return `odla-ai chapter edge pair --leader ${leaderId} --follower ${targetId}${scope2}`;
|
|
10858
|
+
}
|
|
10859
|
+
function edgeUnpaired(leaderId, target, env) {
|
|
10860
|
+
return {
|
|
10861
|
+
code: EDGE_UNPAIRED,
|
|
10862
|
+
target: target.id,
|
|
10863
|
+
secretName: target.secretName,
|
|
10864
|
+
message: `edge to "${target.id}" is unpaired: leader vault slot "${target.secretName}" is empty`,
|
|
10865
|
+
hint: pairingCommand(leaderId, target.id, env)
|
|
10866
|
+
};
|
|
10867
|
+
}
|
|
10868
|
+
function unpairedResponse(unpaired) {
|
|
10869
|
+
return new Response(JSON.stringify({ error: unpaired.message, ...unpaired }), {
|
|
10870
|
+
status: 502,
|
|
10871
|
+
headers: { "content-type": "application/json" }
|
|
10872
|
+
});
|
|
10873
|
+
}
|
|
10874
|
+
|
|
10359
10875
|
// src/worker-network-push.ts
|
|
10360
10876
|
var import_crm12 = require("@odla-ai/crm");
|
|
10361
10877
|
var import_db8 = require("@odla-ai/db");
|
|
@@ -10368,15 +10884,15 @@ async function pushNetworkRecord(db, env, ctx, target, record4) {
|
|
|
10368
10884
|
newId: () => crypto.randomUUID()
|
|
10369
10885
|
};
|
|
10370
10886
|
if (!secret) {
|
|
10371
|
-
const
|
|
10887
|
+
const unpaired = edgeUnpaired(ctx.chapter.id, target, env.ODLA_ENV);
|
|
10372
10888
|
await (0, import_crm12.recordDeliveryAttempt)(crmDeps3, {
|
|
10373
10889
|
recordId: record4.id,
|
|
10374
10890
|
targetId: target.id,
|
|
10375
10891
|
status: "failed",
|
|
10376
10892
|
payloadVersion: 2,
|
|
10377
|
-
error
|
|
10893
|
+
error: unpaired.message
|
|
10378
10894
|
});
|
|
10379
|
-
return { id: target.id, name: target.name, ok: false, error };
|
|
10895
|
+
return { id: target.id, name: target.name, ok: false, error: unpaired.message, code: unpaired.code, hint: unpaired.hint };
|
|
10380
10896
|
}
|
|
10381
10897
|
let payload;
|
|
10382
10898
|
try {
|
|
@@ -10517,17 +11033,20 @@ async function snapshotOne(db, env, ctx, target) {
|
|
|
10517
11033
|
publicSite: { available: null }
|
|
10518
11034
|
};
|
|
10519
11035
|
const secret = await getVaultSecret(db, target.secretName);
|
|
10520
|
-
if (!secret)
|
|
10521
|
-
|
|
10522
|
-
|
|
10523
|
-
|
|
10524
|
-
|
|
10525
|
-
|
|
10526
|
-
|
|
10527
|
-
|
|
10528
|
-
|
|
10529
|
-
|
|
10530
|
-
|
|
11036
|
+
if (!secret) {
|
|
11037
|
+
const unpaired = edgeUnpaired(ctx.chapter.id, target, env.ODLA_ENV);
|
|
11038
|
+
return {
|
|
11039
|
+
...base,
|
|
11040
|
+
available: false,
|
|
11041
|
+
transport: {
|
|
11042
|
+
kind: transportKind,
|
|
11043
|
+
...target.binding ? { binding: target.binding } : {},
|
|
11044
|
+
reachable: null,
|
|
11045
|
+
error: unpaired
|
|
11046
|
+
},
|
|
11047
|
+
error: unpaired.message
|
|
11048
|
+
};
|
|
11049
|
+
}
|
|
10531
11050
|
const destination = new URL("/api/network/snapshot", target.url);
|
|
10532
11051
|
try {
|
|
10533
11052
|
const transport = resolveNetworkTargetTransport(env, target);
|
|
@@ -11002,7 +11521,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
|
11002
11521
|
if (hasFields && Object.keys(fields).some((name) => !capabilities.edit?.fields.some((field) => field.id === name))) {
|
|
11003
11522
|
return json({ error: "use the verified identity or application workflow for these fields", code: "field_forbidden" }, 403);
|
|
11004
11523
|
}
|
|
11005
|
-
const
|
|
11524
|
+
const deps = { crm: ctx.chapter.crm, db };
|
|
11006
11525
|
if (hasFields && ctx.chapter.mode === "chapter" && record4.type === "person") {
|
|
11007
11526
|
const updated2 = await updateChapterPerson({
|
|
11008
11527
|
db,
|
|
@@ -11021,7 +11540,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
|
11021
11540
|
record: await operationalReaderRecord(updated2.record, reader, db, env, ctx)
|
|
11022
11541
|
});
|
|
11023
11542
|
}
|
|
11024
|
-
const updated = hasFields ? await (0, import_crm17.updateRecord)(
|
|
11543
|
+
const updated = hasFields ? await (0, import_crm17.updateRecord)(deps, {
|
|
11025
11544
|
id: record4.id,
|
|
11026
11545
|
input: fields,
|
|
11027
11546
|
mutationId: activityId,
|
|
@@ -11032,7 +11551,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
|
11032
11551
|
authorId: `network:${verified.sender}`,
|
|
11033
11552
|
meta: auditMeta
|
|
11034
11553
|
}
|
|
11035
|
-
}) : await (0, import_crm17.setStage)(
|
|
11554
|
+
}) : await (0, import_crm17.setStage)(deps, {
|
|
11036
11555
|
id: record4.id,
|
|
11037
11556
|
to: input.stage,
|
|
11038
11557
|
authorId: `network:${verified.sender}`,
|
|
@@ -11132,7 +11651,10 @@ function targetFor2(ctx, id6) {
|
|
|
11132
11651
|
}
|
|
11133
11652
|
async function readTargetRecords(db, env, ctx, target, query) {
|
|
11134
11653
|
const secret = await getVaultSecret(db, target.secretName);
|
|
11135
|
-
if (!secret)
|
|
11654
|
+
if (!secret) {
|
|
11655
|
+
const unpaired = edgeUnpaired(ctx.chapter.id, target, env.ODLA_ENV);
|
|
11656
|
+
return { ok: false, error: unpaired.message, unpaired };
|
|
11657
|
+
}
|
|
11136
11658
|
const destination = new URL("/api/network/records", target.url);
|
|
11137
11659
|
destination.search = query.toString();
|
|
11138
11660
|
try {
|
|
@@ -11169,7 +11691,7 @@ async function mutateTargetRecord(db, env, ctx, target, input) {
|
|
|
11169
11691
|
return json({ error: "stage transitions are not enabled for this target", code: "stage_forbidden" }, 403);
|
|
11170
11692
|
}
|
|
11171
11693
|
const secret = await getVaultSecret(db, target.secretName);
|
|
11172
|
-
if (!secret) return
|
|
11694
|
+
if (!secret) return unpairedResponse(edgeUnpaired(ctx.chapter.id, target, env.ODLA_ENV));
|
|
11173
11695
|
const destination = new URL("/api/network/records", target.url);
|
|
11174
11696
|
const body = JSON.stringify(input);
|
|
11175
11697
|
try {
|
|
@@ -11251,7 +11773,9 @@ var handleAdminNetworkRecords = async (req, url, env, ctx) => {
|
|
|
11251
11773
|
if (window.search) query.set("search", window.search);
|
|
11252
11774
|
if (window.stage) query.set("stage", window.stage);
|
|
11253
11775
|
const result = await readTargetRecords(got.db, env, ctx, target, query);
|
|
11254
|
-
if (!result.ok)
|
|
11776
|
+
if (!result.ok) {
|
|
11777
|
+
return result.unpaired ? unpairedResponse(result.unpaired) : json({ error: result.error }, result.status ?? 502);
|
|
11778
|
+
}
|
|
11255
11779
|
if ((window.stage || url.searchParams.has("sort")) && (!result.page.appliedQuery || result.page.appliedQuery.sort !== window.sortId || result.page.appliedQuery.stage !== window.stage)) {
|
|
11256
11780
|
return json({ error: "follower did not apply the requested record query" }, 502);
|
|
11257
11781
|
}
|
|
@@ -11503,7 +12027,7 @@ var handleAdminNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
|
11503
12027
|
body = JSON.stringify({ body: note, mutationId: crypto.randomUUID() });
|
|
11504
12028
|
}
|
|
11505
12029
|
const secret = await getVaultSecret(got.db, target.secretName);
|
|
11506
|
-
if (!secret) return
|
|
12030
|
+
if (!secret) return unpairedResponse(edgeUnpaired(ctx.chapter.id, target, env.ODLA_ENV));
|
|
11507
12031
|
const destination = new URL("/api/network/shared-notes", target.url);
|
|
11508
12032
|
destination.search = new URLSearchParams({ type: recordType, recordId }).toString();
|
|
11509
12033
|
try {
|
|
@@ -11557,11 +12081,11 @@ function validateNetworkPersonImport(input, allowedChapterIds) {
|
|
|
11557
12081
|
if (raw2.approved !== true || raw2.isRealPerson !== true) errors.push("import must be approved and identify a real person");
|
|
11558
12082
|
const sourceSystem = typeof raw2.sourceSystem === "string" ? raw2.sourceSystem.trim().toLowerCase() : "";
|
|
11559
12083
|
const sourceRecordId = typeof raw2.sourceRecordId === "string" ? raw2.sourceRecordId.trim() : "";
|
|
11560
|
-
const
|
|
12084
|
+
const provenance2 = typeof raw2.provenance === "string" ? raw2.provenance.trim() : "";
|
|
11561
12085
|
const displayName = typeof raw2.displayName === "string" ? raw2.displayName.trim() : "";
|
|
11562
12086
|
if (!TOKEN.test(sourceSystem) || /(?:^|[._:-])(fixture|test|development|dev)(?:$|[._:-])/.test(sourceSystem)) errors.push("sourceSystem must name an approved non-development external system");
|
|
11563
12087
|
if (!sourceRecordId || sourceRecordId.length > 200) errors.push("sourceRecordId is required and bounded");
|
|
11564
|
-
if (!
|
|
12088
|
+
if (!provenance2 || provenance2.length > 500) errors.push("provenance is required and bounded");
|
|
11565
12089
|
if (!displayName || displayName.length > 200) errors.push("displayName is required and bounded");
|
|
11566
12090
|
if (FIXTURE_MARKER.test(sourceRecordId) || FIXTURE_MARKER.test(displayName)) {
|
|
11567
12091
|
errors.push("fixture, test, sample, and dummy rows cannot become network Persons");
|
|
@@ -11629,7 +12153,7 @@ function validateNetworkPersonImport(input, allowedChapterIds) {
|
|
|
11629
12153
|
sourceKind: "external_reimport",
|
|
11630
12154
|
sourceSystem,
|
|
11631
12155
|
sourceRecordId,
|
|
11632
|
-
provenance,
|
|
12156
|
+
provenance: provenance2,
|
|
11633
12157
|
approved: true,
|
|
11634
12158
|
isRealPerson: true,
|
|
11635
12159
|
displayName,
|
|
@@ -12334,7 +12858,7 @@ async function revealNetworkPersonRegistryRecord(db, input, mutationId, actor, n
|
|
|
12334
12858
|
}
|
|
12335
12859
|
|
|
12336
12860
|
// src/worker-routes-network-person-registry.ts
|
|
12337
|
-
var
|
|
12861
|
+
var PATH5 = "/api/admin/network-person-registry";
|
|
12338
12862
|
var MAX_BODY2 = 8 * 1024 * 1024;
|
|
12339
12863
|
var MUTATION_ID4 = /^[A-Za-z0-9_-]{16,128}$/;
|
|
12340
12864
|
var positiveRevision = (value) => {
|
|
@@ -12359,7 +12883,7 @@ function registryError(error) {
|
|
|
12359
12883
|
return null;
|
|
12360
12884
|
}
|
|
12361
12885
|
var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
12362
|
-
if (url.pathname !==
|
|
12886
|
+
if (url.pathname !== PATH5) return null;
|
|
12363
12887
|
if (!ctx.chapter.networkPersonRegistry) return json({ error: "not found" }, 404);
|
|
12364
12888
|
if (req.method !== "GET" && req.method !== "POST") return json({ error: "method not allowed" }, 405);
|
|
12365
12889
|
if (req.method === "POST" && Number(req.headers.get("content-length") ?? 0) > MAX_BODY2) {
|
|
@@ -12371,9 +12895,9 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
|
12371
12895
|
if (req.method === "POST") {
|
|
12372
12896
|
let body;
|
|
12373
12897
|
try {
|
|
12374
|
-
const
|
|
12375
|
-
if (new TextEncoder().encode(
|
|
12376
|
-
const parsed = JSON.parse(
|
|
12898
|
+
const text8 = await req.text();
|
|
12899
|
+
if (new TextEncoder().encode(text8).byteLength > MAX_BODY2) return json({ error: "request is too large" }, 413);
|
|
12900
|
+
const parsed = JSON.parse(text8);
|
|
12377
12901
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("invalid");
|
|
12378
12902
|
body = parsed;
|
|
12379
12903
|
} catch {
|
|
@@ -12542,7 +13066,7 @@ var import_db13 = require("@odla-ai/db");
|
|
|
12542
13066
|
// src/network-commercial-parity.ts
|
|
12543
13067
|
var DIGEST = /^[0-9a-f]{64}$/;
|
|
12544
13068
|
var MAX_RECORDS = 500;
|
|
12545
|
-
var
|
|
13069
|
+
var text6 = (value, max = 255) => typeof value === "string" && value.length > 0 && value.length <= max ? value : null;
|
|
12546
13070
|
function parityMessage(partition, kind, value) {
|
|
12547
13071
|
return [
|
|
12548
13072
|
"chapter-commercial-parity",
|
|
@@ -12574,18 +13098,18 @@ async function digestWithKey(key3, partition, kind, value) {
|
|
|
12574
13098
|
async function buildChapterCommercialParitySnapshot(input) {
|
|
12575
13099
|
const requestedMax = input.maxRecords ?? MAX_RECORDS;
|
|
12576
13100
|
const maxRecords = Number.isSafeInteger(requestedMax) ? Math.max(0, Math.min(requestedMax, MAX_RECORDS)) : MAX_RECORDS;
|
|
12577
|
-
const eligible = input.rows.filter((row2) =>
|
|
13101
|
+
const eligible = input.rows.filter((row2) => text6(row2.id) && (text6(row2.stripeCustomerId) || text6(row2.stripeSubscriptionId)));
|
|
12578
13102
|
const selected2 = eligible.slice(0, maxRecords);
|
|
12579
13103
|
const key3 = await parityKey(input.secret);
|
|
12580
13104
|
const records = await Promise.all(selected2.map(async (row2) => {
|
|
12581
|
-
const applicationId =
|
|
12582
|
-
const customerId =
|
|
12583
|
-
const subscriptionId =
|
|
13105
|
+
const applicationId = text6(row2.id);
|
|
13106
|
+
const customerId = text6(row2.stripeCustomerId);
|
|
13107
|
+
const subscriptionId = text6(row2.stripeSubscriptionId);
|
|
12584
13108
|
return {
|
|
12585
13109
|
applicationDigest: await digestWithKey(key3, input.partition, "application", applicationId),
|
|
12586
13110
|
customerDigest: customerId ? await digestWithKey(key3, input.partition, "customer", customerId) : null,
|
|
12587
13111
|
subscriptionDigest: subscriptionId ? await digestWithKey(key3, input.partition, "subscription", subscriptionId) : null,
|
|
12588
|
-
status:
|
|
13112
|
+
status: text6(row2.status, 64),
|
|
12589
13113
|
canceled: row2.canceled === true
|
|
12590
13114
|
};
|
|
12591
13115
|
}));
|
|
@@ -12611,7 +13135,7 @@ function validSnapshot(snapshot, source) {
|
|
|
12611
13135
|
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;
|
|
12612
13136
|
const seen = /* @__PURE__ */ new Set();
|
|
12613
13137
|
for (const record4 of records) {
|
|
12614
|
-
if (!DIGEST.test(record4.applicationDigest) || record4.customerDigest !== null && !DIGEST.test(record4.customerDigest) || record4.subscriptionDigest !== null && !DIGEST.test(record4.subscriptionDigest) || record4.status !== null &&
|
|
13138
|
+
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;
|
|
12615
13139
|
const key3 = recordKey(record4);
|
|
12616
13140
|
if (seen.has(key3)) return false;
|
|
12617
13141
|
seen.add(key3);
|
|
@@ -12622,16 +13146,16 @@ async function reconcileCommercialParitySnapshot(input) {
|
|
|
12622
13146
|
if (!validSnapshot(input.snapshot, input.source)) throw new Error("invalid chapter commercial parity snapshot");
|
|
12623
13147
|
const sourceKey = networkCommercialSourceKey(input.source);
|
|
12624
13148
|
const key3 = await parityKey(input.secret);
|
|
12625
|
-
const shadow = await Promise.all(input.projection.subscriptions.filter((row2) => row2.sourceKey === sourceKey &&
|
|
12626
|
-
const customerId =
|
|
12627
|
-
const subscriptionId =
|
|
13149
|
+
const shadow = await Promise.all(input.projection.subscriptions.filter((row2) => row2.sourceKey === sourceKey && text6(row2.applicationId) && text6(row2.objectId)).map(async (row2) => {
|
|
13150
|
+
const customerId = text6(row2.customerId);
|
|
13151
|
+
const subscriptionId = text6(row2.subscriptionId) ?? text6(row2.objectId);
|
|
12628
13152
|
return {
|
|
12629
13153
|
applicationDigest: await digestWithKey(key3, input.snapshot.partition, "application", row2.applicationId),
|
|
12630
13154
|
customerDigest: customerId ? await digestWithKey(key3, input.snapshot.partition, "customer", customerId) : null,
|
|
12631
13155
|
subscriptionDigest: subscriptionId ? await digestWithKey(key3, input.snapshot.partition, "subscription", subscriptionId) : null,
|
|
12632
13156
|
status: null,
|
|
12633
13157
|
canceled: false,
|
|
12634
|
-
stripeStatus:
|
|
13158
|
+
stripeStatus: text6(row2.status, 64),
|
|
12635
13159
|
stripeCanceled: row2.status === "canceled"
|
|
12636
13160
|
};
|
|
12637
13161
|
}));
|
|
@@ -12757,7 +13281,7 @@ var handleNetworkCommercialParity = async (req, url, env, ctx) => {
|
|
|
12757
13281
|
async function reconcileTarget(db, env, ctx, target, projection) {
|
|
12758
13282
|
const base = { targetId: target.id, name: target.name, url: target.url };
|
|
12759
13283
|
const secret = await getVaultSecret(db, target.secretName);
|
|
12760
|
-
if (!secret) return { ...base, available: false, ok: false, error:
|
|
13284
|
+
if (!secret) return { ...base, available: false, ok: false, error: edgeUnpaired(ctx.chapter.id, target, env.ODLA_ENV) };
|
|
12761
13285
|
const destination = new URL(PARITY_PATH, target.url);
|
|
12762
13286
|
try {
|
|
12763
13287
|
const transport = resolveNetworkTargetTransport(env, target);
|
|
@@ -12913,11 +13437,23 @@ var handleNetworkCommercialStripeWebhook = async (req, url, env, ctx) => {
|
|
|
12913
13437
|
const event = await (0, import_stripe11.constructStripeEvent)(body, signature, secret);
|
|
12914
13438
|
const fact = normalizeNetworkCommercialStripeEvent(event, await stripeEventDigest(body));
|
|
12915
13439
|
if (!fact.eventId || !fact.eventType || !fact.objectId) return json({ error: "Stripe event identity is invalid" }, 400);
|
|
12916
|
-
const result = await ingestNetworkCommercialFact(
|
|
13440
|
+
const result = await ingestNetworkCommercialFact(
|
|
13441
|
+
db,
|
|
13442
|
+
ctx.chapter.networkCommercialReadModel.sources,
|
|
13443
|
+
fact,
|
|
13444
|
+
Date.now(),
|
|
13445
|
+
await stripePayloadDigest(body),
|
|
13446
|
+
networkCommercialAlerts(ctx, WEBHOOK_PATH)
|
|
13447
|
+
);
|
|
12917
13448
|
if (result.status === "collision") {
|
|
12918
13449
|
console.error("network commercial event collision", JSON.stringify({ eventId: fact.eventId, eventType: fact.eventType }));
|
|
12919
13450
|
}
|
|
12920
|
-
return json({
|
|
13451
|
+
return json({
|
|
13452
|
+
ok: true,
|
|
13453
|
+
status: result.status,
|
|
13454
|
+
eventId: fact.eventId,
|
|
13455
|
+
...result.status === "replayed" && result.resolvedCollision ? { resolvedCollision: true } : {}
|
|
13456
|
+
});
|
|
12921
13457
|
} catch (error) {
|
|
12922
13458
|
if (error instanceof import_stripe11.StripeError) return json({ error: error.message, code: error.code }, 400);
|
|
12923
13459
|
throw error;
|
|
@@ -12941,18 +13477,39 @@ var handleAdminNetworkCommercialReadModel = async (req, url, env, ctx) => {
|
|
|
12941
13477
|
} catch {
|
|
12942
13478
|
return json({ error: "invalid JSON body" }, 400);
|
|
12943
13479
|
}
|
|
13480
|
+
const alerts = networkCommercialAlerts(ctx, ADMIN_PATH2);
|
|
12944
13481
|
if (parsed.command === "recover-event") {
|
|
12945
13482
|
try {
|
|
12946
13483
|
return json(await recoverNetworkCommercialEvent(
|
|
12947
13484
|
db,
|
|
12948
13485
|
ctx.chapter.networkCommercialReadModel.sources,
|
|
12949
13486
|
String(parsed.eventId ?? ""),
|
|
12950
|
-
got.user.userId
|
|
13487
|
+
got.user.userId,
|
|
13488
|
+
Date.now(),
|
|
13489
|
+
alerts
|
|
12951
13490
|
));
|
|
12952
13491
|
} catch (error) {
|
|
12953
13492
|
return json({ error: error instanceof Error ? error.message : "event recovery failed" }, 409);
|
|
12954
13493
|
}
|
|
12955
13494
|
}
|
|
13495
|
+
if (parsed.command === "resolve") {
|
|
13496
|
+
if (typeof parsed.mutationId !== "string" || !MUTATION_ID5.test(parsed.mutationId)) return json({ error: "resolve requires a valid mutationId" }, 400);
|
|
13497
|
+
const reason = typeof parsed.reason === "string" ? parsed.reason.trim() : "";
|
|
13498
|
+
if (!reason || reason.length > 500) return json({ error: "resolve requires a reason of at most 500 characters" }, 400);
|
|
13499
|
+
try {
|
|
13500
|
+
const result = await resolveNetworkCommercialCollision(
|
|
13501
|
+
db,
|
|
13502
|
+
ctx.chapter.networkCommercialReadModel.sources,
|
|
13503
|
+
{ eventId: String(parsed.eventId ?? ""), mutationId: parsed.mutationId, actorUserId: got.user.userId, reason },
|
|
13504
|
+
Date.now(),
|
|
13505
|
+
alerts
|
|
13506
|
+
);
|
|
13507
|
+
return json({ resolved: true, eventId: result.eventId, reviewId: result.reviewId, ...safeProjection(result.projection) });
|
|
13508
|
+
} catch (error) {
|
|
13509
|
+
const code = error.code;
|
|
13510
|
+
return json({ error: error instanceof Error ? error.message : "resolve failed", code }, code === "invalid_event" ? 400 : 409);
|
|
13511
|
+
}
|
|
13512
|
+
}
|
|
12956
13513
|
if (parsed.command === "reconcile-chapters") {
|
|
12957
13514
|
const projection2 = await loadNetworkCommercialProjection(db, ctx.chapter.networkCommercialReadModel.sources);
|
|
12958
13515
|
return json(await reconcileChapterCommercialTargets(db, env, ctx, projection2));
|
|
@@ -12980,9 +13537,9 @@ var handleAdminNetworkCommercialReadModel = async (req, url, env, ctx) => {
|
|
|
12980
13537
|
}
|
|
12981
13538
|
}
|
|
12982
13539
|
if (parsed.command !== "rebuild" || typeof parsed.mutationId !== "string" || !MUTATION_ID5.test(parsed.mutationId)) {
|
|
12983
|
-
return json({ error: "rebuild, reconcile-stripe, or reconcile-chapters command is required; rebuild also
|
|
13540
|
+
return json({ error: "rebuild, resolve, recover-event, reconcile-stripe, or reconcile-chapters command is required; rebuild and resolve also require a valid mutationId" }, 400);
|
|
12984
13541
|
}
|
|
12985
|
-
const projection = await rebuildNetworkCommercialProjection(db, ctx.chapter.networkCommercialReadModel.sources, parsed.mutationId);
|
|
13542
|
+
const projection = await rebuildNetworkCommercialProjection(db, ctx.chapter.networkCommercialReadModel.sources, parsed.mutationId, Date.now(), alerts);
|
|
12986
13543
|
return json({ rebuilt: true, ...safeProjection(projection) });
|
|
12987
13544
|
}
|
|
12988
13545
|
return json(safeProjection(await loadNetworkCommercialProjection(db, ctx.chapter.networkCommercialReadModel.sources)));
|
|
@@ -13092,7 +13649,7 @@ var record3 = (value) => value !== null && typeof value === "object" && !Array.i
|
|
|
13092
13649
|
function unknownKeys3(value, allowed2, path, errors) {
|
|
13093
13650
|
for (const key3 of Object.keys(value)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
|
|
13094
13651
|
}
|
|
13095
|
-
function
|
|
13652
|
+
function text7(value, path, errors, max = MAX_TEXT2, required = true) {
|
|
13096
13653
|
if (value === void 0 && !required) return void 0;
|
|
13097
13654
|
if (typeof value !== "string" || required && !value.trim()) {
|
|
13098
13655
|
errors.push(`${path} must be a non-empty string`);
|
|
@@ -13118,7 +13675,7 @@ function scopeValue(value, path, errors) {
|
|
|
13118
13675
|
return { kind: raw2.kind };
|
|
13119
13676
|
}
|
|
13120
13677
|
if (raw2.kind === "chapter") {
|
|
13121
|
-
const chapterId =
|
|
13678
|
+
const chapterId = text7(raw2.chapterId, `${path}.chapterId`, errors, 63) ?? "";
|
|
13122
13679
|
if (chapterId && !APP_ID.test(chapterId)) errors.push(`${path}.chapterId must be a lowercase app slug`);
|
|
13123
13680
|
return { kind: "chapter", chapterId };
|
|
13124
13681
|
}
|
|
@@ -13132,22 +13689,22 @@ function resourceValue(value, path, errors) {
|
|
|
13132
13689
|
return null;
|
|
13133
13690
|
}
|
|
13134
13691
|
unknownKeys3(raw2, RESOURCE_KEYS, path, errors);
|
|
13135
|
-
const id6 =
|
|
13692
|
+
const id6 = text7(raw2.id, `${path}.id`, errors, 80) ?? "";
|
|
13136
13693
|
if (id6 && !ID4.test(id6)) errors.push(`${path}.id must be lowercase kebab-case`);
|
|
13137
|
-
const kind =
|
|
13694
|
+
const kind = text7(raw2.kind, `${path}.kind`, errors) ?? "";
|
|
13138
13695
|
if (!KINDS.has(kind)) errors.push(`${path}.kind is unsupported`);
|
|
13139
|
-
const title =
|
|
13140
|
-
const summary =
|
|
13141
|
-
const status2 =
|
|
13696
|
+
const title = text7(raw2.title, `${path}.title`, errors, 240) ?? "";
|
|
13697
|
+
const summary = text7(raw2.summary, `${path}.summary`, errors, 1e3, false);
|
|
13698
|
+
const status2 = text7(raw2.status, `${path}.status`, errors) ?? "";
|
|
13142
13699
|
if (!STATUSES.has(status2)) errors.push(`${path}.status must be draft, published, or archived`);
|
|
13143
13700
|
const revision = integer4(raw2.revision, `${path}.revision`, errors);
|
|
13144
13701
|
const scope2 = scopeValue(raw2.scope, `${path}.scope`, errors) ?? { kind: "global" };
|
|
13145
|
-
const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) =>
|
|
13702
|
+
const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) => text7(tag, `${path}.tags[${index2}]`, errors, 80) ?? "") : [];
|
|
13146
13703
|
if (!Array.isArray(raw2.tags)) errors.push(`${path}.tags must be an array`);
|
|
13147
13704
|
if (tags.length > MAX_TAGS) errors.push(`${path}.tags may contain at most ${MAX_TAGS} items`);
|
|
13148
13705
|
if (new Set(tags).size !== tags.length) errors.push(`${path}.tags must be unique`);
|
|
13149
|
-
const bodyMarkdown =
|
|
13150
|
-
const url =
|
|
13706
|
+
const bodyMarkdown = text7(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN3, false);
|
|
13707
|
+
const url = text7(raw2.url, `${path}.url`, errors, 2048, false);
|
|
13151
13708
|
if (url) {
|
|
13152
13709
|
try {
|
|
13153
13710
|
if (new URL(url).protocol !== "https:") errors.push(`${path}.url must use https`);
|
|
@@ -13159,9 +13716,9 @@ function resourceValue(value, path, errors) {
|
|
|
13159
13716
|
if (raw2.brandAsset !== void 0 && !asset) errors.push(`${path}.brandAsset must be an object`);
|
|
13160
13717
|
if (asset) unknownKeys3(asset, ASSET_KEYS, `${path}.brandAsset`, errors);
|
|
13161
13718
|
const brandAsset = asset ? {
|
|
13162
|
-
bookId:
|
|
13163
|
-
assetId:
|
|
13164
|
-
...asset.filename === void 0 ? {} : { filename:
|
|
13719
|
+
bookId: text7(asset.bookId, `${path}.brandAsset.bookId`, errors, 160) ?? "",
|
|
13720
|
+
assetId: text7(asset.assetId, `${path}.brandAsset.assetId`, errors, 160) ?? "",
|
|
13721
|
+
...asset.filename === void 0 ? {} : { filename: text7(asset.filename, `${path}.brandAsset.filename`, errors, 240) ?? "" }
|
|
13165
13722
|
} : void 0;
|
|
13166
13723
|
if ((kind === "document" || kind === "note") && !bodyMarkdown) errors.push(`${path}.bodyMarkdown is required for ${kind}`);
|
|
13167
13724
|
if ((kind === "link" || kind === "inspiration") && !url) errors.push(`${path}.url is required for ${kind}`);
|
|
@@ -13176,21 +13733,21 @@ function resourceValue(value, path, errors) {
|
|
|
13176
13733
|
if (provenanceRaw?.promotedFrom !== void 0 && !promotedRaw) errors.push(`${path}.provenance.promotedFrom must be an object`);
|
|
13177
13734
|
if (promotedRaw) unknownKeys3(promotedRaw, PROMOTION_KEYS, `${path}.provenance.promotedFrom`, errors);
|
|
13178
13735
|
const promotedFrom = promotedRaw ? {
|
|
13179
|
-
chapterId:
|
|
13180
|
-
resourceId:
|
|
13736
|
+
chapterId: text7(promotedRaw.chapterId, `${path}.provenance.promotedFrom.chapterId`, errors, 63) ?? "",
|
|
13737
|
+
resourceId: text7(promotedRaw.resourceId, `${path}.provenance.promotedFrom.resourceId`, errors, 80) ?? "",
|
|
13181
13738
|
resourceRevision: integer4(promotedRaw.resourceRevision, `${path}.provenance.promotedFrom.resourceRevision`, errors),
|
|
13182
13739
|
documentRevision: integer4(promotedRaw.documentRevision, `${path}.provenance.promotedFrom.documentRevision`, errors),
|
|
13183
|
-
promotedBy:
|
|
13740
|
+
promotedBy: text7(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
|
|
13184
13741
|
promotedAt: integer4(promotedRaw.promotedAt, `${path}.provenance.promotedFrom.promotedAt`, errors, 0)
|
|
13185
13742
|
} : void 0;
|
|
13186
|
-
const
|
|
13187
|
-
createdBy:
|
|
13743
|
+
const provenance2 = {
|
|
13744
|
+
createdBy: text7(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
|
|
13188
13745
|
createdAt: integer4(provenanceRaw?.createdAt, `${path}.provenance.createdAt`, errors, 0),
|
|
13189
|
-
updatedBy:
|
|
13746
|
+
updatedBy: text7(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
|
|
13190
13747
|
updatedAt: integer4(provenanceRaw?.updatedAt, `${path}.provenance.updatedAt`, errors, 0),
|
|
13191
13748
|
...promotedFrom ? { promotedFrom } : {}
|
|
13192
13749
|
};
|
|
13193
|
-
return { id: id6, kind, title, ...summary ? { summary } : {}, tags, scope: scope2, status: status2, revision, ...bodyMarkdown ? { bodyMarkdown } : {}, ...url ? { url } : {}, ...brandAsset ? { brandAsset } : {}, provenance };
|
|
13750
|
+
return { id: id6, kind, title, ...summary ? { summary } : {}, tags, scope: scope2, status: status2, revision, ...bodyMarkdown ? { bodyMarkdown } : {}, ...url ? { url } : {}, ...brandAsset ? { brandAsset } : {}, provenance: provenance2 };
|
|
13194
13751
|
}
|
|
13195
13752
|
function validateBuilderLibraryDocument(value, options = {}) {
|
|
13196
13753
|
const errors = [];
|
|
@@ -13205,7 +13762,7 @@ function validateBuilderLibraryDocument(value, options = {}) {
|
|
|
13205
13762
|
if (sourceRaw) unknownKeys3(sourceRaw, SOURCE_KEYS2, "document.source", errors);
|
|
13206
13763
|
const sourceType = sourceRaw?.type;
|
|
13207
13764
|
if (sourceType !== "chapter" && sourceType !== "built_not_found") errors.push("document.source.type must be chapter or built_not_found");
|
|
13208
|
-
const sourceAppId =
|
|
13765
|
+
const sourceAppId = text7(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
|
|
13209
13766
|
if (sourceAppId && !APP_ID.test(sourceAppId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
13210
13767
|
if (options.sourceType && sourceType !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
|
|
13211
13768
|
if (options.sourceAppId && sourceAppId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
|
|
@@ -13213,8 +13770,8 @@ function validateBuilderLibraryDocument(value, options = {}) {
|
|
|
13213
13770
|
if (raw2.target !== void 0 && !targetRaw) errors.push("document.target must be an object");
|
|
13214
13771
|
if (targetRaw) unknownKeys3(targetRaw, TARGET_KEYS2, "document.target", errors);
|
|
13215
13772
|
const target = targetRaw ? {
|
|
13216
|
-
appId:
|
|
13217
|
-
environment:
|
|
13773
|
+
appId: text7(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
|
|
13774
|
+
environment: text7(targetRaw.environment, "document.target.environment", errors, 80) ?? ""
|
|
13218
13775
|
} : void 0;
|
|
13219
13776
|
if (options.targetAppId && target?.appId !== options.targetAppId) errors.push(`document.target.appId must be ${options.targetAppId}`);
|
|
13220
13777
|
if (!Array.isArray(raw2.resources)) errors.push("document.resources must be an array");
|
|
@@ -15226,7 +15783,7 @@ async function readBoundedJson(response, signal, maxBytes, label) {
|
|
|
15226
15783
|
};
|
|
15227
15784
|
signal.addEventListener("abort", cancel, { once: true });
|
|
15228
15785
|
let size = 0;
|
|
15229
|
-
let
|
|
15786
|
+
let text8 = "";
|
|
15230
15787
|
const decoder = new TextDecoder();
|
|
15231
15788
|
try {
|
|
15232
15789
|
while (true) {
|
|
@@ -15236,10 +15793,10 @@ async function readBoundedJson(response, signal, maxBytes, label) {
|
|
|
15236
15793
|
if (done) break;
|
|
15237
15794
|
size += value.byteLength;
|
|
15238
15795
|
if (size > maxBytes) throw new TypeError(`${label} exceeds limit`);
|
|
15239
|
-
|
|
15796
|
+
text8 += decoder.decode(value, { stream: true });
|
|
15240
15797
|
}
|
|
15241
15798
|
try {
|
|
15242
|
-
return JSON.parse(
|
|
15799
|
+
return JSON.parse(text8 + decoder.decode());
|
|
15243
15800
|
} catch {
|
|
15244
15801
|
throw new TypeError(`invalid ${label}`);
|
|
15245
15802
|
}
|
|
@@ -15901,6 +16458,7 @@ var BUILTIN_ROUTES = [
|
|
|
15901
16458
|
handlePaymentRecovery,
|
|
15902
16459
|
handleAdminNamedSeat,
|
|
15903
16460
|
handleAdminMembershipSeats,
|
|
16461
|
+
handleAdminMembershipPolicyVersions,
|
|
15904
16462
|
handleAdminMeetings,
|
|
15905
16463
|
handleAdminScheduling,
|
|
15906
16464
|
// Roster + identity
|