@odla-ai/chapter 0.42.3 → 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 +600 -112
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +196 -4
- package/dist/index.d.ts +196 -4
- package/dist/index.js +600 -112
- 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.js
CHANGED
|
@@ -1037,10 +1037,10 @@ function sharedRecordFromCrm(crm, record4, target, sourceId) {
|
|
|
1037
1037
|
if (input[nameField] === void 0) input[nameField] = record4.name;
|
|
1038
1038
|
return sourceId ? { version: 2, source: { siteId: sourceId, recordId: record4.id }, type: record4.type, input } : { version: 1, type: record4.type, hubRecordId: record4.id, input };
|
|
1039
1039
|
}
|
|
1040
|
-
async function upsertPerson(
|
|
1040
|
+
async function upsertPerson(deps2, opts) {
|
|
1041
1041
|
const email = opts.email.toLowerCase();
|
|
1042
|
-
const crmDeps3 = { crm:
|
|
1043
|
-
const { crm_record } = await
|
|
1042
|
+
const crmDeps3 = { crm: deps2.crm, db: deps2.db, now: deps2.now, newId: deps2.newId };
|
|
1043
|
+
const { crm_record } = await deps2.db.query({ crm_record: { $: { where: { type: "person", primaryEmail: email }, limit: 1 } } });
|
|
1044
1044
|
const existing = crm_record?.[0];
|
|
1045
1045
|
if (existing && typeof existing.id === "string") {
|
|
1046
1046
|
await updateRecord(crmDeps3, { id: existing.id, input: opts.input });
|
|
@@ -1049,24 +1049,24 @@ async function upsertPerson(deps, opts) {
|
|
|
1049
1049
|
const created = await createRecord(crmDeps3, { type: "person", input: opts.input, mutationId: opts.mutationId });
|
|
1050
1050
|
return { recordId: created.id };
|
|
1051
1051
|
}
|
|
1052
|
-
async function findSharedRecord(
|
|
1052
|
+
async function findSharedRecord(deps2, record4, tag) {
|
|
1053
1053
|
const structured = await getRecordByOrigin(
|
|
1054
|
-
{ crm:
|
|
1054
|
+
{ crm: deps2.crm, db: deps2.db, now: deps2.now, newId: deps2.newId },
|
|
1055
1055
|
record4.sourceId,
|
|
1056
1056
|
record4.sourceRecordId
|
|
1057
1057
|
);
|
|
1058
1058
|
if (structured) return structured;
|
|
1059
|
-
const mapped = await
|
|
1059
|
+
const mapped = await deps2.db.query({ crm_tag: { $: { where: { tag }, limit: 1 } } });
|
|
1060
1060
|
const mappedId = mapped.crm_tag?.[0]?.recordId;
|
|
1061
1061
|
if (typeof mappedId === "string") {
|
|
1062
|
-
const found = await
|
|
1062
|
+
const found = await deps2.db.query({ crm_record: { $: { where: { id: mappedId }, limit: 1 } } });
|
|
1063
1063
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
1064
1064
|
}
|
|
1065
|
-
const def =
|
|
1065
|
+
const def = deps2.crm.type(record4.type);
|
|
1066
1066
|
const emailField = def.emailField;
|
|
1067
1067
|
if (emailField && typeof record4.input[emailField] === "string") {
|
|
1068
1068
|
const primaryEmail = record4.input[emailField].toLowerCase();
|
|
1069
|
-
const found = await
|
|
1069
|
+
const found = await deps2.db.query({
|
|
1070
1070
|
crm_record: { $: { where: { type: record4.type, primaryEmail }, limit: 1 } }
|
|
1071
1071
|
});
|
|
1072
1072
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
@@ -1074,7 +1074,7 @@ async function findSharedRecord(deps, record4, tag) {
|
|
|
1074
1074
|
const domain = record4.input.domain;
|
|
1075
1075
|
const domainSlot = def.fields.domain?.slot;
|
|
1076
1076
|
if (typeof domain === "string" && domainSlot) {
|
|
1077
|
-
const found = await
|
|
1077
|
+
const found = await deps2.db.query({
|
|
1078
1078
|
crm_record: { $: { where: { type: record4.type, [domainSlot]: domain }, limit: 1 } }
|
|
1079
1079
|
});
|
|
1080
1080
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
@@ -1082,14 +1082,14 @@ async function findSharedRecord(deps, record4, tag) {
|
|
|
1082
1082
|
const nameField = def.nameField ?? "name";
|
|
1083
1083
|
const name = record4.input[nameField];
|
|
1084
1084
|
if (record4.type === "company" && typeof name === "string" && name.trim()) {
|
|
1085
|
-
const found = await
|
|
1085
|
+
const found = await deps2.db.query({
|
|
1086
1086
|
crm_record: { $: { where: { type: record4.type, name: name.trim() }, limit: 1 } }
|
|
1087
1087
|
});
|
|
1088
1088
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
1089
1089
|
}
|
|
1090
1090
|
return void 0;
|
|
1091
1091
|
}
|
|
1092
|
-
async function projectSharedRecord(
|
|
1092
|
+
async function projectSharedRecord(deps2, shared, options = {}) {
|
|
1093
1093
|
const record4 = normalizeSharedRecord(shared, options.sourceId);
|
|
1094
1094
|
if (!record4.type.trim() || !record4.sourceId.trim() || !record4.sourceRecordId.trim()) {
|
|
1095
1095
|
throw new Error("type, source site id, and source record id must be non-empty");
|
|
@@ -1098,8 +1098,8 @@ async function projectSharedRecord(deps, shared, options = {}) {
|
|
|
1098
1098
|
throw new Error("signed sender does not match payload source");
|
|
1099
1099
|
}
|
|
1100
1100
|
const tag = networkSourceTag(record4.type, record4.sourceRecordId, record4.version === 2 ? record4.sourceId : void 0);
|
|
1101
|
-
const crmDeps3 = { crm:
|
|
1102
|
-
const existing = await findSharedRecord(
|
|
1101
|
+
const crmDeps3 = { crm: deps2.crm, db: deps2.db, now: deps2.now, newId: deps2.newId };
|
|
1102
|
+
const existing = await findSharedRecord(deps2, record4, tag);
|
|
1103
1103
|
let recordId;
|
|
1104
1104
|
if (existing && typeof existing.id === "string") {
|
|
1105
1105
|
await updateRecord(crmDeps3, { id: existing.id, input: record4.input });
|
|
@@ -1112,8 +1112,8 @@ async function projectSharedRecord(deps, shared, options = {}) {
|
|
|
1112
1112
|
mutationId: `share-create:${tag}`
|
|
1113
1113
|
});
|
|
1114
1114
|
}
|
|
1115
|
-
await
|
|
1116
|
-
[{ t: "update", ns: "crm_tag", id: tag, attrs: { key: `${recordId}:${tag}`, recordId, tag, createdAt:
|
|
1115
|
+
await deps2.db.transact(
|
|
1116
|
+
[{ t: "update", ns: "crm_tag", id: tag, attrs: { key: `${recordId}:${tag}`, recordId, tag, createdAt: deps2.now() } }],
|
|
1117
1117
|
{ mutationId: `share-map:${tag}:${recordId}` }
|
|
1118
1118
|
);
|
|
1119
1119
|
await upsertRecordOrigin(crmDeps3, {
|
|
@@ -1125,7 +1125,7 @@ async function projectSharedRecord(deps, shared, options = {}) {
|
|
|
1125
1125
|
});
|
|
1126
1126
|
return { recordId };
|
|
1127
1127
|
}
|
|
1128
|
-
async function projectApplicant(
|
|
1128
|
+
async function projectApplicant(deps2, applicant) {
|
|
1129
1129
|
const base = sharedPersonInput({
|
|
1130
1130
|
email: applicant.email,
|
|
1131
1131
|
firstName: applicant.firstName,
|
|
@@ -1136,11 +1136,11 @@ async function projectApplicant(deps, applicant) {
|
|
|
1136
1136
|
});
|
|
1137
1137
|
const mutationId = `apply:${applicant.applicationId}`;
|
|
1138
1138
|
const extra = applicant.extra ?? {};
|
|
1139
|
-
if (Object.keys(extra).length === 0) return upsertPerson(
|
|
1139
|
+
if (Object.keys(extra).length === 0) return upsertPerson(deps2, { email: applicant.email, input: base, mutationId });
|
|
1140
1140
|
try {
|
|
1141
|
-
return await upsertPerson(
|
|
1141
|
+
return await upsertPerson(deps2, { email: applicant.email, input: { ...base, ...extra }, mutationId });
|
|
1142
1142
|
} catch {
|
|
1143
|
-
return upsertPerson(
|
|
1143
|
+
return upsertPerson(deps2, { email: applicant.email, input: base, mutationId });
|
|
1144
1144
|
}
|
|
1145
1145
|
}
|
|
1146
1146
|
|
|
@@ -1178,12 +1178,12 @@ function billingColumns(app) {
|
|
|
1178
1178
|
if (typeof app.renewalAt === "number") cols.renewalAt = app.renewalAt;
|
|
1179
1179
|
return cols;
|
|
1180
1180
|
}
|
|
1181
|
-
async function syncApplicationToCrm(
|
|
1181
|
+
async function syncApplicationToCrm(deps2, opts) {
|
|
1182
1182
|
const emailKey = str(opts.app.email).toLowerCase();
|
|
1183
1183
|
if (!emailKey) return null;
|
|
1184
|
-
const recordDeps = { crm:
|
|
1185
|
-
const input = personInputFromApp(
|
|
1186
|
-
const { crm_record } = await
|
|
1184
|
+
const recordDeps = { crm: deps2.crm, db: deps2.db, now: deps2.now, newId: deps2.newId };
|
|
1185
|
+
const input = personInputFromApp(deps2.chapter, opts.app);
|
|
1186
|
+
const { crm_record } = await deps2.db.query({
|
|
1187
1187
|
crm_record: { $: { where: { type: "person", primaryEmail: emailKey }, limit: 1 } }
|
|
1188
1188
|
});
|
|
1189
1189
|
const existing = crm_record?.[0] ?? null;
|
|
@@ -1199,12 +1199,12 @@ async function syncApplicationToCrm(deps, opts) {
|
|
|
1199
1199
|
if (existing && stage && existing.stage !== stage) {
|
|
1200
1200
|
await setStage(recordDeps, { id: recordId, to: stage, authorId: "system", mutationId: `crm:stage:${recordId}:${stage}` }).catch(() => void 0);
|
|
1201
1201
|
}
|
|
1202
|
-
await
|
|
1202
|
+
await deps2.db.transact([{ t: "update", ns: "crm_record", id: recordId, attrs: billingColumns(opts.app) }]);
|
|
1203
1203
|
await linkIdentity(recordDeps, { recordId, email: emailKey, mutationId: `crm:link:${recordId}:${emailKey}` }).catch(() => void 0);
|
|
1204
1204
|
return recordId;
|
|
1205
1205
|
}
|
|
1206
|
-
async function backfillCrm(
|
|
1207
|
-
const data = await
|
|
1206
|
+
async function backfillCrm(deps2) {
|
|
1207
|
+
const data = await deps2.db.query({
|
|
1208
1208
|
applications: { $: { order: { createdAt: "desc" }, limit: 1e3 } },
|
|
1209
1209
|
$users: { $: { limit: 1e3 } }
|
|
1210
1210
|
});
|
|
@@ -1216,7 +1216,7 @@ async function backfillCrm(deps) {
|
|
|
1216
1216
|
if (!key || seen.has(key)) return;
|
|
1217
1217
|
seen.add(key);
|
|
1218
1218
|
try {
|
|
1219
|
-
await syncApplicationToCrm(
|
|
1219
|
+
await syncApplicationToCrm(deps2, { app, stage });
|
|
1220
1220
|
synced += 1;
|
|
1221
1221
|
} catch (err) {
|
|
1222
1222
|
errors.push({ email: key, error: err instanceof Error ? err.message : String(err) });
|
|
@@ -1268,13 +1268,13 @@ function planDelivery(input) {
|
|
|
1268
1268
|
|
|
1269
1269
|
// src/notify.ts
|
|
1270
1270
|
var EMAIL_TEMPLATE_NAMES = ["adminNotification", "paymentConfirmation", "prepEmail", "onboardingInvite"];
|
|
1271
|
-
async function sendTemplated(
|
|
1272
|
-
const { emailLog } = await
|
|
1271
|
+
async function sendTemplated(deps2, input) {
|
|
1272
|
+
const { emailLog } = await deps2.db.query({ emailLog: { $: { where: { dedupeKey: input.dedupeKey } } } });
|
|
1273
1273
|
const prior = Array.isArray(emailLog) ? emailLog : [];
|
|
1274
1274
|
if (isAlreadySent(prior)) return { sent: true, reason: "already-sent" };
|
|
1275
|
-
const cloudflareReady = Boolean(
|
|
1275
|
+
const cloudflareReady = Boolean(deps2.sender && deps2.from);
|
|
1276
1276
|
const decision = planDelivery({
|
|
1277
|
-
envName:
|
|
1277
|
+
envName: deps2.envName,
|
|
1278
1278
|
group: input.group,
|
|
1279
1279
|
template: input.template,
|
|
1280
1280
|
to: input.to,
|
|
@@ -1285,10 +1285,10 @@ async function sendTemplated(deps, input) {
|
|
|
1285
1285
|
if (!decision.deliver) return { sent: false, reason: decision.reason };
|
|
1286
1286
|
let error;
|
|
1287
1287
|
let messageId;
|
|
1288
|
-
if (decision.transport === "cloudflare" &&
|
|
1288
|
+
if (decision.transport === "cloudflare" && deps2.sender && deps2.from) {
|
|
1289
1289
|
try {
|
|
1290
|
-
const res = await
|
|
1291
|
-
from:
|
|
1290
|
+
const res = await deps2.sender.send({
|
|
1291
|
+
from: deps2.from,
|
|
1292
1292
|
to: [decision.to],
|
|
1293
1293
|
subject: decision.subject,
|
|
1294
1294
|
text: decision.text,
|
|
@@ -1299,7 +1299,7 @@ async function sendTemplated(deps, input) {
|
|
|
1299
1299
|
error = e instanceof Error ? e.message : String(e);
|
|
1300
1300
|
}
|
|
1301
1301
|
}
|
|
1302
|
-
const id =
|
|
1302
|
+
const id = deps2.newId();
|
|
1303
1303
|
const row = {
|
|
1304
1304
|
id,
|
|
1305
1305
|
groupId: input.group.id,
|
|
@@ -1310,12 +1310,12 @@ async function sendTemplated(deps, input) {
|
|
|
1310
1310
|
transport: decision.transport,
|
|
1311
1311
|
redirected: decision.redirected,
|
|
1312
1312
|
dedupeKey: input.dedupeKey,
|
|
1313
|
-
sentAt:
|
|
1313
|
+
sentAt: deps2.now(),
|
|
1314
1314
|
...input.applicationId ? { applicationId: input.applicationId } : {},
|
|
1315
1315
|
...messageId ? { messageId } : {},
|
|
1316
1316
|
...error ? { error } : {}
|
|
1317
1317
|
};
|
|
1318
|
-
await
|
|
1318
|
+
await deps2.db.transact([{ t: "update", ns: "emailLog", id, attrs: row }], error ? void 0 : { mutationId: `email:${input.dedupeKey}` });
|
|
1319
1319
|
return error ? { sent: false, reason: error } : { sent: true };
|
|
1320
1320
|
}
|
|
1321
1321
|
function emailGroupFrom(row) {
|
|
@@ -3802,6 +3802,7 @@ function findApplicationRef(obj) {
|
|
|
3802
3802
|
const environment = candidates.map((metadata) => typeof metadata.odlaEnvironment === "string" ? metadata.odlaEnvironment : void 0).find(Boolean);
|
|
3803
3803
|
const dataEnvironment = candidates.map((metadata) => typeof metadata.odlaDataEnvironment === "string" ? metadata.odlaDataEnvironment : void 0).find(Boolean);
|
|
3804
3804
|
const runtime = candidates.map((metadata) => typeof metadata.odlaRuntime === "string" ? metadata.odlaRuntime : void 0).find(Boolean);
|
|
3805
|
+
const chapterId = candidates.map((metadata) => typeof metadata.chapterId === "string" ? metadata.chapterId : void 0).find(Boolean);
|
|
3805
3806
|
const customerId = typeof obj.customer === "string" ? obj.customer : void 0;
|
|
3806
3807
|
return {
|
|
3807
3808
|
...applicationId ? { applicationId } : {},
|
|
@@ -3809,7 +3810,8 @@ function findApplicationRef(obj) {
|
|
|
3809
3810
|
...appId ? { appId } : {},
|
|
3810
3811
|
...environment ? { environment } : {},
|
|
3811
3812
|
...dataEnvironment ? { dataEnvironment } : {},
|
|
3812
|
-
...runtime ? { runtime } : {}
|
|
3813
|
+
...runtime ? { runtime } : {},
|
|
3814
|
+
...chapterId ? { chapterId } : {}
|
|
3813
3815
|
};
|
|
3814
3816
|
}
|
|
3815
3817
|
function findGiftRef(obj) {
|
|
@@ -3825,13 +3827,15 @@ function findGiftRef(obj) {
|
|
|
3825
3827
|
const environment = pickIdentity("odlaEnvironment");
|
|
3826
3828
|
const dataEnvironment = pickIdentity("odlaDataEnvironment");
|
|
3827
3829
|
const runtime = pickIdentity("odlaRuntime");
|
|
3830
|
+
const chapterId = pickIdentity("chapterId");
|
|
3828
3831
|
return {
|
|
3829
3832
|
...giftIntentId ? { giftIntentId } : {},
|
|
3830
3833
|
providerObjectId: subscriptionId ?? String(obj.id ?? ""),
|
|
3831
3834
|
...appId ? { appId } : {},
|
|
3832
3835
|
...environment ? { environment } : {},
|
|
3833
3836
|
...dataEnvironment ? { dataEnvironment } : {},
|
|
3834
|
-
...runtime ? { runtime } : {}
|
|
3837
|
+
...runtime ? { runtime } : {},
|
|
3838
|
+
...chapterId ? { chapterId } : {}
|
|
3835
3839
|
};
|
|
3836
3840
|
}
|
|
3837
3841
|
function normalizeWebhookEvent(event) {
|
|
@@ -3879,8 +3883,83 @@ function canceledPatch() {
|
|
|
3879
3883
|
return { canceled: true };
|
|
3880
3884
|
}
|
|
3881
3885
|
|
|
3886
|
+
// src/payment-notifications.ts
|
|
3887
|
+
var groupFor = async (db, app) => {
|
|
3888
|
+
const result = await db.query({ groups: { $: { where: { id: String(app.groupId ?? "") }, limit: 1 } } });
|
|
3889
|
+
return result.groups?.[0];
|
|
3890
|
+
};
|
|
3891
|
+
var deps = (db, env) => ({
|
|
3892
|
+
db,
|
|
3893
|
+
envName: env.ODLA_ENV,
|
|
3894
|
+
sender: env.SEND_EMAIL,
|
|
3895
|
+
from: env.EMAIL_FROM,
|
|
3896
|
+
now: () => Date.now(),
|
|
3897
|
+
newId: () => crypto.randomUUID()
|
|
3898
|
+
});
|
|
3899
|
+
async function notifyPaymentConfirmed(db, env, eventId, app) {
|
|
3900
|
+
try {
|
|
3901
|
+
if (typeof app.email !== "string" || !app.email) return;
|
|
3902
|
+
const group = await groupFor(db, app);
|
|
3903
|
+
if (!group) return;
|
|
3904
|
+
await sendTemplated(deps(db, env), {
|
|
3905
|
+
group: emailGroupFrom(group),
|
|
3906
|
+
template: "paymentConfirmation",
|
|
3907
|
+
to: app.email,
|
|
3908
|
+
vars: { firstName: typeof app.firstName === "string" ? app.firstName : "" },
|
|
3909
|
+
dedupeKey: `${eventId}:confirm`,
|
|
3910
|
+
applicationId: String(app.id)
|
|
3911
|
+
});
|
|
3912
|
+
} catch {
|
|
3913
|
+
}
|
|
3914
|
+
}
|
|
3915
|
+
async function notifyAdminOfPayment(db, env, eventId, app) {
|
|
3916
|
+
try {
|
|
3917
|
+
const group = await groupFor(db, app);
|
|
3918
|
+
if (!group || typeof group.notificationEmail !== "string" || !group.notificationEmail) return;
|
|
3919
|
+
const text4 = (value) => typeof value === "string" ? value : "";
|
|
3920
|
+
await sendTemplated(deps(db, env), {
|
|
3921
|
+
group: emailGroupFrom(group),
|
|
3922
|
+
template: "adminNotification",
|
|
3923
|
+
to: group.notificationEmail,
|
|
3924
|
+
vars: {
|
|
3925
|
+
firstName: text4(app.firstName),
|
|
3926
|
+
lastName: text4(app.lastName),
|
|
3927
|
+
email: text4(app.email),
|
|
3928
|
+
phone: text4(app.phone),
|
|
3929
|
+
state: text4(app.state)
|
|
3930
|
+
},
|
|
3931
|
+
dedupeKey: `${eventId}:admin`,
|
|
3932
|
+
applicationId: String(app.id)
|
|
3933
|
+
});
|
|
3934
|
+
} catch {
|
|
3935
|
+
}
|
|
3936
|
+
}
|
|
3937
|
+
|
|
3882
3938
|
// src/payments-stripe.ts
|
|
3883
3939
|
import { stripeRequest, verifyStripeSignature as verifyStripeSignature2 } from "@odla-ai/stripe";
|
|
3940
|
+
|
|
3941
|
+
// src/stripe-runtime.ts
|
|
3942
|
+
function stripeWebhookSecretName(runtime) {
|
|
3943
|
+
if (!runtime) return "stripe_webhook_secret";
|
|
3944
|
+
if (!/^[a-z0-9]{2,12}$/.test(runtime) || runtime === "prod" || runtime === "production") {
|
|
3945
|
+
throw new Error("ODLA_RUNTIME is invalid");
|
|
3946
|
+
}
|
|
3947
|
+
return `stripe_webhook_secret__${runtime}`;
|
|
3948
|
+
}
|
|
3949
|
+
function stripeRuntimeIdentity(runtime) {
|
|
3950
|
+
return runtime || "default";
|
|
3951
|
+
}
|
|
3952
|
+
function stripePartitionMetadata(input) {
|
|
3953
|
+
return {
|
|
3954
|
+
odlaAppId: input.appId,
|
|
3955
|
+
odlaEnvironment: input.environment,
|
|
3956
|
+
odlaDataEnvironment: input.environment,
|
|
3957
|
+
odlaRuntime: stripeRuntimeIdentity(input.runtime),
|
|
3958
|
+
chapterId: input.chapterId
|
|
3959
|
+
};
|
|
3960
|
+
}
|
|
3961
|
+
|
|
3962
|
+
// src/payments-stripe.ts
|
|
3884
3963
|
async function stripeCall(sk, method, path, params, idempotencyKey) {
|
|
3885
3964
|
return stripeRequest(
|
|
3886
3965
|
{ secretKey: sk },
|
|
@@ -3919,10 +3998,7 @@ function createStripeProvider(config) {
|
|
|
3919
3998
|
applicationId: input.applicationId,
|
|
3920
3999
|
groupId: input.groupId,
|
|
3921
4000
|
email: input.email,
|
|
3922
|
-
|
|
3923
|
-
odlaEnvironment: input.environment,
|
|
3924
|
-
odlaDataEnvironment: input.environment,
|
|
3925
|
-
...input.runtime ? { odlaRuntime: input.runtime } : {}
|
|
4001
|
+
...stripePartitionMetadata(input)
|
|
3926
4002
|
};
|
|
3927
4003
|
let customerId = input.existingCustomerId;
|
|
3928
4004
|
if (!customerId) {
|
|
@@ -3947,6 +4023,7 @@ function createStripeProvider(config) {
|
|
|
3947
4023
|
"payment_settings[save_default_payment_method]": "on_subscription",
|
|
3948
4024
|
"payment_settings[payment_method_types][0]": "card",
|
|
3949
4025
|
"expand[0]": "latest_invoice.confirmation_secret",
|
|
4026
|
+
"expand[1]": "latest_invoice.payment_intent",
|
|
3950
4027
|
metadata: meta
|
|
3951
4028
|
},
|
|
3952
4029
|
// Hardening: one subscription per application, so a client
|
|
@@ -3954,6 +4031,19 @@ function createStripeProvider(config) {
|
|
|
3954
4031
|
`sub:${input.applicationId}`
|
|
3955
4032
|
);
|
|
3956
4033
|
if (!sub.ok) fail("subscription create", sub);
|
|
4034
|
+
const invoice = sub.body.latest_invoice;
|
|
4035
|
+
const paymentIntent = invoice?.payment_intent;
|
|
4036
|
+
const paymentIntentId = typeof paymentIntent === "string" ? paymentIntent : paymentIntent && typeof paymentIntent === "object" ? String(paymentIntent.id ?? "") : "";
|
|
4037
|
+
if (paymentIntentId) {
|
|
4038
|
+
const tagged = await stripeCall(
|
|
4039
|
+
sk,
|
|
4040
|
+
"POST",
|
|
4041
|
+
`/v1/payment_intents/${encodeURIComponent(paymentIntentId)}`,
|
|
4042
|
+
{ metadata: meta },
|
|
4043
|
+
`payment-partition:${input.applicationId}`
|
|
4044
|
+
);
|
|
4045
|
+
if (!tagged.ok) fail("payment intent metadata", tagged);
|
|
4046
|
+
}
|
|
3957
4047
|
const clientSecret2 = clientSecretOf(sub.body);
|
|
3958
4048
|
if (!clientSecret2) fail("subscription confirmation-secret missing", sub);
|
|
3959
4049
|
return { customerId, subscriptionId: String(sub.body.id), clientSecret: clientSecret2 };
|
|
@@ -4231,16 +4321,12 @@ var clientSecret = (value) => {
|
|
|
4231
4321
|
const nested = confirmation?.client_secret ?? intent?.client_secret;
|
|
4232
4322
|
return typeof nested === "string" && nested ? nested : null;
|
|
4233
4323
|
};
|
|
4234
|
-
async function startGiftProviderCheckout(secretKey, intent, email, existingCustomerId, runtime) {
|
|
4324
|
+
async function startGiftProviderCheckout(secretKey, intent, email, existingCustomerId, runtime, appId = intent.chapterId) {
|
|
4235
4325
|
const offer = intent.offerSnapshot;
|
|
4236
4326
|
const metadata = {
|
|
4237
4327
|
giftIntentId: intent.id,
|
|
4238
|
-
chapterId: intent.chapterId,
|
|
4239
4328
|
environment: intent.environment,
|
|
4240
|
-
|
|
4241
|
-
odlaEnvironment: intent.environment,
|
|
4242
|
-
odlaDataEnvironment: intent.environment,
|
|
4243
|
-
...runtime ? { odlaRuntime: runtime } : {},
|
|
4329
|
+
...stripePartitionMetadata({ appId, chapterId: intent.chapterId, environment: intent.environment, runtime }),
|
|
4244
4330
|
offerId: intent.offerId
|
|
4245
4331
|
};
|
|
4246
4332
|
let customerId = existingCustomerId;
|
|
@@ -4257,10 +4343,20 @@ async function startGiftProviderCheckout(secretKey, intent, email, existingCusto
|
|
|
4257
4343
|
"payment_settings[save_default_payment_method]": "on_subscription",
|
|
4258
4344
|
"payment_settings[payment_method_types][0]": "card",
|
|
4259
4345
|
"expand[0]": "latest_invoice.confirmation_secret",
|
|
4346
|
+
"expand[1]": "latest_invoice.payment_intent",
|
|
4260
4347
|
metadata
|
|
4261
4348
|
}, `gift-subscription:${intent.id}`);
|
|
4262
4349
|
const secret2 = created2.ok ? clientSecret(created2.body) : null;
|
|
4263
4350
|
if (!created2.ok || !secret2) throw new Error(`gift subscription create failed (${created2.status})`);
|
|
4351
|
+
const invoice = created2.body.latest_invoice;
|
|
4352
|
+
const paymentIntent = invoice?.payment_intent;
|
|
4353
|
+
const paymentIntentId = typeof paymentIntent === "string" ? paymentIntent : paymentIntent && typeof paymentIntent === "object" ? String(paymentIntent.id ?? "") : "";
|
|
4354
|
+
if (paymentIntentId) {
|
|
4355
|
+
const tagged = await stripeCall(secretKey, "POST", `/v1/payment_intents/${encodeURIComponent(paymentIntentId)}`, {
|
|
4356
|
+
metadata
|
|
4357
|
+
}, `gift-payment-partition:${intent.id}`);
|
|
4358
|
+
if (!tagged.ok) throw new Error(`gift payment intent metadata failed (${tagged.status})`);
|
|
4359
|
+
}
|
|
4264
4360
|
return { clientSecret: secret2, providerObjectId: String(created2.body.id), customerId };
|
|
4265
4361
|
}
|
|
4266
4362
|
const price = await stripeCall(secretKey, "GET", `/v1/prices/${encodeURIComponent(offer.stripePriceId)}`);
|
|
@@ -4319,7 +4415,8 @@ async function startGiftIntent(db, input) {
|
|
|
4319
4415
|
stored,
|
|
4320
4416
|
input.purchaserEmail,
|
|
4321
4417
|
typeof stored.providerCustomerId === "string" ? stored.providerCustomerId : input.existingCustomerId,
|
|
4322
|
-
input.runtime
|
|
4418
|
+
input.runtime,
|
|
4419
|
+
input.appId
|
|
4323
4420
|
);
|
|
4324
4421
|
await db.transact([{ t: "update", ns: "giftPurchaseIntents", id: input.id, attrs: {
|
|
4325
4422
|
status: "checkout_ready",
|
|
@@ -4338,15 +4435,6 @@ async function startGiftIntent(db, input) {
|
|
|
4338
4435
|
};
|
|
4339
4436
|
}
|
|
4340
4437
|
|
|
4341
|
-
// src/stripe-runtime.ts
|
|
4342
|
-
function stripeWebhookSecretName(runtime) {
|
|
4343
|
-
if (!runtime) return "stripe_webhook_secret";
|
|
4344
|
-
if (!/^[a-z0-9]{2,12}$/.test(runtime) || runtime === "prod" || runtime === "production") {
|
|
4345
|
-
throw new Error("ODLA_RUNTIME is invalid");
|
|
4346
|
-
}
|
|
4347
|
-
return `stripe_webhook_secret__${runtime}`;
|
|
4348
|
-
}
|
|
4349
|
-
|
|
4350
4438
|
// src/payments-route-support.ts
|
|
4351
4439
|
var paymentErrorCode = (error) => error && typeof error === "object" && typeof error.code === "string" ? error.code : "unknown";
|
|
4352
4440
|
function paymentLineItems(group) {
|
|
@@ -4355,59 +4443,100 @@ function paymentLineItems(group) {
|
|
|
4355
4443
|
return { standardCents: standard, discountCents: discount, dueTodayCents: standard - discount };
|
|
4356
4444
|
}
|
|
4357
4445
|
|
|
4446
|
+
// src/member-system-contract.ts
|
|
4447
|
+
function classifyMemberStripeEvent(identity, target, hasProvenLegacyLocalLink) {
|
|
4448
|
+
const keys = ["appId", "environment", "dataEnvironment", "runtime", "chapterId"];
|
|
4449
|
+
const supplied = keys.filter((key) => identity[key] !== void 0);
|
|
4450
|
+
if (supplied.some((key) => identity[key] !== target[key])) return "ignore_sibling";
|
|
4451
|
+
if (supplied.length === keys.length) return "apply";
|
|
4452
|
+
return hasProvenLegacyLocalLink ? "apply_legacy" : "quarantine_unmatched";
|
|
4453
|
+
}
|
|
4454
|
+
|
|
4455
|
+
// src/stripe-webhook-partition.ts
|
|
4456
|
+
var firstRow2 = async (db, ns, q) => {
|
|
4457
|
+
const rows4 = (await db.query({ [ns]: { $: q } }))[ns];
|
|
4458
|
+
return Array.isArray(rows4) ? rows4[0] : void 0;
|
|
4459
|
+
};
|
|
4460
|
+
var stripeEventDigest = async (rawBody) => {
|
|
4461
|
+
const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(rawBody));
|
|
4462
|
+
return [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
4463
|
+
};
|
|
4464
|
+
var stripeEventIdentity = (event) => ({
|
|
4465
|
+
...event.appId ? { appId: event.appId } : {},
|
|
4466
|
+
...event.environment ? { environment: event.environment } : {},
|
|
4467
|
+
...event.dataEnvironment ? { dataEnvironment: event.dataEnvironment } : {},
|
|
4468
|
+
...event.runtime ? { runtime: event.runtime } : {},
|
|
4469
|
+
...event.chapterId ? { chapterId: event.chapterId } : {}
|
|
4470
|
+
});
|
|
4471
|
+
var stripeEventTarget = (env, ctx) => ({
|
|
4472
|
+
appId: env.ODLA_APP_ID,
|
|
4473
|
+
environment: env.ODLA_ENV,
|
|
4474
|
+
dataEnvironment: env.ODLA_ENV,
|
|
4475
|
+
runtime: stripeRuntimeIdentity(env.ODLA_RUNTIME),
|
|
4476
|
+
chapterId: ctx.chapter.id
|
|
4477
|
+
});
|
|
4478
|
+
var receiptFor = (db, eventId) => firstRow2(db, "stripeEventReceipts", { where: { eventId }, limit: 1 });
|
|
4479
|
+
function reportStripePartition(level, eventId, event, disposition) {
|
|
4480
|
+
console[level]("chapter stripe partition", JSON.stringify({
|
|
4481
|
+
eventId,
|
|
4482
|
+
kind: event.kind,
|
|
4483
|
+
disposition,
|
|
4484
|
+
...stripeEventIdentity(event)
|
|
4485
|
+
}));
|
|
4486
|
+
}
|
|
4487
|
+
async function storeStripeReceipt(db, input) {
|
|
4488
|
+
const existing = await receiptFor(db, input.eventId);
|
|
4489
|
+
if (existing) return existing.digest === input.digest ? "replayed" : "collision";
|
|
4490
|
+
const id = `stripe-event:${input.eventId}`;
|
|
4491
|
+
const write = await db.transact([{ t: "update", ns: "stripeEventReceipts", id, attrs: {
|
|
4492
|
+
id,
|
|
4493
|
+
eventId: input.eventId,
|
|
4494
|
+
digest: input.digest,
|
|
4495
|
+
disposition: input.disposition,
|
|
4496
|
+
kind: input.event.kind,
|
|
4497
|
+
matched: input.matched,
|
|
4498
|
+
...stripeEventIdentity(input.event),
|
|
4499
|
+
receivedAt: Date.now()
|
|
4500
|
+
} }], { mutationId: `stripe-receipt:${input.eventId}` });
|
|
4501
|
+
if (!write.duplicate) return "stored";
|
|
4502
|
+
const raced = await receiptFor(db, input.eventId);
|
|
4503
|
+
return raced?.digest === input.digest ? "replayed" : "collision";
|
|
4504
|
+
}
|
|
4505
|
+
async function quarantineStripeEvent(db, eventId, digest, event) {
|
|
4506
|
+
const disposition = "quarantine_unmatched";
|
|
4507
|
+
const stored = await storeStripeReceipt(db, { eventId, digest, event, disposition, matched: false });
|
|
4508
|
+
if (stored === "collision") {
|
|
4509
|
+
reportStripePartition("error", eventId, event, "quarantine_collision");
|
|
4510
|
+
return json({ ok: true, matched: false, collision: true });
|
|
4511
|
+
}
|
|
4512
|
+
reportStripePartition("warn", eventId, event, disposition);
|
|
4513
|
+
return json({ ok: true, matched: false, disposition, ...stored === "replayed" ? { replayed: true } : {} });
|
|
4514
|
+
}
|
|
4515
|
+
async function replayedStripeReceipt(db, eventId, digest, event) {
|
|
4516
|
+
const existing = await receiptFor(db, eventId);
|
|
4517
|
+
if (!existing) return null;
|
|
4518
|
+
if (existing.digest !== digest) {
|
|
4519
|
+
reportStripePartition("error", eventId, event, "quarantine_collision");
|
|
4520
|
+
return json({ ok: true, matched: false, collision: true });
|
|
4521
|
+
}
|
|
4522
|
+
return json({ ok: true, matched: existing.matched === true, replayed: true });
|
|
4523
|
+
}
|
|
4524
|
+
|
|
4358
4525
|
// src/worker-routes-payments.ts
|
|
4359
|
-
async function
|
|
4526
|
+
async function firstRow3(db, ns, q) {
|
|
4360
4527
|
const res = await db.query({ [ns]: { $: q } });
|
|
4361
4528
|
const rows4 = res[ns];
|
|
4362
4529
|
return Array.isArray(rows4) ? rows4[0] : void 0;
|
|
4363
4530
|
}
|
|
4364
4531
|
async function findApplication(db, event) {
|
|
4365
4532
|
if ("applicationId" in event && event.applicationId) {
|
|
4366
|
-
return
|
|
4533
|
+
return firstRow3(db, "applications", { where: { id: event.applicationId }, limit: 1 });
|
|
4367
4534
|
}
|
|
4368
4535
|
if ("customerId" in event && event.customerId) {
|
|
4369
|
-
return
|
|
4536
|
+
return firstRow3(db, "applications", { where: { stripeCustomerId: event.customerId }, order: { createdAt: "desc" }, limit: 1 });
|
|
4370
4537
|
}
|
|
4371
4538
|
return void 0;
|
|
4372
4539
|
}
|
|
4373
|
-
async function notifyPaymentConfirmed(db, env, eventId, app) {
|
|
4374
|
-
try {
|
|
4375
|
-
if (typeof app.email !== "string" || !app.email) return;
|
|
4376
|
-
const group = await firstRow2(db, "groups", { where: { id: String(app.groupId ?? "") }, limit: 1 });
|
|
4377
|
-
if (!group) return;
|
|
4378
|
-
await sendTemplated(
|
|
4379
|
-
{ db, envName: env.ODLA_ENV, sender: env.SEND_EMAIL, from: env.EMAIL_FROM, now: () => Date.now(), newId: () => crypto.randomUUID() },
|
|
4380
|
-
{
|
|
4381
|
-
group: emailGroupFrom(group),
|
|
4382
|
-
template: "paymentConfirmation",
|
|
4383
|
-
to: app.email,
|
|
4384
|
-
vars: { firstName: typeof app.firstName === "string" ? app.firstName : "" },
|
|
4385
|
-
dedupeKey: `${eventId}:confirm`,
|
|
4386
|
-
applicationId: String(app.id)
|
|
4387
|
-
}
|
|
4388
|
-
);
|
|
4389
|
-
} catch {
|
|
4390
|
-
}
|
|
4391
|
-
}
|
|
4392
|
-
async function notifyAdminOfPayment(db, env, eventId, app) {
|
|
4393
|
-
try {
|
|
4394
|
-
const group = await firstRow2(db, "groups", { where: { id: String(app.groupId ?? "") }, limit: 1 });
|
|
4395
|
-
if (!group || typeof group.notificationEmail !== "string" || !group.notificationEmail) return;
|
|
4396
|
-
const s = (v) => typeof v === "string" ? v : "";
|
|
4397
|
-
await sendTemplated(
|
|
4398
|
-
{ db, envName: env.ODLA_ENV, sender: env.SEND_EMAIL, from: env.EMAIL_FROM, now: () => Date.now(), newId: () => crypto.randomUUID() },
|
|
4399
|
-
{
|
|
4400
|
-
group: emailGroupFrom(group),
|
|
4401
|
-
template: "adminNotification",
|
|
4402
|
-
to: group.notificationEmail,
|
|
4403
|
-
vars: { firstName: s(app.firstName), lastName: s(app.lastName), email: s(app.email), phone: s(app.phone), state: s(app.state) },
|
|
4404
|
-
dedupeKey: `${eventId}:admin`,
|
|
4405
|
-
applicationId: String(app.id)
|
|
4406
|
-
}
|
|
4407
|
-
);
|
|
4408
|
-
} catch {
|
|
4409
|
-
}
|
|
4410
|
-
}
|
|
4411
4540
|
function webhookPatch(event, status) {
|
|
4412
4541
|
switch (event.kind) {
|
|
4413
4542
|
case "first_payment":
|
|
@@ -4433,10 +4562,10 @@ async function startSubscription(req, env, ctx) {
|
|
|
4433
4562
|
if (!applicationId) return json({ error: "applicationId required" }, 400);
|
|
4434
4563
|
if (body.refundPolicyAck !== true) return json({ error: "refundPolicyAck required" }, 400);
|
|
4435
4564
|
const db = ctx.makeDb(env);
|
|
4436
|
-
const app = await
|
|
4565
|
+
const app = await firstRow3(db, "applications", { where: { id: applicationId }, limit: 1 });
|
|
4437
4566
|
if (!app) return json({ error: "not found" }, 404);
|
|
4438
4567
|
if (app.status !== "submitted") return json({ error: "already processed" }, 409);
|
|
4439
|
-
const group = await
|
|
4568
|
+
const group = await firstRow3(db, "groups", { where: { id: String(app.groupId ?? ctx.chapter.id) }, limit: 1 });
|
|
4440
4569
|
const secretKey = await getVaultSecret(db, "stripe_secret_key");
|
|
4441
4570
|
if (!group || !secretKey) return json({ error: "payments not configured" }, 503);
|
|
4442
4571
|
if (typeof app.giftOfferId === "string") {
|
|
@@ -4448,6 +4577,7 @@ async function startSubscription(req, env, ctx) {
|
|
|
4448
4577
|
try {
|
|
4449
4578
|
const started = await startGiftIntent(db, {
|
|
4450
4579
|
id: typeof app.giftIntentId === "string" ? app.giftIntentId : `gift-intent:${applicationId}`,
|
|
4580
|
+
appId: env.ODLA_APP_ID,
|
|
4451
4581
|
chapterId: ctx.chapter.id,
|
|
4452
4582
|
environment: env.ODLA_ENV,
|
|
4453
4583
|
offer,
|
|
@@ -4483,8 +4613,9 @@ async function startSubscription(req, env, ctx) {
|
|
|
4483
4613
|
try {
|
|
4484
4614
|
result = await provider.createSubscription({
|
|
4485
4615
|
appId: env.ODLA_APP_ID,
|
|
4616
|
+
chapterId: ctx.chapter.id,
|
|
4486
4617
|
environment: env.ODLA_ENV,
|
|
4487
|
-
|
|
4618
|
+
runtime: stripeRuntimeIdentity(env.ODLA_RUNTIME),
|
|
4488
4619
|
applicationId,
|
|
4489
4620
|
groupId: String(group.id),
|
|
4490
4621
|
email: String(app.email ?? ""),
|
|
@@ -4503,7 +4634,7 @@ async function startSubscription(req, env, ctx) {
|
|
|
4503
4634
|
attrs: {
|
|
4504
4635
|
stripeCustomerId: result.customerId,
|
|
4505
4636
|
stripeSubscriptionId: result.subscriptionId,
|
|
4506
|
-
|
|
4637
|
+
stripeRuntime: stripeRuntimeIdentity(env.ODLA_RUNTIME),
|
|
4507
4638
|
refundPolicyAckAt: Date.now()
|
|
4508
4639
|
}
|
|
4509
4640
|
}
|
|
@@ -4519,27 +4650,41 @@ async function ingestWebhook(req, env, ctx) {
|
|
|
4519
4650
|
if (!ingest.ok) return json({ error: "invalid signature" }, 400);
|
|
4520
4651
|
const { eventId, event } = ingest;
|
|
4521
4652
|
if (event.kind === "ignored") return json({ ok: true, ignored: event.type });
|
|
4522
|
-
const
|
|
4523
|
-
const
|
|
4524
|
-
|
|
4525
|
-
|
|
4653
|
+
const digest = await stripeEventDigest(rawBody);
|
|
4654
|
+
const identity = stripeEventIdentity(event);
|
|
4655
|
+
const target = stripeEventTarget(env, ctx);
|
|
4656
|
+
const preliminary = classifyMemberStripeEvent(identity, target, false);
|
|
4657
|
+
if (preliminary === "ignore_sibling") {
|
|
4658
|
+
reportStripePartition("warn", eventId, event, preliminary);
|
|
4659
|
+
return json({ ok: true, matched: false, disposition: preliminary });
|
|
4526
4660
|
}
|
|
4527
4661
|
if (event.kind === "gift_paid" || event.kind === "gift_revoked") {
|
|
4528
|
-
const intent = await
|
|
4529
|
-
if (
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4662
|
+
const intent = await firstRow3(db, "giftPurchaseIntents", { where: { id: event.giftIntentId }, limit: 1 });
|
|
4663
|
+
if (intent && typeof intent.runtime === "string" && intent.runtime !== target.runtime) {
|
|
4664
|
+
reportStripePartition("warn", eventId, event, "ignore_sibling");
|
|
4665
|
+
return json({ ok: true, matched: false, disposition: "ignore_sibling" });
|
|
4666
|
+
}
|
|
4667
|
+
const replay2 = await replayedStripeReceipt(db, eventId, digest, event);
|
|
4668
|
+
if (replay2) return replay2;
|
|
4669
|
+
const localIntent = Boolean(intent && intent.chapterId === ctx.chapter.id && intent.environment === env.ODLA_ENV && (typeof intent.runtime !== "string" || intent.runtime === target.runtime));
|
|
4670
|
+
const disposition2 = classifyMemberStripeEvent(identity, target, localIntent);
|
|
4671
|
+
if (!localIntent || disposition2 === "quarantine_unmatched") {
|
|
4672
|
+
return quarantineStripeEvent(db, eventId, digest, event);
|
|
4534
4673
|
}
|
|
4535
4674
|
if (event.kind === "gift_paid") {
|
|
4536
|
-
if (intent.providerObjectId !== event.providerObjectId
|
|
4537
|
-
|
|
4675
|
+
if (intent.providerObjectId !== event.providerObjectId || intent.status === "refunded" || intent.status === "canceled") {
|
|
4676
|
+
return quarantineStripeEvent(db, eventId, digest, event);
|
|
4677
|
+
}
|
|
4538
4678
|
try {
|
|
4539
4679
|
const issued = await issueGiftClaim(db, intent, {
|
|
4540
4680
|
eventId,
|
|
4541
4681
|
objectId: event.providerObjectId
|
|
4542
4682
|
}, Date.now());
|
|
4683
|
+
const stored3 = await storeStripeReceipt(db, { eventId, digest, event, disposition: disposition2, matched: true });
|
|
4684
|
+
if (stored3 === "collision") {
|
|
4685
|
+
reportStripePartition("error", eventId, event, "quarantine_collision");
|
|
4686
|
+
return json({ ok: true, matched: false, collision: true });
|
|
4687
|
+
}
|
|
4543
4688
|
return json({ ok: true, matched: true, claimId: issued.claimId });
|
|
4544
4689
|
} catch {
|
|
4545
4690
|
return json({ error: "gift issuance failed" }, 503);
|
|
@@ -4549,14 +4694,26 @@ async function ingestWebhook(req, env, ctx) {
|
|
|
4549
4694
|
status: event.reason,
|
|
4550
4695
|
updatedAt: Date.now()
|
|
4551
4696
|
} }], { mutationId: `gift-revoked-intent:${eventId}` });
|
|
4552
|
-
const claim = await
|
|
4697
|
+
const claim = await firstRow3(db, "giftClaims", { where: { intentId: event.giftIntentId }, limit: 1 });
|
|
4553
4698
|
if (claim) await revokeGiftClaim(db, claim, event.reason, Date.now());
|
|
4699
|
+
const stored2 = await storeStripeReceipt(db, { eventId, digest, event, disposition: disposition2, matched: Boolean(claim) });
|
|
4700
|
+
if (stored2 === "collision") {
|
|
4701
|
+
reportStripePartition("error", eventId, event, "quarantine_collision");
|
|
4702
|
+
return json({ ok: true, matched: false, collision: true });
|
|
4703
|
+
}
|
|
4554
4704
|
return json({ ok: true, matched: Boolean(claim) });
|
|
4555
4705
|
}
|
|
4556
4706
|
const app = await findApplication(db, event);
|
|
4557
|
-
if (
|
|
4558
|
-
|
|
4559
|
-
return json({ ok: true, matched: false });
|
|
4707
|
+
if (app && typeof app.stripeRuntime === "string" && app.stripeRuntime !== target.runtime) {
|
|
4708
|
+
reportStripePartition("warn", eventId, event, "ignore_sibling");
|
|
4709
|
+
return json({ ok: true, matched: false, disposition: "ignore_sibling" });
|
|
4710
|
+
}
|
|
4711
|
+
const replay = await replayedStripeReceipt(db, eventId, digest, event);
|
|
4712
|
+
if (replay) return replay;
|
|
4713
|
+
const localApplication = Boolean(app && (typeof app.stripeRuntime !== "string" || app.stripeRuntime === target.runtime));
|
|
4714
|
+
const disposition = classifyMemberStripeEvent(identity, target, localApplication);
|
|
4715
|
+
if (!localApplication || disposition === "quarantine_unmatched") {
|
|
4716
|
+
return quarantineStripeEvent(db, eventId, digest, event);
|
|
4560
4717
|
}
|
|
4561
4718
|
const patch = webhookPatch(event, String(app.status ?? ""));
|
|
4562
4719
|
let next = app;
|
|
@@ -4579,6 +4736,11 @@ async function ingestWebhook(req, env, ctx) {
|
|
|
4579
4736
|
});
|
|
4580
4737
|
}
|
|
4581
4738
|
}
|
|
4739
|
+
const stored = await storeStripeReceipt(db, { eventId, digest, event, disposition, matched: true });
|
|
4740
|
+
if (stored === "collision") {
|
|
4741
|
+
reportStripePartition("error", eventId, event, "quarantine_collision");
|
|
4742
|
+
return json({ ok: true, matched: false, collision: true });
|
|
4743
|
+
}
|
|
4582
4744
|
return json({ ok: true });
|
|
4583
4745
|
}
|
|
4584
4746
|
var handlePayments = async (req, url, env, ctx) => {
|
|
@@ -4656,6 +4818,7 @@ var handleGifts = async (req, url, env, ctx) => {
|
|
|
4656
4818
|
try {
|
|
4657
4819
|
return json(await startGiftIntent(db, {
|
|
4658
4820
|
id: `gift-intent:${ctx.chapter.id}:${purchaseId}`,
|
|
4821
|
+
appId: env.ODLA_APP_ID,
|
|
4659
4822
|
chapterId: ctx.chapter.id,
|
|
4660
4823
|
environment: env.ODLA_ENV,
|
|
4661
4824
|
offer,
|
|
@@ -5405,7 +5568,19 @@ var handleAdminRefund = async (req, url, env, ctx) => {
|
|
|
5405
5568
|
const succeeded = (charges.body.data ?? []).filter((c) => c.status === "succeeded" && c.refunded !== true);
|
|
5406
5569
|
const firstCharge = succeeded[succeeded.length - 1];
|
|
5407
5570
|
if (!firstCharge) return json({ error: "no paid charge to refund" }, 409);
|
|
5408
|
-
const refund = await stripeCall(sk, "POST", "/v1/refunds", {
|
|
5571
|
+
const refund = await stripeCall(sk, "POST", "/v1/refunds", {
|
|
5572
|
+
charge: String(firstCharge.id),
|
|
5573
|
+
metadata: {
|
|
5574
|
+
...stripePartitionMetadata({
|
|
5575
|
+
appId: env.ODLA_APP_ID,
|
|
5576
|
+
chapterId: ctx.chapter.id,
|
|
5577
|
+
environment: env.ODLA_ENV,
|
|
5578
|
+
runtime: env.ODLA_RUNTIME
|
|
5579
|
+
}),
|
|
5580
|
+
applicationId: String(app.id),
|
|
5581
|
+
groupId: String(app.groupId ?? ctx.chapter.id)
|
|
5582
|
+
}
|
|
5583
|
+
}, `refund:${String(app.id)}:${String(firstCharge.id)}`);
|
|
5409
5584
|
if (!refund.ok) return json({ error: "refund failed upstream" }, 502);
|
|
5410
5585
|
let subscriptionCanceled = false;
|
|
5411
5586
|
if (ctx.chapter.operations.refund.cancelSubscription && subscriptionId) {
|
|
@@ -6076,8 +6251,8 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
|
6076
6251
|
}
|
|
6077
6252
|
};
|
|
6078
6253
|
try {
|
|
6079
|
-
const
|
|
6080
|
-
const updated = hasFields ? await updateRecord3(
|
|
6254
|
+
const deps2 = { crm: ctx.chapter.crm, db };
|
|
6255
|
+
const updated = hasFields ? await updateRecord3(deps2, {
|
|
6081
6256
|
id: record4.id,
|
|
6082
6257
|
input: fields,
|
|
6083
6258
|
mutationId: activityId,
|
|
@@ -6088,7 +6263,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
|
6088
6263
|
authorId: `network:${verified.sender}`,
|
|
6089
6264
|
meta: auditMeta
|
|
6090
6265
|
}
|
|
6091
|
-
}) : await setStage2(
|
|
6266
|
+
}) : await setStage2(deps2, {
|
|
6092
6267
|
id: record4.id,
|
|
6093
6268
|
to: input.stage,
|
|
6094
6269
|
authorId: `network:${verified.sender}`,
|