@odla-ai/chapter 0.42.4 → 0.43.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +135 -106
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -3
- package/dist/index.d.ts +7 -3
- package/dist/index.js +135 -106
- package/dist/index.js.map +1 -1
- package/dist/worker/index.cjs +300 -125
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.js +300 -125
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
- package/runbooks/adopt-existing.md +11 -0
- package/runbooks/greenfield.md +12 -0
package/dist/worker/index.cjs
CHANGED
|
@@ -1067,10 +1067,10 @@ function sharedRecordFromCrm(crm, record4, target, sourceId) {
|
|
|
1067
1067
|
if (input[nameField] === void 0) input[nameField] = record4.name;
|
|
1068
1068
|
return sourceId ? { version: 2, source: { siteId: sourceId, recordId: record4.id }, type: record4.type, input } : { version: 1, type: record4.type, hubRecordId: record4.id, input };
|
|
1069
1069
|
}
|
|
1070
|
-
async function upsertPerson(
|
|
1070
|
+
async function upsertPerson(deps2, opts) {
|
|
1071
1071
|
const email = opts.email.toLowerCase();
|
|
1072
|
-
const crmDeps3 = { crm:
|
|
1073
|
-
const { crm_record } = await
|
|
1072
|
+
const crmDeps3 = { crm: deps2.crm, db: deps2.db, now: deps2.now, newId: deps2.newId };
|
|
1073
|
+
const { crm_record } = await deps2.db.query({ crm_record: { $: { where: { type: "person", primaryEmail: email }, limit: 1 } } });
|
|
1074
1074
|
const existing = crm_record?.[0];
|
|
1075
1075
|
if (existing && typeof existing.id === "string") {
|
|
1076
1076
|
await (0, import_crm2.updateRecord)(crmDeps3, { id: existing.id, input: opts.input });
|
|
@@ -1079,24 +1079,24 @@ async function upsertPerson(deps, opts) {
|
|
|
1079
1079
|
const created = await (0, import_crm2.createRecord)(crmDeps3, { type: "person", input: opts.input, mutationId: opts.mutationId });
|
|
1080
1080
|
return { recordId: created.id };
|
|
1081
1081
|
}
|
|
1082
|
-
async function findSharedRecord(
|
|
1082
|
+
async function findSharedRecord(deps2, record4, tag) {
|
|
1083
1083
|
const structured = await (0, import_crm2.getRecordByOrigin)(
|
|
1084
|
-
{ crm:
|
|
1084
|
+
{ crm: deps2.crm, db: deps2.db, now: deps2.now, newId: deps2.newId },
|
|
1085
1085
|
record4.sourceId,
|
|
1086
1086
|
record4.sourceRecordId
|
|
1087
1087
|
);
|
|
1088
1088
|
if (structured) return structured;
|
|
1089
|
-
const mapped = await
|
|
1089
|
+
const mapped = await deps2.db.query({ crm_tag: { $: { where: { tag }, limit: 1 } } });
|
|
1090
1090
|
const mappedId = mapped.crm_tag?.[0]?.recordId;
|
|
1091
1091
|
if (typeof mappedId === "string") {
|
|
1092
|
-
const found = await
|
|
1092
|
+
const found = await deps2.db.query({ crm_record: { $: { where: { id: mappedId }, limit: 1 } } });
|
|
1093
1093
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
1094
1094
|
}
|
|
1095
|
-
const def =
|
|
1095
|
+
const def = deps2.crm.type(record4.type);
|
|
1096
1096
|
const emailField = def.emailField;
|
|
1097
1097
|
if (emailField && typeof record4.input[emailField] === "string") {
|
|
1098
1098
|
const primaryEmail = record4.input[emailField].toLowerCase();
|
|
1099
|
-
const found = await
|
|
1099
|
+
const found = await deps2.db.query({
|
|
1100
1100
|
crm_record: { $: { where: { type: record4.type, primaryEmail }, limit: 1 } }
|
|
1101
1101
|
});
|
|
1102
1102
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
@@ -1104,7 +1104,7 @@ async function findSharedRecord(deps, record4, tag) {
|
|
|
1104
1104
|
const domain = record4.input.domain;
|
|
1105
1105
|
const domainSlot = def.fields.domain?.slot;
|
|
1106
1106
|
if (typeof domain === "string" && domainSlot) {
|
|
1107
|
-
const found = await
|
|
1107
|
+
const found = await deps2.db.query({
|
|
1108
1108
|
crm_record: { $: { where: { type: record4.type, [domainSlot]: domain }, limit: 1 } }
|
|
1109
1109
|
});
|
|
1110
1110
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
@@ -1112,14 +1112,14 @@ async function findSharedRecord(deps, record4, tag) {
|
|
|
1112
1112
|
const nameField = def.nameField ?? "name";
|
|
1113
1113
|
const name = record4.input[nameField];
|
|
1114
1114
|
if (record4.type === "company" && typeof name === "string" && name.trim()) {
|
|
1115
|
-
const found = await
|
|
1115
|
+
const found = await deps2.db.query({
|
|
1116
1116
|
crm_record: { $: { where: { type: record4.type, name: name.trim() }, limit: 1 } }
|
|
1117
1117
|
});
|
|
1118
1118
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
1119
1119
|
}
|
|
1120
1120
|
return void 0;
|
|
1121
1121
|
}
|
|
1122
|
-
async function projectSharedRecord(
|
|
1122
|
+
async function projectSharedRecord(deps2, shared, options = {}) {
|
|
1123
1123
|
const record4 = normalizeSharedRecord(shared, options.sourceId);
|
|
1124
1124
|
if (!record4.type.trim() || !record4.sourceId.trim() || !record4.sourceRecordId.trim()) {
|
|
1125
1125
|
throw new Error("type, source site id, and source record id must be non-empty");
|
|
@@ -1128,8 +1128,8 @@ async function projectSharedRecord(deps, shared, options = {}) {
|
|
|
1128
1128
|
throw new Error("signed sender does not match payload source");
|
|
1129
1129
|
}
|
|
1130
1130
|
const tag = networkSourceTag(record4.type, record4.sourceRecordId, record4.version === 2 ? record4.sourceId : void 0);
|
|
1131
|
-
const crmDeps3 = { crm:
|
|
1132
|
-
const existing = await findSharedRecord(
|
|
1131
|
+
const crmDeps3 = { crm: deps2.crm, db: deps2.db, now: deps2.now, newId: deps2.newId };
|
|
1132
|
+
const existing = await findSharedRecord(deps2, record4, tag);
|
|
1133
1133
|
let recordId;
|
|
1134
1134
|
if (existing && typeof existing.id === "string") {
|
|
1135
1135
|
await (0, import_crm2.updateRecord)(crmDeps3, { id: existing.id, input: record4.input });
|
|
@@ -1142,8 +1142,8 @@ async function projectSharedRecord(deps, shared, options = {}) {
|
|
|
1142
1142
|
mutationId: `share-create:${tag}`
|
|
1143
1143
|
});
|
|
1144
1144
|
}
|
|
1145
|
-
await
|
|
1146
|
-
[{ t: "update", ns: "crm_tag", id: tag, attrs: { key: `${recordId}:${tag}`, recordId, tag, createdAt:
|
|
1145
|
+
await deps2.db.transact(
|
|
1146
|
+
[{ t: "update", ns: "crm_tag", id: tag, attrs: { key: `${recordId}:${tag}`, recordId, tag, createdAt: deps2.now() } }],
|
|
1147
1147
|
{ mutationId: `share-map:${tag}:${recordId}` }
|
|
1148
1148
|
);
|
|
1149
1149
|
await (0, import_crm2.upsertRecordOrigin)(crmDeps3, {
|
|
@@ -1155,7 +1155,7 @@ async function projectSharedRecord(deps, shared, options = {}) {
|
|
|
1155
1155
|
});
|
|
1156
1156
|
return { recordId };
|
|
1157
1157
|
}
|
|
1158
|
-
async function projectApplicant(
|
|
1158
|
+
async function projectApplicant(deps2, applicant) {
|
|
1159
1159
|
const base = sharedPersonInput({
|
|
1160
1160
|
email: applicant.email,
|
|
1161
1161
|
firstName: applicant.firstName,
|
|
@@ -1166,11 +1166,11 @@ async function projectApplicant(deps, applicant) {
|
|
|
1166
1166
|
});
|
|
1167
1167
|
const mutationId = `apply:${applicant.applicationId}`;
|
|
1168
1168
|
const extra = applicant.extra ?? {};
|
|
1169
|
-
if (Object.keys(extra).length === 0) return upsertPerson(
|
|
1169
|
+
if (Object.keys(extra).length === 0) return upsertPerson(deps2, { email: applicant.email, input: base, mutationId });
|
|
1170
1170
|
try {
|
|
1171
|
-
return await upsertPerson(
|
|
1171
|
+
return await upsertPerson(deps2, { email: applicant.email, input: { ...base, ...extra }, mutationId });
|
|
1172
1172
|
} catch {
|
|
1173
|
-
return upsertPerson(
|
|
1173
|
+
return upsertPerson(deps2, { email: applicant.email, input: base, mutationId });
|
|
1174
1174
|
}
|
|
1175
1175
|
}
|
|
1176
1176
|
|
|
@@ -1208,12 +1208,12 @@ function billingColumns(app) {
|
|
|
1208
1208
|
if (typeof app.renewalAt === "number") cols.renewalAt = app.renewalAt;
|
|
1209
1209
|
return cols;
|
|
1210
1210
|
}
|
|
1211
|
-
async function syncApplicationToCrm(
|
|
1211
|
+
async function syncApplicationToCrm(deps2, opts) {
|
|
1212
1212
|
const emailKey = str(opts.app.email).toLowerCase();
|
|
1213
1213
|
if (!emailKey) return null;
|
|
1214
|
-
const recordDeps = { crm:
|
|
1215
|
-
const input = personInputFromApp(
|
|
1216
|
-
const { crm_record } = await
|
|
1214
|
+
const recordDeps = { crm: deps2.crm, db: deps2.db, now: deps2.now, newId: deps2.newId };
|
|
1215
|
+
const input = personInputFromApp(deps2.chapter, opts.app);
|
|
1216
|
+
const { crm_record } = await deps2.db.query({
|
|
1217
1217
|
crm_record: { $: { where: { type: "person", primaryEmail: emailKey }, limit: 1 } }
|
|
1218
1218
|
});
|
|
1219
1219
|
const existing = crm_record?.[0] ?? null;
|
|
@@ -1229,12 +1229,12 @@ async function syncApplicationToCrm(deps, opts) {
|
|
|
1229
1229
|
if (existing && stage && existing.stage !== stage) {
|
|
1230
1230
|
await (0, import_crm3.setStage)(recordDeps, { id: recordId, to: stage, authorId: "system", mutationId: `crm:stage:${recordId}:${stage}` }).catch(() => void 0);
|
|
1231
1231
|
}
|
|
1232
|
-
await
|
|
1232
|
+
await deps2.db.transact([{ t: "update", ns: "crm_record", id: recordId, attrs: billingColumns(opts.app) }]);
|
|
1233
1233
|
await (0, import_crm3.linkIdentity)(recordDeps, { recordId, email: emailKey, mutationId: `crm:link:${recordId}:${emailKey}` }).catch(() => void 0);
|
|
1234
1234
|
return recordId;
|
|
1235
1235
|
}
|
|
1236
|
-
async function backfillCrm(
|
|
1237
|
-
const data = await
|
|
1236
|
+
async function backfillCrm(deps2) {
|
|
1237
|
+
const data = await deps2.db.query({
|
|
1238
1238
|
applications: { $: { order: { createdAt: "desc" }, limit: 1e3 } },
|
|
1239
1239
|
$users: { $: { limit: 1e3 } }
|
|
1240
1240
|
});
|
|
@@ -1246,7 +1246,7 @@ async function backfillCrm(deps) {
|
|
|
1246
1246
|
if (!key || seen.has(key)) return;
|
|
1247
1247
|
seen.add(key);
|
|
1248
1248
|
try {
|
|
1249
|
-
await syncApplicationToCrm(
|
|
1249
|
+
await syncApplicationToCrm(deps2, { app, stage });
|
|
1250
1250
|
synced += 1;
|
|
1251
1251
|
} catch (err) {
|
|
1252
1252
|
errors.push({ email: key, error: err instanceof Error ? err.message : String(err) });
|
|
@@ -1298,13 +1298,13 @@ function planDelivery(input) {
|
|
|
1298
1298
|
|
|
1299
1299
|
// src/notify.ts
|
|
1300
1300
|
var EMAIL_TEMPLATE_NAMES = ["adminNotification", "paymentConfirmation", "prepEmail", "onboardingInvite"];
|
|
1301
|
-
async function sendTemplated(
|
|
1302
|
-
const { emailLog } = await
|
|
1301
|
+
async function sendTemplated(deps2, input) {
|
|
1302
|
+
const { emailLog } = await deps2.db.query({ emailLog: { $: { where: { dedupeKey: input.dedupeKey } } } });
|
|
1303
1303
|
const prior = Array.isArray(emailLog) ? emailLog : [];
|
|
1304
1304
|
if (isAlreadySent(prior)) return { sent: true, reason: "already-sent" };
|
|
1305
|
-
const cloudflareReady = Boolean(
|
|
1305
|
+
const cloudflareReady = Boolean(deps2.sender && deps2.from);
|
|
1306
1306
|
const decision = planDelivery({
|
|
1307
|
-
envName:
|
|
1307
|
+
envName: deps2.envName,
|
|
1308
1308
|
group: input.group,
|
|
1309
1309
|
template: input.template,
|
|
1310
1310
|
to: input.to,
|
|
@@ -1315,10 +1315,10 @@ async function sendTemplated(deps, input) {
|
|
|
1315
1315
|
if (!decision.deliver) return { sent: false, reason: decision.reason };
|
|
1316
1316
|
let error;
|
|
1317
1317
|
let messageId;
|
|
1318
|
-
if (decision.transport === "cloudflare" &&
|
|
1318
|
+
if (decision.transport === "cloudflare" && deps2.sender && deps2.from) {
|
|
1319
1319
|
try {
|
|
1320
|
-
const res = await
|
|
1321
|
-
from:
|
|
1320
|
+
const res = await deps2.sender.send({
|
|
1321
|
+
from: deps2.from,
|
|
1322
1322
|
to: [decision.to],
|
|
1323
1323
|
subject: decision.subject,
|
|
1324
1324
|
text: decision.text,
|
|
@@ -1329,7 +1329,7 @@ async function sendTemplated(deps, input) {
|
|
|
1329
1329
|
error = e instanceof Error ? e.message : String(e);
|
|
1330
1330
|
}
|
|
1331
1331
|
}
|
|
1332
|
-
const id =
|
|
1332
|
+
const id = deps2.newId();
|
|
1333
1333
|
const row = {
|
|
1334
1334
|
id,
|
|
1335
1335
|
groupId: input.group.id,
|
|
@@ -1340,12 +1340,12 @@ async function sendTemplated(deps, input) {
|
|
|
1340
1340
|
transport: decision.transport,
|
|
1341
1341
|
redirected: decision.redirected,
|
|
1342
1342
|
dedupeKey: input.dedupeKey,
|
|
1343
|
-
sentAt:
|
|
1343
|
+
sentAt: deps2.now(),
|
|
1344
1344
|
...input.applicationId ? { applicationId: input.applicationId } : {},
|
|
1345
1345
|
...messageId ? { messageId } : {},
|
|
1346
1346
|
...error ? { error } : {}
|
|
1347
1347
|
};
|
|
1348
|
-
await
|
|
1348
|
+
await deps2.db.transact([{ t: "update", ns: "emailLog", id, attrs: row }], error ? void 0 : { mutationId: `email:${input.dedupeKey}` });
|
|
1349
1349
|
return error ? { sent: false, reason: error } : { sent: true };
|
|
1350
1350
|
}
|
|
1351
1351
|
function emailGroupFrom(row) {
|
|
@@ -3832,6 +3832,7 @@ function findApplicationRef(obj) {
|
|
|
3832
3832
|
const environment = candidates.map((metadata) => typeof metadata.odlaEnvironment === "string" ? metadata.odlaEnvironment : void 0).find(Boolean);
|
|
3833
3833
|
const dataEnvironment = candidates.map((metadata) => typeof metadata.odlaDataEnvironment === "string" ? metadata.odlaDataEnvironment : void 0).find(Boolean);
|
|
3834
3834
|
const runtime = candidates.map((metadata) => typeof metadata.odlaRuntime === "string" ? metadata.odlaRuntime : void 0).find(Boolean);
|
|
3835
|
+
const chapterId = candidates.map((metadata) => typeof metadata.chapterId === "string" ? metadata.chapterId : void 0).find(Boolean);
|
|
3835
3836
|
const customerId = typeof obj.customer === "string" ? obj.customer : void 0;
|
|
3836
3837
|
return {
|
|
3837
3838
|
...applicationId ? { applicationId } : {},
|
|
@@ -3839,7 +3840,8 @@ function findApplicationRef(obj) {
|
|
|
3839
3840
|
...appId ? { appId } : {},
|
|
3840
3841
|
...environment ? { environment } : {},
|
|
3841
3842
|
...dataEnvironment ? { dataEnvironment } : {},
|
|
3842
|
-
...runtime ? { runtime } : {}
|
|
3843
|
+
...runtime ? { runtime } : {},
|
|
3844
|
+
...chapterId ? { chapterId } : {}
|
|
3843
3845
|
};
|
|
3844
3846
|
}
|
|
3845
3847
|
function findGiftRef(obj) {
|
|
@@ -3855,13 +3857,15 @@ function findGiftRef(obj) {
|
|
|
3855
3857
|
const environment = pickIdentity("odlaEnvironment");
|
|
3856
3858
|
const dataEnvironment = pickIdentity("odlaDataEnvironment");
|
|
3857
3859
|
const runtime = pickIdentity("odlaRuntime");
|
|
3860
|
+
const chapterId = pickIdentity("chapterId");
|
|
3858
3861
|
return {
|
|
3859
3862
|
...giftIntentId ? { giftIntentId } : {},
|
|
3860
3863
|
providerObjectId: subscriptionId ?? String(obj.id ?? ""),
|
|
3861
3864
|
...appId ? { appId } : {},
|
|
3862
3865
|
...environment ? { environment } : {},
|
|
3863
3866
|
...dataEnvironment ? { dataEnvironment } : {},
|
|
3864
|
-
...runtime ? { runtime } : {}
|
|
3867
|
+
...runtime ? { runtime } : {},
|
|
3868
|
+
...chapterId ? { chapterId } : {}
|
|
3865
3869
|
};
|
|
3866
3870
|
}
|
|
3867
3871
|
function normalizeWebhookEvent(event) {
|
|
@@ -3909,8 +3913,83 @@ function canceledPatch() {
|
|
|
3909
3913
|
return { canceled: true };
|
|
3910
3914
|
}
|
|
3911
3915
|
|
|
3916
|
+
// src/payment-notifications.ts
|
|
3917
|
+
var groupFor = async (db, app) => {
|
|
3918
|
+
const result = await db.query({ groups: { $: { where: { id: String(app.groupId ?? "") }, limit: 1 } } });
|
|
3919
|
+
return result.groups?.[0];
|
|
3920
|
+
};
|
|
3921
|
+
var deps = (db, env) => ({
|
|
3922
|
+
db,
|
|
3923
|
+
envName: env.ODLA_ENV,
|
|
3924
|
+
sender: env.SEND_EMAIL,
|
|
3925
|
+
from: env.EMAIL_FROM,
|
|
3926
|
+
now: () => Date.now(),
|
|
3927
|
+
newId: () => crypto.randomUUID()
|
|
3928
|
+
});
|
|
3929
|
+
async function notifyPaymentConfirmed(db, env, eventId, app) {
|
|
3930
|
+
try {
|
|
3931
|
+
if (typeof app.email !== "string" || !app.email) return;
|
|
3932
|
+
const group = await groupFor(db, app);
|
|
3933
|
+
if (!group) return;
|
|
3934
|
+
await sendTemplated(deps(db, env), {
|
|
3935
|
+
group: emailGroupFrom(group),
|
|
3936
|
+
template: "paymentConfirmation",
|
|
3937
|
+
to: app.email,
|
|
3938
|
+
vars: { firstName: typeof app.firstName === "string" ? app.firstName : "" },
|
|
3939
|
+
dedupeKey: `${eventId}:confirm`,
|
|
3940
|
+
applicationId: String(app.id)
|
|
3941
|
+
});
|
|
3942
|
+
} catch {
|
|
3943
|
+
}
|
|
3944
|
+
}
|
|
3945
|
+
async function notifyAdminOfPayment(db, env, eventId, app) {
|
|
3946
|
+
try {
|
|
3947
|
+
const group = await groupFor(db, app);
|
|
3948
|
+
if (!group || typeof group.notificationEmail !== "string" || !group.notificationEmail) return;
|
|
3949
|
+
const text4 = (value) => typeof value === "string" ? value : "";
|
|
3950
|
+
await sendTemplated(deps(db, env), {
|
|
3951
|
+
group: emailGroupFrom(group),
|
|
3952
|
+
template: "adminNotification",
|
|
3953
|
+
to: group.notificationEmail,
|
|
3954
|
+
vars: {
|
|
3955
|
+
firstName: text4(app.firstName),
|
|
3956
|
+
lastName: text4(app.lastName),
|
|
3957
|
+
email: text4(app.email),
|
|
3958
|
+
phone: text4(app.phone),
|
|
3959
|
+
state: text4(app.state)
|
|
3960
|
+
},
|
|
3961
|
+
dedupeKey: `${eventId}:admin`,
|
|
3962
|
+
applicationId: String(app.id)
|
|
3963
|
+
});
|
|
3964
|
+
} catch {
|
|
3965
|
+
}
|
|
3966
|
+
}
|
|
3967
|
+
|
|
3912
3968
|
// src/payments-stripe.ts
|
|
3913
3969
|
var import_stripe2 = require("@odla-ai/stripe");
|
|
3970
|
+
|
|
3971
|
+
// src/stripe-runtime.ts
|
|
3972
|
+
function stripeWebhookSecretName(runtime) {
|
|
3973
|
+
if (!runtime) return "stripe_webhook_secret";
|
|
3974
|
+
if (!/^[a-z0-9]{2,12}$/.test(runtime) || runtime === "prod" || runtime === "production") {
|
|
3975
|
+
throw new Error("ODLA_RUNTIME is invalid");
|
|
3976
|
+
}
|
|
3977
|
+
return `stripe_webhook_secret__${runtime}`;
|
|
3978
|
+
}
|
|
3979
|
+
function stripeRuntimeIdentity(runtime) {
|
|
3980
|
+
return runtime || "default";
|
|
3981
|
+
}
|
|
3982
|
+
function stripePartitionMetadata(input) {
|
|
3983
|
+
return {
|
|
3984
|
+
odlaAppId: input.appId,
|
|
3985
|
+
odlaEnvironment: input.environment,
|
|
3986
|
+
odlaDataEnvironment: input.environment,
|
|
3987
|
+
odlaRuntime: stripeRuntimeIdentity(input.runtime),
|
|
3988
|
+
chapterId: input.chapterId
|
|
3989
|
+
};
|
|
3990
|
+
}
|
|
3991
|
+
|
|
3992
|
+
// src/payments-stripe.ts
|
|
3914
3993
|
async function stripeCall(sk, method, path, params, idempotencyKey) {
|
|
3915
3994
|
return (0, import_stripe2.stripeRequest)(
|
|
3916
3995
|
{ secretKey: sk },
|
|
@@ -3949,10 +4028,7 @@ function createStripeProvider(config) {
|
|
|
3949
4028
|
applicationId: input.applicationId,
|
|
3950
4029
|
groupId: input.groupId,
|
|
3951
4030
|
email: input.email,
|
|
3952
|
-
|
|
3953
|
-
odlaEnvironment: input.environment,
|
|
3954
|
-
odlaDataEnvironment: input.environment,
|
|
3955
|
-
...input.runtime ? { odlaRuntime: input.runtime } : {}
|
|
4031
|
+
...stripePartitionMetadata(input)
|
|
3956
4032
|
};
|
|
3957
4033
|
let customerId = input.existingCustomerId;
|
|
3958
4034
|
if (!customerId) {
|
|
@@ -3977,6 +4053,7 @@ function createStripeProvider(config) {
|
|
|
3977
4053
|
"payment_settings[save_default_payment_method]": "on_subscription",
|
|
3978
4054
|
"payment_settings[payment_method_types][0]": "card",
|
|
3979
4055
|
"expand[0]": "latest_invoice.confirmation_secret",
|
|
4056
|
+
"expand[1]": "latest_invoice.payment_intent",
|
|
3980
4057
|
metadata: meta
|
|
3981
4058
|
},
|
|
3982
4059
|
// Hardening: one subscription per application, so a client
|
|
@@ -3984,6 +4061,19 @@ function createStripeProvider(config) {
|
|
|
3984
4061
|
`sub:${input.applicationId}`
|
|
3985
4062
|
);
|
|
3986
4063
|
if (!sub.ok) fail("subscription create", sub);
|
|
4064
|
+
const invoice = sub.body.latest_invoice;
|
|
4065
|
+
const paymentIntent = invoice?.payment_intent;
|
|
4066
|
+
const paymentIntentId = typeof paymentIntent === "string" ? paymentIntent : paymentIntent && typeof paymentIntent === "object" ? String(paymentIntent.id ?? "") : "";
|
|
4067
|
+
if (paymentIntentId) {
|
|
4068
|
+
const tagged = await stripeCall(
|
|
4069
|
+
sk,
|
|
4070
|
+
"POST",
|
|
4071
|
+
`/v1/payment_intents/${encodeURIComponent(paymentIntentId)}`,
|
|
4072
|
+
{ metadata: meta },
|
|
4073
|
+
`payment-partition:${input.applicationId}`
|
|
4074
|
+
);
|
|
4075
|
+
if (!tagged.ok) fail("payment intent metadata", tagged);
|
|
4076
|
+
}
|
|
3987
4077
|
const clientSecret2 = clientSecretOf(sub.body);
|
|
3988
4078
|
if (!clientSecret2) fail("subscription confirmation-secret missing", sub);
|
|
3989
4079
|
return { customerId, subscriptionId: String(sub.body.id), clientSecret: clientSecret2 };
|
|
@@ -4261,16 +4351,12 @@ var clientSecret = (value) => {
|
|
|
4261
4351
|
const nested = confirmation?.client_secret ?? intent?.client_secret;
|
|
4262
4352
|
return typeof nested === "string" && nested ? nested : null;
|
|
4263
4353
|
};
|
|
4264
|
-
async function startGiftProviderCheckout(secretKey, intent, email, existingCustomerId, runtime) {
|
|
4354
|
+
async function startGiftProviderCheckout(secretKey, intent, email, existingCustomerId, runtime, appId = intent.chapterId) {
|
|
4265
4355
|
const offer = intent.offerSnapshot;
|
|
4266
4356
|
const metadata = {
|
|
4267
4357
|
giftIntentId: intent.id,
|
|
4268
|
-
chapterId: intent.chapterId,
|
|
4269
4358
|
environment: intent.environment,
|
|
4270
|
-
|
|
4271
|
-
odlaEnvironment: intent.environment,
|
|
4272
|
-
odlaDataEnvironment: intent.environment,
|
|
4273
|
-
...runtime ? { odlaRuntime: runtime } : {},
|
|
4359
|
+
...stripePartitionMetadata({ appId, chapterId: intent.chapterId, environment: intent.environment, runtime }),
|
|
4274
4360
|
offerId: intent.offerId
|
|
4275
4361
|
};
|
|
4276
4362
|
let customerId = existingCustomerId;
|
|
@@ -4287,10 +4373,20 @@ async function startGiftProviderCheckout(secretKey, intent, email, existingCusto
|
|
|
4287
4373
|
"payment_settings[save_default_payment_method]": "on_subscription",
|
|
4288
4374
|
"payment_settings[payment_method_types][0]": "card",
|
|
4289
4375
|
"expand[0]": "latest_invoice.confirmation_secret",
|
|
4376
|
+
"expand[1]": "latest_invoice.payment_intent",
|
|
4290
4377
|
metadata
|
|
4291
4378
|
}, `gift-subscription:${intent.id}`);
|
|
4292
4379
|
const secret2 = created2.ok ? clientSecret(created2.body) : null;
|
|
4293
4380
|
if (!created2.ok || !secret2) throw new Error(`gift subscription create failed (${created2.status})`);
|
|
4381
|
+
const invoice = created2.body.latest_invoice;
|
|
4382
|
+
const paymentIntent = invoice?.payment_intent;
|
|
4383
|
+
const paymentIntentId = typeof paymentIntent === "string" ? paymentIntent : paymentIntent && typeof paymentIntent === "object" ? String(paymentIntent.id ?? "") : "";
|
|
4384
|
+
if (paymentIntentId) {
|
|
4385
|
+
const tagged = await stripeCall(secretKey, "POST", `/v1/payment_intents/${encodeURIComponent(paymentIntentId)}`, {
|
|
4386
|
+
metadata
|
|
4387
|
+
}, `gift-payment-partition:${intent.id}`);
|
|
4388
|
+
if (!tagged.ok) throw new Error(`gift payment intent metadata failed (${tagged.status})`);
|
|
4389
|
+
}
|
|
4294
4390
|
return { clientSecret: secret2, providerObjectId: String(created2.body.id), customerId };
|
|
4295
4391
|
}
|
|
4296
4392
|
const price = await stripeCall(secretKey, "GET", `/v1/prices/${encodeURIComponent(offer.stripePriceId)}`);
|
|
@@ -4349,7 +4445,8 @@ async function startGiftIntent(db, input) {
|
|
|
4349
4445
|
stored,
|
|
4350
4446
|
input.purchaserEmail,
|
|
4351
4447
|
typeof stored.providerCustomerId === "string" ? stored.providerCustomerId : input.existingCustomerId,
|
|
4352
|
-
input.runtime
|
|
4448
|
+
input.runtime,
|
|
4449
|
+
input.appId
|
|
4353
4450
|
);
|
|
4354
4451
|
await db.transact([{ t: "update", ns: "giftPurchaseIntents", id: input.id, attrs: {
|
|
4355
4452
|
status: "checkout_ready",
|
|
@@ -4368,15 +4465,6 @@ async function startGiftIntent(db, input) {
|
|
|
4368
4465
|
};
|
|
4369
4466
|
}
|
|
4370
4467
|
|
|
4371
|
-
// src/stripe-runtime.ts
|
|
4372
|
-
function stripeWebhookSecretName(runtime) {
|
|
4373
|
-
if (!runtime) return "stripe_webhook_secret";
|
|
4374
|
-
if (!/^[a-z0-9]{2,12}$/.test(runtime) || runtime === "prod" || runtime === "production") {
|
|
4375
|
-
throw new Error("ODLA_RUNTIME is invalid");
|
|
4376
|
-
}
|
|
4377
|
-
return `stripe_webhook_secret__${runtime}`;
|
|
4378
|
-
}
|
|
4379
|
-
|
|
4380
4468
|
// src/payments-route-support.ts
|
|
4381
4469
|
var paymentErrorCode = (error) => error && typeof error === "object" && typeof error.code === "string" ? error.code : "unknown";
|
|
4382
4470
|
function paymentLineItems(group) {
|
|
@@ -4385,59 +4473,100 @@ function paymentLineItems(group) {
|
|
|
4385
4473
|
return { standardCents: standard, discountCents: discount, dueTodayCents: standard - discount };
|
|
4386
4474
|
}
|
|
4387
4475
|
|
|
4476
|
+
// src/member-system-contract.ts
|
|
4477
|
+
function classifyMemberStripeEvent(identity, target, hasProvenLegacyLocalLink) {
|
|
4478
|
+
const keys = ["appId", "environment", "dataEnvironment", "runtime", "chapterId"];
|
|
4479
|
+
const supplied = keys.filter((key) => identity[key] !== void 0);
|
|
4480
|
+
if (supplied.some((key) => identity[key] !== target[key])) return "ignore_sibling";
|
|
4481
|
+
if (supplied.length === keys.length) return "apply";
|
|
4482
|
+
return hasProvenLegacyLocalLink ? "apply_legacy" : "quarantine_unmatched";
|
|
4483
|
+
}
|
|
4484
|
+
|
|
4485
|
+
// src/stripe-webhook-partition.ts
|
|
4486
|
+
var firstRow2 = async (db, ns, q) => {
|
|
4487
|
+
const rows4 = (await db.query({ [ns]: { $: q } }))[ns];
|
|
4488
|
+
return Array.isArray(rows4) ? rows4[0] : void 0;
|
|
4489
|
+
};
|
|
4490
|
+
var stripeEventDigest = async (rawBody) => {
|
|
4491
|
+
const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(rawBody));
|
|
4492
|
+
return [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
4493
|
+
};
|
|
4494
|
+
var stripeEventIdentity = (event) => ({
|
|
4495
|
+
...event.appId ? { appId: event.appId } : {},
|
|
4496
|
+
...event.environment ? { environment: event.environment } : {},
|
|
4497
|
+
...event.dataEnvironment ? { dataEnvironment: event.dataEnvironment } : {},
|
|
4498
|
+
...event.runtime ? { runtime: event.runtime } : {},
|
|
4499
|
+
...event.chapterId ? { chapterId: event.chapterId } : {}
|
|
4500
|
+
});
|
|
4501
|
+
var stripeEventTarget = (env, ctx) => ({
|
|
4502
|
+
appId: env.ODLA_APP_ID,
|
|
4503
|
+
environment: env.ODLA_ENV,
|
|
4504
|
+
dataEnvironment: env.ODLA_ENV,
|
|
4505
|
+
runtime: stripeRuntimeIdentity(env.ODLA_RUNTIME),
|
|
4506
|
+
chapterId: ctx.chapter.id
|
|
4507
|
+
});
|
|
4508
|
+
var receiptFor = (db, eventId) => firstRow2(db, "stripeEventReceipts", { where: { eventId }, limit: 1 });
|
|
4509
|
+
function reportStripePartition(level, eventId, event, disposition) {
|
|
4510
|
+
console[level]("chapter stripe partition", JSON.stringify({
|
|
4511
|
+
eventId,
|
|
4512
|
+
kind: event.kind,
|
|
4513
|
+
disposition,
|
|
4514
|
+
...stripeEventIdentity(event)
|
|
4515
|
+
}));
|
|
4516
|
+
}
|
|
4517
|
+
async function storeStripeReceipt(db, input) {
|
|
4518
|
+
const existing = await receiptFor(db, input.eventId);
|
|
4519
|
+
if (existing) return existing.digest === input.digest ? "replayed" : "collision";
|
|
4520
|
+
const id = `stripe-event:${input.eventId}`;
|
|
4521
|
+
const write = await db.transact([{ t: "update", ns: "stripeEventReceipts", id, attrs: {
|
|
4522
|
+
id,
|
|
4523
|
+
eventId: input.eventId,
|
|
4524
|
+
digest: input.digest,
|
|
4525
|
+
disposition: input.disposition,
|
|
4526
|
+
kind: input.event.kind,
|
|
4527
|
+
matched: input.matched,
|
|
4528
|
+
...stripeEventIdentity(input.event),
|
|
4529
|
+
receivedAt: Date.now()
|
|
4530
|
+
} }], { mutationId: `stripe-receipt:${input.eventId}` });
|
|
4531
|
+
if (!write.duplicate) return "stored";
|
|
4532
|
+
const raced = await receiptFor(db, input.eventId);
|
|
4533
|
+
return raced?.digest === input.digest ? "replayed" : "collision";
|
|
4534
|
+
}
|
|
4535
|
+
async function quarantineStripeEvent(db, eventId, digest, event) {
|
|
4536
|
+
const disposition = "quarantine_unmatched";
|
|
4537
|
+
const stored = await storeStripeReceipt(db, { eventId, digest, event, disposition, matched: false });
|
|
4538
|
+
if (stored === "collision") {
|
|
4539
|
+
reportStripePartition("error", eventId, event, "quarantine_collision");
|
|
4540
|
+
return json({ ok: true, matched: false, collision: true });
|
|
4541
|
+
}
|
|
4542
|
+
reportStripePartition("warn", eventId, event, disposition);
|
|
4543
|
+
return json({ ok: true, matched: false, disposition, ...stored === "replayed" ? { replayed: true } : {} });
|
|
4544
|
+
}
|
|
4545
|
+
async function replayedStripeReceipt(db, eventId, digest, event) {
|
|
4546
|
+
const existing = await receiptFor(db, eventId);
|
|
4547
|
+
if (!existing) return null;
|
|
4548
|
+
if (existing.digest !== digest) {
|
|
4549
|
+
reportStripePartition("error", eventId, event, "quarantine_collision");
|
|
4550
|
+
return json({ ok: true, matched: false, collision: true });
|
|
4551
|
+
}
|
|
4552
|
+
return json({ ok: true, matched: existing.matched === true, replayed: true });
|
|
4553
|
+
}
|
|
4554
|
+
|
|
4388
4555
|
// src/worker-routes-payments.ts
|
|
4389
|
-
async function
|
|
4556
|
+
async function firstRow3(db, ns, q) {
|
|
4390
4557
|
const res = await db.query({ [ns]: { $: q } });
|
|
4391
4558
|
const rows4 = res[ns];
|
|
4392
4559
|
return Array.isArray(rows4) ? rows4[0] : void 0;
|
|
4393
4560
|
}
|
|
4394
4561
|
async function findApplication(db, event) {
|
|
4395
4562
|
if ("applicationId" in event && event.applicationId) {
|
|
4396
|
-
return
|
|
4563
|
+
return firstRow3(db, "applications", { where: { id: event.applicationId }, limit: 1 });
|
|
4397
4564
|
}
|
|
4398
4565
|
if ("customerId" in event && event.customerId) {
|
|
4399
|
-
return
|
|
4566
|
+
return firstRow3(db, "applications", { where: { stripeCustomerId: event.customerId }, order: { createdAt: "desc" }, limit: 1 });
|
|
4400
4567
|
}
|
|
4401
4568
|
return void 0;
|
|
4402
4569
|
}
|
|
4403
|
-
async function notifyPaymentConfirmed(db, env, eventId, app) {
|
|
4404
|
-
try {
|
|
4405
|
-
if (typeof app.email !== "string" || !app.email) return;
|
|
4406
|
-
const group = await firstRow2(db, "groups", { where: { id: String(app.groupId ?? "") }, limit: 1 });
|
|
4407
|
-
if (!group) return;
|
|
4408
|
-
await sendTemplated(
|
|
4409
|
-
{ db, envName: env.ODLA_ENV, sender: env.SEND_EMAIL, from: env.EMAIL_FROM, now: () => Date.now(), newId: () => crypto.randomUUID() },
|
|
4410
|
-
{
|
|
4411
|
-
group: emailGroupFrom(group),
|
|
4412
|
-
template: "paymentConfirmation",
|
|
4413
|
-
to: app.email,
|
|
4414
|
-
vars: { firstName: typeof app.firstName === "string" ? app.firstName : "" },
|
|
4415
|
-
dedupeKey: `${eventId}:confirm`,
|
|
4416
|
-
applicationId: String(app.id)
|
|
4417
|
-
}
|
|
4418
|
-
);
|
|
4419
|
-
} catch {
|
|
4420
|
-
}
|
|
4421
|
-
}
|
|
4422
|
-
async function notifyAdminOfPayment(db, env, eventId, app) {
|
|
4423
|
-
try {
|
|
4424
|
-
const group = await firstRow2(db, "groups", { where: { id: String(app.groupId ?? "") }, limit: 1 });
|
|
4425
|
-
if (!group || typeof group.notificationEmail !== "string" || !group.notificationEmail) return;
|
|
4426
|
-
const s = (v) => typeof v === "string" ? v : "";
|
|
4427
|
-
await sendTemplated(
|
|
4428
|
-
{ db, envName: env.ODLA_ENV, sender: env.SEND_EMAIL, from: env.EMAIL_FROM, now: () => Date.now(), newId: () => crypto.randomUUID() },
|
|
4429
|
-
{
|
|
4430
|
-
group: emailGroupFrom(group),
|
|
4431
|
-
template: "adminNotification",
|
|
4432
|
-
to: group.notificationEmail,
|
|
4433
|
-
vars: { firstName: s(app.firstName), lastName: s(app.lastName), email: s(app.email), phone: s(app.phone), state: s(app.state) },
|
|
4434
|
-
dedupeKey: `${eventId}:admin`,
|
|
4435
|
-
applicationId: String(app.id)
|
|
4436
|
-
}
|
|
4437
|
-
);
|
|
4438
|
-
} catch {
|
|
4439
|
-
}
|
|
4440
|
-
}
|
|
4441
4570
|
function webhookPatch(event, status) {
|
|
4442
4571
|
switch (event.kind) {
|
|
4443
4572
|
case "first_payment":
|
|
@@ -4463,10 +4592,10 @@ async function startSubscription(req, env, ctx) {
|
|
|
4463
4592
|
if (!applicationId) return json({ error: "applicationId required" }, 400);
|
|
4464
4593
|
if (body.refundPolicyAck !== true) return json({ error: "refundPolicyAck required" }, 400);
|
|
4465
4594
|
const db = ctx.makeDb(env);
|
|
4466
|
-
const app = await
|
|
4595
|
+
const app = await firstRow3(db, "applications", { where: { id: applicationId }, limit: 1 });
|
|
4467
4596
|
if (!app) return json({ error: "not found" }, 404);
|
|
4468
4597
|
if (app.status !== "submitted") return json({ error: "already processed" }, 409);
|
|
4469
|
-
const group = await
|
|
4598
|
+
const group = await firstRow3(db, "groups", { where: { id: String(app.groupId ?? ctx.chapter.id) }, limit: 1 });
|
|
4470
4599
|
const secretKey = await getVaultSecret(db, "stripe_secret_key");
|
|
4471
4600
|
if (!group || !secretKey) return json({ error: "payments not configured" }, 503);
|
|
4472
4601
|
if (typeof app.giftOfferId === "string") {
|
|
@@ -4478,6 +4607,7 @@ async function startSubscription(req, env, ctx) {
|
|
|
4478
4607
|
try {
|
|
4479
4608
|
const started = await startGiftIntent(db, {
|
|
4480
4609
|
id: typeof app.giftIntentId === "string" ? app.giftIntentId : `gift-intent:${applicationId}`,
|
|
4610
|
+
appId: env.ODLA_APP_ID,
|
|
4481
4611
|
chapterId: ctx.chapter.id,
|
|
4482
4612
|
environment: env.ODLA_ENV,
|
|
4483
4613
|
offer,
|
|
@@ -4513,8 +4643,9 @@ async function startSubscription(req, env, ctx) {
|
|
|
4513
4643
|
try {
|
|
4514
4644
|
result = await provider.createSubscription({
|
|
4515
4645
|
appId: env.ODLA_APP_ID,
|
|
4646
|
+
chapterId: ctx.chapter.id,
|
|
4516
4647
|
environment: env.ODLA_ENV,
|
|
4517
|
-
|
|
4648
|
+
runtime: stripeRuntimeIdentity(env.ODLA_RUNTIME),
|
|
4518
4649
|
applicationId,
|
|
4519
4650
|
groupId: String(group.id),
|
|
4520
4651
|
email: String(app.email ?? ""),
|
|
@@ -4533,7 +4664,7 @@ async function startSubscription(req, env, ctx) {
|
|
|
4533
4664
|
attrs: {
|
|
4534
4665
|
stripeCustomerId: result.customerId,
|
|
4535
4666
|
stripeSubscriptionId: result.subscriptionId,
|
|
4536
|
-
|
|
4667
|
+
stripeRuntime: stripeRuntimeIdentity(env.ODLA_RUNTIME),
|
|
4537
4668
|
refundPolicyAckAt: Date.now()
|
|
4538
4669
|
}
|
|
4539
4670
|
}
|
|
@@ -4549,27 +4680,41 @@ async function ingestWebhook(req, env, ctx) {
|
|
|
4549
4680
|
if (!ingest.ok) return json({ error: "invalid signature" }, 400);
|
|
4550
4681
|
const { eventId, event } = ingest;
|
|
4551
4682
|
if (event.kind === "ignored") return json({ ok: true, ignored: event.type });
|
|
4552
|
-
const
|
|
4553
|
-
const
|
|
4554
|
-
|
|
4555
|
-
|
|
4683
|
+
const digest = await stripeEventDigest(rawBody);
|
|
4684
|
+
const identity = stripeEventIdentity(event);
|
|
4685
|
+
const target = stripeEventTarget(env, ctx);
|
|
4686
|
+
const preliminary = classifyMemberStripeEvent(identity, target, false);
|
|
4687
|
+
if (preliminary === "ignore_sibling") {
|
|
4688
|
+
reportStripePartition("warn", eventId, event, preliminary);
|
|
4689
|
+
return json({ ok: true, matched: false, disposition: preliminary });
|
|
4556
4690
|
}
|
|
4557
4691
|
if (event.kind === "gift_paid" || event.kind === "gift_revoked") {
|
|
4558
|
-
const intent = await
|
|
4559
|
-
if (
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
|
|
4692
|
+
const intent = await firstRow3(db, "giftPurchaseIntents", { where: { id: event.giftIntentId }, limit: 1 });
|
|
4693
|
+
if (intent && typeof intent.runtime === "string" && intent.runtime !== target.runtime) {
|
|
4694
|
+
reportStripePartition("warn", eventId, event, "ignore_sibling");
|
|
4695
|
+
return json({ ok: true, matched: false, disposition: "ignore_sibling" });
|
|
4696
|
+
}
|
|
4697
|
+
const replay2 = await replayedStripeReceipt(db, eventId, digest, event);
|
|
4698
|
+
if (replay2) return replay2;
|
|
4699
|
+
const localIntent = Boolean(intent && intent.chapterId === ctx.chapter.id && intent.environment === env.ODLA_ENV && (typeof intent.runtime !== "string" || intent.runtime === target.runtime));
|
|
4700
|
+
const disposition2 = classifyMemberStripeEvent(identity, target, localIntent);
|
|
4701
|
+
if (!localIntent || disposition2 === "quarantine_unmatched") {
|
|
4702
|
+
return quarantineStripeEvent(db, eventId, digest, event);
|
|
4564
4703
|
}
|
|
4565
4704
|
if (event.kind === "gift_paid") {
|
|
4566
|
-
if (intent.providerObjectId !== event.providerObjectId
|
|
4567
|
-
|
|
4705
|
+
if (intent.providerObjectId !== event.providerObjectId || intent.status === "refunded" || intent.status === "canceled") {
|
|
4706
|
+
return quarantineStripeEvent(db, eventId, digest, event);
|
|
4707
|
+
}
|
|
4568
4708
|
try {
|
|
4569
4709
|
const issued = await issueGiftClaim(db, intent, {
|
|
4570
4710
|
eventId,
|
|
4571
4711
|
objectId: event.providerObjectId
|
|
4572
4712
|
}, Date.now());
|
|
4713
|
+
const stored3 = await storeStripeReceipt(db, { eventId, digest, event, disposition: disposition2, matched: true });
|
|
4714
|
+
if (stored3 === "collision") {
|
|
4715
|
+
reportStripePartition("error", eventId, event, "quarantine_collision");
|
|
4716
|
+
return json({ ok: true, matched: false, collision: true });
|
|
4717
|
+
}
|
|
4573
4718
|
return json({ ok: true, matched: true, claimId: issued.claimId });
|
|
4574
4719
|
} catch {
|
|
4575
4720
|
return json({ error: "gift issuance failed" }, 503);
|
|
@@ -4579,14 +4724,26 @@ async function ingestWebhook(req, env, ctx) {
|
|
|
4579
4724
|
status: event.reason,
|
|
4580
4725
|
updatedAt: Date.now()
|
|
4581
4726
|
} }], { mutationId: `gift-revoked-intent:${eventId}` });
|
|
4582
|
-
const claim = await
|
|
4727
|
+
const claim = await firstRow3(db, "giftClaims", { where: { intentId: event.giftIntentId }, limit: 1 });
|
|
4583
4728
|
if (claim) await revokeGiftClaim(db, claim, event.reason, Date.now());
|
|
4729
|
+
const stored2 = await storeStripeReceipt(db, { eventId, digest, event, disposition: disposition2, matched: Boolean(claim) });
|
|
4730
|
+
if (stored2 === "collision") {
|
|
4731
|
+
reportStripePartition("error", eventId, event, "quarantine_collision");
|
|
4732
|
+
return json({ ok: true, matched: false, collision: true });
|
|
4733
|
+
}
|
|
4584
4734
|
return json({ ok: true, matched: Boolean(claim) });
|
|
4585
4735
|
}
|
|
4586
4736
|
const app = await findApplication(db, event);
|
|
4587
|
-
if (
|
|
4588
|
-
|
|
4589
|
-
return json({ ok: true, matched: false });
|
|
4737
|
+
if (app && typeof app.stripeRuntime === "string" && app.stripeRuntime !== target.runtime) {
|
|
4738
|
+
reportStripePartition("warn", eventId, event, "ignore_sibling");
|
|
4739
|
+
return json({ ok: true, matched: false, disposition: "ignore_sibling" });
|
|
4740
|
+
}
|
|
4741
|
+
const replay = await replayedStripeReceipt(db, eventId, digest, event);
|
|
4742
|
+
if (replay) return replay;
|
|
4743
|
+
const localApplication = Boolean(app && (typeof app.stripeRuntime !== "string" || app.stripeRuntime === target.runtime));
|
|
4744
|
+
const disposition = classifyMemberStripeEvent(identity, target, localApplication);
|
|
4745
|
+
if (!localApplication || disposition === "quarantine_unmatched") {
|
|
4746
|
+
return quarantineStripeEvent(db, eventId, digest, event);
|
|
4590
4747
|
}
|
|
4591
4748
|
const patch = webhookPatch(event, String(app.status ?? ""));
|
|
4592
4749
|
let next = app;
|
|
@@ -4609,6 +4766,11 @@ async function ingestWebhook(req, env, ctx) {
|
|
|
4609
4766
|
});
|
|
4610
4767
|
}
|
|
4611
4768
|
}
|
|
4769
|
+
const stored = await storeStripeReceipt(db, { eventId, digest, event, disposition, matched: true });
|
|
4770
|
+
if (stored === "collision") {
|
|
4771
|
+
reportStripePartition("error", eventId, event, "quarantine_collision");
|
|
4772
|
+
return json({ ok: true, matched: false, collision: true });
|
|
4773
|
+
}
|
|
4612
4774
|
return json({ ok: true });
|
|
4613
4775
|
}
|
|
4614
4776
|
var handlePayments = async (req, url, env, ctx) => {
|
|
@@ -4686,6 +4848,7 @@ var handleGifts = async (req, url, env, ctx) => {
|
|
|
4686
4848
|
try {
|
|
4687
4849
|
return json(await startGiftIntent(db, {
|
|
4688
4850
|
id: `gift-intent:${ctx.chapter.id}:${purchaseId}`,
|
|
4851
|
+
appId: env.ODLA_APP_ID,
|
|
4689
4852
|
chapterId: ctx.chapter.id,
|
|
4690
4853
|
environment: env.ODLA_ENV,
|
|
4691
4854
|
offer,
|
|
@@ -5435,7 +5598,19 @@ var handleAdminRefund = async (req, url, env, ctx) => {
|
|
|
5435
5598
|
const succeeded = (charges.body.data ?? []).filter((c) => c.status === "succeeded" && c.refunded !== true);
|
|
5436
5599
|
const firstCharge = succeeded[succeeded.length - 1];
|
|
5437
5600
|
if (!firstCharge) return json({ error: "no paid charge to refund" }, 409);
|
|
5438
|
-
const refund = await stripeCall(sk, "POST", "/v1/refunds", {
|
|
5601
|
+
const refund = await stripeCall(sk, "POST", "/v1/refunds", {
|
|
5602
|
+
charge: String(firstCharge.id),
|
|
5603
|
+
metadata: {
|
|
5604
|
+
...stripePartitionMetadata({
|
|
5605
|
+
appId: env.ODLA_APP_ID,
|
|
5606
|
+
chapterId: ctx.chapter.id,
|
|
5607
|
+
environment: env.ODLA_ENV,
|
|
5608
|
+
runtime: env.ODLA_RUNTIME
|
|
5609
|
+
}),
|
|
5610
|
+
applicationId: String(app.id),
|
|
5611
|
+
groupId: String(app.groupId ?? ctx.chapter.id)
|
|
5612
|
+
}
|
|
5613
|
+
}, `refund:${String(app.id)}:${String(firstCharge.id)}`);
|
|
5439
5614
|
if (!refund.ok) return json({ error: "refund failed upstream" }, 502);
|
|
5440
5615
|
let subscriptionCanceled = false;
|
|
5441
5616
|
if (ctx.chapter.operations.refund.cancelSubscription && subscriptionId) {
|
|
@@ -6106,8 +6281,8 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
|
6106
6281
|
}
|
|
6107
6282
|
};
|
|
6108
6283
|
try {
|
|
6109
|
-
const
|
|
6110
|
-
const updated = hasFields ? await (0, import_crm9.updateRecord)(
|
|
6284
|
+
const deps2 = { crm: ctx.chapter.crm, db };
|
|
6285
|
+
const updated = hasFields ? await (0, import_crm9.updateRecord)(deps2, {
|
|
6111
6286
|
id: record4.id,
|
|
6112
6287
|
input: fields,
|
|
6113
6288
|
mutationId: activityId,
|
|
@@ -6118,7 +6293,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
|
6118
6293
|
authorId: `network:${verified.sender}`,
|
|
6119
6294
|
meta: auditMeta
|
|
6120
6295
|
}
|
|
6121
|
-
}) : await (0, import_crm9.setStage)(
|
|
6296
|
+
}) : await (0, import_crm9.setStage)(deps2, {
|
|
6122
6297
|
id: record4.id,
|
|
6123
6298
|
to: input.stage,
|
|
6124
6299
|
authorId: `network:${verified.sender}`,
|