@odla-ai/chapter 0.50.0 → 0.52.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +109 -8
- package/dist/{chunk-4EA5V4JE.js → chunk-7N4VDWM5.js} +239 -136
- package/dist/chunk-7N4VDWM5.js.map +1 -0
- package/dist/{chunk-OS46ZE46.js → chunk-7RIT2UN5.js} +3 -7
- package/dist/chunk-7RIT2UN5.js.map +1 -0
- package/dist/{chunk-JXX4BFDP.js → chunk-A6W53GVM.js} +5 -1
- package/dist/chunk-A6W53GVM.js.map +1 -0
- package/dist/{copy-context-Ddat__H5.d.ts → copy-context-CYLlPzAc.d.ts} +11 -5
- package/dist/index.cjs +111 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +77 -13
- package/dist/index.d.ts +77 -13
- package/dist/index.js +111 -18
- package/dist/index.js.map +1 -1
- package/dist/ui/admin/index.d.ts +14 -3
- package/dist/ui/admin/index.js +4 -2
- package/dist/ui/index.d.ts +2 -2
- package/dist/ui/index.js +5 -3
- package/dist/ui/member/index.d.ts +2 -2
- package/dist/ui/member/index.js +2 -2
- package/dist/worker/index.cjs +983 -368
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +52 -10
- package/dist/worker/index.d.ts +52 -10
- package/dist/worker/index.js +983 -368
- package/dist/worker/index.js.map +1 -1
- package/package.json +2 -1
- package/dist/chunk-4EA5V4JE.js.map +0 -1
- package/dist/chunk-JXX4BFDP.js.map +0 -1
- package/dist/chunk-OS46ZE46.js.map +0 -1
package/dist/worker/index.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),
|
|
@@ -1702,6 +1787,11 @@ function canApprove(status2, p) {
|
|
|
1702
1787
|
}
|
|
1703
1788
|
|
|
1704
1789
|
// src/membership-policy.ts
|
|
1790
|
+
function seatPurchaseDeadline(offering, member) {
|
|
1791
|
+
if (offering.seatPurchaseWindowCalendarMonths === void 0) return Number(member.termEndsAt);
|
|
1792
|
+
if (Number.isSafeInteger(member.seatPurchaseEndsAt)) return Number(member.seatPurchaseEndsAt);
|
|
1793
|
+
return addMembershipCalendarMonths(Number(member.initialTermStartedAt ?? member.termStartedAt), offering.seatPurchaseWindowCalendarMonths);
|
|
1794
|
+
}
|
|
1705
1795
|
var fail2 = (field) => {
|
|
1706
1796
|
throw new TypeError(`invalid membership policy: ${field}`);
|
|
1707
1797
|
};
|
|
@@ -1758,7 +1848,7 @@ function membershipOfferingVersion(value) {
|
|
|
1758
1848
|
autoRenew: true,
|
|
1759
1849
|
paymentGraceDays: integer2(input.paymentGraceDays, 0, 60),
|
|
1760
1850
|
seatPriceCents: integer2(input.seatPriceCents, 1, Number.MAX_SAFE_INTEGER),
|
|
1761
|
-
seatPurchaseWindowCalendarMonths: integer2(input.seatPurchaseWindowCalendarMonths, 1, 120),
|
|
1851
|
+
...input.seatPurchaseWindowCalendarMonths === void 0 || input.seatPurchaseWindowCalendarMonths === null ? {} : { seatPurchaseWindowCalendarMonths: integer2(input.seatPurchaseWindowCalendarMonths, 1, 120) },
|
|
1762
1852
|
requiredDisclosures: ids(input.requiredDisclosures)
|
|
1763
1853
|
};
|
|
1764
1854
|
}
|
|
@@ -2149,7 +2239,7 @@ async function confirmMembershipPayment(db, input, now) {
|
|
|
2149
2239
|
initialTermEndsAt: endsAt,
|
|
2150
2240
|
termStartedAt: paidAt,
|
|
2151
2241
|
termEndsAt: endsAt,
|
|
2152
|
-
seatPurchaseEndsAt:
|
|
2242
|
+
seatPurchaseEndsAt: seatPurchaseDeadline(offering, { initialTermStartedAt: paidAt, termEndsAt: endsAt }),
|
|
2153
2243
|
autoRenew: true,
|
|
2154
2244
|
refundedCents: 0,
|
|
2155
2245
|
revision: 1,
|
|
@@ -2806,24 +2896,22 @@ async function revokeRefundedApplicationRole(db, env, ctx, applicationId) {
|
|
|
2806
2896
|
}
|
|
2807
2897
|
}
|
|
2808
2898
|
|
|
2809
|
-
// src/
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
});
|
|
2823
|
-
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) {
|
|
2824
2912
|
const intent = (await db.query({ subscriptionCheckoutIntents: { $: { where: { id: String(app.id) }, limit: 1 } } })).subscriptionCheckoutIntents?.[0];
|
|
2825
2913
|
const offer = intent?.membershipOffer, contract = offer?.contract;
|
|
2826
|
-
const offering = contract?.offering;
|
|
2914
|
+
const offering = contract?.offering, quote = intent?.paymentQuote;
|
|
2827
2915
|
const amount = contract?.dueTodayCents ?? intent?.priceCents;
|
|
2828
2916
|
const currency = offering?.currency ?? intent?.currency;
|
|
2829
2917
|
let formatted = "";
|
|
@@ -2831,9 +2919,75 @@ async function paymentConfirmationVariables(db, app) {
|
|
|
2831
2919
|
formatted = new Intl.NumberFormat("en-US", { style: "currency", currency: currency.toUpperCase() }).format(Number(amount) / 100);
|
|
2832
2920
|
}
|
|
2833
2921
|
return {
|
|
2834
|
-
firstName: typeof app.firstName === "string" ? app.firstName : "",
|
|
2835
2922
|
paymentAmount: formatted,
|
|
2836
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),
|
|
2837
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."
|
|
2838
2992
|
};
|
|
2839
2993
|
}
|
|
@@ -2841,7 +2995,7 @@ async function notifyPaymentConfirmed(db, env, eventId, app) {
|
|
|
2841
2995
|
if (typeof app.email !== "string" || !app.email) throw new Error("payment notice recipient missing");
|
|
2842
2996
|
const group = await groupFor(db, app);
|
|
2843
2997
|
if (!group) throw new Error("payment notice configuration missing");
|
|
2844
|
-
const result = await sendTemplated(
|
|
2998
|
+
const result = await sendTemplated(lifecycleNotifyDeps(db, env), {
|
|
2845
2999
|
group: emailGroupFrom(group),
|
|
2846
3000
|
template: "paymentConfirmation",
|
|
2847
3001
|
to: app.email,
|
|
@@ -2851,21 +3005,20 @@ async function notifyPaymentConfirmed(db, env, eventId, app) {
|
|
|
2851
3005
|
});
|
|
2852
3006
|
if (!result.sent && !["disabled", "template-missing"].includes(result.reason ?? "")) throw new Error("payment notice reconciliation pending");
|
|
2853
3007
|
}
|
|
2854
|
-
async function notifyAdminOfPayment(db, env, eventId, app) {
|
|
3008
|
+
async function notifyAdminOfPayment(db, env, eventId, app, origin = "") {
|
|
2855
3009
|
try {
|
|
2856
3010
|
const group = await groupFor(db, app);
|
|
2857
3011
|
if (!group || typeof group.notificationEmail !== "string" || !group.notificationEmail) return;
|
|
2858
|
-
const
|
|
2859
|
-
await sendTemplated(
|
|
3012
|
+
const { tierName: tierName2 } = await frozenCheckoutVariables(db, app);
|
|
3013
|
+
await sendTemplated(lifecycleNotifyDeps(db, env), {
|
|
2860
3014
|
group: emailGroupFrom(group),
|
|
2861
3015
|
template: "adminNotification",
|
|
2862
3016
|
to: group.notificationEmail,
|
|
2863
3017
|
vars: {
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
state: text6(app.state)
|
|
3018
|
+
...applicantVariables(app),
|
|
3019
|
+
tier: tierName2,
|
|
3020
|
+
tierName: tierName2,
|
|
3021
|
+
...origin ? { adminUrl: adminUrlFor(origin), membersUrl: membersUrlFor(origin) } : {}
|
|
2869
3022
|
},
|
|
2870
3023
|
dedupeKey: `${eventId}:admin`,
|
|
2871
3024
|
applicationId: String(app.id)
|
|
@@ -3185,6 +3338,26 @@ function reportStripePartition(level, eventId, event, disposition) {
|
|
|
3185
3338
|
...stripeEventIdentity(event)
|
|
3186
3339
|
}));
|
|
3187
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
|
+
}
|
|
3188
3361
|
async function storeStripeReceipt(db, input) {
|
|
3189
3362
|
const existing = await receiptFor(db, input.eventId);
|
|
3190
3363
|
if (existing?.retryReady === true && existing.reviewedDigest === input.digest) {
|
|
@@ -3217,17 +3390,18 @@ async function storeStripeReceipt(db, input) {
|
|
|
3217
3390
|
const raced = await receiptFor(db, input.eventId);
|
|
3218
3391
|
return raced?.digest === input.digest || raced?.digest === input.legacyDigest ? "replayed" : "collision";
|
|
3219
3392
|
}
|
|
3220
|
-
async function quarantineStripeEvent(db, eventId, digest, event) {
|
|
3393
|
+
async function quarantineStripeEvent(db, eventId, digest, event, ctx) {
|
|
3221
3394
|
const disposition = "quarantine_unmatched";
|
|
3222
3395
|
const stored = await storeStripeReceipt(db, { eventId, digest, event, disposition, matched: false });
|
|
3223
3396
|
if (stored === "collision") {
|
|
3224
|
-
|
|
3397
|
+
await reportStripeCollision(db, ctx, eventId, event);
|
|
3225
3398
|
return json({ ok: true, matched: false, collision: true });
|
|
3226
3399
|
}
|
|
3227
3400
|
reportStripePartition("warn", eventId, event, disposition);
|
|
3401
|
+
if (stored === "stored") await alertStripeQuarantine(db, ctx, eventId, disposition);
|
|
3228
3402
|
return json({ ok: true, matched: false, disposition, ...stored === "replayed" ? { replayed: true } : {} });
|
|
3229
3403
|
}
|
|
3230
|
-
async function replayedStripeReceipt(db, eventId, digest, event, legacyDigest) {
|
|
3404
|
+
async function replayedStripeReceipt(db, eventId, digest, event, legacyDigest, ctx) {
|
|
3231
3405
|
const existing = await receiptFor(db, eventId);
|
|
3232
3406
|
if (!existing) return null;
|
|
3233
3407
|
if (existing.retryReady === true && existing.reviewedDigest === digest) return null;
|
|
@@ -3237,7 +3411,7 @@ async function replayedStripeReceipt(db, eventId, digest, event, legacyDigest) {
|
|
|
3237
3411
|
collisionDigest: digest,
|
|
3238
3412
|
retryReady: false
|
|
3239
3413
|
} }], { guards: [{ ns: "stripeEventReceipts", id: String(existing.id), exists: true, equals: { digest: existing.digest } }] });
|
|
3240
|
-
|
|
3414
|
+
await reportStripeCollision(db, ctx, eventId, event);
|
|
3241
3415
|
return json({ ok: true, matched: false, collision: true });
|
|
3242
3416
|
}
|
|
3243
3417
|
return json({ ok: true, matched: existing.matched === true, replayed: true });
|
|
@@ -3268,16 +3442,16 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
|
|
|
3268
3442
|
reportStripePartition("warn", eventId, event, "ignore_sibling");
|
|
3269
3443
|
return json({ ok: true, matched: false, disposition: "ignore_sibling" });
|
|
3270
3444
|
}
|
|
3271
|
-
const replay2 = await replayedStripeReceipt(db, eventId, digest, event, legacyDigest);
|
|
3445
|
+
const replay2 = await replayedStripeReceipt(db, eventId, digest, event, legacyDigest, ctx);
|
|
3272
3446
|
if (replay2) return replay2;
|
|
3273
3447
|
const localIntent = Boolean(intent && intent.chapterId === ctx.chapter.id && intent.environment === env.ODLA_ENV && (typeof intent.runtime !== "string" || intent.runtime === target.runtime));
|
|
3274
3448
|
const disposition2 = classifyMemberStripeEvent(identity, target, localIntent);
|
|
3275
3449
|
if (!localIntent || disposition2 === "quarantine_unmatched") {
|
|
3276
|
-
return quarantineStripeEvent(db, eventId, digest, event);
|
|
3450
|
+
return quarantineStripeEvent(db, eventId, digest, event, ctx);
|
|
3277
3451
|
}
|
|
3278
3452
|
if (event.kind === "gift_paid") {
|
|
3279
3453
|
if (intent.providerObjectId !== event.providerObjectId || intent.status === "refunded" || intent.status === "canceled") {
|
|
3280
|
-
return quarantineStripeEvent(db, eventId, digest, event);
|
|
3454
|
+
return quarantineStripeEvent(db, eventId, digest, event, ctx);
|
|
3281
3455
|
}
|
|
3282
3456
|
try {
|
|
3283
3457
|
const issued = await issueGiftClaim(db, intent, {
|
|
@@ -3286,7 +3460,7 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
|
|
|
3286
3460
|
}, Date.now());
|
|
3287
3461
|
const stored3 = await storeStripeReceipt(db, { eventId, digest, legacyDigest, event, disposition: disposition2, matched: true });
|
|
3288
3462
|
if (stored3 === "collision") {
|
|
3289
|
-
|
|
3463
|
+
await reportStripeCollision(db, ctx, eventId, event);
|
|
3290
3464
|
return json({ ok: true, matched: false, collision: true });
|
|
3291
3465
|
}
|
|
3292
3466
|
return json({ ok: true, matched: true, claimId: issued.claimId });
|
|
@@ -3302,7 +3476,7 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
|
|
|
3302
3476
|
if (claim) await revokeGiftClaim(db, claim, event.reason, Date.now());
|
|
3303
3477
|
const stored2 = await storeStripeReceipt(db, { eventId, digest, legacyDigest, event, disposition: disposition2, matched: Boolean(claim) });
|
|
3304
3478
|
if (stored2 === "collision") {
|
|
3305
|
-
|
|
3479
|
+
await reportStripeCollision(db, ctx, eventId, event);
|
|
3306
3480
|
return json({ ok: true, matched: false, collision: true });
|
|
3307
3481
|
}
|
|
3308
3482
|
return json({ ok: true, matched: Boolean(claim) });
|
|
@@ -3312,12 +3486,12 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
|
|
|
3312
3486
|
reportStripePartition("warn", eventId, event, "ignore_sibling");
|
|
3313
3487
|
return json({ ok: true, matched: false, disposition: "ignore_sibling" });
|
|
3314
3488
|
}
|
|
3315
|
-
const replay = await replayedStripeReceipt(db, eventId, digest, event, legacyDigest);
|
|
3489
|
+
const replay = await replayedStripeReceipt(db, eventId, digest, event, legacyDigest, ctx);
|
|
3316
3490
|
if (replay) return replay;
|
|
3317
3491
|
const localApplication = Boolean(app && String(app.groupId ?? ctx.chapter.id) === ctx.chapter.id && (typeof app.stripeRuntime !== "string" || app.stripeRuntime === target.runtime));
|
|
3318
3492
|
const disposition = classifyMemberStripeEvent(identity, target, localApplication);
|
|
3319
3493
|
if (!localApplication || disposition === "quarantine_unmatched") {
|
|
3320
|
-
return quarantineStripeEvent(db, eventId, digest, event);
|
|
3494
|
+
return quarantineStripeEvent(db, eventId, digest, event, ctx);
|
|
3321
3495
|
}
|
|
3322
3496
|
if (await archivedChapterMembershipEvent(db, rawBody, app, env, ctx)) {
|
|
3323
3497
|
await storeStripeReceipt(db, { eventId, digest, legacyDigest, event, disposition, matched: true });
|
|
@@ -3334,7 +3508,7 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
|
|
|
3334
3508
|
if (receipt.seatId !== event.seatId || receipt.reconciled !== true) throw new Error("invalid seat receipt");
|
|
3335
3509
|
const stored2 = await storeStripeReceipt(db, { eventId, digest, legacyDigest, event, disposition, matched: true });
|
|
3336
3510
|
if (stored2 === "collision") {
|
|
3337
|
-
|
|
3511
|
+
await reportStripeCollision(db, ctx, eventId, event);
|
|
3338
3512
|
return json({ ok: true, matched: false, collision: true });
|
|
3339
3513
|
}
|
|
3340
3514
|
return json({ ok: true, seatId: event.seatId });
|
|
@@ -3358,7 +3532,7 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
|
|
|
3358
3532
|
if (intent) {
|
|
3359
3533
|
const evidence = verifyInitialMembershipInvoice(intent, event.invoice);
|
|
3360
3534
|
if (evidence.status === "retry") return json({ error: "checkout reconciliation pending" }, 503);
|
|
3361
|
-
if (evidence.status === "mismatch") return quarantineStripeEvent(db, eventId, digest, event);
|
|
3535
|
+
if (evidence.status === "mismatch") return quarantineStripeEvent(db, eventId, digest, event, ctx);
|
|
3362
3536
|
if (evidence.renewalAt !== void 0) authoritativeEvent = { ...event, renewalAt: evidence.renewalAt };
|
|
3363
3537
|
}
|
|
3364
3538
|
if (app.canceled !== true && !["refunded", "declined", "canceled"].includes(String(app.status))) {
|
|
@@ -3409,7 +3583,7 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
|
|
|
3409
3583
|
} catch {
|
|
3410
3584
|
return json({ error: "payment notice reconciliation pending" }, 503);
|
|
3411
3585
|
}
|
|
3412
|
-
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);
|
|
3413
3587
|
if (typeof next.interviewWaivedAt === "number") {
|
|
3414
3588
|
const approval = await approveApplication({
|
|
3415
3589
|
db,
|
|
@@ -3424,15 +3598,132 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
|
|
|
3424
3598
|
}
|
|
3425
3599
|
const stored = await storeStripeReceipt(db, { eventId, digest, legacyDigest, event, disposition, matched: true });
|
|
3426
3600
|
if (stored === "collision") {
|
|
3427
|
-
|
|
3601
|
+
await reportStripeCollision(db, ctx, eventId, event);
|
|
3428
3602
|
return json({ ok: true, matched: false, collision: true });
|
|
3429
3603
|
}
|
|
3430
3604
|
return json({ ok: true });
|
|
3431
3605
|
}
|
|
3432
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
|
+
|
|
3433
3723
|
// src/worker-routes-payment-recovery.ts
|
|
3434
3724
|
var handlePaymentRecovery = async (req, url, env, ctx) => {
|
|
3435
|
-
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);
|
|
3436
3727
|
if (!["GET", "POST"].includes(req.method)) return json({ error: "method not allowed" }, 405);
|
|
3437
3728
|
const raw2 = ctx.makeDb(env), user = await ctx.verifyUser(req, env);
|
|
3438
3729
|
if (!user) return json({ error: "unauthorized" }, 401);
|
|
@@ -3494,10 +3785,10 @@ var handlePaymentRecovery = async (req, url, env, ctx) => {
|
|
|
3494
3785
|
};
|
|
3495
3786
|
|
|
3496
3787
|
// src/worker-routes-admin-named-seat.ts
|
|
3497
|
-
var
|
|
3788
|
+
var PATH2 = /^\/api\/admin\/applications\/([0-9a-f-]+)\/named-seat(\/refund)?$/;
|
|
3498
3789
|
var REFUNDABLE = /* @__PURE__ */ new Set(["renewal_pending", "pending_acceptance", "pending_approval", "pending_primary", "active"]);
|
|
3499
3790
|
var handleAdminNamedSeat = async (req, url, env, ctx) => {
|
|
3500
|
-
const match = url.pathname.match(
|
|
3791
|
+
const match = url.pathname.match(PATH2);
|
|
3501
3792
|
if (!match) return null;
|
|
3502
3793
|
const seats = ctx.membershipAuthority?.namedSeats;
|
|
3503
3794
|
if (ctx.chapter.mode !== "chapter" || !seats) return json({ error: "named seats are not configured" }, 404);
|
|
@@ -3550,42 +3841,42 @@ async function closeRecipientApplication(db, env, ctx, seatId) {
|
|
|
3550
3841
|
|
|
3551
3842
|
// src/membership-seat-refund-stripe.ts
|
|
3552
3843
|
var import_stripe6 = require("@odla-ai/stripe");
|
|
3553
|
-
var
|
|
3844
|
+
var fail4 = (code = "named_seat_refund_mismatch") => {
|
|
3554
3845
|
throw Object.assign(new Error(code), { code });
|
|
3555
3846
|
};
|
|
3556
3847
|
async function refundNamedSeatStripePayment(secretKey, seat) {
|
|
3557
|
-
if (!seat.providerPaymentIntentId || seat.status !== "refund_pending")
|
|
3848
|
+
if (!seat.providerPaymentIntentId || seat.status !== "refund_pending") fail4();
|
|
3558
3849
|
const call = async (method, path, params, idempotencyKey) => {
|
|
3559
3850
|
const result = await (0, import_stripe6.stripeRequest)({ secretKey }, method, path, params, idempotencyKey ? { idempotencyKey } : {});
|
|
3560
|
-
if (!result.ok || !result.body) return
|
|
3851
|
+
if (!result.ok || !result.body) return fail4("named_seat_refund_provider_unavailable");
|
|
3561
3852
|
return result.body;
|
|
3562
3853
|
};
|
|
3563
3854
|
const metadata = { ...stripePartitionMetadata(seat), applicationId: seat.applicationId, membershipId: seat.primaryMembershipId, seatId: seat.id };
|
|
3564
3855
|
const payment = await call("GET", `/v1/payment_intents/${encodeURIComponent(seat.providerPaymentIntentId)}`);
|
|
3565
|
-
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();
|
|
3566
3857
|
const sub = await call("GET", `/v1/subscriptions/${encodeURIComponent(seat.providerSubscriptionId)}`);
|
|
3567
|
-
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();
|
|
3568
3859
|
const items = sub.items.data.filter((item) => item.metadata?.seatId === seat.id);
|
|
3569
|
-
if (items.length > 1)
|
|
3860
|
+
if (items.length > 1) fail4();
|
|
3570
3861
|
if (items.length && sub.status !== "canceled") {
|
|
3571
3862
|
const item = items[0];
|
|
3572
|
-
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();
|
|
3573
3864
|
const removed = await call("DELETE", `/v1/subscription_items/${encodeURIComponent(item.id)}`, { proration_behavior: "none" });
|
|
3574
|
-
if (removed.id !== item.id || removed.deleted !== true)
|
|
3865
|
+
if (removed.id !== item.id || removed.deleted !== true) fail4("named_seat_renewal_removal_unconfirmed");
|
|
3575
3866
|
}
|
|
3576
3867
|
const list = await call("GET", "/v1/refunds", { payment_intent: seat.providerPaymentIntentId, limit: 100 });
|
|
3577
|
-
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");
|
|
3578
3869
|
let refundedCents = 0, pendingCents = 0;
|
|
3579
3870
|
const refundIds = [];
|
|
3580
3871
|
for (const refund of list.data) {
|
|
3581
|
-
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();
|
|
3582
3873
|
if (refund.status === "succeeded") {
|
|
3583
3874
|
refundedCents += refund.amount;
|
|
3584
3875
|
refundIds.push(refund.id);
|
|
3585
3876
|
} else if (refund.status === "pending" || refund.status === "requires_action") pendingCents += refund.amount;
|
|
3586
3877
|
}
|
|
3587
|
-
if (refundedCents + pendingCents > seat.amountCents)
|
|
3588
|
-
if (pendingCents)
|
|
3878
|
+
if (refundedCents + pendingCents > seat.amountCents) fail4();
|
|
3879
|
+
if (pendingCents) fail4("named_seat_refund_pending");
|
|
3589
3880
|
if (refundedCents < seat.amountCents) {
|
|
3590
3881
|
const remaining = seat.amountCents - refundedCents;
|
|
3591
3882
|
const refund = await call("POST", "/v1/refunds", {
|
|
@@ -3594,8 +3885,8 @@ async function refundNamedSeatStripePayment(secretKey, seat) {
|
|
|
3594
3885
|
reason: "requested_by_customer",
|
|
3595
3886
|
metadata
|
|
3596
3887
|
}, `named-seat-refund:${seat.id}:${remaining}`);
|
|
3597
|
-
if (refund.payment_intent !== seat.providerPaymentIntentId || refund.amount !== remaining || refund.currency !== seat.currency || !/^re_[A-Za-z0-9_]+$/.test(refund.id))
|
|
3598
|
-
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");
|
|
3599
3890
|
refundedCents += remaining;
|
|
3600
3891
|
refundIds.push(refund.id);
|
|
3601
3892
|
}
|
|
@@ -3684,9 +3975,37 @@ async function refundNamedSeatByOperator(db, input, stripeSecretKey, now) {
|
|
|
3684
3975
|
return { seatId: String(seat.id), status: "refunded", refundedCents: Number(refunded.refundedCents), subscriptionCanceled: false };
|
|
3685
3976
|
}
|
|
3686
3977
|
|
|
3978
|
+
// src/membership-seat-window.ts
|
|
3979
|
+
async function liftSeatPurchaseWindow(db, scope2, now, limit = 100) {
|
|
3980
|
+
membershipClock(now);
|
|
3981
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200) throw new TypeError("invalid lift limit");
|
|
3982
|
+
const state = await loadMembershipAuthorityState(db, scope2);
|
|
3983
|
+
const rows5 = (await db.query({ networkMembership: { $: { where: { ...scope2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkMembership ?? [];
|
|
3984
|
+
let lifted = 0;
|
|
3985
|
+
for (const member of rows5.slice(0, limit)) {
|
|
3986
|
+
if (Object.entries(scope2).some(([key3, value]) => member[key3] !== value)) throw new Error("membership lift scope mismatch");
|
|
3987
|
+
const offering = membershipOfferingVersion(member.offeringSnapshot);
|
|
3988
|
+
if (offering.seatPurchaseWindowCalendarMonths === void 0) continue;
|
|
3989
|
+
const { seatPurchaseWindowCalendarMonths: _window, ...windowless } = offering;
|
|
3990
|
+
await db.transact([membershipUpdate("networkMembership", String(member.id), {
|
|
3991
|
+
offeringSnapshot: windowless,
|
|
3992
|
+
seatPurchaseEndsAt: Number(member.termEndsAt),
|
|
3993
|
+
revision: Number(member.revision) + 1,
|
|
3994
|
+
updatedAt: now
|
|
3995
|
+
})], { mutationId: `seat-window-lift:${member.id}:${member.revision}`, guards: [
|
|
3996
|
+
{ ns: "networkMembership", id: String(member.id), exists: true, equals: { revision: member.revision } },
|
|
3997
|
+
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative" } }
|
|
3998
|
+
] });
|
|
3999
|
+
lifted++;
|
|
4000
|
+
}
|
|
4001
|
+
return { lifted, more: rows5.length > limit };
|
|
4002
|
+
}
|
|
4003
|
+
|
|
3687
4004
|
// src/worker-routes-admin-membership-seats.ts
|
|
3688
4005
|
var LIST = "/api/admin/membership-seats";
|
|
4006
|
+
var LIFT = "/api/admin/membership-seats/lift-purchase-window";
|
|
3689
4007
|
var ONE = /^\/api\/admin\/membership-seats\/([A-Za-z0-9][A-Za-z0-9_.:-]{2,159})\/refund$/;
|
|
4008
|
+
var SCOPE = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
3690
4009
|
var PUBLIC = [
|
|
3691
4010
|
"id",
|
|
3692
4011
|
"appId",
|
|
@@ -3711,11 +4030,11 @@ var PUBLIC = [
|
|
|
3711
4030
|
];
|
|
3712
4031
|
var project = (row2) => Object.fromEntries(PUBLIC.filter((key3) => row2[key3] !== void 0).map((key3) => [key3, row2[key3]]));
|
|
3713
4032
|
var handleAdminMembershipSeats = async (req, url, env, ctx) => {
|
|
3714
|
-
const one = url.pathname.match(ONE);
|
|
3715
|
-
if (url.pathname !== LIST && !one) return null;
|
|
4033
|
+
const one = url.pathname.match(ONE), lift = url.pathname === LIFT;
|
|
4034
|
+
if (url.pathname !== LIST && !one && !lift) return null;
|
|
3716
4035
|
const operator = ctx.membershipAuthorityOperator;
|
|
3717
4036
|
if (!operator) return json({ error: "not found" }, 404);
|
|
3718
|
-
if (req.method !== (one ? "POST" : "GET")) return json({ error: "method not allowed" }, 405);
|
|
4037
|
+
if (req.method !== (one || lift ? "POST" : "GET")) return json({ error: "method not allowed" }, 405);
|
|
3719
4038
|
const raw2 = ctx.makeDb(env), user = await ctx.verifyUser(req, env);
|
|
3720
4039
|
if (!user) return json({ error: "unauthorized" }, 401);
|
|
3721
4040
|
if (!await ctx.isSuperAdminEmail(raw2, user.email, user.userId)) return json({ error: "super-admin required", code: "super_admin_required" }, 403);
|
|
@@ -3725,6 +4044,24 @@ var handleAdminMembershipSeats = async (req, url, env, ctx) => {
|
|
|
3725
4044
|
} catch {
|
|
3726
4045
|
return json({ error: "the membership authority is not hosted by this deployment" }, 404);
|
|
3727
4046
|
}
|
|
4047
|
+
if (lift) {
|
|
4048
|
+
let body;
|
|
4049
|
+
try {
|
|
4050
|
+
body = await req.json();
|
|
4051
|
+
} catch {
|
|
4052
|
+
return json({ error: "chapterId and runtime are required" }, 400);
|
|
4053
|
+
}
|
|
4054
|
+
const appId = body.appId ?? body.chapterId;
|
|
4055
|
+
if (typeof body.chapterId !== "string" || !SCOPE.test(body.chapterId) || typeof appId !== "string" || !SCOPE.test(appId) || typeof body.runtime !== "string" || !/^[a-z0-9]{2,12}$/.test(body.runtime)) {
|
|
4056
|
+
return json({ error: "chapterId and runtime are required" }, 400);
|
|
4057
|
+
}
|
|
4058
|
+
try {
|
|
4059
|
+
return json({ ok: true, ...await liftSeatPurchaseWindow(db, { appId, chapterId: body.chapterId, environment: env.ODLA_ENV, runtime: body.runtime }, Date.now()) });
|
|
4060
|
+
} catch (error) {
|
|
4061
|
+
const code = error.code;
|
|
4062
|
+
return json({ error: "purchase window lift is pending; retry to confirm", ...code ? { code } : {} }, code ? 409 : 503);
|
|
4063
|
+
}
|
|
4064
|
+
}
|
|
3728
4065
|
if (!one) {
|
|
3729
4066
|
const rows5 = (await db.query({ networkSeat: { $: { where: { environment: env.ODLA_ENV }, order: { updatedAt: "desc" }, limit: 201 } } })).networkSeat ?? [];
|
|
3730
4067
|
return json({ seats: rows5.slice(0, 200).map(project), truncated: rows5.length > 200 });
|
|
@@ -3754,6 +4091,107 @@ var handleAdminMembershipSeats = async (req, url, env, ctx) => {
|
|
|
3754
4091
|
}
|
|
3755
4092
|
};
|
|
3756
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
|
+
|
|
3757
4195
|
// src/worker-routes-membership-cancel.ts
|
|
3758
4196
|
var handleMembershipCancellation = async (req, url, env, ctx) => {
|
|
3759
4197
|
const match = url.pathname.match(/^\/api\/admin\/applications\/([0-9a-f-]+)\/cancel-renewal$/);
|
|
@@ -4606,8 +5044,8 @@ var APPLICATION_LABELS = {
|
|
|
4606
5044
|
state: "State"
|
|
4607
5045
|
};
|
|
4608
5046
|
function humanize(id6) {
|
|
4609
|
-
const
|
|
4610
|
-
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;
|
|
4611
5049
|
}
|
|
4612
5050
|
function applicationDefs(chapter) {
|
|
4613
5051
|
try {
|
|
@@ -4674,7 +5112,7 @@ var ID2 = /^[a-z][a-z0-9-]{1,79}$/;
|
|
|
4674
5112
|
var PRICE = /^price_[A-Za-z0-9]{12,}$/;
|
|
4675
5113
|
var CURRENCY = /^[a-z]{3}$/;
|
|
4676
5114
|
var record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
4677
|
-
var
|
|
5115
|
+
var text3 = (value, path, errors, max) => {
|
|
4678
5116
|
if (typeof value !== "string" || !value.trim()) {
|
|
4679
5117
|
errors.push(`${path} is required`);
|
|
4680
5118
|
return "";
|
|
@@ -4702,23 +5140,23 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
4702
5140
|
}
|
|
4703
5141
|
const allowed2 = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
|
|
4704
5142
|
for (const key3 of Object.keys(item)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
|
|
4705
|
-
const id6 =
|
|
5143
|
+
const id6 = text3(item.id, `${path}.id`, errors, 80);
|
|
4706
5144
|
if (id6 && !ID2.test(id6)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
4707
5145
|
if (ids2.has(id6)) errors.push(`${path}.id duplicates "${id6}"`);
|
|
4708
5146
|
ids2.add(id6);
|
|
4709
5147
|
const targetRaw = record(item.target);
|
|
4710
5148
|
let target = { type: "entitlement", entitlementKey: "invalid" };
|
|
4711
5149
|
if (targetRaw?.type === "tier") {
|
|
4712
|
-
const tierId =
|
|
5150
|
+
const tierId = text3(targetRaw.tierId, `${path}.target.tierId`, errors, 80);
|
|
4713
5151
|
if (!tierIds.has(tierId)) errors.push(`${path}.target.tierId must reference a declared tier`);
|
|
4714
5152
|
target = { type: "tier", tierId };
|
|
4715
5153
|
} else if (targetRaw?.type === "entitlement") {
|
|
4716
|
-
const entitlementKey =
|
|
5154
|
+
const entitlementKey = text3(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
|
|
4717
5155
|
if (entitlementKey && !ID2.test(entitlementKey)) errors.push(`${path}.target.entitlementKey must be a lowercase kebab-case id`);
|
|
4718
5156
|
target = { type: "entitlement", entitlementKey };
|
|
4719
5157
|
} else errors.push(`${path}.target.type must be tier or entitlement`);
|
|
4720
5158
|
if (item.billingMode !== "one_time" && item.billingMode !== "subscription") errors.push(`${path}.billingMode must be one_time or subscription`);
|
|
4721
|
-
const stripePriceId =
|
|
5159
|
+
const stripePriceId = text3(item.stripePriceId, `${path}.stripePriceId`, errors, 128);
|
|
4722
5160
|
if (stripePriceId && !PRICE.test(stripePriceId)) errors.push(`${path}.stripePriceId must be a real Stripe Price id`);
|
|
4723
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`);
|
|
4724
5162
|
const currency = typeof item.currency === "string" ? item.currency.trim().toLowerCase() : "";
|
|
@@ -4740,9 +5178,9 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
4740
5178
|
redemptionLifetimeHours: lifetime,
|
|
4741
5179
|
...activeFrom !== void 0 ? { activeFrom } : {},
|
|
4742
5180
|
...activeUntil !== void 0 ? { activeUntil } : {},
|
|
4743
|
-
title:
|
|
4744
|
-
description:
|
|
4745
|
-
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)
|
|
4746
5184
|
});
|
|
4747
5185
|
}
|
|
4748
5186
|
return errors.length ? { ok: false, errors } : { ok: true, value: { schemaVersion: 1, offers } };
|
|
@@ -4960,22 +5398,22 @@ async function createPaymentQuote(input) {
|
|
|
4960
5398
|
// src/membership-quote-projection.ts
|
|
4961
5399
|
var MEMBERSHIP_QUOTE_NAMESPACE = "membershipQuoteProjections";
|
|
4962
5400
|
var keys2 = ["appId", "chapterId", "environment", "runtime", "applicationId", "skuId"];
|
|
4963
|
-
var
|
|
5401
|
+
var fail5 = (code) => {
|
|
4964
5402
|
throw Object.assign(new Error(code), { code });
|
|
4965
5403
|
};
|
|
4966
5404
|
var load3 = async (db, ns, id6) => (await db.query({ [ns]: { $: { where: { id: id6 }, limit: 1 } } }))[ns]?.[0];
|
|
4967
5405
|
async function restore2(row2, target, now, allowExpired = false) {
|
|
4968
|
-
if (!row2 || keys2.some((key3) => row2[key3] !== target[key3]) || !Number.isSafeInteger(row2.revision) || Number(row2.revision) < 1 || typeof row2.requestId !== "string") return
|
|
4969
|
-
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");
|
|
4970
5408
|
const offer = await verifyIssuedMembershipOffer(row2.offer, { ...target, offerId: await membershipOfferIdForRequest({
|
|
4971
5409
|
...target,
|
|
4972
5410
|
requestId: row2.requestId
|
|
4973
5411
|
}), now, lifetimeMs: 6e4 });
|
|
4974
|
-
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");
|
|
4975
5413
|
const tier = row2.tier, quote = row2.quote;
|
|
4976
5414
|
const offering = offer.contract.offering;
|
|
4977
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) {
|
|
4978
|
-
return
|
|
5416
|
+
return fail5("membership_catalog_mismatch");
|
|
4979
5417
|
}
|
|
4980
5418
|
const expected = await createPaymentQuote({
|
|
4981
5419
|
...target,
|
|
@@ -4986,7 +5424,7 @@ async function restore2(row2, target, now, allowExpired = false) {
|
|
|
4986
5424
|
presentation: quote.presentation,
|
|
4987
5425
|
founding: foundingOf(offer)
|
|
4988
5426
|
});
|
|
4989
|
-
if (quote.digest !== expected.digest) return
|
|
5427
|
+
if (quote.digest !== expected.digest) return fail5("membership_quote_conflict");
|
|
4990
5428
|
return { tier, quote: expected, billingTerms: offer.contract.billingTerms, membershipOffer: offer, projectionRevision: Number(row2.revision) };
|
|
4991
5429
|
}
|
|
4992
5430
|
async function readMembershipQuote(db, target, now) {
|
|
@@ -4994,9 +5432,9 @@ async function readMembershipQuote(db, target, now) {
|
|
|
4994
5432
|
}
|
|
4995
5433
|
async function prepareMembershipQuote(db, input, issue) {
|
|
4996
5434
|
const { applicationId: id6, now, app } = input;
|
|
4997
|
-
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");
|
|
4998
5436
|
let row2 = await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, id6);
|
|
4999
|
-
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");
|
|
5000
5438
|
if (row2?.status === "ready") {
|
|
5001
5439
|
try {
|
|
5002
5440
|
return await restore2(row2, input, now);
|
|
@@ -5041,7 +5479,7 @@ async function prepareMembershipQuote(db, input, issue) {
|
|
|
5041
5479
|
}
|
|
5042
5480
|
if (row2?.status === "ready") return restore2(row2, input, now);
|
|
5043
5481
|
if (!row2 || row2.status !== "pending" || keys2.some((key3) => row2[key3] !== input[key3]) || typeof row2.requestId !== "string") {
|
|
5044
|
-
return
|
|
5482
|
+
return fail5("membership_quote_pending");
|
|
5045
5483
|
}
|
|
5046
5484
|
const target = Object.fromEntries(keys2.map((key3) => [key3, input[key3]]));
|
|
5047
5485
|
const offer = await verifyIssuedMembershipOffer(await issue({ ...target, requestId: row2.requestId }), {
|
|
@@ -5305,17 +5743,17 @@ async function readBoundedRequestText(req, maxBytes) {
|
|
|
5305
5743
|
if (!reader) return "";
|
|
5306
5744
|
const decoder = new TextDecoder();
|
|
5307
5745
|
let bytes = 0;
|
|
5308
|
-
let
|
|
5746
|
+
let text8 = "";
|
|
5309
5747
|
for (; ; ) {
|
|
5310
5748
|
const { done, value } = await reader.read();
|
|
5311
5749
|
if (req.signal.aborted) return invalid2();
|
|
5312
5750
|
if (done) {
|
|
5313
5751
|
complete = true;
|
|
5314
|
-
return
|
|
5752
|
+
return text8 + decoder.decode();
|
|
5315
5753
|
}
|
|
5316
5754
|
bytes += value.byteLength;
|
|
5317
5755
|
if (bytes > maxBytes) return json({ error: "request body too large" }, 413);
|
|
5318
|
-
|
|
5756
|
+
text8 += decoder.decode(value, { stream: true });
|
|
5319
5757
|
}
|
|
5320
5758
|
} catch {
|
|
5321
5759
|
return invalid2();
|
|
@@ -5668,37 +6106,61 @@ async function provisionApplicant(db, chapter, applicationId, fields) {
|
|
|
5668
6106
|
} catch {
|
|
5669
6107
|
}
|
|
5670
6108
|
}
|
|
5671
|
-
|
|
6109
|
+
|
|
6110
|
+
// src/submit-notifications.ts
|
|
6111
|
+
async function notifyAdminOfApplication(db, env, notice) {
|
|
5672
6112
|
try {
|
|
5673
|
-
const
|
|
5674
|
-
|
|
5675
|
-
|
|
5676
|
-
|
|
5677
|
-
|
|
5678
|
-
|
|
5679
|
-
|
|
5680
|
-
|
|
5681
|
-
|
|
5682
|
-
|
|
5683
|
-
|
|
5684
|
-
|
|
5685
|
-
|
|
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)
|
|
5686
6160
|
},
|
|
5687
|
-
{
|
|
5688
|
-
|
|
5689
|
-
|
|
5690
|
-
to: group.notificationEmail,
|
|
5691
|
-
vars: {
|
|
5692
|
-
firstName: stringValue(fields.firstName),
|
|
5693
|
-
lastName: stringValue(fields.lastName),
|
|
5694
|
-
email: stringValue(fields.email),
|
|
5695
|
-
phone: stringValue(fields.phone),
|
|
5696
|
-
state: stringValue(fields.state)
|
|
5697
|
-
},
|
|
5698
|
-
dedupeKey: `apply:${applicationId}:admin`,
|
|
5699
|
-
applicationId
|
|
5700
|
-
}
|
|
5701
|
-
);
|
|
6161
|
+
dedupeKey: `apply:${String(app.id)}:confirm`,
|
|
6162
|
+
applicationId: String(app.id)
|
|
6163
|
+
});
|
|
5702
6164
|
} catch {
|
|
5703
6165
|
}
|
|
5704
6166
|
}
|
|
@@ -5896,17 +6358,17 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
5896
6358
|
if (provisioningFailure.status === 409) await rejectUnprovisionedSignup(db, result.id, canonical3.status);
|
|
5897
6359
|
return json(provisioningFailure.body, provisioningFailure.status);
|
|
5898
6360
|
}
|
|
5899
|
-
if (!result.duplicate && effectiveChapter.sends.adminNotification === "submit") {
|
|
5900
|
-
await notifyAdminOfApplication(db, env, chapter.id, result.id, canonical3);
|
|
5901
|
-
}
|
|
5902
6361
|
const groups = (await db.query({ groups: { $: { where: { id: chapter.id }, limit: 1 } } })).groups;
|
|
5903
6362
|
const group = Array.isArray(groups) ? groups[0] : void 0;
|
|
5904
|
-
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;
|
|
5905
6366
|
if (!state) return json({
|
|
5906
6367
|
error: "Application saved; its next step is temporarily unavailable. Retry this submission.",
|
|
5907
6368
|
code: "application_continuation_unavailable",
|
|
5908
6369
|
id: result.id
|
|
5909
6370
|
}, 503);
|
|
6371
|
+
if (notice && !result.duplicate) await notifySubmitConfirmed(db, env, { ...notice, state });
|
|
5910
6372
|
const final = (await db.query({ applications: { $: { where: { id: result.id }, limit: 1 } } })).applications?.[0];
|
|
5911
6373
|
if (!final || final.groupId !== chapter.id || state === "accepted" && final.status !== "approved") {
|
|
5912
6374
|
return json({ error: "application changed; reload its current state" }, 409);
|
|
@@ -6026,10 +6488,10 @@ function boundedString(value, path, errors, max, required = false) {
|
|
|
6026
6488
|
errors.push(`${path} must be a string`);
|
|
6027
6489
|
return void 0;
|
|
6028
6490
|
}
|
|
6029
|
-
const
|
|
6030
|
-
if (required && !
|
|
6031
|
-
if (
|
|
6032
|
-
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;
|
|
6033
6495
|
}
|
|
6034
6496
|
function validateMembershipPageDocument(input, options = {}) {
|
|
6035
6497
|
const errors = [];
|
|
@@ -6078,8 +6540,8 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
6078
6540
|
else {
|
|
6079
6541
|
if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
|
|
6080
6542
|
benefits = item.benefits.slice(0, MAX_BENEFITS).flatMap((benefit, benefitIndex) => {
|
|
6081
|
-
const
|
|
6082
|
-
return
|
|
6543
|
+
const text8 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
|
|
6544
|
+
return text8 ? [text8] : [];
|
|
6083
6545
|
});
|
|
6084
6546
|
}
|
|
6085
6547
|
}
|
|
@@ -6173,7 +6635,7 @@ var recordOf = (value) => value !== null && typeof value === "object" && !Array.
|
|
|
6173
6635
|
function unknownKeys(value, allowed2, path, errors) {
|
|
6174
6636
|
for (const key3 of Object.keys(value)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
|
|
6175
6637
|
}
|
|
6176
|
-
function
|
|
6638
|
+
function text4(value, path, errors, max = 65536) {
|
|
6177
6639
|
if (typeof value !== "string" || !value.trim()) {
|
|
6178
6640
|
errors.push(`${path} is required`);
|
|
6179
6641
|
return "";
|
|
@@ -6196,11 +6658,11 @@ function tiersOf(value, errors) {
|
|
|
6196
6658
|
return [];
|
|
6197
6659
|
}
|
|
6198
6660
|
unknownKeys(item, TIER_KEYS2, path, errors);
|
|
6199
|
-
const id6 =
|
|
6661
|
+
const id6 = text4(item.id, `${path}.id`, errors, 80);
|
|
6200
6662
|
if (id6 && !TIER.test(id6)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
6201
6663
|
if (seen.has(id6)) errors.push(`${path}.id duplicates "${id6}"`);
|
|
6202
6664
|
seen.add(id6);
|
|
6203
|
-
const name =
|
|
6665
|
+
const name = text4(item.name, `${path}.name`, errors, 160);
|
|
6204
6666
|
if (typeof item.available !== "boolean") errors.push(`${path}.available must be boolean`);
|
|
6205
6667
|
if (!Number.isInteger(item.sortOrder) || Number(item.sortOrder) < 0) errors.push(`${path}.sortOrder must be a non-negative integer`);
|
|
6206
6668
|
if (!Number.isInteger(item.priceCents) || Number(item.priceCents) < 0 || Number(item.priceCents) > 1e8) {
|
|
@@ -6218,7 +6680,7 @@ function tiersOf(value, errors) {
|
|
|
6218
6680
|
if (item.interval !== void 0 && item.interval !== "year" && item.interval !== "month") {
|
|
6219
6681
|
errors.push(`${path}.interval must be year or month`);
|
|
6220
6682
|
}
|
|
6221
|
-
const blurb = item.blurb === void 0 ? void 0 :
|
|
6683
|
+
const blurb = item.blurb === void 0 ? void 0 : text4(item.blurb, `${path}.blurb`, errors, 4e3);
|
|
6222
6684
|
return [{
|
|
6223
6685
|
id: id6,
|
|
6224
6686
|
name,
|
|
@@ -6276,7 +6738,7 @@ function policyOf(value, errors, tiers) {
|
|
|
6276
6738
|
const result = {};
|
|
6277
6739
|
for (const key3 of allowed2) {
|
|
6278
6740
|
if (input[key3] === void 0 && key3 !== "disclaimerText" && key3 !== "refundPolicyText") continue;
|
|
6279
|
-
result[key3] =
|
|
6741
|
+
result[key3] = text4(input[key3], `document.policy.${key3}`, errors);
|
|
6280
6742
|
}
|
|
6281
6743
|
const tierPolicies = {};
|
|
6282
6744
|
if (input.tierPolicies !== void 0) {
|
|
@@ -6291,8 +6753,8 @@ function policyOf(value, errors, tiers) {
|
|
|
6291
6753
|
}
|
|
6292
6754
|
unknownKeys(policy, /* @__PURE__ */ new Set(["refundPolicyText", "trustCopy"]), path, errors);
|
|
6293
6755
|
tierPolicies[id6] = {
|
|
6294
|
-
refundPolicyText:
|
|
6295
|
-
...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) } : {}
|
|
6296
6758
|
};
|
|
6297
6759
|
}
|
|
6298
6760
|
}
|
|
@@ -6332,9 +6794,9 @@ function validateSignupControlDocument(input) {
|
|
|
6332
6794
|
if (!Number.isSafeInteger(root.generatedAt) || Number(root.generatedAt) < 1) errors.push("document.generatedAt must be epoch milliseconds");
|
|
6333
6795
|
const source = recordOf(root.source);
|
|
6334
6796
|
const target = recordOf(root.target);
|
|
6335
|
-
const appId =
|
|
6336
|
-
const chapterId =
|
|
6337
|
-
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);
|
|
6338
6800
|
if (appId && !SLUG.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
6339
6801
|
if (chapterId && !SLUG.test(chapterId)) errors.push("document.target.chapterId must be a lowercase app slug");
|
|
6340
6802
|
if (environment && !ENVIRONMENT.test(environment)) errors.push("document.target.environment is invalid");
|
|
@@ -7513,7 +7975,7 @@ async function deliverAdmissionGrants(options) {
|
|
|
7513
7975
|
}
|
|
7514
7976
|
|
|
7515
7977
|
// src/worker-routes-network-admission.ts
|
|
7516
|
-
var
|
|
7978
|
+
var PATH4 = "/api/admin/network/admissions";
|
|
7517
7979
|
var RETRY_PATH = "/api/admin/network/admissions/retry";
|
|
7518
7980
|
var REVOKE_PATH = "/api/admin/network/admissions/revoke";
|
|
7519
7981
|
var MUTATION_ID2 = /^[A-Za-z0-9_-]{16,128}$/;
|
|
@@ -7593,11 +8055,11 @@ async function parseBody(req) {
|
|
|
7593
8055
|
}
|
|
7594
8056
|
}
|
|
7595
8057
|
var handleAdminNetworkAdmissions = async (req, url, env, ctx) => {
|
|
7596
|
-
if (![
|
|
8058
|
+
if (![PATH4, RETRY_PATH, REVOKE_PATH].includes(url.pathname)) return null;
|
|
7597
8059
|
const auth = await gate2(req, env, ctx);
|
|
7598
8060
|
if ("response" in auth) return auth.response;
|
|
7599
8061
|
const db = auth.db;
|
|
7600
|
-
if (url.pathname ===
|
|
8062
|
+
if (url.pathname === PATH4 && req.method === "GET") {
|
|
7601
8063
|
const targetId2 = url.searchParams.get("targetId") ?? "";
|
|
7602
8064
|
const target2 = targetFor(ctx, targetId2);
|
|
7603
8065
|
if (!target2) return reject6(404, "unknown_target", "admission target is not configured");
|
|
@@ -7698,6 +8160,41 @@ async function retryLeaderAdmissionDeliveries(env, ctx, limit = 100) {
|
|
|
7698
8160
|
|
|
7699
8161
|
// src/worker-routes-schedule.ts
|
|
7700
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
|
|
7701
8198
|
function errCode(err) {
|
|
7702
8199
|
if (err && typeof err === "object") {
|
|
7703
8200
|
const code = err.code;
|
|
@@ -7812,12 +8309,7 @@ async function bookSlot(req, env, ctx) {
|
|
|
7812
8309
|
await db.transact([meetingOp, appOp]);
|
|
7813
8310
|
const booked = { ...app, ...bookingAttrs };
|
|
7814
8311
|
await syncApplicationToCrm(crmDeps(db, ctx), { app: booked, stage: String(booked.status ?? "") }).catch(() => void 0);
|
|
7815
|
-
|
|
7816
|
-
await sendTemplated(
|
|
7817
|
-
{ db, envName: env.ODLA_ENV, sender: env.SEND_EMAIL, from: env.EMAIL_FROM, debugEmail: env.ODLA_DEBUG_EMAIL, now: () => Date.now(), newId: () => crypto.randomUUID() },
|
|
7818
|
-
{ group: emailGroupFrom(group), template: "prepEmail", to: app.email, vars: { firstName: String(app.firstName ?? "") }, dedupeKey: `prep:${applicationId}`, applicationId }
|
|
7819
|
-
).catch(() => void 0);
|
|
7820
|
-
}
|
|
8312
|
+
await notifyBookingConfirmed(db, env, { group, application: app, meeting: { startAt, endAt, timezone: cfg.timezone, meetUrl, htmlLink } });
|
|
7821
8313
|
return json({ ok: true, startAt, endAt, meetUrl, rescheduled: decision.reschedule });
|
|
7822
8314
|
}
|
|
7823
8315
|
var handleSchedule = async (req, url, env, ctx) => {
|
|
@@ -7848,21 +8340,21 @@ var import_stripe7 = require("@odla-ai/stripe");
|
|
|
7848
8340
|
var row = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
7849
8341
|
var id2 = (value) => typeof value === "string" ? value : String(row(value).id ?? "");
|
|
7850
8342
|
var providerId2 = (value, prefix) => new RegExp(`^${prefix}_[A-Za-z0-9]+$`).test(value);
|
|
7851
|
-
function
|
|
8343
|
+
function fail6(code = "refund_target_mismatch") {
|
|
7852
8344
|
throw Object.assign(new Error(code), { code });
|
|
7853
8345
|
}
|
|
7854
8346
|
async function refundSubscriptionPayment(secretKey, input) {
|
|
7855
|
-
if (!providerId2(input.customerId, "cus") || !providerId2(input.subscriptionId, "sub"))
|
|
8347
|
+
if (!providerId2(input.customerId, "cus") || !providerId2(input.subscriptionId, "sub")) fail6();
|
|
7856
8348
|
const call = async (method, path, params, key3) => {
|
|
7857
8349
|
const result = await (0, import_stripe7.stripeRequest)({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
|
|
7858
|
-
if (!result.ok)
|
|
7859
|
-
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");
|
|
7860
8352
|
return result.body;
|
|
7861
8353
|
};
|
|
7862
8354
|
const sub = await call("GET", `/v1/subscriptions/${input.subscriptionId}`);
|
|
7863
|
-
if (id2(sub) !== input.subscriptionId || id2(sub.customer) !== input.customerId)
|
|
8355
|
+
if (id2(sub) !== input.subscriptionId || id2(sub.customer) !== input.customerId) fail6();
|
|
7864
8356
|
for (const [key3, value] of Object.entries(input.metadata ?? {})) {
|
|
7865
|
-
if (row(sub.metadata)[key3] !== value)
|
|
8357
|
+
if (row(sub.metadata)[key3] !== value) fail6();
|
|
7866
8358
|
}
|
|
7867
8359
|
let cursor;
|
|
7868
8360
|
let earliest;
|
|
@@ -7875,55 +8367,55 @@ async function refundSubscriptionPayment(secretKey, input) {
|
|
|
7875
8367
|
limit: 100,
|
|
7876
8368
|
...cursor ? { starting_after: cursor } : {}
|
|
7877
8369
|
});
|
|
7878
|
-
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");
|
|
7879
8371
|
for (const value of list.data) {
|
|
7880
8372
|
const invoice2 = row(value);
|
|
7881
8373
|
const subscription = invoice2.subscription ?? row(row(invoice2.parent).subscription_details).subscription;
|
|
7882
|
-
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();
|
|
7883
8375
|
seen.add(id2(invoice2));
|
|
7884
8376
|
if (Number(invoice2.amount_paid) > 0 && (!earliest || Number(invoice2.created) < Number(earliest.created))) earliest = invoice2;
|
|
7885
8377
|
}
|
|
7886
8378
|
if (!list.has_more) break;
|
|
7887
|
-
if (!list.data.length || page === 9)
|
|
8379
|
+
if (!list.data.length || page === 9) fail6("refund_history_incomplete");
|
|
7888
8380
|
cursor = id2(list.data[list.data.length - 1]);
|
|
7889
8381
|
}
|
|
7890
|
-
if (!earliest)
|
|
8382
|
+
if (!earliest) fail6("no_charge");
|
|
7891
8383
|
const invoice = earliest;
|
|
7892
|
-
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();
|
|
7893
8385
|
let chargeId = id2(invoice.charge);
|
|
7894
8386
|
let paymentIntentId = id2(invoice.payment_intent);
|
|
7895
8387
|
if (!chargeId && !paymentIntentId) {
|
|
7896
8388
|
const payments = await call("GET", "/v1/invoice_payments", { invoice: id2(invoice), status: "paid", limit: 100 });
|
|
7897
|
-
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");
|
|
7898
8390
|
const payment = row(payments.data[0]);
|
|
7899
|
-
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();
|
|
7900
8392
|
paymentIntentId = id2(row(payment.payment).payment_intent);
|
|
7901
8393
|
}
|
|
7902
8394
|
if (!chargeId) {
|
|
7903
|
-
if (!providerId2(paymentIntentId, "pi"))
|
|
8395
|
+
if (!providerId2(paymentIntentId, "pi")) fail6();
|
|
7904
8396
|
const payment = await call("GET", `/v1/payment_intents/${paymentIntentId}`);
|
|
7905
|
-
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();
|
|
7906
8398
|
chargeId = id2(payment.latest_charge);
|
|
7907
8399
|
}
|
|
7908
|
-
if (!providerId2(chargeId, "ch"))
|
|
8400
|
+
if (!providerId2(chargeId, "ch")) fail6();
|
|
7909
8401
|
const charge = await call("GET", `/v1/charges/${chargeId}`);
|
|
7910
|
-
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))
|
|
7911
|
-
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();
|
|
7912
8404
|
if (charge.amount_refunded !== charge.amount) {
|
|
7913
8405
|
const refund = await call("POST", "/v1/refunds", {
|
|
7914
8406
|
charge: chargeId,
|
|
7915
8407
|
...input.metadata ? { metadata: input.metadata } : {}
|
|
7916
8408
|
}, `chapter-initial-refund:${input.subscriptionId}:${chargeId}`);
|
|
7917
|
-
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");
|
|
7918
8410
|
}
|
|
7919
8411
|
let subscriptionCanceled = sub.status === "canceled";
|
|
7920
8412
|
if (input.cancelSubscription !== false && !subscriptionCanceled) {
|
|
7921
8413
|
const canceled = await call("DELETE", `/v1/subscriptions/${input.subscriptionId}`);
|
|
7922
|
-
if (id2(canceled) !== input.subscriptionId || canceled.status !== "canceled")
|
|
8414
|
+
if (id2(canceled) !== input.subscriptionId || canceled.status !== "canceled") fail6("refund_cancel_incomplete");
|
|
7923
8415
|
subscriptionCanceled = true;
|
|
7924
8416
|
}
|
|
7925
8417
|
const confirmed = await call("GET", `/v1/charges/${chargeId}`);
|
|
7926
|
-
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");
|
|
7927
8419
|
return { refundedCents: Number(charge.amount), subscriptionCanceled };
|
|
7928
8420
|
}
|
|
7929
8421
|
|
|
@@ -7989,7 +8481,7 @@ async function stripeCall(sk, method, path, params, idempotencyKey) {
|
|
|
7989
8481
|
idempotencyKey ? { idempotencyKey } : {}
|
|
7990
8482
|
);
|
|
7991
8483
|
}
|
|
7992
|
-
function
|
|
8484
|
+
function fail7(op, r) {
|
|
7993
8485
|
const err = new Error(`stripe ${op} failed: ${r.status}`);
|
|
7994
8486
|
err.code = "stripe_error";
|
|
7995
8487
|
throw err;
|
|
@@ -8039,7 +8531,7 @@ function createStripeProvider(config) {
|
|
|
8039
8531
|
{ email: input.email, name: input.name, metadata: meta },
|
|
8040
8532
|
`cus:${input.applicationId}`
|
|
8041
8533
|
);
|
|
8042
|
-
if (!cust.ok)
|
|
8534
|
+
if (!cust.ok) fail7("customer create", cust);
|
|
8043
8535
|
if (typeof cust.body.id !== "string" || !/^cus_[A-Za-z0-9]+$/.test(cust.body.id)) subscriptionContractMismatch();
|
|
8044
8536
|
customerId = cust.body.id;
|
|
8045
8537
|
}
|
|
@@ -8071,7 +8563,7 @@ function createStripeProvider(config) {
|
|
|
8071
8563
|
// retry between create and the db write can't orphan a second one.
|
|
8072
8564
|
`sub:${input.applicationId}${input.purchaseAttemptId ? `:${input.purchaseAttemptId}` : ""}`
|
|
8073
8565
|
);
|
|
8074
|
-
if (!sub.ok)
|
|
8566
|
+
if (!sub.ok) fail7("subscription create", sub);
|
|
8075
8567
|
if (input.billingTerms?.discount?.duration === "once" && Array.isArray(sub.body.discounts) && sub.body.discounts.length === 0) {
|
|
8076
8568
|
const subscriptionId = sub.body.id;
|
|
8077
8569
|
if (typeof subscriptionId !== "string" || !/^sub_[A-Za-z0-9]+$/.test(subscriptionId)) subscriptionContractMismatch();
|
|
@@ -8081,14 +8573,14 @@ function createStripeProvider(config) {
|
|
|
8081
8573
|
"expand[2]": "discounts",
|
|
8082
8574
|
"expand[3]": "latest_invoice.discounts"
|
|
8083
8575
|
});
|
|
8084
|
-
if (!expanded.ok)
|
|
8576
|
+
if (!expanded.ok) fail7("subscription invoice evidence", expanded);
|
|
8085
8577
|
if (expanded.body.id !== subscriptionId) subscriptionContractMismatch();
|
|
8086
8578
|
sub = expanded;
|
|
8087
8579
|
}
|
|
8088
8580
|
verifySubscriptionResult(sub.body, input, customerId, meta);
|
|
8089
8581
|
const invoice = sub.body.latest_invoice;
|
|
8090
8582
|
const clientSecret2 = clientSecretOf(sub.body);
|
|
8091
|
-
if (!clientSecret2)
|
|
8583
|
+
if (!clientSecret2) fail7("subscription confirmation-secret missing", sub);
|
|
8092
8584
|
await tagSubscriptionPayment(
|
|
8093
8585
|
sk,
|
|
8094
8586
|
invoice,
|
|
@@ -8772,15 +9264,16 @@ async function readNamedSeatOffer(db, input, now) {
|
|
|
8772
9264
|
const offering = membershipOfferingVersion(member.offeringSnapshot);
|
|
8773
9265
|
const seat = member.namedSeatId ? await membershipLoad(db, "networkSeat", String(member.namedSeatId)) : void 0;
|
|
8774
9266
|
if (seat) requireMembershipScope(seat, input);
|
|
9267
|
+
const purchaseEndsAt = seatPurchaseDeadline(offering, member);
|
|
8775
9268
|
const offer = {
|
|
8776
9269
|
membershipId: id6,
|
|
8777
9270
|
amountCents: offering.seatPriceCents,
|
|
8778
9271
|
currency: offering.currency,
|
|
8779
9272
|
interval: offering.interval,
|
|
8780
9273
|
termEndsAt: Number(member.termEndsAt),
|
|
8781
|
-
purchaseEndsAt
|
|
9274
|
+
purchaseEndsAt,
|
|
8782
9275
|
autoRenew: member.autoRenew === true,
|
|
8783
|
-
eligible: ["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) && member.refundedCents === 0 && now >= Number(member.initialTermStartedAt) && now < Number(member.termEndsAt) && now <=
|
|
9276
|
+
eligible: ["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) && member.refundedCents === 0 && now >= Number(member.initialTermStartedAt) && now < Number(member.termEndsAt) && now <= purchaseEndsAt && (!seat || ["payment_pending", "payment_processing"].includes(String(seat.status))),
|
|
8784
9277
|
...seat ? { seat: {
|
|
8785
9278
|
seatId: String(seat.id),
|
|
8786
9279
|
status: String(seat.status),
|
|
@@ -8788,7 +9281,7 @@ async function readNamedSeatOffer(db, input, now) {
|
|
|
8788
9281
|
recipientEmail: String(seat.recipientEmailNormalized)
|
|
8789
9282
|
} } : {}
|
|
8790
9283
|
};
|
|
8791
|
-
return { ...offer, digest: await namedSeatOfferDigest(offer) };
|
|
9284
|
+
return { ...offer, digest: await namedSeatOfferDigest(offer), purchaseWindowed: offering.seatPurchaseWindowCalendarMonths !== void 0 };
|
|
8792
9285
|
}
|
|
8793
9286
|
|
|
8794
9287
|
// src/named-seat-request.ts
|
|
@@ -8912,6 +9405,58 @@ var handleNamedSeat = async (req, url, env, ctx) => {
|
|
|
8912
9405
|
}
|
|
8913
9406
|
};
|
|
8914
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
|
+
|
|
8915
9460
|
// src/worker-routes-named-seat-recipient.ts
|
|
8916
9461
|
var handleNamedSeatRecipient = async (req, url, env, ctx) => {
|
|
8917
9462
|
if (url.pathname === "/api/named-seat/invitation") {
|
|
@@ -9029,6 +9574,13 @@ var handleNamedSeatRecipient = async (req, url, env, ctx) => {
|
|
|
9029
9574
|
const current = await membershipLoad(db, "applications", body.applicationId), group = await membershipLoad(db, "groups", ctx.chapter.id);
|
|
9030
9575
|
if (!current || current.namedSeatClaimPending !== false || !current.namedSeatAcceptedAt || !group) throw new Error("seat application reconciliation pending");
|
|
9031
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 }));
|
|
9032
9584
|
await syncApplicationToCrm(crmDeps(db, ctx), { app: current, stage: String(current.status) }).catch(() => void 0);
|
|
9033
9585
|
const state = await continueJoin(db, ctx, env, url.origin, current, group);
|
|
9034
9586
|
if (!state) throw new Error("seat Journey reconciliation pending");
|
|
@@ -9389,8 +9941,8 @@ var handleAdminMeetings = async (req, url, env, ctx) => {
|
|
|
9389
9941
|
};
|
|
9390
9942
|
|
|
9391
9943
|
// src/crm-account-repair.ts
|
|
9392
|
-
async function repairAccountPeoplePage(
|
|
9393
|
-
const users = (await
|
|
9944
|
+
async function repairAccountPeoplePage(deps, after) {
|
|
9945
|
+
const users = (await deps.db.query({ $users: { $: {
|
|
9394
9946
|
...after ? { where: { id: { $gt: after } } } : {},
|
|
9395
9947
|
order: { id: "asc" },
|
|
9396
9948
|
limit: 50
|
|
@@ -9399,7 +9951,7 @@ async function repairAccountPeoplePage(deps2, after) {
|
|
|
9399
9951
|
const errors = [];
|
|
9400
9952
|
for (const user of users) {
|
|
9401
9953
|
try {
|
|
9402
|
-
if (await syncAccountToCrm(
|
|
9954
|
+
if (await syncAccountToCrm(deps, String(user.id))) synced++;
|
|
9403
9955
|
} catch (error) {
|
|
9404
9956
|
const message = error instanceof Error ? error.message : "";
|
|
9405
9957
|
errors.push({ userId: String(user.id), code: /^crm_projection_identity_(ambiguous|archived|conflict)$/.test(message) ? message : "crm_projection_unavailable" });
|
|
@@ -9411,8 +9963,8 @@ async function repairAccountPeoplePage(deps2, after) {
|
|
|
9411
9963
|
// src/crm-application-repair.ts
|
|
9412
9964
|
var import_crm11 = require("@odla-ai/crm");
|
|
9413
9965
|
async function repairApplicationLink(db, chapter, recordId, actorId) {
|
|
9414
|
-
const
|
|
9415
|
-
const record4 = await (0, import_crm11.getRecord)(
|
|
9966
|
+
const deps = { crm: chapter.crm, db };
|
|
9967
|
+
const record4 = await (0, import_crm11.getRecord)(deps, recordId);
|
|
9416
9968
|
if (!record4 || record4.type !== "person" || record4.archived === true) return { status: 404, error: "person_not_found" };
|
|
9417
9969
|
if (!chapter.crm.type("person").fields.applicationId) return { status: 409, error: "application_link_field_not_configured" };
|
|
9418
9970
|
const fields = record4.fields;
|
|
@@ -9430,7 +9982,7 @@ async function repairApplicationLink(db, chapter, recordId, actorId) {
|
|
|
9430
9982
|
}
|
|
9431
9983
|
const app = [...applications.values()][0];
|
|
9432
9984
|
if (!app || typeof app.id !== "string" || typeof record4.updatedAt !== "number") return { status: 409, error: "application_link_evidence_incomplete" };
|
|
9433
|
-
await (0, import_crm11.updateRecord)(
|
|
9985
|
+
await (0, import_crm11.updateRecord)(deps, {
|
|
9434
9986
|
id: recordId,
|
|
9435
9987
|
input: applicationCrmFields(chapter.crm, { applicationId: app.id, tierId: app.tierId }),
|
|
9436
9988
|
expectedUpdatedAt: record4.updatedAt,
|
|
@@ -10128,15 +10680,6 @@ async function loadGroup2(db, id6) {
|
|
|
10128
10680
|
return groups?.[0] ?? null;
|
|
10129
10681
|
}
|
|
10130
10682
|
var str2 = (v, d = "") => typeof v === "string" ? v : d;
|
|
10131
|
-
var notifyDeps = (db, env) => ({
|
|
10132
|
-
db,
|
|
10133
|
-
debugEmail: env.ODLA_DEBUG_EMAIL,
|
|
10134
|
-
envName: env.ODLA_ENV,
|
|
10135
|
-
sender: env.SEND_EMAIL,
|
|
10136
|
-
from: env.EMAIL_FROM,
|
|
10137
|
-
now: () => Date.now(),
|
|
10138
|
-
newId: () => crypto.randomUUID()
|
|
10139
|
-
});
|
|
10140
10683
|
var handleAdminGroupEmail = async (req, url, env, ctx) => {
|
|
10141
10684
|
if (url.pathname !== "/api/admin/group/email" || req.method !== "GET" && req.method !== "PUT") return null;
|
|
10142
10685
|
const got = await gate5(req, env, ctx);
|
|
@@ -10155,6 +10698,7 @@ var handleAdminGroupEmail = async (req, url, env, ctx) => {
|
|
|
10155
10698
|
groupId: group.id,
|
|
10156
10699
|
name: group.name,
|
|
10157
10700
|
replyTo: str2(group.replyTo),
|
|
10701
|
+
fromName: str2(group.fromName),
|
|
10158
10702
|
notificationEmail: str2(group.notificationEmail),
|
|
10159
10703
|
debugEmail: str2(group.debugEmail),
|
|
10160
10704
|
emailTemplates,
|
|
@@ -10181,11 +10725,11 @@ var handleAdminGroupEmail = async (req, url, env, ctx) => {
|
|
|
10181
10725
|
for (const key3 of EMAIL_TEMPLATE_NAMES) {
|
|
10182
10726
|
const t = templates[key3];
|
|
10183
10727
|
const subject = typeof t?.subject === "string" ? t.subject.trim() : "";
|
|
10184
|
-
const
|
|
10185
|
-
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);
|
|
10186
10730
|
if (/[\r\n]/.test(subject) || subject.length > 200) return json({ error: `template "${key3}" subject must be a single line under 200 characters` }, 400);
|
|
10187
|
-
if (
|
|
10188
|
-
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 };
|
|
10189
10733
|
}
|
|
10190
10734
|
const emailish = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
10191
10735
|
const notificationEmail = str2(body.notificationEmail).trim();
|
|
@@ -10194,13 +10738,15 @@ var handleAdminGroupEmail = async (req, url, env, ctx) => {
|
|
|
10194
10738
|
if (!emailish.test(notificationEmail)) return json({ error: "notification address must be a valid email" }, 400);
|
|
10195
10739
|
if (!emailish.test(replyTo)) return json({ error: "reply-to address must be a valid email" }, 400);
|
|
10196
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);
|
|
10197
10743
|
const commitmentText = str2(body.commitmentText);
|
|
10198
10744
|
const normsText = str2(body.normsText);
|
|
10199
10745
|
if (commitmentText.length > 5e3 || normsText.length > 5e3) return json({ error: "commitment/norms text is too long" }, 400);
|
|
10200
10746
|
const id6 = { ns: "groups", attr: "id", value: ctx.chapter.id };
|
|
10201
10747
|
await db.transact([
|
|
10202
10748
|
{ t: "merge", ns: "groups", id: id6, attrs: { emailTemplates: clean } },
|
|
10203
|
-
{ 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 } }
|
|
10204
10750
|
]);
|
|
10205
10751
|
return json({ ok: true });
|
|
10206
10752
|
};
|
|
@@ -10249,11 +10795,11 @@ var handleAdminEmailTest = async (req, url, env, ctx) => {
|
|
|
10249
10795
|
}
|
|
10250
10796
|
const group = await loadGroup2(db, ctx.chapter.id);
|
|
10251
10797
|
if (!group) return json({ error: "not found" }, 404);
|
|
10252
|
-
const res = await sendTemplated(
|
|
10798
|
+
const res = await sendTemplated(lifecycleNotifyDeps(db, env), {
|
|
10253
10799
|
group: emailGroupFrom(group),
|
|
10254
10800
|
template,
|
|
10255
10801
|
to: str2(group.notificationEmail),
|
|
10256
|
-
vars:
|
|
10802
|
+
vars: sampleVariables(url.origin, resolveScheduling(group.schedulingJson).timezone),
|
|
10257
10803
|
dedupeKey: `test:${template}:${Date.now()}`,
|
|
10258
10804
|
force: true
|
|
10259
10805
|
});
|
|
@@ -10280,7 +10826,7 @@ var handleAdminComms = async (req, url, env, ctx) => {
|
|
|
10280
10826
|
const app = data.applications?.[0] ?? null;
|
|
10281
10827
|
if (!app || !chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME)) return json({ error: "not found" }, 404);
|
|
10282
10828
|
const group = await loadGroup2(db, str2(app?.groupId, ctx.chapter.id));
|
|
10283
|
-
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;
|
|
10284
10830
|
const emails = (data.emailLog ?? []).filter((r) => r.template !== "adminNotification").map((r) => {
|
|
10285
10831
|
let mailBody = typeof r.body === "string" ? r.body : null;
|
|
10286
10832
|
if (!mailBody && group && vars) mailBody = renderTemplateBody(emailGroupFrom(group), String(r.template), vars);
|
|
@@ -10304,6 +10850,28 @@ var handleAdminComms = async (req, url, env, ctx) => {
|
|
|
10304
10850
|
var import_crm13 = require("@odla-ai/crm");
|
|
10305
10851
|
var import_db9 = require("@odla-ai/db");
|
|
10306
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
|
+
|
|
10307
10875
|
// src/worker-network-push.ts
|
|
10308
10876
|
var import_crm12 = require("@odla-ai/crm");
|
|
10309
10877
|
var import_db8 = require("@odla-ai/db");
|
|
@@ -10316,15 +10884,15 @@ async function pushNetworkRecord(db, env, ctx, target, record4) {
|
|
|
10316
10884
|
newId: () => crypto.randomUUID()
|
|
10317
10885
|
};
|
|
10318
10886
|
if (!secret) {
|
|
10319
|
-
const
|
|
10887
|
+
const unpaired = edgeUnpaired(ctx.chapter.id, target, env.ODLA_ENV);
|
|
10320
10888
|
await (0, import_crm12.recordDeliveryAttempt)(crmDeps3, {
|
|
10321
10889
|
recordId: record4.id,
|
|
10322
10890
|
targetId: target.id,
|
|
10323
10891
|
status: "failed",
|
|
10324
10892
|
payloadVersion: 2,
|
|
10325
|
-
error
|
|
10893
|
+
error: unpaired.message
|
|
10326
10894
|
});
|
|
10327
|
-
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 };
|
|
10328
10896
|
}
|
|
10329
10897
|
let payload;
|
|
10330
10898
|
try {
|
|
@@ -10465,17 +11033,20 @@ async function snapshotOne(db, env, ctx, target) {
|
|
|
10465
11033
|
publicSite: { available: null }
|
|
10466
11034
|
};
|
|
10467
11035
|
const secret = await getVaultSecret(db, target.secretName);
|
|
10468
|
-
if (!secret)
|
|
10469
|
-
|
|
10470
|
-
|
|
10471
|
-
|
|
10472
|
-
|
|
10473
|
-
|
|
10474
|
-
|
|
10475
|
-
|
|
10476
|
-
|
|
10477
|
-
|
|
10478
|
-
|
|
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
|
+
}
|
|
10479
11050
|
const destination = new URL("/api/network/snapshot", target.url);
|
|
10480
11051
|
try {
|
|
10481
11052
|
const transport = resolveNetworkTargetTransport(env, target);
|
|
@@ -10950,7 +11521,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
|
10950
11521
|
if (hasFields && Object.keys(fields).some((name) => !capabilities.edit?.fields.some((field) => field.id === name))) {
|
|
10951
11522
|
return json({ error: "use the verified identity or application workflow for these fields", code: "field_forbidden" }, 403);
|
|
10952
11523
|
}
|
|
10953
|
-
const
|
|
11524
|
+
const deps = { crm: ctx.chapter.crm, db };
|
|
10954
11525
|
if (hasFields && ctx.chapter.mode === "chapter" && record4.type === "person") {
|
|
10955
11526
|
const updated2 = await updateChapterPerson({
|
|
10956
11527
|
db,
|
|
@@ -10969,7 +11540,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
|
10969
11540
|
record: await operationalReaderRecord(updated2.record, reader, db, env, ctx)
|
|
10970
11541
|
});
|
|
10971
11542
|
}
|
|
10972
|
-
const updated = hasFields ? await (0, import_crm17.updateRecord)(
|
|
11543
|
+
const updated = hasFields ? await (0, import_crm17.updateRecord)(deps, {
|
|
10973
11544
|
id: record4.id,
|
|
10974
11545
|
input: fields,
|
|
10975
11546
|
mutationId: activityId,
|
|
@@ -10980,7 +11551,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
|
10980
11551
|
authorId: `network:${verified.sender}`,
|
|
10981
11552
|
meta: auditMeta
|
|
10982
11553
|
}
|
|
10983
|
-
}) : await (0, import_crm17.setStage)(
|
|
11554
|
+
}) : await (0, import_crm17.setStage)(deps, {
|
|
10984
11555
|
id: record4.id,
|
|
10985
11556
|
to: input.stage,
|
|
10986
11557
|
authorId: `network:${verified.sender}`,
|
|
@@ -11080,7 +11651,10 @@ function targetFor2(ctx, id6) {
|
|
|
11080
11651
|
}
|
|
11081
11652
|
async function readTargetRecords(db, env, ctx, target, query) {
|
|
11082
11653
|
const secret = await getVaultSecret(db, target.secretName);
|
|
11083
|
-
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
|
+
}
|
|
11084
11658
|
const destination = new URL("/api/network/records", target.url);
|
|
11085
11659
|
destination.search = query.toString();
|
|
11086
11660
|
try {
|
|
@@ -11117,7 +11691,7 @@ async function mutateTargetRecord(db, env, ctx, target, input) {
|
|
|
11117
11691
|
return json({ error: "stage transitions are not enabled for this target", code: "stage_forbidden" }, 403);
|
|
11118
11692
|
}
|
|
11119
11693
|
const secret = await getVaultSecret(db, target.secretName);
|
|
11120
|
-
if (!secret) return
|
|
11694
|
+
if (!secret) return unpairedResponse(edgeUnpaired(ctx.chapter.id, target, env.ODLA_ENV));
|
|
11121
11695
|
const destination = new URL("/api/network/records", target.url);
|
|
11122
11696
|
const body = JSON.stringify(input);
|
|
11123
11697
|
try {
|
|
@@ -11199,7 +11773,9 @@ var handleAdminNetworkRecords = async (req, url, env, ctx) => {
|
|
|
11199
11773
|
if (window.search) query.set("search", window.search);
|
|
11200
11774
|
if (window.stage) query.set("stage", window.stage);
|
|
11201
11775
|
const result = await readTargetRecords(got.db, env, ctx, target, query);
|
|
11202
|
-
if (!result.ok)
|
|
11776
|
+
if (!result.ok) {
|
|
11777
|
+
return result.unpaired ? unpairedResponse(result.unpaired) : json({ error: result.error }, result.status ?? 502);
|
|
11778
|
+
}
|
|
11203
11779
|
if ((window.stage || url.searchParams.has("sort")) && (!result.page.appliedQuery || result.page.appliedQuery.sort !== window.sortId || result.page.appliedQuery.stage !== window.stage)) {
|
|
11204
11780
|
return json({ error: "follower did not apply the requested record query" }, 502);
|
|
11205
11781
|
}
|
|
@@ -11451,7 +12027,7 @@ var handleAdminNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
|
11451
12027
|
body = JSON.stringify({ body: note, mutationId: crypto.randomUUID() });
|
|
11452
12028
|
}
|
|
11453
12029
|
const secret = await getVaultSecret(got.db, target.secretName);
|
|
11454
|
-
if (!secret) return
|
|
12030
|
+
if (!secret) return unpairedResponse(edgeUnpaired(ctx.chapter.id, target, env.ODLA_ENV));
|
|
11455
12031
|
const destination = new URL("/api/network/shared-notes", target.url);
|
|
11456
12032
|
destination.search = new URLSearchParams({ type: recordType, recordId }).toString();
|
|
11457
12033
|
try {
|
|
@@ -11505,11 +12081,11 @@ function validateNetworkPersonImport(input, allowedChapterIds) {
|
|
|
11505
12081
|
if (raw2.approved !== true || raw2.isRealPerson !== true) errors.push("import must be approved and identify a real person");
|
|
11506
12082
|
const sourceSystem = typeof raw2.sourceSystem === "string" ? raw2.sourceSystem.trim().toLowerCase() : "";
|
|
11507
12083
|
const sourceRecordId = typeof raw2.sourceRecordId === "string" ? raw2.sourceRecordId.trim() : "";
|
|
11508
|
-
const
|
|
12084
|
+
const provenance2 = typeof raw2.provenance === "string" ? raw2.provenance.trim() : "";
|
|
11509
12085
|
const displayName = typeof raw2.displayName === "string" ? raw2.displayName.trim() : "";
|
|
11510
12086
|
if (!TOKEN.test(sourceSystem) || /(?:^|[._:-])(fixture|test|development|dev)(?:$|[._:-])/.test(sourceSystem)) errors.push("sourceSystem must name an approved non-development external system");
|
|
11511
12087
|
if (!sourceRecordId || sourceRecordId.length > 200) errors.push("sourceRecordId is required and bounded");
|
|
11512
|
-
if (!
|
|
12088
|
+
if (!provenance2 || provenance2.length > 500) errors.push("provenance is required and bounded");
|
|
11513
12089
|
if (!displayName || displayName.length > 200) errors.push("displayName is required and bounded");
|
|
11514
12090
|
if (FIXTURE_MARKER.test(sourceRecordId) || FIXTURE_MARKER.test(displayName)) {
|
|
11515
12091
|
errors.push("fixture, test, sample, and dummy rows cannot become network Persons");
|
|
@@ -11577,7 +12153,7 @@ function validateNetworkPersonImport(input, allowedChapterIds) {
|
|
|
11577
12153
|
sourceKind: "external_reimport",
|
|
11578
12154
|
sourceSystem,
|
|
11579
12155
|
sourceRecordId,
|
|
11580
|
-
provenance,
|
|
12156
|
+
provenance: provenance2,
|
|
11581
12157
|
approved: true,
|
|
11582
12158
|
isRealPerson: true,
|
|
11583
12159
|
displayName,
|
|
@@ -12282,7 +12858,7 @@ async function revealNetworkPersonRegistryRecord(db, input, mutationId, actor, n
|
|
|
12282
12858
|
}
|
|
12283
12859
|
|
|
12284
12860
|
// src/worker-routes-network-person-registry.ts
|
|
12285
|
-
var
|
|
12861
|
+
var PATH5 = "/api/admin/network-person-registry";
|
|
12286
12862
|
var MAX_BODY2 = 8 * 1024 * 1024;
|
|
12287
12863
|
var MUTATION_ID4 = /^[A-Za-z0-9_-]{16,128}$/;
|
|
12288
12864
|
var positiveRevision = (value) => {
|
|
@@ -12307,7 +12883,7 @@ function registryError(error) {
|
|
|
12307
12883
|
return null;
|
|
12308
12884
|
}
|
|
12309
12885
|
var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
12310
|
-
if (url.pathname !==
|
|
12886
|
+
if (url.pathname !== PATH5) return null;
|
|
12311
12887
|
if (!ctx.chapter.networkPersonRegistry) return json({ error: "not found" }, 404);
|
|
12312
12888
|
if (req.method !== "GET" && req.method !== "POST") return json({ error: "method not allowed" }, 405);
|
|
12313
12889
|
if (req.method === "POST" && Number(req.headers.get("content-length") ?? 0) > MAX_BODY2) {
|
|
@@ -12319,9 +12895,9 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
|
12319
12895
|
if (req.method === "POST") {
|
|
12320
12896
|
let body;
|
|
12321
12897
|
try {
|
|
12322
|
-
const
|
|
12323
|
-
if (new TextEncoder().encode(
|
|
12324
|
-
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);
|
|
12325
12901
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("invalid");
|
|
12326
12902
|
body = parsed;
|
|
12327
12903
|
} catch {
|
|
@@ -12490,7 +13066,7 @@ var import_db13 = require("@odla-ai/db");
|
|
|
12490
13066
|
// src/network-commercial-parity.ts
|
|
12491
13067
|
var DIGEST = /^[0-9a-f]{64}$/;
|
|
12492
13068
|
var MAX_RECORDS = 500;
|
|
12493
|
-
var
|
|
13069
|
+
var text6 = (value, max = 255) => typeof value === "string" && value.length > 0 && value.length <= max ? value : null;
|
|
12494
13070
|
function parityMessage(partition, kind, value) {
|
|
12495
13071
|
return [
|
|
12496
13072
|
"chapter-commercial-parity",
|
|
@@ -12522,18 +13098,18 @@ async function digestWithKey(key3, partition, kind, value) {
|
|
|
12522
13098
|
async function buildChapterCommercialParitySnapshot(input) {
|
|
12523
13099
|
const requestedMax = input.maxRecords ?? MAX_RECORDS;
|
|
12524
13100
|
const maxRecords = Number.isSafeInteger(requestedMax) ? Math.max(0, Math.min(requestedMax, MAX_RECORDS)) : MAX_RECORDS;
|
|
12525
|
-
const eligible = input.rows.filter((row2) =>
|
|
13101
|
+
const eligible = input.rows.filter((row2) => text6(row2.id) && (text6(row2.stripeCustomerId) || text6(row2.stripeSubscriptionId)));
|
|
12526
13102
|
const selected2 = eligible.slice(0, maxRecords);
|
|
12527
13103
|
const key3 = await parityKey(input.secret);
|
|
12528
13104
|
const records = await Promise.all(selected2.map(async (row2) => {
|
|
12529
|
-
const applicationId =
|
|
12530
|
-
const customerId =
|
|
12531
|
-
const subscriptionId =
|
|
13105
|
+
const applicationId = text6(row2.id);
|
|
13106
|
+
const customerId = text6(row2.stripeCustomerId);
|
|
13107
|
+
const subscriptionId = text6(row2.stripeSubscriptionId);
|
|
12532
13108
|
return {
|
|
12533
13109
|
applicationDigest: await digestWithKey(key3, input.partition, "application", applicationId),
|
|
12534
13110
|
customerDigest: customerId ? await digestWithKey(key3, input.partition, "customer", customerId) : null,
|
|
12535
13111
|
subscriptionDigest: subscriptionId ? await digestWithKey(key3, input.partition, "subscription", subscriptionId) : null,
|
|
12536
|
-
status:
|
|
13112
|
+
status: text6(row2.status, 64),
|
|
12537
13113
|
canceled: row2.canceled === true
|
|
12538
13114
|
};
|
|
12539
13115
|
}));
|
|
@@ -12559,7 +13135,7 @@ function validSnapshot(snapshot, source) {
|
|
|
12559
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;
|
|
12560
13136
|
const seen = /* @__PURE__ */ new Set();
|
|
12561
13137
|
for (const record4 of records) {
|
|
12562
|
-
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;
|
|
12563
13139
|
const key3 = recordKey(record4);
|
|
12564
13140
|
if (seen.has(key3)) return false;
|
|
12565
13141
|
seen.add(key3);
|
|
@@ -12570,16 +13146,16 @@ async function reconcileCommercialParitySnapshot(input) {
|
|
|
12570
13146
|
if (!validSnapshot(input.snapshot, input.source)) throw new Error("invalid chapter commercial parity snapshot");
|
|
12571
13147
|
const sourceKey = networkCommercialSourceKey(input.source);
|
|
12572
13148
|
const key3 = await parityKey(input.secret);
|
|
12573
|
-
const shadow = await Promise.all(input.projection.subscriptions.filter((row2) => row2.sourceKey === sourceKey &&
|
|
12574
|
-
const customerId =
|
|
12575
|
-
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);
|
|
12576
13152
|
return {
|
|
12577
13153
|
applicationDigest: await digestWithKey(key3, input.snapshot.partition, "application", row2.applicationId),
|
|
12578
13154
|
customerDigest: customerId ? await digestWithKey(key3, input.snapshot.partition, "customer", customerId) : null,
|
|
12579
13155
|
subscriptionDigest: subscriptionId ? await digestWithKey(key3, input.snapshot.partition, "subscription", subscriptionId) : null,
|
|
12580
13156
|
status: null,
|
|
12581
13157
|
canceled: false,
|
|
12582
|
-
stripeStatus:
|
|
13158
|
+
stripeStatus: text6(row2.status, 64),
|
|
12583
13159
|
stripeCanceled: row2.status === "canceled"
|
|
12584
13160
|
};
|
|
12585
13161
|
}));
|
|
@@ -12705,7 +13281,7 @@ var handleNetworkCommercialParity = async (req, url, env, ctx) => {
|
|
|
12705
13281
|
async function reconcileTarget(db, env, ctx, target, projection) {
|
|
12706
13282
|
const base = { targetId: target.id, name: target.name, url: target.url };
|
|
12707
13283
|
const secret = await getVaultSecret(db, target.secretName);
|
|
12708
|
-
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) };
|
|
12709
13285
|
const destination = new URL(PARITY_PATH, target.url);
|
|
12710
13286
|
try {
|
|
12711
13287
|
const transport = resolveNetworkTargetTransport(env, target);
|
|
@@ -12861,11 +13437,23 @@ var handleNetworkCommercialStripeWebhook = async (req, url, env, ctx) => {
|
|
|
12861
13437
|
const event = await (0, import_stripe11.constructStripeEvent)(body, signature, secret);
|
|
12862
13438
|
const fact = normalizeNetworkCommercialStripeEvent(event, await stripeEventDigest(body));
|
|
12863
13439
|
if (!fact.eventId || !fact.eventType || !fact.objectId) return json({ error: "Stripe event identity is invalid" }, 400);
|
|
12864
|
-
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
|
+
);
|
|
12865
13448
|
if (result.status === "collision") {
|
|
12866
13449
|
console.error("network commercial event collision", JSON.stringify({ eventId: fact.eventId, eventType: fact.eventType }));
|
|
12867
13450
|
}
|
|
12868
|
-
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
|
+
});
|
|
12869
13457
|
} catch (error) {
|
|
12870
13458
|
if (error instanceof import_stripe11.StripeError) return json({ error: error.message, code: error.code }, 400);
|
|
12871
13459
|
throw error;
|
|
@@ -12889,18 +13477,39 @@ var handleAdminNetworkCommercialReadModel = async (req, url, env, ctx) => {
|
|
|
12889
13477
|
} catch {
|
|
12890
13478
|
return json({ error: "invalid JSON body" }, 400);
|
|
12891
13479
|
}
|
|
13480
|
+
const alerts = networkCommercialAlerts(ctx, ADMIN_PATH2);
|
|
12892
13481
|
if (parsed.command === "recover-event") {
|
|
12893
13482
|
try {
|
|
12894
13483
|
return json(await recoverNetworkCommercialEvent(
|
|
12895
13484
|
db,
|
|
12896
13485
|
ctx.chapter.networkCommercialReadModel.sources,
|
|
12897
13486
|
String(parsed.eventId ?? ""),
|
|
12898
|
-
got.user.userId
|
|
13487
|
+
got.user.userId,
|
|
13488
|
+
Date.now(),
|
|
13489
|
+
alerts
|
|
12899
13490
|
));
|
|
12900
13491
|
} catch (error) {
|
|
12901
13492
|
return json({ error: error instanceof Error ? error.message : "event recovery failed" }, 409);
|
|
12902
13493
|
}
|
|
12903
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
|
+
}
|
|
12904
13513
|
if (parsed.command === "reconcile-chapters") {
|
|
12905
13514
|
const projection2 = await loadNetworkCommercialProjection(db, ctx.chapter.networkCommercialReadModel.sources);
|
|
12906
13515
|
return json(await reconcileChapterCommercialTargets(db, env, ctx, projection2));
|
|
@@ -12928,9 +13537,9 @@ var handleAdminNetworkCommercialReadModel = async (req, url, env, ctx) => {
|
|
|
12928
13537
|
}
|
|
12929
13538
|
}
|
|
12930
13539
|
if (parsed.command !== "rebuild" || typeof parsed.mutationId !== "string" || !MUTATION_ID5.test(parsed.mutationId)) {
|
|
12931
|
-
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);
|
|
12932
13541
|
}
|
|
12933
|
-
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);
|
|
12934
13543
|
return json({ rebuilt: true, ...safeProjection(projection) });
|
|
12935
13544
|
}
|
|
12936
13545
|
return json(safeProjection(await loadNetworkCommercialProjection(db, ctx.chapter.networkCommercialReadModel.sources)));
|
|
@@ -13040,7 +13649,7 @@ var record3 = (value) => value !== null && typeof value === "object" && !Array.i
|
|
|
13040
13649
|
function unknownKeys3(value, allowed2, path, errors) {
|
|
13041
13650
|
for (const key3 of Object.keys(value)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
|
|
13042
13651
|
}
|
|
13043
|
-
function
|
|
13652
|
+
function text7(value, path, errors, max = MAX_TEXT2, required = true) {
|
|
13044
13653
|
if (value === void 0 && !required) return void 0;
|
|
13045
13654
|
if (typeof value !== "string" || required && !value.trim()) {
|
|
13046
13655
|
errors.push(`${path} must be a non-empty string`);
|
|
@@ -13066,7 +13675,7 @@ function scopeValue(value, path, errors) {
|
|
|
13066
13675
|
return { kind: raw2.kind };
|
|
13067
13676
|
}
|
|
13068
13677
|
if (raw2.kind === "chapter") {
|
|
13069
|
-
const chapterId =
|
|
13678
|
+
const chapterId = text7(raw2.chapterId, `${path}.chapterId`, errors, 63) ?? "";
|
|
13070
13679
|
if (chapterId && !APP_ID.test(chapterId)) errors.push(`${path}.chapterId must be a lowercase app slug`);
|
|
13071
13680
|
return { kind: "chapter", chapterId };
|
|
13072
13681
|
}
|
|
@@ -13080,22 +13689,22 @@ function resourceValue(value, path, errors) {
|
|
|
13080
13689
|
return null;
|
|
13081
13690
|
}
|
|
13082
13691
|
unknownKeys3(raw2, RESOURCE_KEYS, path, errors);
|
|
13083
|
-
const id6 =
|
|
13692
|
+
const id6 = text7(raw2.id, `${path}.id`, errors, 80) ?? "";
|
|
13084
13693
|
if (id6 && !ID4.test(id6)) errors.push(`${path}.id must be lowercase kebab-case`);
|
|
13085
|
-
const kind =
|
|
13694
|
+
const kind = text7(raw2.kind, `${path}.kind`, errors) ?? "";
|
|
13086
13695
|
if (!KINDS.has(kind)) errors.push(`${path}.kind is unsupported`);
|
|
13087
|
-
const title =
|
|
13088
|
-
const summary =
|
|
13089
|
-
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) ?? "";
|
|
13090
13699
|
if (!STATUSES.has(status2)) errors.push(`${path}.status must be draft, published, or archived`);
|
|
13091
13700
|
const revision = integer4(raw2.revision, `${path}.revision`, errors);
|
|
13092
13701
|
const scope2 = scopeValue(raw2.scope, `${path}.scope`, errors) ?? { kind: "global" };
|
|
13093
|
-
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) ?? "") : [];
|
|
13094
13703
|
if (!Array.isArray(raw2.tags)) errors.push(`${path}.tags must be an array`);
|
|
13095
13704
|
if (tags.length > MAX_TAGS) errors.push(`${path}.tags may contain at most ${MAX_TAGS} items`);
|
|
13096
13705
|
if (new Set(tags).size !== tags.length) errors.push(`${path}.tags must be unique`);
|
|
13097
|
-
const bodyMarkdown =
|
|
13098
|
-
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);
|
|
13099
13708
|
if (url) {
|
|
13100
13709
|
try {
|
|
13101
13710
|
if (new URL(url).protocol !== "https:") errors.push(`${path}.url must use https`);
|
|
@@ -13107,9 +13716,9 @@ function resourceValue(value, path, errors) {
|
|
|
13107
13716
|
if (raw2.brandAsset !== void 0 && !asset) errors.push(`${path}.brandAsset must be an object`);
|
|
13108
13717
|
if (asset) unknownKeys3(asset, ASSET_KEYS, `${path}.brandAsset`, errors);
|
|
13109
13718
|
const brandAsset = asset ? {
|
|
13110
|
-
bookId:
|
|
13111
|
-
assetId:
|
|
13112
|
-
...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) ?? "" }
|
|
13113
13722
|
} : void 0;
|
|
13114
13723
|
if ((kind === "document" || kind === "note") && !bodyMarkdown) errors.push(`${path}.bodyMarkdown is required for ${kind}`);
|
|
13115
13724
|
if ((kind === "link" || kind === "inspiration") && !url) errors.push(`${path}.url is required for ${kind}`);
|
|
@@ -13124,21 +13733,21 @@ function resourceValue(value, path, errors) {
|
|
|
13124
13733
|
if (provenanceRaw?.promotedFrom !== void 0 && !promotedRaw) errors.push(`${path}.provenance.promotedFrom must be an object`);
|
|
13125
13734
|
if (promotedRaw) unknownKeys3(promotedRaw, PROMOTION_KEYS, `${path}.provenance.promotedFrom`, errors);
|
|
13126
13735
|
const promotedFrom = promotedRaw ? {
|
|
13127
|
-
chapterId:
|
|
13128
|
-
resourceId:
|
|
13736
|
+
chapterId: text7(promotedRaw.chapterId, `${path}.provenance.promotedFrom.chapterId`, errors, 63) ?? "",
|
|
13737
|
+
resourceId: text7(promotedRaw.resourceId, `${path}.provenance.promotedFrom.resourceId`, errors, 80) ?? "",
|
|
13129
13738
|
resourceRevision: integer4(promotedRaw.resourceRevision, `${path}.provenance.promotedFrom.resourceRevision`, errors),
|
|
13130
13739
|
documentRevision: integer4(promotedRaw.documentRevision, `${path}.provenance.promotedFrom.documentRevision`, errors),
|
|
13131
|
-
promotedBy:
|
|
13740
|
+
promotedBy: text7(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
|
|
13132
13741
|
promotedAt: integer4(promotedRaw.promotedAt, `${path}.provenance.promotedFrom.promotedAt`, errors, 0)
|
|
13133
13742
|
} : void 0;
|
|
13134
|
-
const
|
|
13135
|
-
createdBy:
|
|
13743
|
+
const provenance2 = {
|
|
13744
|
+
createdBy: text7(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
|
|
13136
13745
|
createdAt: integer4(provenanceRaw?.createdAt, `${path}.provenance.createdAt`, errors, 0),
|
|
13137
|
-
updatedBy:
|
|
13746
|
+
updatedBy: text7(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
|
|
13138
13747
|
updatedAt: integer4(provenanceRaw?.updatedAt, `${path}.provenance.updatedAt`, errors, 0),
|
|
13139
13748
|
...promotedFrom ? { promotedFrom } : {}
|
|
13140
13749
|
};
|
|
13141
|
-
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 };
|
|
13142
13751
|
}
|
|
13143
13752
|
function validateBuilderLibraryDocument(value, options = {}) {
|
|
13144
13753
|
const errors = [];
|
|
@@ -13153,7 +13762,7 @@ function validateBuilderLibraryDocument(value, options = {}) {
|
|
|
13153
13762
|
if (sourceRaw) unknownKeys3(sourceRaw, SOURCE_KEYS2, "document.source", errors);
|
|
13154
13763
|
const sourceType = sourceRaw?.type;
|
|
13155
13764
|
if (sourceType !== "chapter" && sourceType !== "built_not_found") errors.push("document.source.type must be chapter or built_not_found");
|
|
13156
|
-
const sourceAppId =
|
|
13765
|
+
const sourceAppId = text7(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
|
|
13157
13766
|
if (sourceAppId && !APP_ID.test(sourceAppId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
13158
13767
|
if (options.sourceType && sourceType !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
|
|
13159
13768
|
if (options.sourceAppId && sourceAppId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
|
|
@@ -13161,8 +13770,8 @@ function validateBuilderLibraryDocument(value, options = {}) {
|
|
|
13161
13770
|
if (raw2.target !== void 0 && !targetRaw) errors.push("document.target must be an object");
|
|
13162
13771
|
if (targetRaw) unknownKeys3(targetRaw, TARGET_KEYS2, "document.target", errors);
|
|
13163
13772
|
const target = targetRaw ? {
|
|
13164
|
-
appId:
|
|
13165
|
-
environment:
|
|
13773
|
+
appId: text7(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
|
|
13774
|
+
environment: text7(targetRaw.environment, "document.target.environment", errors, 80) ?? ""
|
|
13166
13775
|
} : void 0;
|
|
13167
13776
|
if (options.targetAppId && target?.appId !== options.targetAppId) errors.push(`document.target.appId must be ${options.targetAppId}`);
|
|
13168
13777
|
if (!Array.isArray(raw2.resources)) errors.push("document.resources must be an array");
|
|
@@ -13796,7 +14405,11 @@ async function reconcileMembershipRenewal(db, input, now) {
|
|
|
13796
14405
|
membershipUpdate("networkMembership", id6, {
|
|
13797
14406
|
status: status2,
|
|
13798
14407
|
graceEndsAt,
|
|
13799
|
-
...paid ? {
|
|
14408
|
+
...paid ? {
|
|
14409
|
+
termStartedAt: start,
|
|
14410
|
+
termEndsAt: end,
|
|
14411
|
+
...membershipOfferingVersion(member.offeringSnapshot).seatPurchaseWindowCalendarMonths === void 0 ? { seatPurchaseEndsAt: end } : {}
|
|
14412
|
+
} : {},
|
|
13800
14413
|
revision: Number(member.revision) + 1,
|
|
13801
14414
|
updatedAt: now
|
|
13802
14415
|
}),
|
|
@@ -13956,10 +14569,11 @@ async function reserveNamedMembershipSeat(db, input, now) {
|
|
|
13956
14569
|
const id6 = (await membershipOfferIdForRequest({ ...input, requestId: "named_seat" })).replace(/^offer_/, "seat_");
|
|
13957
14570
|
let seat = await membershipLoad(db, "networkSeat", id6);
|
|
13958
14571
|
if (!seat) {
|
|
13959
|
-
|
|
14572
|
+
const offering = membershipOfferingVersion(member.offeringSnapshot), claimId = await recipientKey(input, email);
|
|
14573
|
+
const purchaseEndsAt = seatPurchaseDeadline(offering, member);
|
|
14574
|
+
if (!["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) || Number(member.refundedCents) !== 0 || now < Number(member.initialTermStartedAt) || now >= Number(member.termEndsAt) || now > purchaseEndsAt || !Number.isSafeInteger(purchaseEndsAt) || member.namedSeatId !== void 0) {
|
|
13960
14575
|
membershipFail("named_seat_purchase_unavailable");
|
|
13961
14576
|
}
|
|
13962
|
-
const offering = membershipOfferingVersion(member.offeringSnapshot), claimId = await recipientKey(input, email);
|
|
13963
14577
|
const attrs = {
|
|
13964
14578
|
id: id6,
|
|
13965
14579
|
...membershipScope(input),
|
|
@@ -13978,7 +14592,7 @@ async function reserveNamedMembershipSeat(db, input, now) {
|
|
|
13978
14592
|
status: "payment_pending",
|
|
13979
14593
|
preparedAt: now,
|
|
13980
14594
|
termEndsAt: member.termEndsAt,
|
|
13981
|
-
seatPurchaseEndsAt:
|
|
14595
|
+
seatPurchaseEndsAt: purchaseEndsAt,
|
|
13982
14596
|
offeringSnapshot: offering,
|
|
13983
14597
|
revision: 1,
|
|
13984
14598
|
createdAt: now,
|
|
@@ -14180,7 +14794,7 @@ async function confirmNamedMembershipSeatPayment(db, input, now) {
|
|
|
14180
14794
|
}
|
|
14181
14795
|
if (seat.purchasedAt === void 0) {
|
|
14182
14796
|
if (!["payment_processing", "payment_canceling"].includes(String(seat.status))) membershipFail("named_seat_payment_not_expected");
|
|
14183
|
-
const eligible = seat.status !== "payment_canceling" && ["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) && Number(member.refundedCents) === 0 && paidAt <=
|
|
14797
|
+
const eligible = seat.status !== "payment_canceling" && ["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) && Number(member.refundedCents) === 0 && paidAt <= seatPurchaseDeadline(membershipOfferingVersion(member.offeringSnapshot), member) && now < Number(member.termEndsAt);
|
|
14184
14798
|
const status2 = eligible ? "renewal_pending" : "refund_pending", effectId = `${seat.id}:${eligible ? "attach-renewal" : "refund"}`;
|
|
14185
14799
|
try {
|
|
14186
14800
|
await db.transact([
|
|
@@ -15169,7 +15783,7 @@ async function readBoundedJson(response, signal, maxBytes, label) {
|
|
|
15169
15783
|
};
|
|
15170
15784
|
signal.addEventListener("abort", cancel, { once: true });
|
|
15171
15785
|
let size = 0;
|
|
15172
|
-
let
|
|
15786
|
+
let text8 = "";
|
|
15173
15787
|
const decoder = new TextDecoder();
|
|
15174
15788
|
try {
|
|
15175
15789
|
while (true) {
|
|
@@ -15179,10 +15793,10 @@ async function readBoundedJson(response, signal, maxBytes, label) {
|
|
|
15179
15793
|
if (done) break;
|
|
15180
15794
|
size += value.byteLength;
|
|
15181
15795
|
if (size > maxBytes) throw new TypeError(`${label} exceeds limit`);
|
|
15182
|
-
|
|
15796
|
+
text8 += decoder.decode(value, { stream: true });
|
|
15183
15797
|
}
|
|
15184
15798
|
try {
|
|
15185
|
-
return JSON.parse(
|
|
15799
|
+
return JSON.parse(text8 + decoder.decode());
|
|
15186
15800
|
} catch {
|
|
15187
15801
|
throw new TypeError(`invalid ${label}`);
|
|
15188
15802
|
}
|
|
@@ -15399,7 +16013,7 @@ async function requestNamedSeatReservation(options) {
|
|
|
15399
16013
|
async function requestNamedSeatOffer(options) {
|
|
15400
16014
|
const result = await requestAuthority(options, "seat-offer", { applicationId: options.applicationId, skuId: options.skuId });
|
|
15401
16015
|
const offer = result.seat;
|
|
15402
|
-
if (!offer || offer.membershipId !== await networkMembershipId(options) || !Number.isSafeInteger(offer.amountCents) || offer.amountCents <= 0 || !/^[a-z]{3}$/.test(offer.currency) || !["month", "year"].includes(offer.interval) || typeof offer.eligible !== "boolean" || typeof offer.autoRenew !== "boolean" || !Number.isSafeInteger(offer.termEndsAt) || !Number.isSafeInteger(offer.purchaseEndsAt) || offer.digest !== await namedSeatOfferDigest(offer)) throw new Error("invalid named seat offer");
|
|
16016
|
+
if (!offer || offer.membershipId !== await networkMembershipId(options) || !Number.isSafeInteger(offer.amountCents) || offer.amountCents <= 0 || !/^[a-z]{3}$/.test(offer.currency) || !["month", "year"].includes(offer.interval) || typeof offer.eligible !== "boolean" || typeof offer.autoRenew !== "boolean" || !Number.isSafeInteger(offer.termEndsAt) || !Number.isSafeInteger(offer.purchaseEndsAt) || offer.purchaseWindowed !== void 0 && typeof offer.purchaseWindowed !== "boolean" || offer.digest !== await namedSeatOfferDigest(offer)) throw new Error("invalid named seat offer");
|
|
15403
16017
|
return offer;
|
|
15404
16018
|
}
|
|
15405
16019
|
async function requestNamedSeatInvitation(options) {
|
|
@@ -15844,6 +16458,7 @@ var BUILTIN_ROUTES = [
|
|
|
15844
16458
|
handlePaymentRecovery,
|
|
15845
16459
|
handleAdminNamedSeat,
|
|
15846
16460
|
handleAdminMembershipSeats,
|
|
16461
|
+
handleAdminMembershipPolicyVersions,
|
|
15847
16462
|
handleAdminMeetings,
|
|
15848
16463
|
handleAdminScheduling,
|
|
15849
16464
|
// Roster + identity
|