@odla-ai/chapter 0.56.2 → 0.57.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 +57 -0
- package/dist/{chunk-E7CB5K2D.js → chunk-CKC6X5H2.js} +47 -3
- package/dist/chunk-CKC6X5H2.js.map +1 -0
- package/dist/{chunk-4M2PN5K3.js → chunk-FHKZVF5G.js} +3 -1
- package/dist/chunk-FHKZVF5G.js.map +1 -0
- package/dist/{chunk-ZAE24YKY.js → chunk-L2IPS2FQ.js} +51 -4
- package/dist/chunk-L2IPS2FQ.js.map +1 -0
- package/dist/{copy-context-CaUXIxUI.d.ts → copy-context-DE4YMAbz.d.ts} +1 -1
- package/dist/index.cjs +176 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +36 -3
- package/dist/index.d.ts +36 -3
- package/dist/index.js +176 -13
- package/dist/index.js.map +1 -1
- package/dist/ui/admin/index.d.ts +4 -3
- package/dist/ui/admin/index.js +2 -2
- package/dist/ui/index.d.ts +1 -1
- package/dist/ui/index.js +3 -3
- package/dist/ui/member/index.d.ts +10 -3
- package/dist/ui/member/index.js +2 -2
- package/dist/worker/index.cjs +1558 -1125
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +149 -3
- package/dist/worker/index.d.ts +149 -3
- package/dist/worker/index.js +1558 -1125
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-4M2PN5K3.js.map +0 -1
- package/dist/chunk-E7CB5K2D.js.map +0 -1
- package/dist/chunk-ZAE24YKY.js.map +0 -1
package/dist/worker/index.cjs
CHANGED
|
@@ -21,15 +21,21 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var worker_exports = {};
|
|
22
22
|
__export(worker_exports, {
|
|
23
23
|
AdmissionGrantDeliveryError: () => AdmissionGrantDeliveryError,
|
|
24
|
+
DECLINABLE_FROM: () => DECLINABLE_FROM,
|
|
24
25
|
MEMBERSHIP_AUTHORITY_PATH: () => MEMBERSHIP_AUTHORITY_PATH,
|
|
26
|
+
MEMBERSHIP_CHAPTER_EFFECT_KINDS: () => MEMBERSHIP_CHAPTER_EFFECT_KINDS,
|
|
25
27
|
NetworkTargetTransportError: () => NetworkTargetTransportError,
|
|
28
|
+
REGIFTABLE_SEAT_STATUSES: () => REGIFTABLE_SEAT_STATUSES,
|
|
26
29
|
chapterMembershipAccess: () => chapterMembershipAccess,
|
|
27
30
|
chapterWorker: () => chapterWorker,
|
|
31
|
+
closedApplication: () => closedApplication,
|
|
32
|
+
continueApplicationOnFreeTier: () => continueApplicationOnFreeTier,
|
|
28
33
|
createMembershipAuthorityHandler: () => createMembershipAuthorityHandler,
|
|
29
34
|
createMembershipAuthorityStorage: () => createMembershipAuthorityStorage,
|
|
30
35
|
createMembershipChapterEffectRoute: () => createMembershipChapterEffectRoute,
|
|
31
36
|
createMembershipWorkerAdapter: () => createMembershipWorkerAdapter,
|
|
32
37
|
createWorkerContext: () => createWorkerContext,
|
|
38
|
+
declineApplication: () => declineApplication,
|
|
33
39
|
deliverAdmissionGrants: () => deliverAdmissionGrants,
|
|
34
40
|
deliverBuilderLibrary: () => deliverBuilderLibrary,
|
|
35
41
|
deliverMemberContent: () => deliverMemberContent,
|
|
@@ -40,11 +46,16 @@ __export(worker_exports, {
|
|
|
40
46
|
liftSeatPurchaseWindow: () => liftSeatPurchaseWindow,
|
|
41
47
|
lowerSeatPrice: () => lowerSeatPrice,
|
|
42
48
|
namedSeatIncluded: () => namedSeatIncluded,
|
|
49
|
+
namedSeatInvitationOpen: () => namedSeatInvitationOpen,
|
|
43
50
|
namedSeatRecipientState: () => namedSeatRecipientState,
|
|
51
|
+
namedSeatRegiftable: () => namedSeatRegiftable,
|
|
44
52
|
readNamedSeatOffer: () => readNamedSeatOffer,
|
|
45
53
|
refundNamedSeatByOperator: () => refundNamedSeatByOperator,
|
|
54
|
+
regiftNamedSeat: () => regiftNamedSeat,
|
|
55
|
+
releaseNamedSeatRecipientApplications: () => releaseNamedSeatRecipientApplications,
|
|
46
56
|
renderActiveSignupControlHtml: () => renderActiveSignupControlHtml,
|
|
47
57
|
resolveNetworkTargetTransport: () => resolveNetworkTargetTransport,
|
|
58
|
+
resolveSignupEligibility: () => resolveSignupEligibility,
|
|
48
59
|
retermMembershipDiscount: () => retermMembershipDiscount,
|
|
49
60
|
retryChapterApprovalEffects: () => retryChapterApprovalEffects,
|
|
50
61
|
retryLeaderAdmissionDeliveries: () => retryLeaderAdmissionDeliveries,
|
|
@@ -4085,912 +4096,887 @@ var handlePaymentRecovery = async (req, url, env, ctx) => {
|
|
|
4085
4096
|
}
|
|
4086
4097
|
};
|
|
4087
4098
|
|
|
4088
|
-
// src/
|
|
4089
|
-
var
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
if (!await ctx.isSuperAdminEmail(raw2, user.email, user.userId)) return json({ error: "super-admin required", code: "super_admin_required" }, 403);
|
|
4101
|
-
const db = raw2, app = await membershipLoad(db, "applications", match[1]);
|
|
4102
|
-
if (!app || !chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME) || app.namedSeatId) return json({ error: "not found" }, 404);
|
|
4103
|
-
const intent = await membershipLoad(db, "subscriptionCheckoutIntents", String(app.id));
|
|
4104
|
-
if (!intent) return json({ error: "paid membership required" }, 409);
|
|
4099
|
+
// src/scheduling.ts
|
|
4100
|
+
var SCHEDULING_DEFAULTS = {
|
|
4101
|
+
slotMinutes: 45,
|
|
4102
|
+
days: [1, 2, 3, 4, 5],
|
|
4103
|
+
startHour: 9,
|
|
4104
|
+
endHour: 17,
|
|
4105
|
+
timezone: "America/Los_Angeles",
|
|
4106
|
+
minNoticeHours: 24,
|
|
4107
|
+
windowDays: 14,
|
|
4108
|
+
summaryTemplate: "Introduction call with {{firstName}} {{lastName}}"
|
|
4109
|
+
};
|
|
4110
|
+
function isValidTimeZone(tz) {
|
|
4105
4111
|
try {
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
const seat = offer.seat ?? null, refundable = Boolean(seat && REFUNDABLE.has(seat.status));
|
|
4109
|
-
if (!refund) {
|
|
4110
|
-
return json({
|
|
4111
|
-
seat,
|
|
4112
|
-
amountCents: offer.amountCents,
|
|
4113
|
-
currency: offer.currency,
|
|
4114
|
-
termEndsAt: offer.termEndsAt,
|
|
4115
|
-
purchaseEndsAt: offer.purchaseEndsAt,
|
|
4116
|
-
refundable
|
|
4117
|
-
});
|
|
4118
|
-
}
|
|
4119
|
-
if (!seat) return json({ error: "no purchased seat", code: "seat_not_found" }, 409);
|
|
4120
|
-
if (seat.status === "refunded") {
|
|
4121
|
-
await closeRecipientApplication(db, env, ctx, seat.seatId);
|
|
4122
|
-
return json({ ok: true, seatId: seat.seatId, status: "refunded", replayed: true });
|
|
4123
|
-
}
|
|
4124
|
-
if (!refundable) return json({ error: `seat is ${seat.status.replaceAll("_", " ")}`, code: "seat_not_refundable" }, 409);
|
|
4125
|
-
const receipt = await seats.operatorRefund({ ...target, seatId: seat.seatId }, env);
|
|
4126
|
-
await closeRecipientApplication(db, env, ctx, seat.seatId);
|
|
4127
|
-
return json({ ok: true, ...receipt });
|
|
4112
|
+
new Intl.DateTimeFormat(void 0, { timeZone: tz });
|
|
4113
|
+
return true;
|
|
4128
4114
|
} catch {
|
|
4129
|
-
return
|
|
4115
|
+
return false;
|
|
4130
4116
|
}
|
|
4131
|
-
};
|
|
4132
|
-
async function closeRecipientApplication(db, env, ctx, seatId) {
|
|
4133
|
-
const recipient2 = (await db.query({ applications: { $: { where: { namedSeatId: seatId }, limit: 1 } } })).applications?.[0];
|
|
4134
|
-
if (!recipient2 || !chapterBillingApplication(recipient2, ctx.chapter.id, env.ODLA_RUNTIME) || recipient2.status === "refunded") return;
|
|
4135
|
-
const attrs = { status: "refunded", renewalAt: 0, canceled: true };
|
|
4136
|
-
await db.transact(
|
|
4137
|
-
[{ t: "update", ns: "applications", id: String(recipient2.id), attrs }],
|
|
4138
|
-
{ mutationId: `verified-refund:${String(recipient2.id)}:${seatId}` }
|
|
4139
|
-
);
|
|
4140
|
-
await syncApplicationToCrm(crmDeps(db, ctx), { app: { ...recipient2, ...attrs }, stage: "refunded" }).catch(() => void 0);
|
|
4141
4117
|
}
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4118
|
+
function resolveScheduling(config) {
|
|
4119
|
+
const result = validateScheduling(config);
|
|
4120
|
+
if (result.ok) return result.value;
|
|
4121
|
+
const detail = Object.entries(result.errors).map(([field, message]) => `${field}: ${message}`).join(" ");
|
|
4122
|
+
throw new Error(`scheduling: ${detail}`);
|
|
4123
|
+
}
|
|
4124
|
+
function validateScheduling(config) {
|
|
4125
|
+
const d = config ?? {};
|
|
4126
|
+
const c = {
|
|
4127
|
+
slotMinutes: d.slotMinutes ?? SCHEDULING_DEFAULTS.slotMinutes,
|
|
4128
|
+
days: d.days ?? SCHEDULING_DEFAULTS.days,
|
|
4129
|
+
startHour: d.startHour ?? SCHEDULING_DEFAULTS.startHour,
|
|
4130
|
+
endHour: d.endHour ?? SCHEDULING_DEFAULTS.endHour,
|
|
4131
|
+
timezone: d.timezone ?? SCHEDULING_DEFAULTS.timezone,
|
|
4132
|
+
minNoticeHours: d.minNoticeHours ?? SCHEDULING_DEFAULTS.minNoticeHours,
|
|
4133
|
+
windowDays: d.windowDays ?? SCHEDULING_DEFAULTS.windowDays,
|
|
4134
|
+
summaryTemplate: d.summaryTemplate ?? SCHEDULING_DEFAULTS.summaryTemplate
|
|
4159
4135
|
};
|
|
4160
|
-
const
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
const invoice = await call("GET", `/v1/invoices/${encodeURIComponent(invoiceRef.invoiceId)}`);
|
|
4164
|
-
if (invoice.id !== invoiceRef.invoiceId || id2(invoice.subscription ?? invoice.parent?.subscription_details?.subscription) !== sub.id || id2(invoice.customer) !== seat.providerCustomerId || invoice.status !== "paid" || invoice.billing_reason !== "subscription_create" || invoice.total !== invoiceRef.totalCents || invoice.amount_paid !== invoiceRef.totalCents || invoice.currency !== seat.currency || invoice.livemode !== (seat.environment === "prod")) fail4();
|
|
4165
|
-
let chargeId = id2(invoice.charge), paymentId = id2(invoice.payment_intent);
|
|
4166
|
-
if (!chargeId && !paymentId) {
|
|
4167
|
-
const payments = await call("GET", "/v1/invoice_payments", { invoice: invoice.id, status: "paid", limit: 100 });
|
|
4168
|
-
if (!Array.isArray(payments.data) || payments.data.length !== 1 || payments.has_more !== false) fail4();
|
|
4169
|
-
const payment = payments.data[0];
|
|
4170
|
-
if (id2(payment.invoice) !== invoice.id || payment.status !== "paid" || payment.payment?.type !== "payment_intent") fail4();
|
|
4171
|
-
paymentId = id2(payment.payment.payment_intent);
|
|
4172
|
-
}
|
|
4173
|
-
if (!chargeId) {
|
|
4174
|
-
if (!/^pi_[A-Za-z0-9_]+$/.test(paymentId)) fail4();
|
|
4175
|
-
const payment = await call("GET", `/v1/payment_intents/${encodeURIComponent(paymentId)}`);
|
|
4176
|
-
if (payment.id !== paymentId || id2(payment.customer) !== seat.providerCustomerId || payment.status !== "succeeded" || payment.amount_received !== invoiceRef.totalCents || payment.currency !== seat.currency || payment.livemode !== (seat.environment === "prod")) fail4();
|
|
4177
|
-
chargeId = id2(payment.latest_charge);
|
|
4178
|
-
}
|
|
4179
|
-
if (!/^ch_[A-Za-z0-9_]+$/.test(chargeId)) fail4();
|
|
4180
|
-
const charge = await call("GET", `/v1/charges/${encodeURIComponent(chargeId)}`);
|
|
4181
|
-
if (charge.id !== chargeId || id2(charge.customer) !== seat.providerCustomerId || charge.status !== "succeeded" || charge.livemode !== (seat.environment === "prod") || charge.amount !== invoiceRef.totalCents || charge.currency !== seat.currency || charge.invoice != null && id2(charge.invoice) !== invoice.id || paymentId && id2(charge.payment_intent) !== paymentId || !Number.isSafeInteger(charge.amount_refunded) || charge.amount_refunded < 0 || charge.amount_refunded > charge.amount) fail4();
|
|
4182
|
-
const items = sub.items.data.filter((item) => item.metadata?.seatId === seat.id);
|
|
4183
|
-
if (items.length > 1) fail4();
|
|
4184
|
-
if (items.length && sub.status !== "canceled") {
|
|
4185
|
-
const item = items[0];
|
|
4186
|
-
if (!/^si_[A-Za-z0-9_]+$/.test(item.id) || item.quantity !== 1 || item.price?.id !== seat.priceId) fail4();
|
|
4187
|
-
const removed = await call("DELETE", `/v1/subscription_items/${encodeURIComponent(item.id)}`, { proration_behavior: "none" });
|
|
4188
|
-
if (removed.id !== item.id || removed.deleted !== true) fail4("named_seat_renewal_removal_unconfirmed");
|
|
4136
|
+
const errors = {};
|
|
4137
|
+
if (!(c.slotMinutes >= 15 && c.slotMinutes <= 240)) {
|
|
4138
|
+
errors.slotMinutes = "Slot length must be between 15 and 240 minutes.";
|
|
4189
4139
|
}
|
|
4190
|
-
if (
|
|
4191
|
-
|
|
4192
|
-
if (!Array.isArray(list.data) || list.has_more !== false || list.data.length >= 100) fail4("named_seat_refund_history_incomplete");
|
|
4193
|
-
let refundedCents = 0;
|
|
4194
|
-
const refundIds = [];
|
|
4195
|
-
for (const refund of list.data) {
|
|
4196
|
-
if (id2(refund.charge) !== chargeId || refund.currency !== seat.currency || !Number.isSafeInteger(refund.amount) || refund.amount <= 0 || !/^re_[A-Za-z0-9_]+$/.test(refund.id) || !["succeeded", "pending", "requires_action", "failed", "canceled"].includes(refund.status)) fail4();
|
|
4197
|
-
if (["pending", "requires_action"].includes(refund.status)) fail4("named_seat_refund_pending");
|
|
4198
|
-
if (refund.status === "succeeded") {
|
|
4199
|
-
if (refund.metadata?.seatId !== seat.id || refund.metadata?.membershipId !== seat.primaryMembershipId) fail4("named_seat_refund_allocation_ambiguous");
|
|
4200
|
-
refundedCents += refund.amount;
|
|
4201
|
-
refundIds.push(refund.id);
|
|
4202
|
-
}
|
|
4140
|
+
if (!(c.windowDays >= 1 && c.windowDays <= 62)) {
|
|
4141
|
+
errors.windowDays = "Booking window must be between 1 and 62 days (the calendar caps look-ahead at 62).";
|
|
4203
4142
|
}
|
|
4204
|
-
if (
|
|
4205
|
-
|
|
4206
|
-
const remaining = seat.amountCents - refundedCents;
|
|
4207
|
-
const refund = await call("POST", "/v1/refunds", {
|
|
4208
|
-
charge: chargeId,
|
|
4209
|
-
amount: remaining,
|
|
4210
|
-
reason: "requested_by_customer",
|
|
4211
|
-
metadata: { ...metadata, membershipId: seat.primaryMembershipId, seatId: seat.id }
|
|
4212
|
-
}, `combined-seat-refund:${seat.id}:${remaining}`);
|
|
4213
|
-
if (id2(refund.charge) !== chargeId || refund.amount !== remaining || refund.currency !== seat.currency || !/^re_[A-Za-z0-9_]+$/.test(refund.id)) fail4();
|
|
4214
|
-
if (refund.status !== "succeeded") fail4("named_seat_refund_pending");
|
|
4215
|
-
refundedCents += remaining;
|
|
4216
|
-
refundIds.push(refund.id);
|
|
4143
|
+
if (!(c.minNoticeHours >= 0 && c.minNoticeHours <= 336)) {
|
|
4144
|
+
errors.minNoticeHours = "Minimum notice must be between 0 and 336 hours.";
|
|
4217
4145
|
}
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
// src/membership-seat-refund-stripe.ts
|
|
4222
|
-
var fail5 = (code = "named_seat_refund_mismatch") => {
|
|
4223
|
-
throw Object.assign(new Error(code), { code });
|
|
4224
|
-
};
|
|
4225
|
-
async function refundNamedSeatStripePayment(secretKey, seat) {
|
|
4226
|
-
if (seat.combinedInvoice) return refundCheckoutNamedSeat(secretKey, seat);
|
|
4227
|
-
if (!seat.providerPaymentIntentId || seat.status !== "refund_pending") fail5();
|
|
4228
|
-
const call = async (method, path, params, idempotencyKey) => {
|
|
4229
|
-
const result = await (0, import_stripe7.stripeRequest)({ secretKey }, method, path, params, idempotencyKey ? { idempotencyKey } : {});
|
|
4230
|
-
if (!result.ok || !result.body) return fail5("named_seat_refund_provider_unavailable");
|
|
4231
|
-
return result.body;
|
|
4232
|
-
};
|
|
4233
|
-
const metadata = { ...stripePartitionMetadata(seat), applicationId: seat.applicationId, membershipId: seat.primaryMembershipId, seatId: seat.id };
|
|
4234
|
-
const payment = await call("GET", `/v1/payment_intents/${encodeURIComponent(seat.providerPaymentIntentId)}`);
|
|
4235
|
-
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)) fail5();
|
|
4236
|
-
const sub = await call("GET", `/v1/subscriptions/${encodeURIComponent(seat.providerSubscriptionId)}`);
|
|
4237
|
-
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)) fail5();
|
|
4238
|
-
const items = sub.items.data.filter((item) => item.metadata?.seatId === seat.id);
|
|
4239
|
-
if (items.length > 1) fail5();
|
|
4240
|
-
if (items.length && sub.status !== "canceled") {
|
|
4241
|
-
const item = items[0];
|
|
4242
|
-
if (!/^si_[A-Za-z0-9_]+$/.test(item.id) || item.price?.id !== seat.priceId || item.quantity !== 1) fail5();
|
|
4243
|
-
const removed = await call("DELETE", `/v1/subscription_items/${encodeURIComponent(item.id)}`, { proration_behavior: "none" });
|
|
4244
|
-
if (removed.id !== item.id || removed.deleted !== true) fail5("named_seat_renewal_removal_unconfirmed");
|
|
4146
|
+
if (!(c.startHour >= 0 && c.startHour < c.endHour && c.endHour <= 24)) {
|
|
4147
|
+
errors.hours = "Hours must satisfy 0 \u2264 start < end \u2264 24.";
|
|
4245
4148
|
}
|
|
4246
|
-
const
|
|
4247
|
-
if (!
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
for (const refund of list.data) {
|
|
4251
|
-
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)) fail5();
|
|
4252
|
-
if (refund.status === "succeeded") {
|
|
4253
|
-
refundedCents += refund.amount;
|
|
4254
|
-
refundIds.push(refund.id);
|
|
4255
|
-
} else if (refund.status === "pending" || refund.status === "requires_action") pendingCents += refund.amount;
|
|
4149
|
+
const days = [...c.days];
|
|
4150
|
+
if (!days.length) errors.days = "Pick at least one day.";
|
|
4151
|
+
else if (!days.every((n) => Number.isInteger(n) && n >= 0 && n <= 6)) {
|
|
4152
|
+
errors.days = "Days must be weekday numbers, 0 (Sunday) through 6 (Saturday).";
|
|
4256
4153
|
}
|
|
4257
|
-
if (
|
|
4258
|
-
|
|
4259
|
-
if (refundedCents < seat.amountCents) {
|
|
4260
|
-
const remaining = seat.amountCents - refundedCents;
|
|
4261
|
-
const refund = await call("POST", "/v1/refunds", {
|
|
4262
|
-
payment_intent: seat.providerPaymentIntentId,
|
|
4263
|
-
amount: remaining,
|
|
4264
|
-
reason: "requested_by_customer",
|
|
4265
|
-
metadata
|
|
4266
|
-
}, `named-seat-refund:${seat.id}:${remaining}`);
|
|
4267
|
-
if (refund.payment_intent !== seat.providerPaymentIntentId || refund.amount !== remaining || refund.currency !== seat.currency || !/^re_[A-Za-z0-9_]+$/.test(refund.id)) fail5();
|
|
4268
|
-
if (refund.status !== "succeeded") fail5("named_seat_refund_pending");
|
|
4269
|
-
refundedCents += remaining;
|
|
4270
|
-
refundIds.push(refund.id);
|
|
4154
|
+
if (typeof c.timezone !== "string" || !isValidTimeZone(c.timezone)) {
|
|
4155
|
+
errors.timezone = `"${String(c.timezone)}" is not a valid IANA timezone (for example "America/Los_Angeles").`;
|
|
4271
4156
|
}
|
|
4272
|
-
|
|
4157
|
+
if (typeof c.summaryTemplate !== "string") errors.summaryTemplate = "Calendar summary template must be text.";
|
|
4158
|
+
return Object.keys(errors).length > 0 ? { ok: false, errors } : { ok: true, value: { ...c, days } };
|
|
4273
4159
|
}
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
async function recipientKey(input, email) {
|
|
4277
|
-
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify([input.appId, input.chapterId, input.environment, input.runtime, email])));
|
|
4278
|
-
return `seat-recipient:${[...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
4160
|
+
function slotWindow(now, windowDays) {
|
|
4161
|
+
return { from: now, to: now + windowDays * 864e5 };
|
|
4279
4162
|
}
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
const
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
};
|
|
4322
|
-
try {
|
|
4323
|
-
await db.transact([
|
|
4324
|
-
membershipUpdate("networkSeat", id7, attrs),
|
|
4325
|
-
membershipUpdate("networkSeatRecipient", claimId, {
|
|
4326
|
-
id: claimId,
|
|
4327
|
-
appId: input.appId,
|
|
4328
|
-
chapterId: input.chapterId,
|
|
4329
|
-
environment: input.environment,
|
|
4330
|
-
runtime: input.runtime,
|
|
4331
|
-
seatId: id7,
|
|
4332
|
-
recipientEmailNormalized: email,
|
|
4333
|
-
createdAt: now
|
|
4334
|
-
}),
|
|
4335
|
-
membershipUpdate("networkMembership", memberId, { namedSeatId: id7, revision: Number(member.revision) + 1, updatedAt: now })
|
|
4336
|
-
], { mutationId: `named-seat-reserve:${id7}`, guards: [
|
|
4337
|
-
{ ns: "networkSeat", id: id7, exists: false },
|
|
4338
|
-
{ ns: "networkSeatRecipient", id: claimId, exists: false },
|
|
4339
|
-
{ ns: "networkMembership", id: memberId, exists: true, equals: { revision: member.revision }, absent: ["namedSeatId"] },
|
|
4340
|
-
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
|
|
4341
|
-
] });
|
|
4342
|
-
} catch (error) {
|
|
4343
|
-
if (error.code !== "transact_guard_failed") throw error;
|
|
4344
|
-
}
|
|
4345
|
-
seat = await membershipLoad(db, "networkSeat", id7);
|
|
4346
|
-
if (!seat && await membershipLoad(db, "networkSeatRecipient", claimId)) membershipFail("named_seat_recipient_unavailable");
|
|
4347
|
-
}
|
|
4348
|
-
requireMembershipScope(seat, input);
|
|
4349
|
-
if (seat.primaryMembershipId !== memberId || seat.recipientName !== name || seat.recipientEmailNormalized !== email || seat.priceId !== input.priceId || seat.purchaserUserId !== input.purchaserUserId) membershipFail("named_seat_purchase_conflict");
|
|
4350
|
-
return seat;
|
|
4163
|
+
function endForSlot(startAt, slotMinutes) {
|
|
4164
|
+
return startAt + slotMinutes * 6e4;
|
|
4165
|
+
}
|
|
4166
|
+
function isSlotAvailable(slots, startAt) {
|
|
4167
|
+
return slots.some((s) => s.startAt === startAt);
|
|
4168
|
+
}
|
|
4169
|
+
function renderSummary(template, app) {
|
|
4170
|
+
return template.replace("{{firstName}}", app.firstName ?? "").replace("{{lastName}}", app.lastName ?? "");
|
|
4171
|
+
}
|
|
4172
|
+
function bookingDecision(existing) {
|
|
4173
|
+
const eventId = existing?.googleEventId ?? null;
|
|
4174
|
+
return { reschedule: Boolean(eventId), eventId };
|
|
4175
|
+
}
|
|
4176
|
+
function introIdempotencyKey(applicationId) {
|
|
4177
|
+
return `application:${applicationId}:intro`;
|
|
4178
|
+
}
|
|
4179
|
+
function meetingCreateRow(i) {
|
|
4180
|
+
return {
|
|
4181
|
+
id: i.meetingId,
|
|
4182
|
+
applicationId: i.applicationId,
|
|
4183
|
+
groupId: i.groupId,
|
|
4184
|
+
startAt: i.startAt,
|
|
4185
|
+
endAt: i.endAt,
|
|
4186
|
+
timezone: i.timezone,
|
|
4187
|
+
status: "scheduled",
|
|
4188
|
+
googleEventId: i.googleEventId,
|
|
4189
|
+
...i.meetUrl ? { meetUrl: i.meetUrl } : {},
|
|
4190
|
+
...i.htmlLink ? { htmlLink: i.htmlLink } : {},
|
|
4191
|
+
drift: "none",
|
|
4192
|
+
createdAt: i.createdAt
|
|
4193
|
+
};
|
|
4194
|
+
}
|
|
4195
|
+
function meetingRescheduleUpdate(startAt, endAt) {
|
|
4196
|
+
return { startAt, endAt, drift: "none" };
|
|
4197
|
+
}
|
|
4198
|
+
function applicationBookingUpdate(currentStatus, startAt, htmlLink) {
|
|
4199
|
+
return {
|
|
4200
|
+
meetingAt: startAt,
|
|
4201
|
+
...htmlLink ? { meetingLink: htmlLink } : {},
|
|
4202
|
+
...currentStatus !== "call_scheduled" ? { status: "call_scheduled" } : {}
|
|
4203
|
+
};
|
|
4351
4204
|
}
|
|
4352
4205
|
|
|
4353
|
-
// src/
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4206
|
+
// src/session.ts
|
|
4207
|
+
function applicationSummary(app) {
|
|
4208
|
+
return {
|
|
4209
|
+
id: app.id,
|
|
4210
|
+
firstName: app.firstName ?? null,
|
|
4211
|
+
lastName: app.lastName ?? null,
|
|
4212
|
+
email: app.email ?? null,
|
|
4213
|
+
status: app.status,
|
|
4214
|
+
createdAt: app.createdAt ?? null,
|
|
4215
|
+
meetingLink: app.meetingLink ?? null,
|
|
4216
|
+
paid: Boolean(app.stripeSubscriptionId) && app.status !== "refunded" && app.canceled !== true && !["lapsed", "refunded", "refund_pending", "canceled", "restarting"].includes(app.membershipStatus ?? ""),
|
|
4217
|
+
...app.membershipStatus ? { membershipStatus: app.membershipStatus, membershipGraceEndsAt: app.membershipGraceEndsAt, membershipAutoRenew: app.membershipAutoRenew } : {},
|
|
4218
|
+
renewalAt: app.renewalAt ?? null,
|
|
4219
|
+
canceled: app.canceled === true,
|
|
4220
|
+
tierId: app.tierId ?? null
|
|
4221
|
+
};
|
|
4222
|
+
}
|
|
4223
|
+
function memberApplication(app, meeting, defaultTimezone) {
|
|
4224
|
+
const summary = applicationSummary(app);
|
|
4225
|
+
let meetingAt = app.meetingAt ?? null;
|
|
4226
|
+
let meetUrl = null;
|
|
4227
|
+
let timezone = defaultTimezone;
|
|
4228
|
+
if (meeting) {
|
|
4229
|
+
timezone = meeting.timezone ?? timezone;
|
|
4230
|
+
if (meeting.status === "scheduled") {
|
|
4231
|
+
meetingAt = meeting.startAt ?? null;
|
|
4232
|
+
meetUrl = meeting.meetUrl ?? null;
|
|
4233
|
+
} else {
|
|
4234
|
+
meetingAt = null;
|
|
4235
|
+
}
|
|
4373
4236
|
}
|
|
4237
|
+
return { ...summary, meetingAt, meetUrl, timezone };
|
|
4374
4238
|
}
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4239
|
+
|
|
4240
|
+
// src/member-session-application.ts
|
|
4241
|
+
var local = (row2, chapterId, runtime) => row2.groupId === chapterId && (runtime ? row2.stripeRuntime === runtime : row2.stripeRuntime === void 0 || row2.stripeRuntime === "default");
|
|
4242
|
+
async function memberSessionApplication(db, chapterId, runtime, identity2) {
|
|
4243
|
+
const lookup = async (where) => {
|
|
4244
|
+
const rows5 = (await db.query({ applications: { $: { where: { groupId: chapterId, ...where }, limit: 100 } } })).applications;
|
|
4245
|
+
if (!Array.isArray(rows5) || rows5.length >= 100) throw new Error("member identity lookup incomplete");
|
|
4246
|
+
return rows5.filter((row2) => local(row2, chapterId, runtime));
|
|
4247
|
+
};
|
|
4248
|
+
let apps = await lookup({ clerkUserId: identity2.userId });
|
|
4249
|
+
let emailFallback = false;
|
|
4250
|
+
if (!apps.length && identity2.email) {
|
|
4251
|
+
const normalized = normalizeChapterEmail(identity2.email);
|
|
4252
|
+
const normalizedRows = await lookup({ emailNormalized: normalized });
|
|
4253
|
+
const legacyRows = await lookup({ email: identity2.email });
|
|
4254
|
+
apps = [...new Map([...normalizedRows, ...legacyRows].map((row2) => [row2.id, row2])).values()];
|
|
4255
|
+
if (apps.length !== 1 || apps[0]?.clerkUserId !== void 0 || apps[0]?.clerkAccountPending === true || ["declined", "canceled", "refunded"].includes(String(apps[0]?.status))) return null;
|
|
4256
|
+
emailFallback = true;
|
|
4385
4257
|
}
|
|
4386
|
-
|
|
4387
|
-
|
|
4258
|
+
let app = apps.sort((a, b) => Number(b.createdAt ?? 0) - Number(a.createdAt ?? 0))[0];
|
|
4259
|
+
if (!app) return null;
|
|
4260
|
+
if (emailFallback) {
|
|
4261
|
+
const keys3 = ["groupId", "stripeRuntime", "email", "emailNormalized", "status", "clerkAccountPending"];
|
|
4388
4262
|
try {
|
|
4389
|
-
await db.transact([
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
termEndsAt: member.termEndsAt,
|
|
4395
|
-
acceptanceEndsAt: Number(member.termEndsAt),
|
|
4396
|
-
entitlementId,
|
|
4397
|
-
revision: Number(seat.revision) + 1,
|
|
4398
|
-
updatedAt: now
|
|
4399
|
-
}),
|
|
4400
|
-
membershipUpdate("networkEntitlement", entitlementId, {
|
|
4401
|
-
id: entitlementId,
|
|
4402
|
-
...membershipScope(input),
|
|
4403
|
-
membershipId: member.id,
|
|
4404
|
-
seatId: seat.id,
|
|
4405
|
-
kind: "named_seat",
|
|
4406
|
-
status: "pending_acceptance",
|
|
4407
|
-
termStartedAt: now,
|
|
4408
|
-
termEndsAt: member.termEndsAt,
|
|
4409
|
-
offeringSnapshot: member.offeringSnapshot,
|
|
4410
|
-
sourceEventId: source,
|
|
4411
|
-
revision: 1,
|
|
4412
|
-
createdAt: now,
|
|
4413
|
-
updatedAt: now
|
|
4414
|
-
}),
|
|
4415
|
-
membershipUpdate("membershipEffect", inviteId, {
|
|
4416
|
-
id: inviteId,
|
|
4417
|
-
chapterId: input.chapterId,
|
|
4418
|
-
environment: input.environment,
|
|
4419
|
-
runtime: input.runtime,
|
|
4420
|
-
membershipId: member.id,
|
|
4421
|
-
seatId: seat.id,
|
|
4422
|
-
kind: "invite_named_recipient",
|
|
4423
|
-
status: "pending",
|
|
4424
|
-
attempts: 0,
|
|
4425
|
-
createdAt: now,
|
|
4426
|
-
updatedAt: now
|
|
4427
|
-
})
|
|
4428
|
-
], { mutationId: `named-seat-included:${seat.id}`, guards: [
|
|
4429
|
-
{
|
|
4430
|
-
ns: "networkSeat",
|
|
4431
|
-
id: seat.id,
|
|
4263
|
+
await db.transact([{ t: "update", ns: "applications", id: String(app.id), attrs: { clerkUserId: identity2.userId } }], {
|
|
4264
|
+
mutationId: `member-account-link:${app.id}:${identity2.userId}`,
|
|
4265
|
+
guards: [{
|
|
4266
|
+
ns: "applications",
|
|
4267
|
+
id: String(app.id),
|
|
4432
4268
|
exists: true,
|
|
4433
|
-
equals:
|
|
4434
|
-
absent: ["
|
|
4435
|
-
}
|
|
4436
|
-
|
|
4437
|
-
{ ns: "networkMembership", id: String(member.id), exists: true, equals: { revision: member.revision, namedSeatId: seat.id } },
|
|
4438
|
-
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
|
|
4439
|
-
] });
|
|
4269
|
+
equals: Object.fromEntries(keys3.filter((key3) => app[key3] !== void 0).map((key3) => [key3, app[key3]])),
|
|
4270
|
+
absent: ["clerkUserId", ...keys3.filter((key3) => app[key3] === void 0)]
|
|
4271
|
+
}]
|
|
4272
|
+
});
|
|
4440
4273
|
} catch (error) {
|
|
4441
4274
|
if (error.code !== "transact_guard_failed") throw error;
|
|
4442
4275
|
}
|
|
4276
|
+
const saved = (await db.query({ applications: { $: { where: { id: app.id }, limit: 1 } } })).applications?.[0];
|
|
4277
|
+
if (!saved || !local(saved, chapterId, runtime) || saved.clerkUserId !== identity2.userId || keys3.some((key3) => saved[key3] !== app[key3])) return null;
|
|
4278
|
+
app = saved;
|
|
4443
4279
|
}
|
|
4444
|
-
const
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
|
|
4280
|
+
const meeting = (await db.query({ meetings: { $: {
|
|
4281
|
+
where: { applicationId: app.id, status: "scheduled" },
|
|
4282
|
+
order: { createdAt: "desc" },
|
|
4283
|
+
limit: 1
|
|
4284
|
+
} } })).meetings?.[0];
|
|
4285
|
+
const group = (await db.query({ groups: { $: { where: { id: chapterId }, limit: 1 } } })).groups?.[0];
|
|
4286
|
+
const timezone = resolveScheduling(group?.schedulingJson).timezone;
|
|
4287
|
+
return { source: app, application: memberApplication(
|
|
4288
|
+
app,
|
|
4289
|
+
meeting,
|
|
4290
|
+
timezone
|
|
4291
|
+
) };
|
|
4448
4292
|
}
|
|
4449
4293
|
|
|
4450
|
-
// src/
|
|
4451
|
-
async function
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
const
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
|
|
4469
|
-
membershipId: member.id,
|
|
4470
|
-
seatId: seat.id,
|
|
4471
|
-
kind: "refund_seat",
|
|
4472
|
-
status: "pending",
|
|
4473
|
-
attempts: 0,
|
|
4474
|
-
createdAt: now,
|
|
4475
|
-
updatedAt: now
|
|
4476
|
-
}),
|
|
4477
|
-
...seat.recipientUserId ? [membershipUpdate("membershipEffect", `${seat.id}:revoke`, {
|
|
4478
|
-
id: `${seat.id}:revoke`,
|
|
4479
|
-
chapterId: input.chapterId,
|
|
4480
|
-
environment: input.environment,
|
|
4481
|
-
runtime: input.runtime,
|
|
4482
|
-
membershipId: member.id,
|
|
4483
|
-
seatId: seat.id,
|
|
4484
|
-
kind: "revoke_named_recipient_access",
|
|
4485
|
-
status: "pending",
|
|
4486
|
-
attempts: 0,
|
|
4487
|
-
createdAt: now,
|
|
4488
|
-
updatedAt: now
|
|
4489
|
-
})] : []
|
|
4490
|
-
], { mutationId: `named-seat-refund-request:${seat.id}`, guards: [
|
|
4491
|
-
{ ns: "networkSeat", id: input.seatId, exists: true, equals: { revision: seat.revision } },
|
|
4492
|
-
{ ns: "networkMembership", id: String(member.id), exists: true, equals: { revision: member.revision } },
|
|
4493
|
-
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
|
|
4494
|
-
...entitlement ? [{ ns: "networkEntitlement", id: String(entitlement.id), exists: true, equals: { revision: entitlement.revision } }] : []
|
|
4495
|
-
] });
|
|
4496
|
-
}
|
|
4497
|
-
async function completeNamedSeatRefund(db, seatId, secretKey, now) {
|
|
4498
|
-
membershipClock(now);
|
|
4499
|
-
const seat = await membershipLoad(db, "networkSeat", seatId);
|
|
4500
|
-
if (!seat) return membershipFail("named_seat_not_found");
|
|
4501
|
-
await loadMembershipAuthorityState(db, seat);
|
|
4502
|
-
if (seat.status === "refunded") return;
|
|
4503
|
-
if (seat.status !== "refund_pending") membershipFail("named_seat_refund_not_ready");
|
|
4504
|
-
const result = namedSeatIncluded(seat) ? { refundedCents: 0, refundIds: [] } : await refundNamedSeatStripePayment(secretKey, seat);
|
|
4505
|
-
await db.transact([
|
|
4506
|
-
membershipUpdate("networkSeat", seatId, {
|
|
4507
|
-
status: "refunded",
|
|
4508
|
-
refundedCents: result.refundedCents,
|
|
4509
|
-
providerRefundIds: result.refundIds,
|
|
4510
|
-
revision: Number(seat.revision) + 1,
|
|
4511
|
-
updatedAt: now
|
|
4512
|
-
}),
|
|
4513
|
-
membershipUpdate("membershipEffect", `${seatId}:refund`, { status: "complete", updatedAt: now })
|
|
4514
|
-
], { mutationId: `named-seat-refunded:${seatId}`, guards: [
|
|
4515
|
-
{ ns: "networkSeat", id: seatId, exists: true, equals: { status: "refund_pending", revision: seat.revision } }
|
|
4516
|
-
] });
|
|
4517
|
-
}
|
|
4518
|
-
|
|
4519
|
-
// src/membership-seat-operator-refund.ts
|
|
4520
|
-
async function refundNamedSeatByOperator(db, input, stripeSecretKey, now) {
|
|
4521
|
-
const seat = await membershipLoad(db, "networkSeat", input.seatId);
|
|
4522
|
-
requireMembershipScope(seat, input);
|
|
4523
|
-
await requestNamedSeatRefund(db, { ...input, reason: "operator" }, now);
|
|
4524
|
-
await completeNamedSeatRefund(db, input.seatId, stripeSecretKey, now);
|
|
4525
|
-
const refunded = await membershipLoad(db, "networkSeat", input.seatId);
|
|
4526
|
-
if (refunded?.status !== "refunded" || !Number.isSafeInteger(refunded.refundedCents) || Number(refunded.refundedCents) !== Number(seat.amountCents)) {
|
|
4527
|
-
throw new Error("named seat refund unconfirmed");
|
|
4294
|
+
// src/named-seat-request.ts
|
|
4295
|
+
async function smallRequestBody(req) {
|
|
4296
|
+
const reader = req.body?.getReader();
|
|
4297
|
+
if (!reader) throw new Error("request body required");
|
|
4298
|
+
const chunks = [];
|
|
4299
|
+
let size = 0;
|
|
4300
|
+
try {
|
|
4301
|
+
while (true) {
|
|
4302
|
+
const chunk = await reader.read();
|
|
4303
|
+
if (chunk.done) break;
|
|
4304
|
+
size += chunk.value.byteLength;
|
|
4305
|
+
if (size > 4096) {
|
|
4306
|
+
await reader.cancel();
|
|
4307
|
+
throw new Error("request too large");
|
|
4308
|
+
}
|
|
4309
|
+
chunks.push(chunk.value);
|
|
4310
|
+
}
|
|
4311
|
+
} finally {
|
|
4312
|
+
reader.releaseLock();
|
|
4528
4313
|
}
|
|
4529
|
-
|
|
4314
|
+
const bytes = new Uint8Array(size);
|
|
4315
|
+
let offset = 0;
|
|
4316
|
+
for (const chunk of chunks) {
|
|
4317
|
+
bytes.set(chunk, offset);
|
|
4318
|
+
offset += chunk.byteLength;
|
|
4319
|
+
}
|
|
4320
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
4321
|
+
}
|
|
4322
|
+
async function namedSeatRequest(req) {
|
|
4323
|
+
const body = JSON.parse(await smallRequestBody(req));
|
|
4324
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) throw new Error("object required");
|
|
4325
|
+
return body;
|
|
4530
4326
|
}
|
|
4531
4327
|
|
|
4532
|
-
// src/
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
const
|
|
4537
|
-
const
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
if (
|
|
4541
|
-
|
|
4542
|
-
if (
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
|
|
4328
|
+
// src/named-seat-release.ts
|
|
4329
|
+
var SEAT_FIELDS = ["namedSeatId", "namedSeatPrimaryApplicationId", "namedSeatClaimPending", "namedSeatAcceptedAt"];
|
|
4330
|
+
async function releaseNamedSeatRecipientApplications(db, env, ctx, input) {
|
|
4331
|
+
const bound = (await db.query({ applications: { $: { where: { namedSeatId: input.seatId }, limit: 5 } } })).applications ?? [];
|
|
4332
|
+
const keep = input.keepRecipientEmail ? normalizeChapterEmail(input.keepRecipientEmail) : null;
|
|
4333
|
+
const released = [];
|
|
4334
|
+
for (const app of bound) {
|
|
4335
|
+
if (!chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME)) continue;
|
|
4336
|
+
if (input.recipientApplicationId && app.id !== input.recipientApplicationId) continue;
|
|
4337
|
+
if (input.recipientUserId && app.clerkUserId !== input.recipientUserId) continue;
|
|
4338
|
+
if (keep && normalizeChapterEmail(String(app.emailNormalized ?? app.email ?? "")) === keep) continue;
|
|
4339
|
+
await releaseOne(db, env, ctx, app);
|
|
4340
|
+
released.push(String(app.id));
|
|
4341
|
+
}
|
|
4342
|
+
return { released };
|
|
4343
|
+
}
|
|
4344
|
+
async function releaseOne(db, env, ctx, app) {
|
|
4345
|
+
const id7 = String(app.id), closed = app.status === "declined" && app.canceled === true;
|
|
4346
|
+
if (!closed) {
|
|
4347
|
+
await db.transact([
|
|
4348
|
+
{ t: "update", ns: "applications", id: id7, attrs: { status: "declined", canceled: true } },
|
|
4349
|
+
{ t: "retract", ns: "applications", id: id7, attrs: SEAT_FIELDS.filter((key3) => app[key3] !== void 0) }
|
|
4350
|
+
], { mutationId: `named-seat-release:${id7}:${String(app.namedSeatId)}`, guards: [
|
|
4351
|
+
{ ns: "applications", id: id7, exists: true, equals: { namedSeatId: app.namedSeatId, status: app.status } }
|
|
4552
4352
|
] });
|
|
4553
|
-
|
|
4353
|
+
} else if (SEAT_FIELDS.some((key3) => app[key3] !== void 0)) {
|
|
4354
|
+
await db.transact(
|
|
4355
|
+
[{ t: "retract", ns: "applications", id: id7, attrs: SEAT_FIELDS.filter((key3) => app[key3] !== void 0) }],
|
|
4356
|
+
{ guards: [{ ns: "applications", id: id7, exists: true, equals: { namedSeatId: app.namedSeatId } }] }
|
|
4357
|
+
);
|
|
4554
4358
|
}
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
const state = await loadMembershipAuthorityState(db, scope2);
|
|
4563
|
-
const versions = (await db.query({ membershipOfferingVersion: { $: { where: { chapterId: scope2.chapterId }, limit: 200 } } })).membershipOfferingVersion ?? [];
|
|
4564
|
-
if (versions.length >= 200) throw Object.assign(new Error("policy_history_incomplete"), { code: "policy_history_incomplete" });
|
|
4565
|
-
const current = versions.map((row2) => membershipOfferingVersion(row2.snapshot)).filter((offer) => offer.baseSkuId === scope2.skuId && offer.status === "published" && now >= offer.effectiveFrom && (offer.effectiveUntil === void 0 || now < offer.effectiveUntil)).sort((a, b) => b.version - a.version)[0];
|
|
4566
|
-
if (!current) throw Object.assign(new Error("ambiguous_membership_policy"), { code: "ambiguous_membership_policy" });
|
|
4567
|
-
const target = current.seatPriceCents;
|
|
4568
|
-
const rows5 = (await db.query({ networkMembership: { $: { where: { ...scope2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkMembership ?? [];
|
|
4569
|
-
const receipt = { seatPriceCents: target, repriced: 0, skipped: [], more: rows5.length > limit };
|
|
4570
|
-
for (const member of rows5.slice(0, limit)) {
|
|
4571
|
-
if (Object.entries(scope2).some(([key3, value]) => member[key3] !== value)) throw new Error("membership reprice scope mismatch");
|
|
4572
|
-
const id7 = String(member.id), skip = (reason) => {
|
|
4573
|
-
receipt.skipped.push({ membershipId: id7, reason });
|
|
4574
|
-
};
|
|
4575
|
-
const offering = membershipOfferingVersion(member.offeringSnapshot);
|
|
4576
|
-
if (offering.seatPriceCents <= target) {
|
|
4577
|
-
skip("seat price already at or below the current offering");
|
|
4578
|
-
continue;
|
|
4579
|
-
}
|
|
4580
|
-
if (!["paid_pending_approval", "active", "cancel_at_term_end", "payment_grace"].includes(String(member.status))) {
|
|
4581
|
-
skip(`membership is ${String(member.status)}`);
|
|
4582
|
-
continue;
|
|
4583
|
-
}
|
|
4584
|
-
const seat = member.namedSeatId ? await membershipLoad(db, "networkSeat", String(member.namedSeatId)) : void 0;
|
|
4585
|
-
if (seat && (seat.status !== "payment_pending" || seat.providerPaymentIntentId !== void 0)) {
|
|
4586
|
-
skip(`seat is ${String(seat.status)}`);
|
|
4587
|
-
continue;
|
|
4588
|
-
}
|
|
4589
|
-
const ops = [membershipUpdate("networkMembership", id7, { offeringSnapshot: { ...offering, seatPriceCents: target }, revision: Number(member.revision) + 1, updatedAt: now })];
|
|
4590
|
-
const guards = [
|
|
4591
|
-
{ ns: "networkMembership", id: id7, exists: true, equals: { revision: member.revision } },
|
|
4592
|
-
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative" } }
|
|
4593
|
-
];
|
|
4594
|
-
if (seat) {
|
|
4595
|
-
ops.push(membershipUpdate("networkSeat", String(seat.id), { amountCents: target, offeringSnapshot: { ...offering, seatPriceCents: target }, revision: Number(seat.revision) + 1, updatedAt: now }));
|
|
4596
|
-
guards.push({ ns: "networkSeat", id: String(seat.id), exists: true, equals: { revision: seat.revision, status: "payment_pending" } });
|
|
4597
|
-
}
|
|
4598
|
-
await db.transact(ops, { mutationId: `seat-reprice:${id7}:${member.revision}`, guards });
|
|
4599
|
-
receipt.repriced++;
|
|
4359
|
+
const promoteTo = ctx.chapter.operations.onApprove.promoteTo;
|
|
4360
|
+
if (promoteTo !== false && typeof app.clerkUserId === "string" && app.clerkUserId) {
|
|
4361
|
+
const secret = await getVaultSecret(db, "clerk_secret_key");
|
|
4362
|
+
if (!secret) throw new Error("named seat release provider unavailable");
|
|
4363
|
+
const user = await clerkGetUser(secret, app.clerkUserId, fetch, clerkRoleMetadata(ctx.chapter.auth));
|
|
4364
|
+
if (!user) throw new Error("named seat release provider unavailable");
|
|
4365
|
+
if (user.role !== ctx.chapter.auth.adminRole && user.role === promoteTo && !await clerkSetRole(secret, user.id, ctx.chapter.auth.ladder[0], fetch, clerkRoleMetadata(ctx.chapter.auth))) throw new Error("named seat release role pending");
|
|
4600
4366
|
}
|
|
4601
|
-
|
|
4367
|
+
const seatless = Object.fromEntries(Object.entries({ ...app, status: "declined", canceled: true }).filter(([key3]) => !SEAT_FIELDS.includes(key3)));
|
|
4368
|
+
await syncApplicationToCrm(crmDeps(db, ctx), { app: seatless, stage: "declined" }).catch(() => void 0);
|
|
4602
4369
|
}
|
|
4603
4370
|
|
|
4604
|
-
// src/worker-routes-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
var REPRICE = "/api/admin/membership-seats/reprice";
|
|
4608
|
-
var ONE = /^\/api\/admin\/membership-seats\/([A-Za-z0-9][A-Za-z0-9_.:-]{2,159})\/refund$/;
|
|
4609
|
-
var SCOPE = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
4610
|
-
var PUBLIC = [
|
|
4611
|
-
"id",
|
|
4612
|
-
"appId",
|
|
4613
|
-
"chapterId",
|
|
4614
|
-
"environment",
|
|
4615
|
-
"runtime",
|
|
4616
|
-
"applicationId",
|
|
4617
|
-
"skuId",
|
|
4618
|
-
"status",
|
|
4619
|
-
"amountCents",
|
|
4620
|
-
"currency",
|
|
4621
|
-
"recipientName",
|
|
4622
|
-
"recipientEmailNormalized",
|
|
4623
|
-
"purchaserUserId",
|
|
4624
|
-
"purchasedAt",
|
|
4625
|
-
"acceptedAt",
|
|
4626
|
-
"journeyApprovedAt",
|
|
4627
|
-
"refundedCents",
|
|
4628
|
-
"refundReason",
|
|
4629
|
-
"termEndsAt",
|
|
4630
|
-
"updatedAt"
|
|
4631
|
-
];
|
|
4632
|
-
var project = (row2) => Object.fromEntries(PUBLIC.filter((key3) => row2[key3] !== void 0).map((key3) => [key3, row2[key3]]));
|
|
4633
|
-
var handleAdminMembershipSeats = async (req, url, env, ctx) => {
|
|
4634
|
-
const one = url.pathname.match(ONE), lift = url.pathname === LIFT, reprice = url.pathname === REPRICE;
|
|
4635
|
-
if (url.pathname !== LIST && !one && !lift && !reprice) return null;
|
|
4636
|
-
const operator = ctx.membershipAuthorityOperator;
|
|
4637
|
-
if (!operator) return json({ error: "not found" }, 404);
|
|
4638
|
-
if (req.method !== (one || lift || reprice ? "POST" : "GET")) return json({ error: "method not allowed" }, 405);
|
|
4639
|
-
const raw2 = ctx.makeDb(env), user = await ctx.verifyUser(req, env);
|
|
4640
|
-
if (!user) return json({ error: "unauthorized" }, 401);
|
|
4641
|
-
if (!await ctx.isSuperAdminEmail(raw2, user.email, user.userId)) return json({ error: "super-admin required", code: "super_admin_required" }, 403);
|
|
4642
|
-
let db;
|
|
4371
|
+
// src/worker-routes-named-seat-regift.ts
|
|
4372
|
+
async function recipientFrom(req, purchaserEmail) {
|
|
4373
|
+
let body;
|
|
4643
4374
|
try {
|
|
4644
|
-
|
|
4375
|
+
body = await namedSeatRequest(req);
|
|
4645
4376
|
} catch {
|
|
4646
|
-
return json({ error: "
|
|
4647
|
-
}
|
|
4648
|
-
if (lift || reprice) {
|
|
4649
|
-
let body;
|
|
4650
|
-
try {
|
|
4651
|
-
body = await req.json();
|
|
4652
|
-
} catch {
|
|
4653
|
-
return json({ error: "chapterId and runtime are required" }, 400);
|
|
4654
|
-
}
|
|
4655
|
-
const appId = body.appId ?? body.chapterId;
|
|
4656
|
-
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) || reprice && (typeof body.skuId !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_.:-]{2,159}$/.test(body.skuId))) {
|
|
4657
|
-
return json({ error: reprice ? "chapterId, runtime and skuId are required" : "chapterId and runtime are required" }, 400);
|
|
4658
|
-
}
|
|
4659
|
-
const scope2 = { appId, chapterId: body.chapterId, environment: env.ODLA_ENV, runtime: body.runtime };
|
|
4660
|
-
try {
|
|
4661
|
-
if (reprice) return json({ ok: true, ...await lowerSeatPrice(db, { ...scope2, skuId: String(body.skuId) }, Date.now()) });
|
|
4662
|
-
return json({ ok: true, ...await liftSeatPurchaseWindow(db, scope2, Date.now()) });
|
|
4663
|
-
} catch (error) {
|
|
4664
|
-
const code = error.code;
|
|
4665
|
-
return json({ error: `${reprice ? "seat reprice" : "purchase window lift"} is pending; retry to confirm`, ...code ? { code } : {} }, code ? 409 : 503);
|
|
4666
|
-
}
|
|
4377
|
+
return json({ error: "invalid request" }, 400);
|
|
4667
4378
|
}
|
|
4668
|
-
if (!
|
|
4669
|
-
|
|
4670
|
-
return json({ seats: rows5.slice(0, 200).map(project), truncated: rows5.length > 200 });
|
|
4379
|
+
if (Object.keys(body).some((key3) => !["recipientName", "recipientEmail"].includes(key3)) || typeof body.recipientName !== "string" || typeof body.recipientEmail !== "string" || body.recipientName.length > 160 || body.recipientEmail.length > 254) {
|
|
4380
|
+
return json({ error: "a recipient name and email are required" }, 400);
|
|
4671
4381
|
}
|
|
4672
|
-
const
|
|
4673
|
-
if (
|
|
4674
|
-
|
|
4675
|
-
return json({ ok: true, seatId: seat.id, status: "refunded", refundedCents: seat.refundedCents, subscriptionCanceled: false, replayed: true });
|
|
4382
|
+
const recipientName = body.recipientName.trim().replace(/\s+/g, " "), recipientEmail = normalizeChapterEmail(body.recipientEmail);
|
|
4383
|
+
if (recipientName.length < 2 || /[\u0000-\u001f\u007f]/.test(body.recipientName) || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(recipientEmail) || purchaserEmail && recipientEmail === normalizeChapterEmail(purchaserEmail)) {
|
|
4384
|
+
return json({ error: "a valid name and email for someone other than the purchaser are required" }, 400);
|
|
4676
4385
|
}
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4386
|
+
return { recipientName, recipientEmail };
|
|
4387
|
+
}
|
|
4388
|
+
async function regiftChapterNamedSeat(db, env, ctx, req, target, purchaserEmail) {
|
|
4389
|
+
const seats = ctx.membershipAuthority?.namedSeats;
|
|
4390
|
+
if (!seats?.regift) return json({ error: "naming a different recipient is not available on this network authority" }, 503);
|
|
4391
|
+
const recipient2 = await recipientFrom(req, purchaserEmail);
|
|
4392
|
+
if (recipient2 instanceof Response) return recipient2;
|
|
4393
|
+
const offer = await seats.offer(target, env), seat = offer.seat;
|
|
4394
|
+
if (!seat) return json({ error: "no purchased seat", code: "seat_not_found" }, 409);
|
|
4395
|
+
if (seat.regiftable !== true) return json({ error: `the seat is ${seat.status.replaceAll("_", " ")} and can no longer be given to someone else`, code: "seat_not_regiftable" }, 409);
|
|
4396
|
+
const receipt = await seats.regift({ ...target, seatId: seat.seatId, ...recipient2 }, env);
|
|
4397
|
+
if (receipt.seatId !== seat.seatId || receipt.recipientEmail !== recipient2.recipientEmail) throw new Error("named seat regift differs from the request");
|
|
4398
|
+
const released = await releaseNamedSeatRecipientApplications(db, env, ctx, { seatId: seat.seatId, keepRecipientEmail: receipt.recipientEmail });
|
|
4399
|
+
const consentId = `${offer.membershipId}:named-seat-consent`, consent = await membershipLoad(db, "namedSeatConsents", consentId);
|
|
4400
|
+
if (consent) await db.transact([{ t: "update", ns: "namedSeatConsents", id: consentId, attrs: {
|
|
4401
|
+
recipientName: receipt.recipientName,
|
|
4402
|
+
recipientEmail: receipt.recipientEmail,
|
|
4403
|
+
acceptedAt: Date.now()
|
|
4404
|
+
} }], { guards: [{ ns: "namedSeatConsents", id: consentId, exists: true }] });
|
|
4405
|
+
return json({ ok: true, seat: receipt, releasedApplicationIds: released.released });
|
|
4406
|
+
}
|
|
4407
|
+
var handleNamedSeatRegift = async (req, url, env, ctx) => {
|
|
4408
|
+
if (url.pathname !== "/api/named-seat/regift") return null;
|
|
4409
|
+
const seats = ctx.membershipAuthority?.namedSeats;
|
|
4410
|
+
if (ctx.chapter.mode !== "chapter" || !seats) return json({ error: "named seats are not configured" }, 404);
|
|
4411
|
+
if (req.method !== "POST") return json({ error: "method not allowed" }, 405);
|
|
4412
|
+
const user = await ctx.verifyUser(req, env);
|
|
4413
|
+
if (!user?.email) return json({ error: "unauthorized" }, 401);
|
|
4414
|
+
const db = ctx.makeDb(env);
|
|
4680
4415
|
try {
|
|
4681
|
-
const
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
}, secret, Date.now());
|
|
4690
|
-
return json({ ok: true, ...receipt });
|
|
4691
|
-
} catch (error) {
|
|
4692
|
-
const code = error.code;
|
|
4693
|
-
return json({ error: "seat refund is pending; retry to confirm", ...code ? { code } : {} }, code ? 409 : 503);
|
|
4416
|
+
const member = await memberSessionApplication(db, ctx.chapter.id, env.ODLA_RUNTIME, { userId: user.userId, email: user.email });
|
|
4417
|
+
if (!member || member.source.clerkUserId !== user.userId) return json({ error: "membership required" }, 403);
|
|
4418
|
+
const intent = await membershipLoad(db, "subscriptionCheckoutIntents", String(member.source.id));
|
|
4419
|
+
if (!intent) return json({ error: "paid membership required" }, 409);
|
|
4420
|
+
const target = await membershipTargetForIntent(intent, member.source, env, ctx);
|
|
4421
|
+
return await regiftChapterNamedSeat(db, env, ctx, req, target, user.email);
|
|
4422
|
+
} catch {
|
|
4423
|
+
return json({ error: "named seat service unavailable; refresh to check the seat before trying again" }, 503);
|
|
4694
4424
|
}
|
|
4695
4425
|
};
|
|
4696
4426
|
|
|
4697
|
-
// src/
|
|
4698
|
-
var
|
|
4699
|
-
var
|
|
4700
|
-
var
|
|
4701
|
-
const
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
const
|
|
4706
|
-
|
|
4707
|
-
}
|
|
4708
|
-
var live = (w, now) => w.from <= now && (w.until === void 0 || now < w.until);
|
|
4709
|
-
function stateOf(item, all, now, window, rivals) {
|
|
4710
|
-
if (item.status !== "published") return { state: item.status };
|
|
4711
|
-
const mine = window(item);
|
|
4712
|
-
if (now < mine.from) return { state: "scheduled" };
|
|
4713
|
-
const closed = mine.until !== void 0 && mine.until <= now;
|
|
4714
|
-
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));
|
|
4715
|
-
if (!closed && !higher.some((w) => live(w, now))) return { state: "active" };
|
|
4716
|
-
const dates = higher.map((w) => Math.max(w.from, mine.from));
|
|
4717
|
-
if (closed) dates.push(mine.until);
|
|
4718
|
-
return { state: "superseded", supersededAt: Math.min(...dates) };
|
|
4719
|
-
}
|
|
4720
|
-
var mechanism = (policy2) => policy2.percentOffBps === 0 || policy2.discountEligibleSkuIds.length === 0 ? "none" : policy2.benefitDuration.kind === "while_active" ? "stripe_coupon_forever" : policy2.benefitDuration.kind === "first_term" ? "stripe_coupon_once" : "stripe_coupon_term_bound";
|
|
4721
|
-
var sharesSku = (a, b) => a.cohortEligibleSkuIds.some((sku) => b.cohortEligibleSkuIds.includes(sku));
|
|
4722
|
-
var byChapter = (a, b) => a.chapterId.localeCompare(b.chapterId);
|
|
4723
|
-
function normalize(rows5, collection, parse, invalid2) {
|
|
4724
|
-
const out = [];
|
|
4725
|
-
for (const row2 of rows5) {
|
|
4726
|
-
try {
|
|
4727
|
-
out.push({ value: parse(row2.snapshot), row: row2 });
|
|
4728
|
-
} catch (error) {
|
|
4729
|
-
invalid2.push({ collection, id: String(row2.id ?? ""), error: error instanceof Error ? error.message : String(error) });
|
|
4730
|
-
}
|
|
4731
|
-
}
|
|
4732
|
-
return out;
|
|
4733
|
-
}
|
|
4734
|
-
async function listMembershipPolicyVersions(db, input) {
|
|
4735
|
-
const where = input.chapterId ? { chapterId: input.chapterId } : {};
|
|
4736
|
-
const rows5 = await db.query({
|
|
4737
|
-
membershipOfferingVersion: { $: { where, limit: LIMIT } },
|
|
4738
|
-
discountPolicyVersion: { $: { where, limit: LIMIT } }
|
|
4739
|
-
});
|
|
4740
|
-
const offeringRows = rows5.membershipOfferingVersion ?? [], policyRows = rows5.discountPolicyVersion ?? [];
|
|
4741
|
-
const invalid2 = [];
|
|
4742
|
-
const offerings = normalize(offeringRows, "membershipOfferingVersion", membershipOfferingVersion, invalid2);
|
|
4743
|
-
const policies = normalize(policyRows, "discountPolicyVersion", membershipDiscountPolicyVersion, invalid2);
|
|
4744
|
-
const offeringValues = offerings.map((item) => item.value), policyValues = policies.map((item) => item.value);
|
|
4745
|
-
return {
|
|
4746
|
-
now: input.now,
|
|
4747
|
-
chapterId: input.chapterId ?? null,
|
|
4748
|
-
offerings: offerings.map(({ value, row: row2 }) => ({
|
|
4749
|
-
...value,
|
|
4750
|
-
...provenance(row2),
|
|
4751
|
-
...stateOf(
|
|
4752
|
-
value,
|
|
4753
|
-
offeringValues,
|
|
4754
|
-
input.now,
|
|
4755
|
-
(v) => ({ from: v.effectiveFrom, ...v.effectiveUntil !== void 0 ? { until: v.effectiveUntil } : {} }),
|
|
4756
|
-
(a, b) => a.chapterId === b.chapterId && a.baseSkuId === b.baseSkuId
|
|
4757
|
-
)
|
|
4758
|
-
})).sort((a, b) => byChapter(a, b) || a.baseSkuId.localeCompare(b.baseSkuId) || b.version - a.version),
|
|
4759
|
-
discountPolicies: policies.map(({ value, row: row2 }) => ({
|
|
4760
|
-
...value,
|
|
4761
|
-
...provenance(row2),
|
|
4762
|
-
mechanism: mechanism(value),
|
|
4763
|
-
...stateOf(
|
|
4764
|
-
value,
|
|
4765
|
-
policyValues,
|
|
4766
|
-
input.now,
|
|
4767
|
-
(v) => ({ from: v.qualifyingStartsAt, until: v.qualifyingEndsAt }),
|
|
4768
|
-
(a, b) => a.chapterId === b.chapterId && sharesSku(a, b)
|
|
4769
|
-
)
|
|
4770
|
-
})).sort((a, b) => byChapter(a, b) || b.version - a.version),
|
|
4771
|
-
invalid: invalid2,
|
|
4772
|
-
truncated: offeringRows.length >= LIMIT || policyRows.length >= LIMIT
|
|
4773
|
-
};
|
|
4774
|
-
}
|
|
4775
|
-
|
|
4776
|
-
// src/worker-routes-admin-membership-policy.ts
|
|
4777
|
-
var PATH3 = "/api/admin/membership-policy-versions";
|
|
4778
|
-
var SCOPE2 = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
4779
|
-
var handleAdminMembershipPolicyVersions = async (req, url, env, ctx) => {
|
|
4780
|
-
if (url.pathname !== PATH3) return null;
|
|
4781
|
-
const operator = ctx.membershipAuthorityOperator;
|
|
4782
|
-
if (!operator) return json({ error: "not found" }, 404);
|
|
4783
|
-
if (req.method !== "GET") return json({ error: "method not allowed" }, 405);
|
|
4427
|
+
// src/worker-routes-admin-named-seat.ts
|
|
4428
|
+
var PATH2 = /^\/api\/admin\/applications\/([0-9a-f-]+)\/named-seat(\/refund|\/regift)?$/;
|
|
4429
|
+
var REFUNDABLE = /* @__PURE__ */ new Set(["renewal_pending", "pending_acceptance", "pending_approval", "pending_primary", "active"]);
|
|
4430
|
+
var handleAdminNamedSeat = async (req, url, env, ctx) => {
|
|
4431
|
+
const match = url.pathname.match(PATH2);
|
|
4432
|
+
if (!match) return null;
|
|
4433
|
+
const seats = ctx.membershipAuthority?.namedSeats;
|
|
4434
|
+
if (ctx.chapter.mode !== "chapter" || !seats) return json({ error: "named seats are not configured" }, 404);
|
|
4435
|
+
const refund = match[2] === "/refund", regift = match[2] === "/regift";
|
|
4436
|
+
if (req.method !== (refund || regift ? "POST" : "GET")) return json({ error: "method not allowed" }, 405);
|
|
4784
4437
|
const raw2 = ctx.makeDb(env), user = await ctx.verifyUser(req, env);
|
|
4785
4438
|
if (!user) return json({ error: "unauthorized" }, 401);
|
|
4786
4439
|
if (!await ctx.isSuperAdminEmail(raw2, user.email, user.userId)) return json({ error: "super-admin required", code: "super_admin_required" }, 403);
|
|
4787
|
-
const
|
|
4788
|
-
if (
|
|
4789
|
-
|
|
4440
|
+
const db = raw2, app = await membershipLoad(db, "applications", match[1]);
|
|
4441
|
+
if (!app || !chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME) || app.namedSeatId) return json({ error: "not found" }, 404);
|
|
4442
|
+
const intent = await membershipLoad(db, "subscriptionCheckoutIntents", String(app.id));
|
|
4443
|
+
if (!intent) return json({ error: "paid membership required" }, 409);
|
|
4790
4444
|
try {
|
|
4791
|
-
|
|
4445
|
+
const target = await membershipTargetForIntent(intent, app, env, ctx);
|
|
4446
|
+
if (regift) return await regiftChapterNamedSeat(db, env, ctx, req, target, typeof app.email === "string" ? app.email : void 0);
|
|
4447
|
+
const offer = await seats.offer(target, env);
|
|
4448
|
+
const seat = offer.seat ?? null, refundable = Boolean(seat && REFUNDABLE.has(seat.status));
|
|
4449
|
+
if (!refund) {
|
|
4450
|
+
return json({
|
|
4451
|
+
seat,
|
|
4452
|
+
amountCents: offer.amountCents,
|
|
4453
|
+
currency: offer.currency,
|
|
4454
|
+
termEndsAt: offer.termEndsAt,
|
|
4455
|
+
purchaseEndsAt: offer.purchaseEndsAt,
|
|
4456
|
+
refundable,
|
|
4457
|
+
regiftable: seat?.regiftable === true
|
|
4458
|
+
});
|
|
4459
|
+
}
|
|
4460
|
+
if (!seat) return json({ error: "no purchased seat", code: "seat_not_found" }, 409);
|
|
4461
|
+
if (seat.status === "refunded") {
|
|
4462
|
+
await closeRecipientApplication(db, env, ctx, seat.seatId);
|
|
4463
|
+
return json({ ok: true, seatId: seat.seatId, status: "refunded", replayed: true });
|
|
4464
|
+
}
|
|
4465
|
+
if (!refundable) return json({ error: `seat is ${seat.status.replaceAll("_", " ")}`, code: "seat_not_refundable" }, 409);
|
|
4466
|
+
const receipt = await seats.operatorRefund({ ...target, seatId: seat.seatId }, env);
|
|
4467
|
+
await closeRecipientApplication(db, env, ctx, seat.seatId);
|
|
4468
|
+
return json({ ok: true, ...receipt });
|
|
4792
4469
|
} catch {
|
|
4793
|
-
return json({ error: "
|
|
4470
|
+
return json({ error: "named seat service unavailable; retry to confirm" }, 503);
|
|
4794
4471
|
}
|
|
4795
|
-
return json(await listMembershipPolicyVersions(db, { chapterId, now: Date.now() }));
|
|
4796
4472
|
};
|
|
4797
|
-
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
}
|
|
4806
|
-
async function verifySubscriptionPrice(secretKey, input, resume = false) {
|
|
4807
|
-
if (!/^price_[A-Za-z0-9]+$/.test(input.priceId) || !Number.isSafeInteger(input.priceCents) || input.priceCents <= 0 || !/^[a-z]{3}$/.test(input.currency) || !["month", "year"].includes(input.interval)) subscriptionContractMismatch();
|
|
4808
|
-
const result = await (0, import_stripe8.stripeRequest)({ secretKey }, "GET", `/v1/prices/${encodeURIComponent(input.priceId)}`);
|
|
4809
|
-
const price = result.body;
|
|
4810
|
-
if (!result.ok || !price || price.id !== input.priceId || !resume && price.active !== true || price.unit_amount !== input.priceCents || price.currency !== input.currency || price.type !== "recurring" || price.billing_scheme !== "per_unit" || price.custom_unit_amount || price.transform_quantity || price.recurring?.interval !== input.interval || price.recurring?.interval_count !== 1 || price.recurring?.usage_type !== "licensed" || price.livemode !== (input.environment === "prod")) subscriptionContractMismatch();
|
|
4811
|
-
return typeof price.product === "string" ? price.product : void 0;
|
|
4812
|
-
}
|
|
4813
|
-
function verifySubscriptionResult(sub, input, customerId, metadata) {
|
|
4814
|
-
const items = sub?.items;
|
|
4815
|
-
const seat = input.namedSeat?.amountCents ? input.namedSeat : void 0;
|
|
4816
|
-
const primary = items?.data?.find((item) => item.price?.id === input.priceId);
|
|
4817
|
-
const seatItem = seat && items?.data?.find((item) => item.price?.id === seat.priceId);
|
|
4818
|
-
if (!sub || !/^sub_[A-Za-z0-9]+$/.test(sub.id) || sub.customer !== customerId || input.existingSubscriptionId && sub.id !== input.existingSubscriptionId || !(input.billingTerms?.initialTermEndsAt ? ["incomplete", "active", "trialing"] : ["incomplete", "active"]).includes(sub.status) || sub.livemode !== (input.environment === "prod") || !Object.entries(metadata).every(([key3, value]) => sub.metadata?.[key3] === value) || !Array.isArray(items?.data) || items.has_more !== false || items.data.length !== (seat ? 2 : 1) || primary?.quantity !== 1 || seat && (seatItem?.quantity !== 1 || seatItem.metadata?.seatId !== seat.seatId || seatItem.metadata?.membershipId !== seat.membershipId)) subscriptionContractMismatch();
|
|
4819
|
-
const invoice = sub.latest_invoice;
|
|
4820
|
-
if (!invoice || typeof invoice !== "object" || invoice.customer !== customerId || invoice.currency !== input.currency || invoice.total !== subscriptionAmountDue(input) + (seat?.amountCents ?? 0) || invoice.billing_reason !== "subscription_create") subscriptionContractMismatch();
|
|
4821
|
-
verifySubscriptionBillingResult(sub, input);
|
|
4473
|
+
async function closeRecipientApplication(db, env, ctx, seatId) {
|
|
4474
|
+
const recipient2 = (await db.query({ applications: { $: { where: { namedSeatId: seatId }, limit: 1 } } })).applications?.[0];
|
|
4475
|
+
if (!recipient2 || !chapterBillingApplication(recipient2, ctx.chapter.id, env.ODLA_RUNTIME) || recipient2.status === "refunded") return;
|
|
4476
|
+
const attrs = { status: "refunded", renewalAt: 0, canceled: true };
|
|
4477
|
+
await db.transact(
|
|
4478
|
+
[{ t: "update", ns: "applications", id: String(recipient2.id), attrs }],
|
|
4479
|
+
{ mutationId: `verified-refund:${String(recipient2.id)}:${seatId}` }
|
|
4480
|
+
);
|
|
4481
|
+
await syncApplicationToCrm(crmDeps(db, ctx), { app: { ...recipient2, ...attrs }, stage: "refunded" }).catch(() => void 0);
|
|
4822
4482
|
}
|
|
4823
4483
|
|
|
4824
|
-
// src/membership-
|
|
4825
|
-
var
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
var
|
|
4829
|
-
|
|
4830
|
-
|
|
4484
|
+
// src/membership-seat-refund-stripe.ts
|
|
4485
|
+
var import_stripe7 = require("@odla-ai/stripe");
|
|
4486
|
+
|
|
4487
|
+
// src/membership-checkout-seat-refund.ts
|
|
4488
|
+
var import_stripe6 = require("@odla-ai/stripe");
|
|
4489
|
+
var fail4 = (code = "named_seat_refund_mismatch") => {
|
|
4490
|
+
throw Object.assign(new Error(code), { code });
|
|
4831
4491
|
};
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
return
|
|
4492
|
+
var id2 = (value) => typeof value === "string" ? value : value?.id ?? "";
|
|
4493
|
+
async function refundCheckoutNamedSeat(secretKey, seat) {
|
|
4494
|
+
const invoiceRef = seat.combinedInvoice;
|
|
4495
|
+
if (!invoiceRef || !/^in_[A-Za-z0-9_]+$/.test(invoiceRef.invoiceId) || !Number.isSafeInteger(invoiceRef.totalCents) || invoiceRef.totalCents <= seat.amountCents || seat.amountCents <= 0 || seat.status !== "refund_pending") fail4();
|
|
4496
|
+
const call = async (method, path, params, key3) => {
|
|
4497
|
+
const result = await (0, import_stripe6.stripeRequest)({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
|
|
4498
|
+
if (!result.ok || !result.body) return fail4("named_seat_refund_provider_unavailable");
|
|
4499
|
+
return result.body;
|
|
4500
|
+
};
|
|
4501
|
+
const metadata = { ...stripePartitionMetadata(seat), applicationId: seat.applicationId };
|
|
4502
|
+
const sub = await call("GET", `/v1/subscriptions/${encodeURIComponent(seat.providerSubscriptionId)}`);
|
|
4503
|
+
if (sub.id !== seat.providerSubscriptionId || id2(sub.customer) !== seat.providerCustomerId || sub.livemode !== (seat.environment === "prod") || !Object.entries(metadata).every(([key3, value]) => sub.metadata?.[key3] === value) || !Array.isArray(sub.items?.data) || sub.items.has_more !== false) fail4();
|
|
4504
|
+
const invoice = await call("GET", `/v1/invoices/${encodeURIComponent(invoiceRef.invoiceId)}`);
|
|
4505
|
+
if (invoice.id !== invoiceRef.invoiceId || id2(invoice.subscription ?? invoice.parent?.subscription_details?.subscription) !== sub.id || id2(invoice.customer) !== seat.providerCustomerId || invoice.status !== "paid" || invoice.billing_reason !== "subscription_create" || invoice.total !== invoiceRef.totalCents || invoice.amount_paid !== invoiceRef.totalCents || invoice.currency !== seat.currency || invoice.livemode !== (seat.environment === "prod")) fail4();
|
|
4506
|
+
let chargeId = id2(invoice.charge), paymentId = id2(invoice.payment_intent);
|
|
4507
|
+
if (!chargeId && !paymentId) {
|
|
4508
|
+
const payments = await call("GET", "/v1/invoice_payments", { invoice: invoice.id, status: "paid", limit: 100 });
|
|
4509
|
+
if (!Array.isArray(payments.data) || payments.data.length !== 1 || payments.has_more !== false) fail4();
|
|
4510
|
+
const payment = payments.data[0];
|
|
4511
|
+
if (id2(payment.invoice) !== invoice.id || payment.status !== "paid" || payment.payment?.type !== "payment_intent") fail4();
|
|
4512
|
+
paymentId = id2(payment.payment.payment_intent);
|
|
4840
4513
|
}
|
|
4841
|
-
if (
|
|
4842
|
-
|
|
4843
|
-
}
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
const read2 = await (0, import_stripe9.stripeRequest)({ secretKey: sk }, "GET", path, { "expand[0]": "discounts" });
|
|
4847
|
-
const sub = read2.body, items = sub?.items?.data;
|
|
4848
|
-
const metadata = { ...stripePartitionMetadata(scope2), applicationId: String(member.applicationId) };
|
|
4849
|
-
if (!read2.ok || sub.id !== subId || sub.customer !== member.providerCustomerId || sub.livemode !== (scope2.environment === "prod") || !PROVIDER_LIVE.has(sub.status) || !Object.entries(metadata).every(([key3, value]) => sub.metadata?.[key3] === value) || sub.items?.has_more !== false || !Array.isArray(items) || items.length < 1 || items.length > 2 || items.filter((item) => item.price?.id === member.providerPriceId && item.quantity === 1 && item.price?.product === productId).length !== 1) {
|
|
4850
|
-
throw Object.assign(new Error("provider subscription does not match the frozen membership"), { code: "provider_mismatch" });
|
|
4514
|
+
if (!chargeId) {
|
|
4515
|
+
if (!/^pi_[A-Za-z0-9_]+$/.test(paymentId)) fail4();
|
|
4516
|
+
const payment = await call("GET", `/v1/payment_intents/${encodeURIComponent(paymentId)}`);
|
|
4517
|
+
if (payment.id !== paymentId || id2(payment.customer) !== seat.providerCustomerId || payment.status !== "succeeded" || payment.amount_received !== invoiceRef.totalCents || payment.currency !== seat.currency || payment.livemode !== (seat.environment === "prod")) fail4();
|
|
4518
|
+
chargeId = id2(payment.latest_charge);
|
|
4851
4519
|
}
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
|
|
4856
|
-
if (
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
|
|
4860
|
-
"
|
|
4861
|
-
|
|
4862
|
-
const after = written.body?.discounts;
|
|
4863
|
-
if (!written.ok || written.body?.id !== subId || !Array.isArray(after) || after.length !== 1 || couponOf(after[0]) !== couponId) {
|
|
4864
|
-
membershipFail("membership_reterm_provider_unconfirmed");
|
|
4520
|
+
if (!/^ch_[A-Za-z0-9_]+$/.test(chargeId)) fail4();
|
|
4521
|
+
const charge = await call("GET", `/v1/charges/${encodeURIComponent(chargeId)}`);
|
|
4522
|
+
if (charge.id !== chargeId || id2(charge.customer) !== seat.providerCustomerId || charge.status !== "succeeded" || charge.livemode !== (seat.environment === "prod") || charge.amount !== invoiceRef.totalCents || charge.currency !== seat.currency || charge.invoice != null && id2(charge.invoice) !== invoice.id || paymentId && id2(charge.payment_intent) !== paymentId || !Number.isSafeInteger(charge.amount_refunded) || charge.amount_refunded < 0 || charge.amount_refunded > charge.amount) fail4();
|
|
4523
|
+
const items = sub.items.data.filter((item) => item.metadata?.seatId === seat.id);
|
|
4524
|
+
if (items.length > 1) fail4();
|
|
4525
|
+
if (items.length && sub.status !== "canceled") {
|
|
4526
|
+
const item = items[0];
|
|
4527
|
+
if (!/^si_[A-Za-z0-9_]+$/.test(item.id) || item.quantity !== 1 || item.price?.id !== seat.priceId) fail4();
|
|
4528
|
+
const removed = await call("DELETE", `/v1/subscription_items/${encodeURIComponent(item.id)}`, { proration_behavior: "none" });
|
|
4529
|
+
if (removed.id !== item.id || removed.deleted !== true) fail4("named_seat_renewal_removal_unconfirmed");
|
|
4865
4530
|
}
|
|
4866
|
-
return
|
|
4531
|
+
if (charge.amount_refunded === charge.amount && charge.refunded === true) return { refundedCents: seat.amountCents, refundIds: [] };
|
|
4532
|
+
const list = await call("GET", "/v1/refunds", { charge: chargeId, limit: 100 });
|
|
4533
|
+
if (!Array.isArray(list.data) || list.has_more !== false || list.data.length >= 100) fail4("named_seat_refund_history_incomplete");
|
|
4534
|
+
let refundedCents = 0;
|
|
4535
|
+
const refundIds = [];
|
|
4536
|
+
for (const refund of list.data) {
|
|
4537
|
+
if (id2(refund.charge) !== chargeId || refund.currency !== seat.currency || !Number.isSafeInteger(refund.amount) || refund.amount <= 0 || !/^re_[A-Za-z0-9_]+$/.test(refund.id) || !["succeeded", "pending", "requires_action", "failed", "canceled"].includes(refund.status)) fail4();
|
|
4538
|
+
if (["pending", "requires_action"].includes(refund.status)) fail4("named_seat_refund_pending");
|
|
4539
|
+
if (refund.status === "succeeded") {
|
|
4540
|
+
if (refund.metadata?.seatId !== seat.id || refund.metadata?.membershipId !== seat.primaryMembershipId) fail4("named_seat_refund_allocation_ambiguous");
|
|
4541
|
+
refundedCents += refund.amount;
|
|
4542
|
+
refundIds.push(refund.id);
|
|
4543
|
+
}
|
|
4544
|
+
}
|
|
4545
|
+
if (refundedCents !== charge.amount_refunded || refundedCents > seat.amountCents) fail4();
|
|
4546
|
+
if (refundedCents < seat.amountCents) {
|
|
4547
|
+
const remaining = seat.amountCents - refundedCents;
|
|
4548
|
+
const refund = await call("POST", "/v1/refunds", {
|
|
4549
|
+
charge: chargeId,
|
|
4550
|
+
amount: remaining,
|
|
4551
|
+
reason: "requested_by_customer",
|
|
4552
|
+
metadata: { ...metadata, membershipId: seat.primaryMembershipId, seatId: seat.id }
|
|
4553
|
+
}, `combined-seat-refund:${seat.id}:${remaining}`);
|
|
4554
|
+
if (id2(refund.charge) !== chargeId || refund.amount !== remaining || refund.currency !== seat.currency || !/^re_[A-Za-z0-9_]+$/.test(refund.id)) fail4();
|
|
4555
|
+
if (refund.status !== "succeeded") fail4("named_seat_refund_pending");
|
|
4556
|
+
refundedCents += remaining;
|
|
4557
|
+
refundIds.push(refund.id);
|
|
4558
|
+
}
|
|
4559
|
+
return { refundedCents, refundIds };
|
|
4560
|
+
}
|
|
4561
|
+
|
|
4562
|
+
// src/membership-seat-refund-stripe.ts
|
|
4563
|
+
var fail5 = (code = "named_seat_refund_mismatch") => {
|
|
4564
|
+
throw Object.assign(new Error(code), { code });
|
|
4565
|
+
};
|
|
4566
|
+
async function refundNamedSeatStripePayment(secretKey, seat) {
|
|
4567
|
+
if (seat.combinedInvoice) return refundCheckoutNamedSeat(secretKey, seat);
|
|
4568
|
+
if (!seat.providerPaymentIntentId || seat.status !== "refund_pending") fail5();
|
|
4569
|
+
const call = async (method, path, params, idempotencyKey) => {
|
|
4570
|
+
const result = await (0, import_stripe7.stripeRequest)({ secretKey }, method, path, params, idempotencyKey ? { idempotencyKey } : {});
|
|
4571
|
+
if (!result.ok || !result.body) return fail5("named_seat_refund_provider_unavailable");
|
|
4572
|
+
return result.body;
|
|
4573
|
+
};
|
|
4574
|
+
const metadata = { ...stripePartitionMetadata(seat), applicationId: seat.applicationId, membershipId: seat.primaryMembershipId, seatId: seat.id };
|
|
4575
|
+
const payment = await call("GET", `/v1/payment_intents/${encodeURIComponent(seat.providerPaymentIntentId)}`);
|
|
4576
|
+
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)) fail5();
|
|
4577
|
+
const sub = await call("GET", `/v1/subscriptions/${encodeURIComponent(seat.providerSubscriptionId)}`);
|
|
4578
|
+
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)) fail5();
|
|
4579
|
+
const items = sub.items.data.filter((item) => item.metadata?.seatId === seat.id);
|
|
4580
|
+
if (items.length > 1) fail5();
|
|
4581
|
+
if (items.length && sub.status !== "canceled") {
|
|
4582
|
+
const item = items[0];
|
|
4583
|
+
if (!/^si_[A-Za-z0-9_]+$/.test(item.id) || item.price?.id !== seat.priceId || item.quantity !== 1) fail5();
|
|
4584
|
+
const removed = await call("DELETE", `/v1/subscription_items/${encodeURIComponent(item.id)}`, { proration_behavior: "none" });
|
|
4585
|
+
if (removed.id !== item.id || removed.deleted !== true) fail5("named_seat_renewal_removal_unconfirmed");
|
|
4586
|
+
}
|
|
4587
|
+
const list = await call("GET", "/v1/refunds", { payment_intent: seat.providerPaymentIntentId, limit: 100 });
|
|
4588
|
+
if (!Array.isArray(list.data) || list.has_more !== false || list.data.length >= 100) fail5("named_seat_refund_history_incomplete");
|
|
4589
|
+
let refundedCents = 0, pendingCents = 0;
|
|
4590
|
+
const refundIds = [];
|
|
4591
|
+
for (const refund of list.data) {
|
|
4592
|
+
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)) fail5();
|
|
4593
|
+
if (refund.status === "succeeded") {
|
|
4594
|
+
refundedCents += refund.amount;
|
|
4595
|
+
refundIds.push(refund.id);
|
|
4596
|
+
} else if (refund.status === "pending" || refund.status === "requires_action") pendingCents += refund.amount;
|
|
4597
|
+
}
|
|
4598
|
+
if (refundedCents + pendingCents > seat.amountCents) fail5();
|
|
4599
|
+
if (pendingCents) fail5("named_seat_refund_pending");
|
|
4600
|
+
if (refundedCents < seat.amountCents) {
|
|
4601
|
+
const remaining = seat.amountCents - refundedCents;
|
|
4602
|
+
const refund = await call("POST", "/v1/refunds", {
|
|
4603
|
+
payment_intent: seat.providerPaymentIntentId,
|
|
4604
|
+
amount: remaining,
|
|
4605
|
+
reason: "requested_by_customer",
|
|
4606
|
+
metadata
|
|
4607
|
+
}, `named-seat-refund:${seat.id}:${remaining}`);
|
|
4608
|
+
if (refund.payment_intent !== seat.providerPaymentIntentId || refund.amount !== remaining || refund.currency !== seat.currency || !/^re_[A-Za-z0-9_]+$/.test(refund.id)) fail5();
|
|
4609
|
+
if (refund.status !== "succeeded") fail5("named_seat_refund_pending");
|
|
4610
|
+
refundedCents += remaining;
|
|
4611
|
+
refundIds.push(refund.id);
|
|
4612
|
+
}
|
|
4613
|
+
return { refundedCents, refundIds };
|
|
4614
|
+
}
|
|
4615
|
+
|
|
4616
|
+
// src/membership-seat-reservation.ts
|
|
4617
|
+
async function namedSeatRecipientClaimId(input, email) {
|
|
4618
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify([input.appId, input.chapterId, input.environment, input.runtime, email])));
|
|
4619
|
+
return `seat-recipient:${[...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
4620
|
+
}
|
|
4621
|
+
async function reserveNamedMembershipSeat(db, input, now) {
|
|
4622
|
+
membershipClock(now);
|
|
4623
|
+
const name = typeof input.recipientName === "string" ? input.recipientName.trim().replace(/\s+/g, " ") : "";
|
|
4624
|
+
const email = normalizeChapterEmail(typeof input.recipientEmail === "string" ? input.recipientEmail : "");
|
|
4625
|
+
const purchaser = normalizeChapterEmail(typeof input.purchaserEmail === "string" ? input.purchaserEmail : "");
|
|
4626
|
+
if (name.length < 2 || name.length > 160 || /[\u0000-\u001f\u007f]/.test(input.recipientName) || email.length > 254 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) || !purchaser || email === purchaser || !/^price_[A-Za-z0-9_]+$/.test(input.priceId) || !/^user_[A-Za-z0-9_]+$/.test(input.purchaserUserId)) membershipFail("invalid_named_seat_recipient");
|
|
4627
|
+
const state = await loadMembershipAuthorityState(db, input);
|
|
4628
|
+
const memberId = await networkMembershipId(input), member = await membershipLoad(db, "networkMembership", memberId);
|
|
4629
|
+
requireMembershipScope(member, input);
|
|
4630
|
+
if (!Number.isSafeInteger(member.revision) || !/^price_[A-Za-z0-9_]+$/.test(String(member.providerPriceId)) || member.providerPriceId === input.priceId) membershipFail("named_seat_purchase_unavailable");
|
|
4631
|
+
const id7 = (await membershipOfferIdForRequest({ ...input, requestId: "named_seat" })).replace(/^offer_/, "seat_");
|
|
4632
|
+
let seat = await membershipLoad(db, "networkSeat", id7);
|
|
4633
|
+
if (!seat) {
|
|
4634
|
+
const offering = membershipOfferingVersion(member.offeringSnapshot), claimId = await namedSeatRecipientClaimId(input, email);
|
|
4635
|
+
const purchaseEndsAt = seatPurchaseDeadline(offering, member);
|
|
4636
|
+
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) {
|
|
4637
|
+
membershipFail("named_seat_purchase_unavailable");
|
|
4638
|
+
}
|
|
4639
|
+
const attrs = {
|
|
4640
|
+
id: id7,
|
|
4641
|
+
...membershipScope(input),
|
|
4642
|
+
primaryMembershipId: memberId,
|
|
4643
|
+
recipientName: name,
|
|
4644
|
+
recipientEmailNormalized: email,
|
|
4645
|
+
purchaserUserId: input.purchaserUserId,
|
|
4646
|
+
recipientClaimId: claimId,
|
|
4647
|
+
priceId: input.priceId,
|
|
4648
|
+
primaryPriceId: member.providerPriceId,
|
|
4649
|
+
amountCents: offering.seatPriceCents,
|
|
4650
|
+
currency: offering.currency,
|
|
4651
|
+
interval: offering.interval,
|
|
4652
|
+
providerCustomerId: member.providerCustomerId,
|
|
4653
|
+
providerSubscriptionId: member.providerSubscriptionId,
|
|
4654
|
+
status: "payment_pending",
|
|
4655
|
+
preparedAt: now,
|
|
4656
|
+
termEndsAt: member.termEndsAt,
|
|
4657
|
+
seatPurchaseEndsAt: purchaseEndsAt,
|
|
4658
|
+
offeringSnapshot: offering,
|
|
4659
|
+
revision: 1,
|
|
4660
|
+
createdAt: now,
|
|
4661
|
+
updatedAt: now
|
|
4662
|
+
};
|
|
4663
|
+
try {
|
|
4664
|
+
await db.transact([
|
|
4665
|
+
membershipUpdate("networkSeat", id7, attrs),
|
|
4666
|
+
membershipUpdate("networkSeatRecipient", claimId, {
|
|
4667
|
+
id: claimId,
|
|
4668
|
+
appId: input.appId,
|
|
4669
|
+
chapterId: input.chapterId,
|
|
4670
|
+
environment: input.environment,
|
|
4671
|
+
runtime: input.runtime,
|
|
4672
|
+
seatId: id7,
|
|
4673
|
+
recipientEmailNormalized: email,
|
|
4674
|
+
createdAt: now
|
|
4675
|
+
}),
|
|
4676
|
+
membershipUpdate("networkMembership", memberId, { namedSeatId: id7, revision: Number(member.revision) + 1, updatedAt: now })
|
|
4677
|
+
], { mutationId: `named-seat-reserve:${id7}`, guards: [
|
|
4678
|
+
{ ns: "networkSeat", id: id7, exists: false },
|
|
4679
|
+
{ ns: "networkSeatRecipient", id: claimId, exists: false },
|
|
4680
|
+
{ ns: "networkMembership", id: memberId, exists: true, equals: { revision: member.revision }, absent: ["namedSeatId"] },
|
|
4681
|
+
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
|
|
4682
|
+
] });
|
|
4683
|
+
} catch (error) {
|
|
4684
|
+
if (error.code !== "transact_guard_failed") throw error;
|
|
4685
|
+
}
|
|
4686
|
+
seat = await membershipLoad(db, "networkSeat", id7);
|
|
4687
|
+
if (!seat && await membershipLoad(db, "networkSeatRecipient", claimId)) membershipFail("named_seat_recipient_unavailable");
|
|
4688
|
+
}
|
|
4689
|
+
requireMembershipScope(seat, input);
|
|
4690
|
+
if (seat.primaryMembershipId !== memberId || seat.recipientName !== name || seat.recipientEmailNormalized !== email || seat.priceId !== input.priceId || seat.purchaserUserId !== input.purchaserUserId) membershipFail("named_seat_purchase_conflict");
|
|
4691
|
+
return seat;
|
|
4692
|
+
}
|
|
4693
|
+
|
|
4694
|
+
// src/membership-seat-included.ts
|
|
4695
|
+
var namedSeatIncluded = (seat) => Number(seat.amountCents) === 0;
|
|
4696
|
+
var namedSeatSettled = (seat) => namedSeatIncluded(seat) ? seat.purchasedAt !== void 0 : Boolean((seat.providerPaymentIntentId || seat.combinedInvoice) && seat.providerSubscriptionItemId);
|
|
4697
|
+
function namedSeatRecipientState(seat) {
|
|
4698
|
+
switch (String(seat.status)) {
|
|
4699
|
+
case "renewal_pending":
|
|
4700
|
+
case "pending_acceptance":
|
|
4701
|
+
return "invited";
|
|
4702
|
+
case "pending_approval":
|
|
4703
|
+
case "pending_primary":
|
|
4704
|
+
return "accepted";
|
|
4705
|
+
case "active":
|
|
4706
|
+
return "joined";
|
|
4707
|
+
case "refund_pending":
|
|
4708
|
+
case "refunded":
|
|
4709
|
+
return seat.refundReason === "declined" ? "declined" : "refunded";
|
|
4710
|
+
case "canceled":
|
|
4711
|
+
return "canceled";
|
|
4712
|
+
default:
|
|
4713
|
+
return "payment_pending";
|
|
4714
|
+
}
|
|
4715
|
+
}
|
|
4716
|
+
var OPEN = ["pending_acceptance", "pending_approval", "pending_primary", "active"];
|
|
4717
|
+
async function grantIncludedNamedSeat(db, input, now) {
|
|
4718
|
+
membershipClock(now);
|
|
4719
|
+
const seat = await reserveNamedMembershipSeat(db, input, now);
|
|
4720
|
+
if (!namedSeatIncluded(seat)) membershipFail("named_seat_not_included");
|
|
4721
|
+
const state = await loadMembershipAuthorityState(db, input);
|
|
4722
|
+
const member = await membershipLoad(db, "networkMembership", seat.primaryMembershipId);
|
|
4723
|
+
requireMembershipScope(member, input);
|
|
4724
|
+
if (!["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) || Number(member.refundedCents) !== 0 || member.namedSeatId !== seat.id || now >= Number(member.termEndsAt)) {
|
|
4725
|
+
membershipFail("named_seat_purchase_unavailable");
|
|
4726
|
+
}
|
|
4727
|
+
if (seat.status === "payment_pending") {
|
|
4728
|
+
const entitlementId = `${seat.id}:entitlement`, inviteId = `${seat.id}:invite`, source = `included:${seat.id}`;
|
|
4729
|
+
try {
|
|
4730
|
+
await db.transact([
|
|
4731
|
+
membershipUpdate("networkSeat", seat.id, {
|
|
4732
|
+
status: "pending_acceptance",
|
|
4733
|
+
purchasedAt: now,
|
|
4734
|
+
termStartedAt: now,
|
|
4735
|
+
termEndsAt: member.termEndsAt,
|
|
4736
|
+
acceptanceEndsAt: Number(member.termEndsAt),
|
|
4737
|
+
entitlementId,
|
|
4738
|
+
revision: Number(seat.revision) + 1,
|
|
4739
|
+
updatedAt: now
|
|
4740
|
+
}),
|
|
4741
|
+
membershipUpdate("networkEntitlement", entitlementId, {
|
|
4742
|
+
id: entitlementId,
|
|
4743
|
+
...membershipScope(input),
|
|
4744
|
+
membershipId: member.id,
|
|
4745
|
+
seatId: seat.id,
|
|
4746
|
+
kind: "named_seat",
|
|
4747
|
+
status: "pending_acceptance",
|
|
4748
|
+
termStartedAt: now,
|
|
4749
|
+
termEndsAt: member.termEndsAt,
|
|
4750
|
+
offeringSnapshot: member.offeringSnapshot,
|
|
4751
|
+
sourceEventId: source,
|
|
4752
|
+
revision: 1,
|
|
4753
|
+
createdAt: now,
|
|
4754
|
+
updatedAt: now
|
|
4755
|
+
}),
|
|
4756
|
+
membershipUpdate("membershipEffect", inviteId, {
|
|
4757
|
+
id: inviteId,
|
|
4758
|
+
chapterId: input.chapterId,
|
|
4759
|
+
environment: input.environment,
|
|
4760
|
+
runtime: input.runtime,
|
|
4761
|
+
membershipId: member.id,
|
|
4762
|
+
seatId: seat.id,
|
|
4763
|
+
kind: "invite_named_recipient",
|
|
4764
|
+
status: "pending",
|
|
4765
|
+
attempts: 0,
|
|
4766
|
+
createdAt: now,
|
|
4767
|
+
updatedAt: now
|
|
4768
|
+
})
|
|
4769
|
+
], { mutationId: `named-seat-included:${seat.id}`, guards: [
|
|
4770
|
+
{
|
|
4771
|
+
ns: "networkSeat",
|
|
4772
|
+
id: seat.id,
|
|
4773
|
+
exists: true,
|
|
4774
|
+
equals: { status: "payment_pending", revision: seat.revision, amountCents: 0 },
|
|
4775
|
+
absent: ["purchasedAt", "providerPaymentIntentId"]
|
|
4776
|
+
},
|
|
4777
|
+
{ ns: "networkEntitlement", id: entitlementId, exists: false },
|
|
4778
|
+
{ ns: "networkMembership", id: String(member.id), exists: true, equals: { revision: member.revision, namedSeatId: seat.id } },
|
|
4779
|
+
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
|
|
4780
|
+
] });
|
|
4781
|
+
} catch (error) {
|
|
4782
|
+
if (error.code !== "transact_guard_failed") throw error;
|
|
4783
|
+
}
|
|
4784
|
+
}
|
|
4785
|
+
const current = await membershipLoad(db, "networkSeat", seat.id);
|
|
4786
|
+
requireMembershipScope(current, input);
|
|
4787
|
+
if (!namedSeatIncluded(current) || current.purchasedAt === void 0 || !OPEN.includes(String(current.status))) membershipFail("named_seat_purchase_unavailable");
|
|
4788
|
+
return { seatId: seat.id, amountCents: 0, currency: seat.currency, termEndsAt: Number(current.termEndsAt), status: String(current.status), included: true };
|
|
4789
|
+
}
|
|
4790
|
+
|
|
4791
|
+
// src/membership-seat-refund.ts
|
|
4792
|
+
async function requestNamedSeatRefund(db, input, now) {
|
|
4793
|
+
membershipClock(now);
|
|
4794
|
+
const state = await loadMembershipAuthorityState(db, input);
|
|
4795
|
+
const seat = await membershipLoad(db, "networkSeat", input.seatId);
|
|
4796
|
+
requireMembershipScope(seat, input);
|
|
4797
|
+
const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
|
|
4798
|
+
requireMembershipScope(member, input);
|
|
4799
|
+
if (seat.status === "refunded" || seat.status === "refund_pending") return;
|
|
4800
|
+
if (!seat.purchasedAt || !seat.providerPaymentIntentId && !seat.combinedInvoice && !namedSeatIncluded(seat) || input.reason === "declined" && seat.journeyApprovedAt !== void 0 || input.reason === "primary_closed" && !["refunded", "declined"].includes(String(member.status))) membershipFail("named_seat_refund_not_eligible");
|
|
4801
|
+
const entitlement = seat.entitlementId ? await membershipLoad(db, "networkEntitlement", String(seat.entitlementId)) : void 0;
|
|
4802
|
+
await db.transact([
|
|
4803
|
+
membershipUpdate("networkSeat", input.seatId, { status: "refund_pending", refundReason: input.reason, revision: Number(seat.revision) + 1, updatedAt: now }),
|
|
4804
|
+
...entitlement ? [membershipUpdate("networkEntitlement", String(entitlement.id), { status: "closed", revision: Number(entitlement.revision) + 1, updatedAt: now })] : [],
|
|
4805
|
+
membershipUpdate("membershipEffect", `${seat.id}:refund`, {
|
|
4806
|
+
id: `${seat.id}:refund`,
|
|
4807
|
+
chapterId: input.chapterId,
|
|
4808
|
+
environment: input.environment,
|
|
4809
|
+
runtime: input.runtime,
|
|
4810
|
+
membershipId: member.id,
|
|
4811
|
+
seatId: seat.id,
|
|
4812
|
+
kind: "refund_seat",
|
|
4813
|
+
status: "pending",
|
|
4814
|
+
attempts: 0,
|
|
4815
|
+
createdAt: now,
|
|
4816
|
+
updatedAt: now
|
|
4817
|
+
}),
|
|
4818
|
+
...seat.recipientUserId ? [membershipUpdate("membershipEffect", `${seat.id}:revoke`, {
|
|
4819
|
+
id: `${seat.id}:revoke`,
|
|
4820
|
+
chapterId: input.chapterId,
|
|
4821
|
+
environment: input.environment,
|
|
4822
|
+
runtime: input.runtime,
|
|
4823
|
+
membershipId: member.id,
|
|
4824
|
+
seatId: seat.id,
|
|
4825
|
+
kind: "revoke_named_recipient_access",
|
|
4826
|
+
status: "pending",
|
|
4827
|
+
attempts: 0,
|
|
4828
|
+
createdAt: now,
|
|
4829
|
+
updatedAt: now
|
|
4830
|
+
})] : []
|
|
4831
|
+
], { mutationId: `named-seat-refund-request:${seat.id}`, guards: [
|
|
4832
|
+
{ ns: "networkSeat", id: input.seatId, exists: true, equals: { revision: seat.revision } },
|
|
4833
|
+
{ ns: "networkMembership", id: String(member.id), exists: true, equals: { revision: member.revision } },
|
|
4834
|
+
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
|
|
4835
|
+
...entitlement ? [{ ns: "networkEntitlement", id: String(entitlement.id), exists: true, equals: { revision: entitlement.revision } }] : []
|
|
4836
|
+
] });
|
|
4867
4837
|
}
|
|
4868
|
-
async function
|
|
4838
|
+
async function completeNamedSeatRefund(db, seatId, secretKey, now) {
|
|
4869
4839
|
membershipClock(now);
|
|
4870
|
-
const
|
|
4871
|
-
if (!
|
|
4872
|
-
|
|
4873
|
-
if (
|
|
4840
|
+
const seat = await membershipLoad(db, "networkSeat", seatId);
|
|
4841
|
+
if (!seat) return membershipFail("named_seat_not_found");
|
|
4842
|
+
await loadMembershipAuthorityState(db, seat);
|
|
4843
|
+
if (seat.status === "refunded") return;
|
|
4844
|
+
if (seat.status !== "refund_pending") membershipFail("named_seat_refund_not_ready");
|
|
4845
|
+
const result = namedSeatIncluded(seat) ? { refundedCents: 0, refundIds: [] } : await refundNamedSeatStripePayment(secretKey, seat);
|
|
4846
|
+
await db.transact([
|
|
4847
|
+
membershipUpdate("networkSeat", seatId, {
|
|
4848
|
+
status: "refunded",
|
|
4849
|
+
refundedCents: result.refundedCents,
|
|
4850
|
+
providerRefundIds: result.refundIds,
|
|
4851
|
+
revision: Number(seat.revision) + 1,
|
|
4852
|
+
updatedAt: now
|
|
4853
|
+
}),
|
|
4854
|
+
membershipUpdate("membershipEffect", `${seatId}:refund`, { status: "complete", updatedAt: now })
|
|
4855
|
+
], { mutationId: `named-seat-refunded:${seatId}`, guards: [
|
|
4856
|
+
{ ns: "networkSeat", id: seatId, exists: true, equals: { status: "refund_pending", revision: seat.revision } }
|
|
4857
|
+
] });
|
|
4858
|
+
}
|
|
4859
|
+
|
|
4860
|
+
// src/membership-seat-operator-refund.ts
|
|
4861
|
+
async function refundNamedSeatByOperator(db, input, stripeSecretKey, now) {
|
|
4862
|
+
const seat = await membershipLoad(db, "networkSeat", input.seatId);
|
|
4863
|
+
requireMembershipScope(seat, input);
|
|
4864
|
+
await requestNamedSeatRefund(db, { ...input, reason: "operator" }, now);
|
|
4865
|
+
await completeNamedSeatRefund(db, input.seatId, stripeSecretKey, now);
|
|
4866
|
+
const refunded = await membershipLoad(db, "networkSeat", input.seatId);
|
|
4867
|
+
if (refunded?.status !== "refunded" || !Number.isSafeInteger(refunded.refundedCents) || Number(refunded.refundedCents) !== Number(seat.amountCents)) {
|
|
4868
|
+
throw new Error("named seat refund unconfirmed");
|
|
4869
|
+
}
|
|
4870
|
+
return { seatId: String(seat.id), status: "refunded", refundedCents: Number(refunded.refundedCents), subscriptionCanceled: false };
|
|
4871
|
+
}
|
|
4872
|
+
|
|
4873
|
+
// src/membership-seat-window.ts
|
|
4874
|
+
async function liftSeatPurchaseWindow(db, scope2, now, limit = 100) {
|
|
4875
|
+
membershipClock(now);
|
|
4876
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200) throw new TypeError("invalid lift limit");
|
|
4874
4877
|
const state = await loadMembershipAuthorityState(db, scope2);
|
|
4875
|
-
const
|
|
4876
|
-
|
|
4877
|
-
const
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
receipt.more = found.length > limit;
|
|
4893
|
-
rows5 = found.slice(0, limit);
|
|
4878
|
+
const rows5 = (await db.query({ networkMembership: { $: { where: { ...scope2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkMembership ?? [];
|
|
4879
|
+
let lifted = 0;
|
|
4880
|
+
for (const member of rows5.slice(0, limit)) {
|
|
4881
|
+
if (Object.entries(scope2).some(([key3, value]) => member[key3] !== value)) throw new Error("membership lift scope mismatch");
|
|
4882
|
+
const offering = membershipOfferingVersion(member.offeringSnapshot);
|
|
4883
|
+
if (offering.seatPurchaseWindowCalendarMonths === void 0) continue;
|
|
4884
|
+
const { seatPurchaseWindowCalendarMonths: _window, ...windowless } = offering;
|
|
4885
|
+
await db.transact([membershipUpdate("networkMembership", String(member.id), {
|
|
4886
|
+
offeringSnapshot: windowless,
|
|
4887
|
+
seatPurchaseEndsAt: Number(member.termEndsAt),
|
|
4888
|
+
revision: Number(member.revision) + 1,
|
|
4889
|
+
updatedAt: now
|
|
4890
|
+
})], { mutationId: `seat-window-lift:${member.id}:${member.revision}`, guards: [
|
|
4891
|
+
{ ns: "networkMembership", id: String(member.id), exists: true, equals: { revision: member.revision } },
|
|
4892
|
+
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative" } }
|
|
4893
|
+
] });
|
|
4894
|
+
lifted++;
|
|
4894
4895
|
}
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4896
|
+
return { lifted, more: rows5.length > limit };
|
|
4897
|
+
}
|
|
4898
|
+
|
|
4899
|
+
// src/membership-seat-reprice.ts
|
|
4900
|
+
async function lowerSeatPrice(db, scope2, now, limit = 100) {
|
|
4901
|
+
membershipClock(now);
|
|
4902
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200) throw new TypeError("invalid reprice limit");
|
|
4903
|
+
const state = await loadMembershipAuthorityState(db, scope2);
|
|
4904
|
+
const versions = (await db.query({ membershipOfferingVersion: { $: { where: { chapterId: scope2.chapterId }, limit: 200 } } })).membershipOfferingVersion ?? [];
|
|
4905
|
+
if (versions.length >= 200) throw Object.assign(new Error("policy_history_incomplete"), { code: "policy_history_incomplete" });
|
|
4906
|
+
const current = versions.map((row2) => membershipOfferingVersion(row2.snapshot)).filter((offer) => offer.baseSkuId === scope2.skuId && offer.status === "published" && now >= offer.effectiveFrom && (offer.effectiveUntil === void 0 || now < offer.effectiveUntil)).sort((a, b) => b.version - a.version)[0];
|
|
4907
|
+
if (!current) throw Object.assign(new Error("ambiguous_membership_policy"), { code: "ambiguous_membership_policy" });
|
|
4908
|
+
const target = current.seatPriceCents;
|
|
4909
|
+
const rows5 = (await db.query({ networkMembership: { $: { where: { ...scope2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkMembership ?? [];
|
|
4910
|
+
const receipt = { seatPriceCents: target, repriced: 0, skipped: [], more: rows5.length > limit };
|
|
4911
|
+
for (const member of rows5.slice(0, limit)) {
|
|
4912
|
+
if (Object.entries(scope2).some(([key3, value]) => member[key3] !== value)) throw new Error("membership reprice scope mismatch");
|
|
4913
|
+
const id7 = String(member.id), skip = (reason) => {
|
|
4914
|
+
receipt.skipped.push({ membershipId: id7, reason });
|
|
4915
|
+
};
|
|
4916
|
+
const offering = membershipOfferingVersion(member.offeringSnapshot);
|
|
4917
|
+
if (offering.seatPriceCents <= target) {
|
|
4918
|
+
skip("seat price already at or below the current offering");
|
|
4911
4919
|
continue;
|
|
4912
4920
|
}
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
skip(id7, "sku_not_discounted");
|
|
4921
|
+
if (!["paid_pending_approval", "active", "cancel_at_term_end", "payment_grace"].includes(String(member.status))) {
|
|
4922
|
+
skip(`membership is ${String(member.status)}`);
|
|
4916
4923
|
continue;
|
|
4917
4924
|
}
|
|
4918
|
-
const
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
skip(id7, "would_not_widen");
|
|
4925
|
+
const seat = member.namedSeatId ? await membershipLoad(db, "networkSeat", String(member.namedSeatId)) : void 0;
|
|
4926
|
+
if (seat && (seat.status !== "payment_pending" || seat.providerPaymentIntentId !== void 0)) {
|
|
4927
|
+
skip(`seat is ${String(seat.status)}`);
|
|
4922
4928
|
continue;
|
|
4923
4929
|
}
|
|
4924
|
-
const
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
|
|
4931
|
-
|
|
4932
|
-
environment: scope2.environment
|
|
4933
|
-
}, true);
|
|
4934
|
-
if (!productId) throw Object.assign(new Error("price has no product"), { code: "provider_mismatch" });
|
|
4935
|
-
await prepareSubscriptionBillingTerms(stripeSecretKey, {
|
|
4936
|
-
...scope2,
|
|
4937
|
-
priceCents: base,
|
|
4938
|
-
currency: String(member.currency),
|
|
4939
|
-
discountProductId: productId,
|
|
4940
|
-
billingTerms: {
|
|
4941
|
-
offeringVersionId: String(member.offeringVersionId),
|
|
4942
|
-
discount: { policyVersionId: policy2.id, couponId, percentOffBps: policy2.percentOffBps, duration: "forever" }
|
|
4943
|
-
}
|
|
4944
|
-
}, false);
|
|
4945
|
-
providerUpdated = await attachProviderDiscount(stripeSecretKey, scope2, member, policy2, couponId, productId);
|
|
4946
|
-
} catch (error) {
|
|
4947
|
-
const code = error.code;
|
|
4948
|
-
if (code === "provider_mismatch" || code === "stripe_contract_mismatch") {
|
|
4949
|
-
skip(id7, "provider_mismatch");
|
|
4950
|
-
continue;
|
|
4951
|
-
}
|
|
4952
|
-
throw error;
|
|
4953
|
-
}
|
|
4954
|
-
try {
|
|
4955
|
-
await db.transact([membershipUpdate("networkMembership", id7, {
|
|
4956
|
-
discountPolicyVersionId: policy2.id,
|
|
4957
|
-
discountSnapshot: policy2,
|
|
4958
|
-
renewalAmountCents: renewal,
|
|
4959
|
-
revision: Number(member.revision) + 1,
|
|
4960
|
-
updatedAt: now
|
|
4961
|
-
})], {
|
|
4962
|
-
mutationId: `membership-reterm:${id7}:${policy2.id}:${member.revision}`,
|
|
4963
|
-
guards: [
|
|
4964
|
-
{ ns: "networkMembership", id: id7, exists: true, equals: { revision: member.revision, status: member.status } },
|
|
4965
|
-
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative" } },
|
|
4966
|
-
{ ns: "discountPolicyVersion", id: policy2.id, exists: true }
|
|
4967
|
-
]
|
|
4968
|
-
});
|
|
4969
|
-
} catch (error) {
|
|
4970
|
-
if (error.code !== "transact_guard_failed") throw error;
|
|
4971
|
-
return membershipFail("membership_reterm_retry_required");
|
|
4930
|
+
const ops = [membershipUpdate("networkMembership", id7, { offeringSnapshot: { ...offering, seatPriceCents: target }, revision: Number(member.revision) + 1, updatedAt: now })];
|
|
4931
|
+
const guards = [
|
|
4932
|
+
{ ns: "networkMembership", id: id7, exists: true, equals: { revision: member.revision } },
|
|
4933
|
+
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative" } }
|
|
4934
|
+
];
|
|
4935
|
+
if (seat) {
|
|
4936
|
+
ops.push(membershipUpdate("networkSeat", String(seat.id), { amountCents: target, offeringSnapshot: { ...offering, seatPriceCents: target }, revision: Number(seat.revision) + 1, updatedAt: now }));
|
|
4937
|
+
guards.push({ ns: "networkSeat", id: String(seat.id), exists: true, equals: { revision: seat.revision, status: "payment_pending" } });
|
|
4972
4938
|
}
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
...typeof member.discountPolicyVersionId === "string" ? { previousPolicyVersionId: member.discountPolicyVersionId } : {},
|
|
4976
|
-
previousRenewalAmountCents: current,
|
|
4977
|
-
renewalAmountCents: renewal,
|
|
4978
|
-
providerUpdated
|
|
4979
|
-
});
|
|
4939
|
+
await db.transact(ops, { mutationId: `seat-reprice:${id7}:${member.revision}`, guards });
|
|
4940
|
+
receipt.repriced++;
|
|
4980
4941
|
}
|
|
4981
4942
|
return receipt;
|
|
4982
4943
|
}
|
|
4983
4944
|
|
|
4984
|
-
// src/worker-routes-admin-membership-
|
|
4985
|
-
var
|
|
4986
|
-
var
|
|
4987
|
-
var
|
|
4988
|
-
var
|
|
4989
|
-
var
|
|
4990
|
-
|
|
4945
|
+
// src/worker-routes-admin-membership-seats.ts
|
|
4946
|
+
var LIST = "/api/admin/membership-seats";
|
|
4947
|
+
var LIFT = "/api/admin/membership-seats/lift-purchase-window";
|
|
4948
|
+
var REPRICE = "/api/admin/membership-seats/reprice";
|
|
4949
|
+
var ONE = /^\/api\/admin\/membership-seats\/([A-Za-z0-9][A-Za-z0-9_.:-]{2,159})\/refund$/;
|
|
4950
|
+
var SCOPE = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
4951
|
+
var PUBLIC = [
|
|
4952
|
+
"id",
|
|
4953
|
+
"appId",
|
|
4954
|
+
"chapterId",
|
|
4955
|
+
"environment",
|
|
4956
|
+
"runtime",
|
|
4957
|
+
"applicationId",
|
|
4958
|
+
"skuId",
|
|
4959
|
+
"status",
|
|
4960
|
+
"amountCents",
|
|
4961
|
+
"currency",
|
|
4962
|
+
"recipientName",
|
|
4963
|
+
"recipientEmailNormalized",
|
|
4964
|
+
"purchaserUserId",
|
|
4965
|
+
"purchasedAt",
|
|
4966
|
+
"acceptedAt",
|
|
4967
|
+
"journeyApprovedAt",
|
|
4968
|
+
"refundedCents",
|
|
4969
|
+
"refundReason",
|
|
4970
|
+
"termEndsAt",
|
|
4971
|
+
"updatedAt"
|
|
4972
|
+
];
|
|
4973
|
+
var project = (row2) => Object.fromEntries(PUBLIC.filter((key3) => row2[key3] !== void 0).map((key3) => [key3, row2[key3]]));
|
|
4974
|
+
var handleAdminMembershipSeats = async (req, url, env, ctx) => {
|
|
4975
|
+
const one = url.pathname.match(ONE), lift = url.pathname === LIFT, reprice = url.pathname === REPRICE;
|
|
4976
|
+
if (url.pathname !== LIST && !one && !lift && !reprice) return null;
|
|
4991
4977
|
const operator = ctx.membershipAuthorityOperator;
|
|
4992
4978
|
if (!operator) return json({ error: "not found" }, 404);
|
|
4993
|
-
if (req.method !== "POST") return json({ error: "method not allowed" }, 405);
|
|
4979
|
+
if (req.method !== (one || lift || reprice ? "POST" : "GET")) return json({ error: "method not allowed" }, 405);
|
|
4994
4980
|
const raw2 = ctx.makeDb(env), user = await ctx.verifyUser(req, env);
|
|
4995
4981
|
if (!user) return json({ error: "unauthorized" }, 401);
|
|
4996
4982
|
if (!await ctx.isSuperAdminEmail(raw2, user.email, user.userId)) return json({ error: "super-admin required", code: "super_admin_required" }, 403);
|
|
@@ -5000,267 +4986,427 @@ var handleAdminMembershipReterm = async (req, url, env, ctx) => {
|
|
|
5000
4986
|
} catch {
|
|
5001
4987
|
return json({ error: "the membership authority is not hosted by this deployment" }, 404);
|
|
5002
4988
|
}
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
4989
|
+
if (lift || reprice) {
|
|
4990
|
+
let body;
|
|
4991
|
+
try {
|
|
4992
|
+
body = await req.json();
|
|
4993
|
+
} catch {
|
|
4994
|
+
return json({ error: "chapterId and runtime are required" }, 400);
|
|
4995
|
+
}
|
|
4996
|
+
const appId = body.appId ?? body.chapterId;
|
|
4997
|
+
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) || reprice && (typeof body.skuId !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_.:-]{2,159}$/.test(body.skuId))) {
|
|
4998
|
+
return json({ error: reprice ? "chapterId, runtime and skuId are required" : "chapterId and runtime are required" }, 400);
|
|
4999
|
+
}
|
|
5000
|
+
const scope2 = { appId, chapterId: body.chapterId, environment: env.ODLA_ENV, runtime: body.runtime };
|
|
5001
|
+
try {
|
|
5002
|
+
if (reprice) return json({ ok: true, ...await lowerSeatPrice(db, { ...scope2, skuId: String(body.skuId) }, Date.now()) });
|
|
5003
|
+
return json({ ok: true, ...await liftSeatPurchaseWindow(db, scope2, Date.now()) });
|
|
5004
|
+
} catch (error) {
|
|
5005
|
+
const code = error.code;
|
|
5006
|
+
return json({ error: `${reprice ? "seat reprice" : "purchase window lift"} is pending; retry to confirm`, ...code ? { code } : {} }, code ? 409 : 503);
|
|
5007
|
+
}
|
|
5008
5008
|
}
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
return json({
|
|
5009
|
+
if (!one) {
|
|
5010
|
+
const rows5 = (await db.query({ networkSeat: { $: { where: { environment: env.ODLA_ENV }, order: { updatedAt: "desc" }, limit: 201 } } })).networkSeat ?? [];
|
|
5011
|
+
return json({ seats: rows5.slice(0, 200).map(project), truncated: rows5.length > 200 });
|
|
5012
|
+
}
|
|
5013
|
+
const seat = await membershipLoad(db, "networkSeat", one[1]);
|
|
5014
|
+
if (!seat || seat.environment !== env.ODLA_ENV) return json({ error: "not found" }, 404);
|
|
5015
|
+
if (seat.status === "refunded") {
|
|
5016
|
+
return json({ ok: true, seatId: seat.id, status: "refunded", refundedCents: seat.refundedCents, subscriptionCanceled: false, replayed: true });
|
|
5012
5017
|
}
|
|
5018
|
+
if (!seat.purchasedAt) return json({ error: "seat has no completed payment to refund", code: "seat_not_refundable" }, 409);
|
|
5013
5019
|
const secret = await getVaultSecret(db, operator.stripeSecretName ?? "stripe_secret_key");
|
|
5014
5020
|
if (!secret) return json({ error: "network payment provider unavailable" }, 503);
|
|
5015
5021
|
try {
|
|
5016
|
-
const receipt = await
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
Date.now()
|
|
5026
|
-
);
|
|
5022
|
+
const receipt = await refundNamedSeatByOperator(db, {
|
|
5023
|
+
appId: String(seat.appId),
|
|
5024
|
+
chapterId: String(seat.chapterId),
|
|
5025
|
+
environment: String(seat.environment),
|
|
5026
|
+
runtime: String(seat.runtime),
|
|
5027
|
+
applicationId: String(seat.applicationId),
|
|
5028
|
+
skuId: String(seat.skuId),
|
|
5029
|
+
seatId: String(seat.id)
|
|
5030
|
+
}, secret, Date.now());
|
|
5027
5031
|
return json({ ok: true, ...receipt });
|
|
5028
5032
|
} catch (error) {
|
|
5029
5033
|
const code = error.code;
|
|
5030
|
-
return json({ error: "
|
|
5031
|
-
}
|
|
5032
|
-
};
|
|
5033
|
-
|
|
5034
|
-
// src/worker-routes-membership-cancel.ts
|
|
5035
|
-
var handleMembershipCancellation = async (req, url, env, ctx) => {
|
|
5036
|
-
const match = url.pathname.match(/^\/api\/admin\/applications\/([0-9a-f-]+)\/cancel-renewal$/);
|
|
5037
|
-
if (!match) return null;
|
|
5038
|
-
if (req.method !== "POST") return json({ error: "method not allowed" }, 405);
|
|
5039
|
-
const user = await ctx.verifyUser(req, env), raw2 = ctx.makeDb(env);
|
|
5040
|
-
if (!user) return json({ error: "unauthorized" }, 401);
|
|
5041
|
-
if (!await ctx.isAdmin(raw2, user)) return json({ error: "forbidden" }, 403);
|
|
5042
|
-
if (!ctx.membershipAuthority?.cancelRenewal) return json({ error: "renewal cancellation unavailable" }, 503);
|
|
5043
|
-
const db = raw2, app = await membershipLoad(db, "applications", match[1]);
|
|
5044
|
-
if (!app || !chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME)) return json({ error: "not found" }, 404);
|
|
5045
|
-
if (app.namedSeatId || app.giftClaimId) return json({ error: "this membership has no independently renewing subscription" }, 409);
|
|
5046
|
-
const intent = await membershipLoad(db, "subscriptionCheckoutIntents", String(app.id));
|
|
5047
|
-
if (!intent) return json({ error: "membership history needs reconciliation" }, 409);
|
|
5048
|
-
try {
|
|
5049
|
-
const target = await membershipTargetForIntent(intent, app, env, ctx);
|
|
5050
|
-
const receipt = await ctx.membershipAuthority.cancelRenewal(target, env);
|
|
5051
|
-
if (receipt.membershipId !== await networkMembershipId(target) || receipt.autoRenew !== false || !Number.isSafeInteger(receipt.termEndsAt) || !["cancel_at_term_end", "lapsed", "paid_pending_approval"].includes(receipt.status)) throw new Error("invalid cancellation receipt");
|
|
5052
|
-
await db.transact([{ t: "update", ns: "applications", id: String(app.id), attrs: {
|
|
5053
|
-
membershipStatus: receipt.status,
|
|
5054
|
-
membershipAutoRenew: false,
|
|
5055
|
-
membershipGraceEndsAt: 0,
|
|
5056
|
-
renewalAt: receipt.termEndsAt
|
|
5057
|
-
} }], { guards: [{
|
|
5058
|
-
ns: "applications",
|
|
5059
|
-
id: String(app.id),
|
|
5060
|
-
exists: true,
|
|
5061
|
-
equals: { status: app.status, stripeSubscriptionId: app.stripeSubscriptionId }
|
|
5062
|
-
}] });
|
|
5063
|
-
return json({ ok: true, accessEndsAt: receipt.termEndsAt, status: receipt.status });
|
|
5064
|
-
} catch {
|
|
5065
|
-
return json({ error: "renewal cancellation is pending; retry to confirm" }, 503);
|
|
5034
|
+
return json({ error: "seat refund is pending; retry to confirm", ...code ? { code } : {} }, code ? 409 : 503);
|
|
5066
5035
|
}
|
|
5067
5036
|
};
|
|
5068
5037
|
|
|
5069
|
-
// src/
|
|
5070
|
-
var
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
timezone: "America/Los_Angeles",
|
|
5076
|
-
minNoticeHours: 24,
|
|
5077
|
-
windowDays: 14,
|
|
5078
|
-
summaryTemplate: "Introduction call with {{firstName}} {{lastName}}"
|
|
5038
|
+
// src/membership-policy-versions.ts
|
|
5039
|
+
var LIMIT = 200;
|
|
5040
|
+
var text2 = (value) => typeof value === "string" && value.length > 0 && value.length <= 200 ? value : void 0;
|
|
5041
|
+
var instant = (value) => {
|
|
5042
|
+
const at2 = typeof value === "number" ? value : typeof value === "string" ? Date.parse(value) : NaN;
|
|
5043
|
+
return Number.isSafeInteger(at2) && at2 >= 0 ? at2 : void 0;
|
|
5079
5044
|
};
|
|
5080
|
-
function
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
return true;
|
|
5084
|
-
} catch {
|
|
5085
|
-
return false;
|
|
5086
|
-
}
|
|
5045
|
+
function provenance(row2) {
|
|
5046
|
+
const source = text2(row2.source), actor = text2(row2.actor ?? row2.$createdBy), recordedAt = instant(row2.recordedAt ?? row2.createdAt ?? row2.$createdAt);
|
|
5047
|
+
return { ...source ? { source } : {}, ...actor ? { actor } : {}, ...recordedAt !== void 0 ? { recordedAt } : {} };
|
|
5087
5048
|
}
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
if (
|
|
5091
|
-
const
|
|
5092
|
-
|
|
5049
|
+
var live = (w, now) => w.from <= now && (w.until === void 0 || now < w.until);
|
|
5050
|
+
function stateOf(item, all, now, window, rivals) {
|
|
5051
|
+
if (item.status !== "published") return { state: item.status };
|
|
5052
|
+
const mine = window(item);
|
|
5053
|
+
if (now < mine.from) return { state: "scheduled" };
|
|
5054
|
+
const closed = mine.until !== void 0 && mine.until <= now;
|
|
5055
|
+
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));
|
|
5056
|
+
if (!closed && !higher.some((w) => live(w, now))) return { state: "active" };
|
|
5057
|
+
const dates = higher.map((w) => Math.max(w.from, mine.from));
|
|
5058
|
+
if (closed) dates.push(mine.until);
|
|
5059
|
+
return { state: "superseded", supersededAt: Math.min(...dates) };
|
|
5093
5060
|
}
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
};
|
|
5106
|
-
const errors = {};
|
|
5107
|
-
if (!(c.slotMinutes >= 15 && c.slotMinutes <= 240)) {
|
|
5108
|
-
errors.slotMinutes = "Slot length must be between 15 and 240 minutes.";
|
|
5109
|
-
}
|
|
5110
|
-
if (!(c.windowDays >= 1 && c.windowDays <= 62)) {
|
|
5111
|
-
errors.windowDays = "Booking window must be between 1 and 62 days (the calendar caps look-ahead at 62).";
|
|
5112
|
-
}
|
|
5113
|
-
if (!(c.minNoticeHours >= 0 && c.minNoticeHours <= 336)) {
|
|
5114
|
-
errors.minNoticeHours = "Minimum notice must be between 0 and 336 hours.";
|
|
5115
|
-
}
|
|
5116
|
-
if (!(c.startHour >= 0 && c.startHour < c.endHour && c.endHour <= 24)) {
|
|
5117
|
-
errors.hours = "Hours must satisfy 0 \u2264 start < end \u2264 24.";
|
|
5118
|
-
}
|
|
5119
|
-
const days = [...c.days];
|
|
5120
|
-
if (!days.length) errors.days = "Pick at least one day.";
|
|
5121
|
-
else if (!days.every((n) => Number.isInteger(n) && n >= 0 && n <= 6)) {
|
|
5122
|
-
errors.days = "Days must be weekday numbers, 0 (Sunday) through 6 (Saturday).";
|
|
5123
|
-
}
|
|
5124
|
-
if (typeof c.timezone !== "string" || !isValidTimeZone(c.timezone)) {
|
|
5125
|
-
errors.timezone = `"${String(c.timezone)}" is not a valid IANA timezone (for example "America/Los_Angeles").`;
|
|
5061
|
+
var mechanism = (policy2) => policy2.percentOffBps === 0 || policy2.discountEligibleSkuIds.length === 0 ? "none" : policy2.benefitDuration.kind === "while_active" ? "stripe_coupon_forever" : policy2.benefitDuration.kind === "first_term" ? "stripe_coupon_once" : "stripe_coupon_term_bound";
|
|
5062
|
+
var sharesSku = (a, b) => a.cohortEligibleSkuIds.some((sku) => b.cohortEligibleSkuIds.includes(sku));
|
|
5063
|
+
var byChapter = (a, b) => a.chapterId.localeCompare(b.chapterId);
|
|
5064
|
+
function normalize(rows5, collection, parse, invalid2) {
|
|
5065
|
+
const out = [];
|
|
5066
|
+
for (const row2 of rows5) {
|
|
5067
|
+
try {
|
|
5068
|
+
out.push({ value: parse(row2.snapshot), row: row2 });
|
|
5069
|
+
} catch (error) {
|
|
5070
|
+
invalid2.push({ collection, id: String(row2.id ?? ""), error: error instanceof Error ? error.message : String(error) });
|
|
5071
|
+
}
|
|
5126
5072
|
}
|
|
5127
|
-
|
|
5128
|
-
return Object.keys(errors).length > 0 ? { ok: false, errors } : { ok: true, value: { ...c, days } };
|
|
5129
|
-
}
|
|
5130
|
-
function slotWindow(now, windowDays) {
|
|
5131
|
-
return { from: now, to: now + windowDays * 864e5 };
|
|
5132
|
-
}
|
|
5133
|
-
function endForSlot(startAt, slotMinutes) {
|
|
5134
|
-
return startAt + slotMinutes * 6e4;
|
|
5135
|
-
}
|
|
5136
|
-
function isSlotAvailable(slots, startAt) {
|
|
5137
|
-
return slots.some((s) => s.startAt === startAt);
|
|
5138
|
-
}
|
|
5139
|
-
function renderSummary(template, app) {
|
|
5140
|
-
return template.replace("{{firstName}}", app.firstName ?? "").replace("{{lastName}}", app.lastName ?? "");
|
|
5141
|
-
}
|
|
5142
|
-
function bookingDecision(existing) {
|
|
5143
|
-
const eventId = existing?.googleEventId ?? null;
|
|
5144
|
-
return { reschedule: Boolean(eventId), eventId };
|
|
5145
|
-
}
|
|
5146
|
-
function introIdempotencyKey(applicationId) {
|
|
5147
|
-
return `application:${applicationId}:intro`;
|
|
5073
|
+
return out;
|
|
5148
5074
|
}
|
|
5149
|
-
function
|
|
5075
|
+
async function listMembershipPolicyVersions(db, input) {
|
|
5076
|
+
const where = input.chapterId ? { chapterId: input.chapterId } : {};
|
|
5077
|
+
const rows5 = await db.query({
|
|
5078
|
+
membershipOfferingVersion: { $: { where, limit: LIMIT } },
|
|
5079
|
+
discountPolicyVersion: { $: { where, limit: LIMIT } }
|
|
5080
|
+
});
|
|
5081
|
+
const offeringRows = rows5.membershipOfferingVersion ?? [], policyRows = rows5.discountPolicyVersion ?? [];
|
|
5082
|
+
const invalid2 = [];
|
|
5083
|
+
const offerings = normalize(offeringRows, "membershipOfferingVersion", membershipOfferingVersion, invalid2);
|
|
5084
|
+
const policies = normalize(policyRows, "discountPolicyVersion", membershipDiscountPolicyVersion, invalid2);
|
|
5085
|
+
const offeringValues = offerings.map((item) => item.value), policyValues = policies.map((item) => item.value);
|
|
5150
5086
|
return {
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5087
|
+
now: input.now,
|
|
5088
|
+
chapterId: input.chapterId ?? null,
|
|
5089
|
+
offerings: offerings.map(({ value, row: row2 }) => ({
|
|
5090
|
+
...value,
|
|
5091
|
+
...provenance(row2),
|
|
5092
|
+
...stateOf(
|
|
5093
|
+
value,
|
|
5094
|
+
offeringValues,
|
|
5095
|
+
input.now,
|
|
5096
|
+
(v) => ({ from: v.effectiveFrom, ...v.effectiveUntil !== void 0 ? { until: v.effectiveUntil } : {} }),
|
|
5097
|
+
(a, b) => a.chapterId === b.chapterId && a.baseSkuId === b.baseSkuId
|
|
5098
|
+
)
|
|
5099
|
+
})).sort((a, b) => byChapter(a, b) || a.baseSkuId.localeCompare(b.baseSkuId) || b.version - a.version),
|
|
5100
|
+
discountPolicies: policies.map(({ value, row: row2 }) => ({
|
|
5101
|
+
...value,
|
|
5102
|
+
...provenance(row2),
|
|
5103
|
+
mechanism: mechanism(value),
|
|
5104
|
+
...stateOf(
|
|
5105
|
+
value,
|
|
5106
|
+
policyValues,
|
|
5107
|
+
input.now,
|
|
5108
|
+
(v) => ({ from: v.qualifyingStartsAt, until: v.qualifyingEndsAt }),
|
|
5109
|
+
(a, b) => a.chapterId === b.chapterId && sharesSku(a, b)
|
|
5110
|
+
)
|
|
5111
|
+
})).sort((a, b) => byChapter(a, b) || b.version - a.version),
|
|
5112
|
+
invalid: invalid2,
|
|
5113
|
+
truncated: offeringRows.length >= LIMIT || policyRows.length >= LIMIT
|
|
5163
5114
|
};
|
|
5164
5115
|
}
|
|
5165
|
-
|
|
5166
|
-
|
|
5116
|
+
|
|
5117
|
+
// src/worker-routes-admin-membership-policy.ts
|
|
5118
|
+
var PATH3 = "/api/admin/membership-policy-versions";
|
|
5119
|
+
var SCOPE2 = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
5120
|
+
var handleAdminMembershipPolicyVersions = async (req, url, env, ctx) => {
|
|
5121
|
+
if (url.pathname !== PATH3) return null;
|
|
5122
|
+
const operator = ctx.membershipAuthorityOperator;
|
|
5123
|
+
if (!operator) return json({ error: "not found" }, 404);
|
|
5124
|
+
if (req.method !== "GET") return json({ error: "method not allowed" }, 405);
|
|
5125
|
+
const raw2 = ctx.makeDb(env), user = await ctx.verifyUser(req, env);
|
|
5126
|
+
if (!user) return json({ error: "unauthorized" }, 401);
|
|
5127
|
+
if (!await ctx.isSuperAdminEmail(raw2, user.email, user.userId)) return json({ error: "super-admin required", code: "super_admin_required" }, 403);
|
|
5128
|
+
const chapterId = url.searchParams.get("chapterId");
|
|
5129
|
+
if (chapterId !== null && !SCOPE2.test(chapterId)) return json({ error: "chapterId is invalid" }, 400);
|
|
5130
|
+
let db;
|
|
5131
|
+
try {
|
|
5132
|
+
db = operator.makeDb(env);
|
|
5133
|
+
} catch {
|
|
5134
|
+
return json({ error: "the membership authority is not hosted by this deployment" }, 404);
|
|
5135
|
+
}
|
|
5136
|
+
return json(await listMembershipPolicyVersions(db, { chapterId, now: Date.now() }));
|
|
5137
|
+
};
|
|
5138
|
+
|
|
5139
|
+
// src/membership-discount-reterm.ts
|
|
5140
|
+
var import_stripe9 = require("@odla-ai/stripe");
|
|
5141
|
+
|
|
5142
|
+
// src/subscription-provider-contract.ts
|
|
5143
|
+
var import_stripe8 = require("@odla-ai/stripe");
|
|
5144
|
+
function subscriptionContractMismatch() {
|
|
5145
|
+
throw Object.assign(new Error("Stripe subscription does not match the checkout contract"), { code: "stripe_contract_mismatch" });
|
|
5167
5146
|
}
|
|
5168
|
-
function
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
|
|
5147
|
+
async function verifySubscriptionPrice(secretKey, input, resume = false) {
|
|
5148
|
+
if (!/^price_[A-Za-z0-9]+$/.test(input.priceId) || !Number.isSafeInteger(input.priceCents) || input.priceCents <= 0 || !/^[a-z]{3}$/.test(input.currency) || !["month", "year"].includes(input.interval)) subscriptionContractMismatch();
|
|
5149
|
+
const result = await (0, import_stripe8.stripeRequest)({ secretKey }, "GET", `/v1/prices/${encodeURIComponent(input.priceId)}`);
|
|
5150
|
+
const price = result.body;
|
|
5151
|
+
if (!result.ok || !price || price.id !== input.priceId || !resume && price.active !== true || price.unit_amount !== input.priceCents || price.currency !== input.currency || price.type !== "recurring" || price.billing_scheme !== "per_unit" || price.custom_unit_amount || price.transform_quantity || price.recurring?.interval !== input.interval || price.recurring?.interval_count !== 1 || price.recurring?.usage_type !== "licensed" || price.livemode !== (input.environment === "prod")) subscriptionContractMismatch();
|
|
5152
|
+
return typeof price.product === "string" ? price.product : void 0;
|
|
5153
|
+
}
|
|
5154
|
+
function verifySubscriptionResult(sub, input, customerId, metadata) {
|
|
5155
|
+
const items = sub?.items;
|
|
5156
|
+
const seat = input.namedSeat?.amountCents ? input.namedSeat : void 0;
|
|
5157
|
+
const primary = items?.data?.find((item) => item.price?.id === input.priceId);
|
|
5158
|
+
const seatItem = seat && items?.data?.find((item) => item.price?.id === seat.priceId);
|
|
5159
|
+
if (!sub || !/^sub_[A-Za-z0-9]+$/.test(sub.id) || sub.customer !== customerId || input.existingSubscriptionId && sub.id !== input.existingSubscriptionId || !(input.billingTerms?.initialTermEndsAt ? ["incomplete", "active", "trialing"] : ["incomplete", "active"]).includes(sub.status) || sub.livemode !== (input.environment === "prod") || !Object.entries(metadata).every(([key3, value]) => sub.metadata?.[key3] === value) || !Array.isArray(items?.data) || items.has_more !== false || items.data.length !== (seat ? 2 : 1) || primary?.quantity !== 1 || seat && (seatItem?.quantity !== 1 || seatItem.metadata?.seatId !== seat.seatId || seatItem.metadata?.membershipId !== seat.membershipId)) subscriptionContractMismatch();
|
|
5160
|
+
const invoice = sub.latest_invoice;
|
|
5161
|
+
if (!invoice || typeof invoice !== "object" || invoice.customer !== customerId || invoice.currency !== input.currency || invoice.total !== subscriptionAmountDue(input) + (seat?.amountCents ?? 0) || invoice.billing_reason !== "subscription_create") subscriptionContractMismatch();
|
|
5162
|
+
verifySubscriptionBillingResult(sub, input);
|
|
5174
5163
|
}
|
|
5175
5164
|
|
|
5176
|
-
// src/
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5165
|
+
// src/membership-discount-reterm.ts
|
|
5166
|
+
var SAFE = /^[A-Za-z0-9][A-Za-z0-9_.:-]{2,159}$/;
|
|
5167
|
+
var RETERMABLE = /* @__PURE__ */ new Set(["active", "paid_pending_approval", "cancel_at_term_end", "payment_grace"]);
|
|
5168
|
+
var PROVIDER_LIVE = /* @__PURE__ */ new Set(["active", "trialing", "past_due"]);
|
|
5169
|
+
var couponOf = (discount) => {
|
|
5170
|
+
const ref = discount?.source?.coupon ?? discount?.coupon;
|
|
5171
|
+
return typeof ref === "string" ? ref : typeof ref?.id === "string" ? ref.id : void 0;
|
|
5172
|
+
};
|
|
5173
|
+
async function retermPolicy(db, scope2, id7) {
|
|
5174
|
+
const row2 = await membershipLoad(db, "discountPolicyVersion", id7);
|
|
5175
|
+
if (!row2) membershipFail("membership_reterm_policy_unknown");
|
|
5176
|
+
let policy2;
|
|
5177
|
+
try {
|
|
5178
|
+
policy2 = membershipDiscountPolicyVersion(row2.snapshot);
|
|
5179
|
+
} catch {
|
|
5180
|
+
return membershipFail("membership_reterm_policy_ineligible");
|
|
5181
|
+
}
|
|
5182
|
+
if (policy2.id !== id7 || policy2.chapterId !== scope2.chapterId || policy2.status !== "published" || policy2.benefitDuration.kind !== "while_active" || policy2.percentOffBps <= 0 || policy2.discountEligibleSkuIds.length === 0) membershipFail("membership_reterm_policy_ineligible");
|
|
5183
|
+
return policy2;
|
|
5192
5184
|
}
|
|
5193
|
-
function
|
|
5194
|
-
const
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
if (
|
|
5199
|
-
|
|
5200
|
-
if (meeting.status === "scheduled") {
|
|
5201
|
-
meetingAt = meeting.startAt ?? null;
|
|
5202
|
-
meetUrl = meeting.meetUrl ?? null;
|
|
5203
|
-
} else {
|
|
5204
|
-
meetingAt = null;
|
|
5205
|
-
}
|
|
5185
|
+
async function attachProviderDiscount(sk, scope2, member, policy2, couponId, productId) {
|
|
5186
|
+
const subId = String(member.providerSubscriptionId), path = `/v1/subscriptions/${encodeURIComponent(subId)}`;
|
|
5187
|
+
const read2 = await (0, import_stripe9.stripeRequest)({ secretKey: sk }, "GET", path, { "expand[0]": "discounts" });
|
|
5188
|
+
const sub = read2.body, items = sub?.items?.data;
|
|
5189
|
+
const metadata = { ...stripePartitionMetadata(scope2), applicationId: String(member.applicationId) };
|
|
5190
|
+
if (!read2.ok || sub.id !== subId || sub.customer !== member.providerCustomerId || sub.livemode !== (scope2.environment === "prod") || !PROVIDER_LIVE.has(sub.status) || !Object.entries(metadata).every(([key3, value]) => sub.metadata?.[key3] === value) || sub.items?.has_more !== false || !Array.isArray(items) || items.length < 1 || items.length > 2 || items.filter((item) => item.price?.id === member.providerPriceId && item.quantity === 1 && item.price?.product === productId).length !== 1) {
|
|
5191
|
+
throw Object.assign(new Error("provider subscription does not match the frozen membership"), { code: "provider_mismatch" });
|
|
5206
5192
|
}
|
|
5207
|
-
|
|
5193
|
+
const discounts = sub.discounts ?? (sub.discount ? [sub.discount] : []);
|
|
5194
|
+
if (!Array.isArray(discounts)) throw Object.assign(new Error("provider discounts unreadable"), { code: "provider_mismatch" });
|
|
5195
|
+
const present = discounts.map(couponOf);
|
|
5196
|
+
if (present.length === 1 && present[0] === couponId) return false;
|
|
5197
|
+
if (present.length > 0) throw Object.assign(new Error("provider subscription carries an unrelated discount"), { code: "provider_mismatch" });
|
|
5198
|
+
const written = await (0, import_stripe9.stripeRequest)({ secretKey: sk }, "POST", path, {
|
|
5199
|
+
"discounts[0][coupon]": couponId,
|
|
5200
|
+
"expand[0]": "discounts",
|
|
5201
|
+
"metadata[discountPolicyVersionId]": policy2.id
|
|
5202
|
+
}, { idempotencyKey: `membership-reterm:${String(member.id)}:${policy2.id}` });
|
|
5203
|
+
const after = written.body?.discounts;
|
|
5204
|
+
if (!written.ok || written.body?.id !== subId || !Array.isArray(after) || after.length !== 1 || couponOf(after[0]) !== couponId) {
|
|
5205
|
+
membershipFail("membership_reterm_provider_unconfirmed");
|
|
5206
|
+
}
|
|
5207
|
+
return true;
|
|
5208
5208
|
}
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5209
|
+
async function retermMembershipDiscount(db, stripeSecretKey, scope2, input, now) {
|
|
5210
|
+
membershipClock(now);
|
|
5211
|
+
const limit = input.limit ?? 100;
|
|
5212
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200) throw new TypeError("invalid reterm limit");
|
|
5213
|
+
if (typeof input.policyVersionId !== "string" || !SAFE.test(input.policyVersionId)) throw new TypeError("invalid policy version id");
|
|
5214
|
+
if (input.membershipIds !== void 0 && (!Array.isArray(input.membershipIds) || input.membershipIds.length === 0 || input.membershipIds.length > limit || input.membershipIds.some((id7) => typeof id7 !== "string" || !SAFE.test(id7)))) throw new TypeError("invalid membership ids");
|
|
5215
|
+
const state = await loadMembershipAuthorityState(db, scope2);
|
|
5216
|
+
const policy2 = await retermPolicy(db, scope2, input.policyVersionId);
|
|
5217
|
+
const receipt = { policyVersionId: policy2.id, retermed: [], unchanged: [], skipped: [], more: false };
|
|
5218
|
+
const skip = (membershipId, reason) => {
|
|
5219
|
+
receipt.skipped.push({ membershipId, reason });
|
|
5217
5220
|
};
|
|
5218
|
-
let
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
const
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
5221
|
+
let rows5;
|
|
5222
|
+
if (input.membershipIds) {
|
|
5223
|
+
rows5 = [];
|
|
5224
|
+
for (const id7 of input.membershipIds) {
|
|
5225
|
+
const member = await membershipLoad(db, "networkMembership", id7);
|
|
5226
|
+
if (!member) skip(id7, "not_found");
|
|
5227
|
+
else if (Object.entries(scope2).some(([key3, value]) => member[key3] !== value)) skip(id7, "scope_mismatch");
|
|
5228
|
+
else rows5.push(member);
|
|
5229
|
+
}
|
|
5230
|
+
} else {
|
|
5231
|
+
const found = (await db.query({ networkMembership: { $: { where: { ...scope2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkMembership ?? [];
|
|
5232
|
+
if (found.some((member) => Object.entries(scope2).some(([key3, value]) => member[key3] !== value))) throw new Error("membership reterm scope mismatch");
|
|
5233
|
+
receipt.more = found.length > limit;
|
|
5234
|
+
rows5 = found.slice(0, limit);
|
|
5227
5235
|
}
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5236
|
+
for (const member of rows5) {
|
|
5237
|
+
const id7 = String(member.id);
|
|
5238
|
+
if (member.discountPolicyVersionId === policy2.id) {
|
|
5239
|
+
receipt.unchanged.push(id7);
|
|
5240
|
+
continue;
|
|
5241
|
+
}
|
|
5242
|
+
if (!RETERMABLE.has(String(member.status))) {
|
|
5243
|
+
skip(id7, "status");
|
|
5244
|
+
continue;
|
|
5245
|
+
}
|
|
5246
|
+
if (member.founding !== true) {
|
|
5247
|
+
skip(id7, "not_founding");
|
|
5248
|
+
continue;
|
|
5249
|
+
}
|
|
5250
|
+
if (member.cohort !== policy2.cohortLabel) {
|
|
5251
|
+
skip(id7, "cohort_mismatch");
|
|
5252
|
+
continue;
|
|
5253
|
+
}
|
|
5254
|
+
const skuId = String(member.baseSkuId);
|
|
5255
|
+
if (!policy2.discountEligibleSkuIds.includes(skuId)) {
|
|
5256
|
+
skip(id7, "sku_not_discounted");
|
|
5257
|
+
continue;
|
|
5258
|
+
}
|
|
5259
|
+
const base = Number(member.baseAmountCents), current = Number(member.renewalAmountCents);
|
|
5260
|
+
const renewal = Math.round(base * (1e4 - policy2.percentOffBps) / 1e4);
|
|
5261
|
+
if (!Number.isSafeInteger(base) || base <= 0 || !Number.isSafeInteger(current) || renewal > current) {
|
|
5262
|
+
skip(id7, "would_not_widen");
|
|
5263
|
+
continue;
|
|
5264
|
+
}
|
|
5265
|
+
const couponId = await membershipDiscountCouponId({ ...scope2, skuId }, policy2.id);
|
|
5266
|
+
let providerUpdated;
|
|
5232
5267
|
try {
|
|
5233
|
-
|
|
5234
|
-
|
|
5235
|
-
|
|
5236
|
-
|
|
5237
|
-
|
|
5238
|
-
|
|
5239
|
-
|
|
5240
|
-
|
|
5241
|
-
|
|
5268
|
+
const productId = await verifySubscriptionPrice(stripeSecretKey, {
|
|
5269
|
+
priceId: String(member.providerPriceId),
|
|
5270
|
+
priceCents: base,
|
|
5271
|
+
currency: String(member.currency),
|
|
5272
|
+
interval: member.interval === "month" ? "month" : "year",
|
|
5273
|
+
environment: scope2.environment
|
|
5274
|
+
}, true);
|
|
5275
|
+
if (!productId) throw Object.assign(new Error("price has no product"), { code: "provider_mismatch" });
|
|
5276
|
+
await prepareSubscriptionBillingTerms(stripeSecretKey, {
|
|
5277
|
+
...scope2,
|
|
5278
|
+
priceCents: base,
|
|
5279
|
+
currency: String(member.currency),
|
|
5280
|
+
discountProductId: productId,
|
|
5281
|
+
billingTerms: {
|
|
5282
|
+
offeringVersionId: String(member.offeringVersionId),
|
|
5283
|
+
discount: { policyVersionId: policy2.id, couponId, percentOffBps: policy2.percentOffBps, duration: "forever" }
|
|
5284
|
+
}
|
|
5285
|
+
}, false);
|
|
5286
|
+
providerUpdated = await attachProviderDiscount(stripeSecretKey, scope2, member, policy2, couponId, productId);
|
|
5287
|
+
} catch (error) {
|
|
5288
|
+
const code = error.code;
|
|
5289
|
+
if (code === "provider_mismatch" || code === "stripe_contract_mismatch") {
|
|
5290
|
+
skip(id7, "provider_mismatch");
|
|
5291
|
+
continue;
|
|
5292
|
+
}
|
|
5293
|
+
throw error;
|
|
5294
|
+
}
|
|
5295
|
+
try {
|
|
5296
|
+
await db.transact([membershipUpdate("networkMembership", id7, {
|
|
5297
|
+
discountPolicyVersionId: policy2.id,
|
|
5298
|
+
discountSnapshot: policy2,
|
|
5299
|
+
renewalAmountCents: renewal,
|
|
5300
|
+
revision: Number(member.revision) + 1,
|
|
5301
|
+
updatedAt: now
|
|
5302
|
+
})], {
|
|
5303
|
+
mutationId: `membership-reterm:${id7}:${policy2.id}:${member.revision}`,
|
|
5304
|
+
guards: [
|
|
5305
|
+
{ ns: "networkMembership", id: id7, exists: true, equals: { revision: member.revision, status: member.status } },
|
|
5306
|
+
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative" } },
|
|
5307
|
+
{ ns: "discountPolicyVersion", id: policy2.id, exists: true }
|
|
5308
|
+
]
|
|
5242
5309
|
});
|
|
5243
5310
|
} catch (error) {
|
|
5244
5311
|
if (error.code !== "transact_guard_failed") throw error;
|
|
5312
|
+
return membershipFail("membership_reterm_retry_required");
|
|
5245
5313
|
}
|
|
5246
|
-
|
|
5247
|
-
|
|
5248
|
-
|
|
5314
|
+
receipt.retermed.push({
|
|
5315
|
+
membershipId: id7,
|
|
5316
|
+
...typeof member.discountPolicyVersionId === "string" ? { previousPolicyVersionId: member.discountPolicyVersionId } : {},
|
|
5317
|
+
previousRenewalAmountCents: current,
|
|
5318
|
+
renewalAmountCents: renewal,
|
|
5319
|
+
providerUpdated
|
|
5320
|
+
});
|
|
5249
5321
|
}
|
|
5250
|
-
|
|
5251
|
-
where: { applicationId: app.id, status: "scheduled" },
|
|
5252
|
-
order: { createdAt: "desc" },
|
|
5253
|
-
limit: 1
|
|
5254
|
-
} } })).meetings?.[0];
|
|
5255
|
-
const group = (await db.query({ groups: { $: { where: { id: chapterId }, limit: 1 } } })).groups?.[0];
|
|
5256
|
-
const timezone = resolveScheduling(group?.schedulingJson).timezone;
|
|
5257
|
-
return { source: app, application: memberApplication(
|
|
5258
|
-
app,
|
|
5259
|
-
meeting,
|
|
5260
|
-
timezone
|
|
5261
|
-
) };
|
|
5322
|
+
return receipt;
|
|
5262
5323
|
}
|
|
5263
5324
|
|
|
5325
|
+
// src/worker-routes-admin-membership-reterm.ts
|
|
5326
|
+
var PATH4 = "/api/admin/membership-discount/reterm";
|
|
5327
|
+
var SCOPE3 = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
5328
|
+
var ID = /^[A-Za-z0-9][A-Za-z0-9_.:-]{2,159}$/;
|
|
5329
|
+
var BAD = "chapterId and policyVersionId are required; runtime, appId, membershipIds and limit are optional";
|
|
5330
|
+
var handleAdminMembershipReterm = async (req, url, env, ctx) => {
|
|
5331
|
+
if (url.pathname !== PATH4) return null;
|
|
5332
|
+
const operator = ctx.membershipAuthorityOperator;
|
|
5333
|
+
if (!operator) return json({ error: "not found" }, 404);
|
|
5334
|
+
if (req.method !== "POST") return json({ error: "method not allowed" }, 405);
|
|
5335
|
+
const raw2 = ctx.makeDb(env), user = await ctx.verifyUser(req, env);
|
|
5336
|
+
if (!user) return json({ error: "unauthorized" }, 401);
|
|
5337
|
+
if (!await ctx.isSuperAdminEmail(raw2, user.email, user.userId)) return json({ error: "super-admin required", code: "super_admin_required" }, 403);
|
|
5338
|
+
let db;
|
|
5339
|
+
try {
|
|
5340
|
+
db = operator.makeDb(env);
|
|
5341
|
+
} catch {
|
|
5342
|
+
return json({ error: "the membership authority is not hosted by this deployment" }, 404);
|
|
5343
|
+
}
|
|
5344
|
+
let body;
|
|
5345
|
+
try {
|
|
5346
|
+
body = await req.json();
|
|
5347
|
+
} catch {
|
|
5348
|
+
return json({ error: BAD }, 400);
|
|
5349
|
+
}
|
|
5350
|
+
const appId = body.appId ?? body.chapterId, runtime = body.runtime ?? env.ODLA_RUNTIME;
|
|
5351
|
+
if (typeof body.chapterId !== "string" || !SCOPE3.test(body.chapterId) || typeof appId !== "string" || !SCOPE3.test(appId) || typeof runtime !== "string" || !/^[a-z0-9]{2,12}$/.test(runtime) || typeof body.policyVersionId !== "string" || !ID.test(body.policyVersionId) || body.membershipIds !== void 0 && (!Array.isArray(body.membershipIds) || body.membershipIds.length === 0 || body.membershipIds.length > 200 || body.membershipIds.some((id7) => typeof id7 !== "string" || !ID.test(id7))) || body.limit !== void 0 && (!Number.isSafeInteger(body.limit) || Number(body.limit) < 1 || Number(body.limit) > 200)) {
|
|
5352
|
+
return json({ error: BAD }, 400);
|
|
5353
|
+
}
|
|
5354
|
+
const secret = await getVaultSecret(db, operator.stripeSecretName ?? "stripe_secret_key");
|
|
5355
|
+
if (!secret) return json({ error: "network payment provider unavailable" }, 503);
|
|
5356
|
+
try {
|
|
5357
|
+
const receipt = await retermMembershipDiscount(
|
|
5358
|
+
db,
|
|
5359
|
+
secret,
|
|
5360
|
+
{ appId, chapterId: body.chapterId, environment: env.ODLA_ENV, runtime },
|
|
5361
|
+
{
|
|
5362
|
+
policyVersionId: body.policyVersionId,
|
|
5363
|
+
...body.membershipIds ? { membershipIds: body.membershipIds } : {},
|
|
5364
|
+
...body.limit !== void 0 ? { limit: Number(body.limit) } : {}
|
|
5365
|
+
},
|
|
5366
|
+
Date.now()
|
|
5367
|
+
);
|
|
5368
|
+
return json({ ok: true, ...receipt });
|
|
5369
|
+
} catch (error) {
|
|
5370
|
+
const code = error.code;
|
|
5371
|
+
return json({ error: "re-term is pending; retry to confirm", ...code ? { code } : {} }, code ? 409 : 503);
|
|
5372
|
+
}
|
|
5373
|
+
};
|
|
5374
|
+
|
|
5375
|
+
// src/worker-routes-membership-cancel.ts
|
|
5376
|
+
var handleMembershipCancellation = async (req, url, env, ctx) => {
|
|
5377
|
+
const match = url.pathname.match(/^\/api\/admin\/applications\/([0-9a-f-]+)\/cancel-renewal$/);
|
|
5378
|
+
if (!match) return null;
|
|
5379
|
+
if (req.method !== "POST") return json({ error: "method not allowed" }, 405);
|
|
5380
|
+
const user = await ctx.verifyUser(req, env), raw2 = ctx.makeDb(env);
|
|
5381
|
+
if (!user) return json({ error: "unauthorized" }, 401);
|
|
5382
|
+
if (!await ctx.isAdmin(raw2, user)) return json({ error: "forbidden" }, 403);
|
|
5383
|
+
if (!ctx.membershipAuthority?.cancelRenewal) return json({ error: "renewal cancellation unavailable" }, 503);
|
|
5384
|
+
const db = raw2, app = await membershipLoad(db, "applications", match[1]);
|
|
5385
|
+
if (!app || !chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME)) return json({ error: "not found" }, 404);
|
|
5386
|
+
if (app.namedSeatId || app.giftClaimId) return json({ error: "this membership has no independently renewing subscription" }, 409);
|
|
5387
|
+
const intent = await membershipLoad(db, "subscriptionCheckoutIntents", String(app.id));
|
|
5388
|
+
if (!intent) return json({ error: "membership history needs reconciliation" }, 409);
|
|
5389
|
+
try {
|
|
5390
|
+
const target = await membershipTargetForIntent(intent, app, env, ctx);
|
|
5391
|
+
const receipt = await ctx.membershipAuthority.cancelRenewal(target, env);
|
|
5392
|
+
if (receipt.membershipId !== await networkMembershipId(target) || receipt.autoRenew !== false || !Number.isSafeInteger(receipt.termEndsAt) || !["cancel_at_term_end", "lapsed", "paid_pending_approval"].includes(receipt.status)) throw new Error("invalid cancellation receipt");
|
|
5393
|
+
await db.transact([{ t: "update", ns: "applications", id: String(app.id), attrs: {
|
|
5394
|
+
membershipStatus: receipt.status,
|
|
5395
|
+
membershipAutoRenew: false,
|
|
5396
|
+
membershipGraceEndsAt: 0,
|
|
5397
|
+
renewalAt: receipt.termEndsAt
|
|
5398
|
+
} }], { guards: [{
|
|
5399
|
+
ns: "applications",
|
|
5400
|
+
id: String(app.id),
|
|
5401
|
+
exists: true,
|
|
5402
|
+
equals: { status: app.status, stripeSubscriptionId: app.stripeSubscriptionId }
|
|
5403
|
+
}] });
|
|
5404
|
+
return json({ ok: true, accessEndsAt: receipt.termEndsAt, status: receipt.status });
|
|
5405
|
+
} catch {
|
|
5406
|
+
return json({ error: "renewal cancellation is pending; retry to confirm" }, 503);
|
|
5407
|
+
}
|
|
5408
|
+
};
|
|
5409
|
+
|
|
5264
5410
|
// src/worker-routes-membership-restart.ts
|
|
5265
5411
|
async function memberRestartState(db, env, ctx, app) {
|
|
5266
5412
|
if (!ctx.membershipAuthority?.restart || app.namedSeatId || app.giftClaimId) return null;
|
|
@@ -5488,6 +5634,7 @@ async function submitApplication(db, chapter, fields, opts) {
|
|
|
5488
5634
|
clerkUserId: opts.namedSeat.userId,
|
|
5489
5635
|
clerkAccountPending: false
|
|
5490
5636
|
});
|
|
5637
|
+
if (opts.predecessor?.clerkUserId && !opts.namedSeat) Object.assign(row2, { clerkUserId: opts.predecessor.clerkUserId, clerkAccountPending: false });
|
|
5491
5638
|
if (typeof fields.email === "string" && isValidEmail(fields.email.trim())) {
|
|
5492
5639
|
row2.emailNormalized = normalizeChapterEmail(fields.email);
|
|
5493
5640
|
row2.signupEmailKey = row2.emailNormalized;
|
|
@@ -5506,6 +5653,8 @@ async function submitApplication(db, chapter, fields, opts) {
|
|
|
5506
5653
|
}
|
|
5507
5654
|
if (acked) row2.disclaimerAckAt = opts.now;
|
|
5508
5655
|
const ops = [
|
|
5656
|
+
// The unique signup key leaves the closed predecessor before the new attempt takes it.
|
|
5657
|
+
...opts.predecessor && opts.predecessor.id !== id7 ? [{ t: "retract", ns: "applications", id: opts.predecessor.id, attrs: ["signupEmailKey"] }] : [],
|
|
5509
5658
|
{ t: "update", ns: "applications", id: id7, attrs: row2 }
|
|
5510
5659
|
];
|
|
5511
5660
|
if (opts.admissionGrant?.decision === "waive_interview") {
|
|
@@ -5553,27 +5702,35 @@ function joinConfig(group, paymentsReady, tiers = [], conditions = {}) {
|
|
|
5553
5702
|
|
|
5554
5703
|
// src/signup-eligibility.ts
|
|
5555
5704
|
var signupConflict = { code: "account_already_exists", message: "An account or application already exists for this email. Please sign in to continue." };
|
|
5556
|
-
|
|
5557
|
-
|
|
5705
|
+
var closedApplication = (row2) => row2.canceled === true || ["declined", "refunded", "canceled"].includes(String(row2.status));
|
|
5706
|
+
async function resolveSignupEligibility(db, chapter, fields, submissionId) {
|
|
5707
|
+
if (typeof fields.email !== "string" || !isValidEmail(fields.email.trim())) return { conflict: null, predecessor: null };
|
|
5558
5708
|
const email = normalizeChapterEmail(fields.email);
|
|
5559
5709
|
const applicationId = submissionId ? await applicationIdForSubmission(submissionId) : void 0;
|
|
5560
|
-
const conflict = () => Response.json({ error: signupConflict }, { status: 409 });
|
|
5710
|
+
const conflict = () => ({ conflict: Response.json({ error: signupConflict }, { status: 409 }), predecessor: null });
|
|
5561
5711
|
try {
|
|
5562
5712
|
const data = await db.query({
|
|
5563
|
-
applications: { $: { where: { emailNormalized: email }, limit:
|
|
5713
|
+
applications: { $: { where: { emailNormalized: email }, limit: 10 } },
|
|
5564
5714
|
$users: { $: { where: { email }, limit: 2 } }
|
|
5565
5715
|
});
|
|
5566
|
-
|
|
5567
|
-
const
|
|
5568
|
-
if (
|
|
5569
|
-
|
|
5716
|
+
const rows5 = data.applications ?? [], others = rows5.filter((row2) => row2.id !== applicationId);
|
|
5717
|
+
const closed = others.filter((row2) => row2.groupId === chapter.id && closedApplication(row2)).sort((a, b) => Number(b.createdAt ?? 0) - Number(a.createdAt ?? 0));
|
|
5718
|
+
if (others.length !== closed.length) return conflict();
|
|
5719
|
+
const original = rows5.find((row2) => row2.id === applicationId && row2.groupId === chapter.id);
|
|
5720
|
+
const owned = (userId) => original?.clerkUserId === userId || closed.some((row2) => row2.clerkUserId === userId);
|
|
5721
|
+
if ((data.$users ?? []).some((row2) => row2.deleted !== true && !owned(row2.id))) return conflict();
|
|
5722
|
+
const predecessor = (clerkUserId) => closed.length ? { id: String(closed[0].id), ...clerkUserId ? { clerkUserId } : {} } : null;
|
|
5723
|
+
if (chapter.account === "none") return { conflict: null, predecessor: predecessor(typeof closed[0]?.clerkUserId === "string" ? closed[0].clerkUserId : void 0) };
|
|
5570
5724
|
const secret = await getVaultSecret(db, "clerk_secret_key");
|
|
5571
|
-
if (!secret) return Response.json({ error: { code: "clerk_secret_unavailable", message: "account provisioning credential is unavailable" } }, { status: 503 });
|
|
5725
|
+
if (!secret) return { conflict: Response.json({ error: { code: "clerk_secret_unavailable", message: "account provisioning credential is unavailable" } }, { status: 503 }), predecessor: null };
|
|
5572
5726
|
const account = await findSignupAccount(secret, email);
|
|
5573
|
-
if (account
|
|
5574
|
-
return null;
|
|
5727
|
+
if (!account) return { conflict: null, predecessor: predecessor() };
|
|
5728
|
+
if (original && (original.clerkUserId === account.id || belongsToSignup(account, original.id))) return { conflict: null, predecessor: null };
|
|
5729
|
+
const owner = closed.find((row2) => row2.clerkUserId === account.id || belongsToSignup(account, row2.id));
|
|
5730
|
+
if (!owner) return conflict();
|
|
5731
|
+
return { conflict: null, predecessor: { id: String(owner.id), clerkUserId: account.id } };
|
|
5575
5732
|
} catch {
|
|
5576
|
-
return Response.json({ error: { code: "signup_lookup_unavailable", message: "Signup is temporarily unavailable. Please try again." } }, { status: 503 });
|
|
5733
|
+
return { conflict: Response.json({ error: { code: "signup_lookup_unavailable", message: "Signup is temporarily unavailable. Please try again." } }, { status: 503 }), predecessor: null };
|
|
5577
5734
|
}
|
|
5578
5735
|
}
|
|
5579
5736
|
async function rejectUnprovisionedSignup(db, applicationId, status2) {
|
|
@@ -6739,8 +6896,12 @@ async function namedSeatApplicationContext(req, seatId, env, ctx) {
|
|
|
6739
6896
|
}
|
|
6740
6897
|
async function existingNamedSeatApplication(context, env, ctx) {
|
|
6741
6898
|
const { db, userId, email, invitation } = context;
|
|
6742
|
-
const
|
|
6743
|
-
|
|
6899
|
+
const rows5 = (await db.query({ applications: { $: { where: { emailNormalized: normalizeChapterEmail(email) }, limit: 10 } } })).applications ?? [];
|
|
6900
|
+
const apps = rows5.filter((row2) => !closedApplication(row2));
|
|
6901
|
+
if (!apps.length) {
|
|
6902
|
+
const closed = rows5.filter((row2) => row2.groupId === ctx.chapter.id).sort((a, b) => Number(b.createdAt ?? 0) - Number(a.createdAt ?? 0))[0];
|
|
6903
|
+
return { predecessor: closed ? { id: String(closed.id) } : null };
|
|
6904
|
+
}
|
|
6744
6905
|
const app = apps[0];
|
|
6745
6906
|
if (apps.length !== 1 || app.clerkUserId !== userId || app.groupId !== ctx.chapter.id || stripeRuntimeIdentity(app.stripeRuntime) !== stripeRuntimeIdentity(env.ODLA_RUNTIME) || app.canceled === true || app.stripeSubscriptionId || app.giftClaimId || app.giftOfferId || app.giftIntentId || app.membershipRestartId || (app.namedSeatId ? app.namedSeatId !== invitation.seatId : app.status !== ctx.chapter.pipeline.initial)) {
|
|
6746
6907
|
return json({ error: "Your existing application cannot accept this invitation. Contact the chapter for help.", code: "application_conflict" }, 409);
|
|
@@ -7256,8 +7417,8 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
7256
7417
|
parsed.tierId = named.invitation.skuId;
|
|
7257
7418
|
submissionId = `named-seat:${named.invitation.seatId}:${named.userId}`;
|
|
7258
7419
|
}
|
|
7259
|
-
const
|
|
7260
|
-
if (
|
|
7420
|
+
const eligibility = named ? null : await resolveSignupEligibility(db, chapter, parsed, submissionId);
|
|
7421
|
+
if (eligibility?.conflict) return eligibility.conflict;
|
|
7261
7422
|
const activeControl = chapter.signupControl ? await loadActiveSignupControl(db, chapter.id, env.ODLA_ENV) : null;
|
|
7262
7423
|
const effectiveChapter = activeControl ? { ...chapter, application: controlledApplication(chapter.application, activeControl.document) } : chapter;
|
|
7263
7424
|
const offeredTierIds = typeof parsed.tierId === "string" && parsed.tierId ? (await loadTiers(db, { id: chapter.id })).map((tier) => tier.id) : void 0;
|
|
@@ -7268,7 +7429,8 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
7268
7429
|
const effectiveGrant = !named && !giftOffer && typeof parsed.email === "string" ? await findAdmissionGrant(db, chapter.id, parsed.email, now) : null;
|
|
7269
7430
|
const existing = named ? await existingNamedSeatApplication(named, env, ctx) : null;
|
|
7270
7431
|
if (existing instanceof Response) return existing;
|
|
7271
|
-
const
|
|
7432
|
+
const predecessor = existing && "predecessor" in existing ? existing.predecessor : eligibility?.predecessor ?? null;
|
|
7433
|
+
const result = existing && "ok" in existing ? existing : await submitApplication(db, effectiveChapter, parsed, {
|
|
7272
7434
|
submissionId,
|
|
7273
7435
|
groupId: chapter.id,
|
|
7274
7436
|
runtime: stripeRuntimeIdentity(env.ODLA_RUNTIME),
|
|
@@ -7277,7 +7439,8 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
7277
7439
|
...named ? { namedSeat: { seatId: named.invitation.seatId, primaryApplicationId: named.invitation.primaryApplicationId, userId: named.userId } } : {},
|
|
7278
7440
|
...named ? { tierIds: [named.invitation.skuId] } : offeredTierIds ? { tierIds: offeredTierIds } : {},
|
|
7279
7441
|
...effectiveGrant?.decision === "waive_interview" ? { admissionGrant: effectiveGrant } : {},
|
|
7280
|
-
...giftOffer ? { giftOfferId: giftOffer.id } : {}
|
|
7442
|
+
...giftOffer ? { giftOfferId: giftOffer.id } : {},
|
|
7443
|
+
...predecessor ? { predecessor } : {}
|
|
7281
7444
|
});
|
|
7282
7445
|
if (!result.ok) return json({ error: result.error, ...result.code ? { code: result.code } : {} }, result.status ?? 400);
|
|
7283
7446
|
const apps = (await db.query({ applications: { $: { where: { id: result.id }, limit: 1 } } })).applications;
|
|
@@ -7297,6 +7460,7 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
7297
7460
|
if (provisioningFailure.status === 409) await rejectUnprovisionedSignup(db, result.id, canonical3.status);
|
|
7298
7461
|
return json(provisioningFailure.body, provisioningFailure.status);
|
|
7299
7462
|
}
|
|
7463
|
+
if (predecessor) await syncApplicationToCrm(crmDeps(db, ctx), { app: canonical3, stage: String(canonical3.status) }).catch(() => void 0);
|
|
7300
7464
|
const groups = (await db.query({ groups: { $: { where: { id: chapter.id }, limit: 1 } } })).groups;
|
|
7301
7465
|
const group = Array.isArray(groups) ? groups[0] : void 0;
|
|
7302
7466
|
const notice = group ? { group, application: canonical3, origin: url.origin } : null;
|
|
@@ -10247,6 +10411,128 @@ var handleGifts = async (req, url, env, ctx) => {
|
|
|
10247
10411
|
return null;
|
|
10248
10412
|
};
|
|
10249
10413
|
|
|
10414
|
+
// src/membership-effect-kinds.ts
|
|
10415
|
+
var MEMBERSHIP_CHAPTER_EFFECT_KINDS = [
|
|
10416
|
+
"invite_named_recipient",
|
|
10417
|
+
"grant_named_recipient_access",
|
|
10418
|
+
"revoke_named_recipient_access",
|
|
10419
|
+
"release_named_recipient"
|
|
10420
|
+
];
|
|
10421
|
+
var namedSeatInvitationOpen = (member) => ["active", "cancel_at_term_end"].includes(String(member.status)) && Number.isSafeInteger(member.journeyApprovedAt);
|
|
10422
|
+
|
|
10423
|
+
// src/membership-seat-regift.ts
|
|
10424
|
+
var REGIFTABLE_SEAT_STATUSES = ["pending_acceptance", "pending_approval", "pending_primary"];
|
|
10425
|
+
var namedSeatRegiftable = (seat) => REGIFTABLE_SEAT_STATUSES.includes(String(seat.status));
|
|
10426
|
+
var receiptFor2 = (seat, released) => ({
|
|
10427
|
+
seatId: String(seat.id),
|
|
10428
|
+
status: "pending_acceptance",
|
|
10429
|
+
recipientName: String(seat.recipientName),
|
|
10430
|
+
recipientEmail: String(seat.recipientEmailNormalized),
|
|
10431
|
+
regiftedAt: Number(seat.regiftedAt),
|
|
10432
|
+
releasedRecipientApplicationId: released
|
|
10433
|
+
});
|
|
10434
|
+
async function regiftNamedSeat(db, input, now) {
|
|
10435
|
+
membershipClock(now);
|
|
10436
|
+
const name = typeof input.recipientName === "string" ? input.recipientName.trim().replace(/\s+/g, " ") : "";
|
|
10437
|
+
const email = normalizeChapterEmail(typeof input.recipientEmail === "string" ? input.recipientEmail : "");
|
|
10438
|
+
if (name.length < 2 || name.length > 160 || /[\u0000-\u001f\u007f]/.test(input.recipientName) || email.length > 254 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) membershipFail("invalid_named_seat_recipient");
|
|
10439
|
+
const state = await loadMembershipAuthorityState(db, input);
|
|
10440
|
+
const seat = await membershipLoad(db, "networkSeat", input.seatId);
|
|
10441
|
+
requireMembershipScope(seat, input);
|
|
10442
|
+
const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
|
|
10443
|
+
requireMembershipScope(member, input);
|
|
10444
|
+
if (member.namedSeatId !== seat.id) membershipFail("named_seat_primary_conflict");
|
|
10445
|
+
if (typeof seat.purchaserEmailNormalized === "string" && seat.purchaserEmailNormalized === email) membershipFail("invalid_named_seat_recipient");
|
|
10446
|
+
const history = Array.isArray(seat.formerRecipients) ? seat.formerRecipients : [];
|
|
10447
|
+
if (seat.recipientEmailNormalized === email && seat.recipientName === name && seat.regiftedAt !== void 0 && seat.status === "pending_acceptance") {
|
|
10448
|
+
return receiptFor2(seat, history.at(-1)?.recipientApplicationId ?? null);
|
|
10449
|
+
}
|
|
10450
|
+
if (!namedSeatRegiftable(seat)) membershipFail("named_seat_not_regiftable");
|
|
10451
|
+
if (seat.recipientEmailNormalized === email) membershipFail("named_seat_recipient_unchanged");
|
|
10452
|
+
const entitlement = await membershipLoad(db, "networkEntitlement", String(seat.entitlementId));
|
|
10453
|
+
requireMembershipScope(entitlement, input);
|
|
10454
|
+
const claimId = await namedSeatRecipientClaimId(input, email);
|
|
10455
|
+
const former = {
|
|
10456
|
+
recipientName: String(seat.recipientName),
|
|
10457
|
+
recipientEmail: String(seat.recipientEmailNormalized),
|
|
10458
|
+
recipientClaimId: String(seat.recipientClaimId),
|
|
10459
|
+
releasedAt: now,
|
|
10460
|
+
...typeof seat.recipientUserId === "string" ? { recipientUserId: seat.recipientUserId } : {},
|
|
10461
|
+
...typeof seat.recipientApplicationId === "string" ? { recipientApplicationId: seat.recipientApplicationId } : {},
|
|
10462
|
+
...Number.isSafeInteger(seat.acceptedAt) ? { acceptedAt: Number(seat.acceptedAt) } : {},
|
|
10463
|
+
...Number.isSafeInteger(seat.journeyApprovedAt) ? { journeyApprovedAt: Number(seat.journeyApprovedAt) } : {}
|
|
10464
|
+
};
|
|
10465
|
+
const generation = history.length + 1, inviteId = `${seat.id}:invite:${generation}`, releaseId = `${seat.id}:release:${generation}`;
|
|
10466
|
+
const bound = Boolean(former.recipientUserId && former.recipientApplicationId);
|
|
10467
|
+
const stale = ((await db.query({ membershipEffect: { $: { where: { seatId: seat.id, kind: "invite_named_recipient", status: "pending" }, limit: 10 } } })).membershipEffect ?? []).filter((row2) => row2.chapterId === input.chapterId && row2.environment === input.environment && row2.runtime === input.runtime);
|
|
10468
|
+
const cleared = ["recipientUserId", "recipientApplicationId", "acceptedAt", "journeyApprovedAt"];
|
|
10469
|
+
const effect = (id7, kind, extra = {}) => membershipUpdate("membershipEffect", id7, {
|
|
10470
|
+
id: id7,
|
|
10471
|
+
chapterId: input.chapterId,
|
|
10472
|
+
environment: input.environment,
|
|
10473
|
+
runtime: input.runtime,
|
|
10474
|
+
membershipId: member.id,
|
|
10475
|
+
seatId: seat.id,
|
|
10476
|
+
kind,
|
|
10477
|
+
status: "pending",
|
|
10478
|
+
attempts: 0,
|
|
10479
|
+
createdAt: now,
|
|
10480
|
+
updatedAt: now,
|
|
10481
|
+
...extra
|
|
10482
|
+
});
|
|
10483
|
+
const ops = [
|
|
10484
|
+
membershipUpdate("networkSeat", String(seat.id), {
|
|
10485
|
+
recipientName: name,
|
|
10486
|
+
recipientEmailNormalized: email,
|
|
10487
|
+
recipientClaimId: claimId,
|
|
10488
|
+
status: "pending_acceptance",
|
|
10489
|
+
regiftedAt: now,
|
|
10490
|
+
formerRecipients: [...history, former],
|
|
10491
|
+
revision: Number(seat.revision) + 1,
|
|
10492
|
+
updatedAt: now
|
|
10493
|
+
}),
|
|
10494
|
+
{ t: "retract", ns: "networkSeat", id: String(seat.id), attrs: cleared },
|
|
10495
|
+
membershipUpdate("networkEntitlement", String(entitlement.id), { status: "pending_acceptance", revision: Number(entitlement.revision) + 1, updatedAt: now }),
|
|
10496
|
+
{ t: "retract", ns: "networkEntitlement", id: String(entitlement.id), attrs: cleared },
|
|
10497
|
+
{ t: "delete", ns: "networkSeatRecipient", id: String(seat.recipientClaimId) },
|
|
10498
|
+
membershipUpdate("networkSeatRecipient", claimId, {
|
|
10499
|
+
id: claimId,
|
|
10500
|
+
appId: input.appId,
|
|
10501
|
+
chapterId: input.chapterId,
|
|
10502
|
+
environment: input.environment,
|
|
10503
|
+
runtime: input.runtime,
|
|
10504
|
+
seatId: seat.id,
|
|
10505
|
+
recipientEmailNormalized: email,
|
|
10506
|
+
createdAt: now
|
|
10507
|
+
}),
|
|
10508
|
+
...stale.map((row2) => membershipUpdate("membershipEffect", String(row2.id), { status: "complete", updatedAt: now })),
|
|
10509
|
+
effect(inviteId, "invite_named_recipient"),
|
|
10510
|
+
...bound ? [effect(releaseId, "release_named_recipient", { recipientUserId: former.recipientUserId, recipientApplicationId: former.recipientApplicationId })] : []
|
|
10511
|
+
];
|
|
10512
|
+
try {
|
|
10513
|
+
await db.transact(ops, { mutationId: `named-seat-regift:${seat.id}:${generation}`, guards: [
|
|
10514
|
+
{ ns: "networkSeat", id: String(seat.id), exists: true, equals: { revision: seat.revision, status: seat.status } },
|
|
10515
|
+
{ ns: "networkEntitlement", id: String(entitlement.id), exists: true, equals: { revision: entitlement.revision } },
|
|
10516
|
+
{ ns: "networkMembership", id: String(member.id), exists: true, equals: { revision: member.revision, namedSeatId: seat.id } },
|
|
10517
|
+
{ ns: "networkSeatRecipient", id: claimId, exists: false },
|
|
10518
|
+
...stale.map((row2) => ({ ns: "membershipEffect", id: String(row2.id), exists: true, equals: { status: "pending" } })),
|
|
10519
|
+
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
|
|
10520
|
+
] });
|
|
10521
|
+
} catch (error) {
|
|
10522
|
+
if (error.code !== "transact_guard_failed") throw error;
|
|
10523
|
+
const current = await membershipLoad(db, "networkSeat", input.seatId);
|
|
10524
|
+
requireMembershipScope(current, input);
|
|
10525
|
+
if (current.recipientEmailNormalized === email && current.status === "pending_acceptance") return receiptFor2(current, former.recipientApplicationId ?? null);
|
|
10526
|
+
const claim = await membershipLoad(db, "networkSeatRecipient", claimId);
|
|
10527
|
+
if (claim && claim.seatId !== seat.id) membershipFail("named_seat_recipient_unavailable");
|
|
10528
|
+
membershipFail("named_seat_regift_retry_required");
|
|
10529
|
+
}
|
|
10530
|
+
const saved = await membershipLoad(db, "networkSeat", input.seatId);
|
|
10531
|
+
requireMembershipScope(saved, input);
|
|
10532
|
+
if (saved.recipientEmailNormalized !== email || saved.status !== "pending_acceptance") membershipFail("named_seat_regift_retry_required");
|
|
10533
|
+
return receiptFor2(saved, former.recipientApplicationId ?? null);
|
|
10534
|
+
}
|
|
10535
|
+
|
|
10250
10536
|
// src/membership-seat-offer.ts
|
|
10251
10537
|
async function namedSeatOfferDigest(offer) {
|
|
10252
10538
|
const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify([
|
|
@@ -10290,7 +10576,10 @@ async function readNamedSeatOffer(db, input, now) {
|
|
|
10290
10576
|
recipientState: namedSeatRecipientState(seat),
|
|
10291
10577
|
...at(seat.purchasedAt) !== void 0 ? { purchasedAt: at(seat.purchasedAt) } : {},
|
|
10292
10578
|
...at(seat.acceptedAt) !== void 0 ? { acceptedAt: at(seat.acceptedAt) } : {},
|
|
10293
|
-
...seat.status === "active" && at(seat.journeyApprovedAt) !== void 0 ? { joinedAt: at(seat.journeyApprovedAt) } : {}
|
|
10579
|
+
...seat.status === "active" && at(seat.journeyApprovedAt) !== void 0 ? { joinedAt: at(seat.journeyApprovedAt) } : {},
|
|
10580
|
+
...seat.status === "pending_acceptance" && seat.acceptedAt === void 0 && !namedSeatInvitationOpen(member) ? { invitationHeld: true } : {},
|
|
10581
|
+
...namedSeatRegiftable(seat) ? { regiftable: true } : {},
|
|
10582
|
+
...at(seat.regiftedAt) !== void 0 ? { regiftedAt: at(seat.regiftedAt) } : {}
|
|
10294
10583
|
} } : {}
|
|
10295
10584
|
};
|
|
10296
10585
|
return {
|
|
@@ -10301,40 +10590,6 @@ async function readNamedSeatOffer(db, input, now) {
|
|
|
10301
10590
|
};
|
|
10302
10591
|
}
|
|
10303
10592
|
|
|
10304
|
-
// src/named-seat-request.ts
|
|
10305
|
-
async function smallRequestBody(req) {
|
|
10306
|
-
const reader = req.body?.getReader();
|
|
10307
|
-
if (!reader) throw new Error("request body required");
|
|
10308
|
-
const chunks = [];
|
|
10309
|
-
let size = 0;
|
|
10310
|
-
try {
|
|
10311
|
-
while (true) {
|
|
10312
|
-
const chunk = await reader.read();
|
|
10313
|
-
if (chunk.done) break;
|
|
10314
|
-
size += chunk.value.byteLength;
|
|
10315
|
-
if (size > 4096) {
|
|
10316
|
-
await reader.cancel();
|
|
10317
|
-
throw new Error("request too large");
|
|
10318
|
-
}
|
|
10319
|
-
chunks.push(chunk.value);
|
|
10320
|
-
}
|
|
10321
|
-
} finally {
|
|
10322
|
-
reader.releaseLock();
|
|
10323
|
-
}
|
|
10324
|
-
const bytes = new Uint8Array(size);
|
|
10325
|
-
let offset = 0;
|
|
10326
|
-
for (const chunk of chunks) {
|
|
10327
|
-
bytes.set(chunk, offset);
|
|
10328
|
-
offset += chunk.byteLength;
|
|
10329
|
-
}
|
|
10330
|
-
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
10331
|
-
}
|
|
10332
|
-
async function namedSeatRequest(req) {
|
|
10333
|
-
const body = JSON.parse(await smallRequestBody(req));
|
|
10334
|
-
if (!body || typeof body !== "object" || Array.isArray(body)) throw new Error("object required");
|
|
10335
|
-
return body;
|
|
10336
|
-
}
|
|
10337
|
-
|
|
10338
10593
|
// src/worker-routes-named-seat.ts
|
|
10339
10594
|
var handleNamedSeat = async (req, url, env, ctx) => {
|
|
10340
10595
|
if (!["/api/named-seat", "/api/named-seat/checkout"].includes(url.pathname)) return null;
|
|
@@ -10740,7 +10995,8 @@ function applicationActionStages(app, env, ctx) {
|
|
|
10740
10995
|
if (to === from) return false;
|
|
10741
10996
|
if (to === "approved") return canApprove(from, ctx.chapter.pipeline) && from !== "declined";
|
|
10742
10997
|
if (to === "refunded") return Boolean(app.namedSeatId || app.stripeCustomerId && app.stripeSubscriptionId) && (!ctx.chapter.operations.refund.allowedFrom || ctx.chapter.operations.refund.allowedFrom.includes(from));
|
|
10743
|
-
if (ctx.membershipAuthority &&
|
|
10998
|
+
if (to === "declined" && ctx.membershipAuthority) return !app.namedSeatId && !app.stripeSubscriptionId && canTransition(from, to, ctx.chapter.pipeline);
|
|
10999
|
+
if (ctx.membershipAuthority && ["submitted", "paid_pending_vetting", "canceled"].includes(to)) return false;
|
|
10744
11000
|
return canTransition(from, to, ctx.chapter.pipeline);
|
|
10745
11001
|
});
|
|
10746
11002
|
}
|
|
@@ -11510,6 +11766,116 @@ async function refundApplication(db, env, ctx, app) {
|
|
|
11510
11766
|
}
|
|
11511
11767
|
}
|
|
11512
11768
|
|
|
11769
|
+
// src/decline-application.ts
|
|
11770
|
+
var DECLINABLE_FROM = (status2) => !["approved", "declined", "refunded", "canceled"].includes(status2);
|
|
11771
|
+
async function paymentHeld(db, env, ctx, app) {
|
|
11772
|
+
const intent = await membershipLoad(db, "subscriptionCheckoutIntents", String(app.id));
|
|
11773
|
+
if (ctx.membershipAuthority && intent) {
|
|
11774
|
+
try {
|
|
11775
|
+
const target = await membershipTargetForIntent(intent, app, env, ctx);
|
|
11776
|
+
const receipt = await verifyMembershipAccessReceipt(await ctx.membershipAuthority.readAccess(target, env), target, Date.now());
|
|
11777
|
+
return receipt.status !== "unpaid";
|
|
11778
|
+
} catch {
|
|
11779
|
+
return json({ error: "membership authority unavailable; retry before declining", code: "authority_unavailable" }, 503);
|
|
11780
|
+
}
|
|
11781
|
+
}
|
|
11782
|
+
return applicationSummary(app).paid || Boolean(intent?.paymentReceipt);
|
|
11783
|
+
}
|
|
11784
|
+
async function declineApplication(db, env, ctx, app) {
|
|
11785
|
+
if (!chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME)) return json({ error: "not found" }, 404);
|
|
11786
|
+
const id7 = String(app.id), status2 = String(app.status ?? "");
|
|
11787
|
+
if (status2 === "declined") return json({ ok: true, status: "declined", replayed: true });
|
|
11788
|
+
if (app.canceled === true || !DECLINABLE_FROM(status2)) return json({ error: `cannot decline from status "${status2}"`, code: "not_declinable" }, 409);
|
|
11789
|
+
if (app.namedSeatId) return json({ error: "a gifted seat is released by naming someone else, or refunded", code: "named_seat_recipient" }, 409);
|
|
11790
|
+
if (!ctx.chapter.pipeline.stages.includes("declined")) return json({ error: "this chapter's pipeline has no declined stage" }, 409);
|
|
11791
|
+
const held = await paymentHeld(db, env, ctx, app);
|
|
11792
|
+
if (held instanceof Response) return held;
|
|
11793
|
+
if (held) return json({ error: "a payment is held for this application; refund it instead", code: "payment_held" }, 409);
|
|
11794
|
+
try {
|
|
11795
|
+
await db.transact(
|
|
11796
|
+
[{ t: "update", ns: "applications", id: id7, attrs: { status: "declined", canceled: true } }],
|
|
11797
|
+
{ mutationId: `decline:${id7}:${status2}`, guards: [{ ns: "applications", id: id7, exists: true, equals: { status: status2 } }] }
|
|
11798
|
+
);
|
|
11799
|
+
} catch (error) {
|
|
11800
|
+
if (error.code !== "transact_guard_failed") throw error;
|
|
11801
|
+
return json({ error: "application changed; reload before declining" }, 409);
|
|
11802
|
+
}
|
|
11803
|
+
await syncApplicationToCrm(crmDeps(db, ctx), { app: { ...app, status: "declined", canceled: true }, stage: "declined" }).catch(() => void 0);
|
|
11804
|
+
return json({ ok: true, status: "declined" });
|
|
11805
|
+
}
|
|
11806
|
+
|
|
11807
|
+
// src/refund-continue.ts
|
|
11808
|
+
var BILLING_FIELDS = [
|
|
11809
|
+
"stripeSubscriptionId",
|
|
11810
|
+
"stripeCustomerId",
|
|
11811
|
+
"canceled",
|
|
11812
|
+
"renewalAt",
|
|
11813
|
+
"refundPolicyAckAt",
|
|
11814
|
+
"membershipStatus",
|
|
11815
|
+
"membershipGraceEndsAt",
|
|
11816
|
+
"membershipAutoRenew",
|
|
11817
|
+
"approvalEffectsPending",
|
|
11818
|
+
"approvalEffectsOrigin",
|
|
11819
|
+
"approvalEffectsLastAttemptAt",
|
|
11820
|
+
"membershipRestartId"
|
|
11821
|
+
];
|
|
11822
|
+
async function resolveFreeTier(db, ctx, app, tierId) {
|
|
11823
|
+
const group = await membershipLoad(db, "groups", String(app.groupId ?? ctx.chapter.id));
|
|
11824
|
+
const tier = group ? findTier(await loadTiers(db, group), tierId) : null;
|
|
11825
|
+
if (!tier || !tierIsFree(tier) || tier.active === false) return json({ error: "continueAs must name an active free tier of this chapter", code: "free_tier_required" }, 400);
|
|
11826
|
+
if (!ctx.chapter.pipeline.approvableFrom.length) return json({ error: "this chapter's pipeline has no approvable stage" }, 409);
|
|
11827
|
+
if (app.namedSeatId) return json({ error: "a gifted seat cannot continue as a free member", code: "named_seat_recipient" }, 409);
|
|
11828
|
+
return tier;
|
|
11829
|
+
}
|
|
11830
|
+
async function continueApplicationOnFreeTier(db, env, ctx, app, tier, origin) {
|
|
11831
|
+
const id7 = String(app.id), now = Date.now();
|
|
11832
|
+
const approvable = ctx.chapter.pipeline.approvableFrom.at(-1);
|
|
11833
|
+
if (!approvable) return json({ error: "this chapter's pipeline has no approvable stage" }, 409);
|
|
11834
|
+
const current = await membershipLoad(db, "applications", id7);
|
|
11835
|
+
if (!current || !chapterBillingApplication(current, ctx.chapter.id, env.ODLA_RUNTIME)) return json({ error: "not found" }, 404);
|
|
11836
|
+
if (current.status === "refunded") {
|
|
11837
|
+
const intent = await membershipLoad(db, "subscriptionCheckoutIntents", id7), quote = await membershipLoad(db, "membershipQuoteProjections", id7);
|
|
11838
|
+
const subscription = typeof intent?.providerSubscriptionId === "string" ? intent.providerSubscriptionId : null;
|
|
11839
|
+
const historyId = `${id7}:continue:${subscription ?? "none"}`;
|
|
11840
|
+
const ops = [
|
|
11841
|
+
...intent && subscription ? [{ t: "update", ns: "membershipCheckoutHistory", id: historyId, attrs: {
|
|
11842
|
+
id: historyId,
|
|
11843
|
+
applicationId: id7,
|
|
11844
|
+
appId: env.ODLA_APP_ID,
|
|
11845
|
+
chapterId: ctx.chapter.id,
|
|
11846
|
+
environment: env.ODLA_ENV,
|
|
11847
|
+
runtime: stripeRuntimeIdentity(env.ODLA_RUNTIME),
|
|
11848
|
+
skuId: String(current.tierId ?? intent.tierId ?? ""),
|
|
11849
|
+
providerSubscriptionId: subscription,
|
|
11850
|
+
applicationSnapshot: current,
|
|
11851
|
+
intentSnapshot: intent,
|
|
11852
|
+
quoteSnapshot: quote ?? {},
|
|
11853
|
+
archivedAt: now
|
|
11854
|
+
} }] : [],
|
|
11855
|
+
...intent ? [{ t: "delete", ns: "subscriptionCheckoutIntents", id: id7 }] : [],
|
|
11856
|
+
...quote ? [{ t: "update", ns: "membershipQuoteProjections", id: id7, attrs: { status: "continued", updatedAt: now } }] : [],
|
|
11857
|
+
{ t: "retract", ns: "applications", id: id7, attrs: BILLING_FIELDS.filter((key3) => current[key3] !== void 0) },
|
|
11858
|
+
{ t: "update", ns: "applications", id: id7, attrs: { tierId: tier.id, status: approvable } }
|
|
11859
|
+
];
|
|
11860
|
+
try {
|
|
11861
|
+
await db.transact(ops, { mutationId: `continue-free:${id7}:${subscription ?? "none"}`, guards: [
|
|
11862
|
+
{ ns: "applications", id: id7, exists: true, equals: { status: "refunded", ...current.tierId !== void 0 ? { tierId: current.tierId } : {} } },
|
|
11863
|
+
...intent && subscription ? [{ ns: "membershipCheckoutHistory", id: historyId, exists: false }] : []
|
|
11864
|
+
] });
|
|
11865
|
+
} catch (error) {
|
|
11866
|
+
if (error.code !== "transact_guard_failed") throw error;
|
|
11867
|
+
return json({ error: "application changed; reload before continuing", code: "continue_conflict" }, 409);
|
|
11868
|
+
}
|
|
11869
|
+
} else if (current.tierId !== tier.id || ![approvable, "approved"].includes(String(current.status))) {
|
|
11870
|
+
return json({ error: `cannot continue as ${tier.id} from status "${String(current.status)}"; refund first`, code: "continue_conflict" }, 409);
|
|
11871
|
+
}
|
|
11872
|
+
const converted = await membershipLoad(db, "applications", id7);
|
|
11873
|
+
if (!converted || converted.tierId !== tier.id) return json({ error: "conversion pending; retry", code: "continue_pending" }, 503);
|
|
11874
|
+
const approved = await approveApplication({ db, env, ctx, app: converted, origin, reason: "manual" });
|
|
11875
|
+
if (!approved.ok) return json({ error: approved.error, code: "continue_pending" }, approved.status);
|
|
11876
|
+
return json({ ok: true, continued: { tierId: tier.id, status: "approved", rolePromoted: approved.rolePromoted, emailLogged: approved.emailLogged } });
|
|
11877
|
+
}
|
|
11878
|
+
|
|
11513
11879
|
// src/patch-application.ts
|
|
11514
11880
|
async function patchApplication(db, env, ctx, app, body) {
|
|
11515
11881
|
const attrs = {};
|
|
@@ -11649,6 +12015,15 @@ var handleAdminApprove = async (req, url, env, ctx) => {
|
|
|
11649
12015
|
if (!result.ok) return json({ error: result.error }, result.status);
|
|
11650
12016
|
return json({ ok: true, status: "approved", rolePromoted: result.rolePromoted, emailLogged: result.emailLogged });
|
|
11651
12017
|
};
|
|
12018
|
+
var handleAdminDecline = async (req, url, env, ctx) => {
|
|
12019
|
+
const m = url.pathname.match(/^\/api\/admin\/applications\/([0-9a-f-]+)\/decline$/);
|
|
12020
|
+
if (req.method !== "POST" || !m) return null;
|
|
12021
|
+
const got = await gate4(req, env, ctx);
|
|
12022
|
+
if (got instanceof Response) return got;
|
|
12023
|
+
const app = await loadApp(got, m[1]);
|
|
12024
|
+
if (!app) return json({ error: "not found" }, 404);
|
|
12025
|
+
return declineApplication(got, env, ctx, app);
|
|
12026
|
+
};
|
|
11652
12027
|
var handleAdminRefund = async (req, url, env, ctx) => {
|
|
11653
12028
|
const m = url.pathname.match(/^\/api\/admin\/applications\/([0-9a-f-]+)\/refund$/);
|
|
11654
12029
|
if (req.method !== "POST" || !m) return null;
|
|
@@ -11664,7 +12039,21 @@ var handleAdminRefund = async (req, url, env, ctx) => {
|
|
|
11664
12039
|
return json({ error: "super-admin required", code: "super_admin_required" }, 403);
|
|
11665
12040
|
}
|
|
11666
12041
|
}
|
|
11667
|
-
|
|
12042
|
+
const body = req.headers.get("content-length") === "0" || !req.body ? {} : await readJson(req);
|
|
12043
|
+
if (body === null) return json({ error: "invalid JSON body" }, 400);
|
|
12044
|
+
const continueAs = typeof body.continueAs === "string" ? body.continueAs : void 0;
|
|
12045
|
+
if (continueAs === void 0) return refundApplication(db, env, ctx, app);
|
|
12046
|
+
const tier = await resolveFreeTier(db, ctx, app, continueAs);
|
|
12047
|
+
if (tier instanceof Response) return tier;
|
|
12048
|
+
let receipt = {};
|
|
12049
|
+
if (app.stripeSubscriptionId || app.namedSeatId) {
|
|
12050
|
+
const refunded = await refundApplication(db, env, ctx, app);
|
|
12051
|
+
if (!refunded.ok) return refunded;
|
|
12052
|
+
receipt = await refunded.json();
|
|
12053
|
+
}
|
|
12054
|
+
const continued = await continueApplicationOnFreeTier(db, env, ctx, app, tier, url.origin);
|
|
12055
|
+
if (!continued.ok) return continued;
|
|
12056
|
+
return json({ ...receipt, ...await continued.json() });
|
|
11668
12057
|
};
|
|
11669
12058
|
var handleAdminApplicationPatch = async (req, url, env, ctx) => {
|
|
11670
12059
|
const m = url.pathname.match(/^\/api\/admin\/applications\/([0-9a-f-]+)$/);
|
|
@@ -12319,6 +12708,7 @@ async function networkApplicationAction(opts) {
|
|
|
12319
12708
|
const approval = await approveApplication({ db, env, ctx, app, origin: opts.origin, reason: "manual" });
|
|
12320
12709
|
result = json(approval, approval.status);
|
|
12321
12710
|
} else if (input.stage === "refunded") result = await refundApplication(db, env, ctx, app);
|
|
12711
|
+
else if (input.stage === "declined" && ctx.membershipAuthority) result = await declineApplication(db, env, ctx, app);
|
|
12322
12712
|
else result = app.status === input.stage ? json({ ok: true }) : await patchApplication(db, env, ctx, app, { status: input.stage });
|
|
12323
12713
|
if (!result.ok) {
|
|
12324
12714
|
const latest = (await db.query({ applications: { $: { where: { id: appId }, limit: 1 } } })).applications?.[0];
|
|
@@ -16030,10 +16420,14 @@ async function readMembershipChapterEffect(db, input, now) {
|
|
|
16030
16420
|
requireMembershipScope(seat, input);
|
|
16031
16421
|
const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
|
|
16032
16422
|
requireMembershipScope(member, input);
|
|
16033
|
-
if (!effect || effect.kind !== input.kind || effect.seatId !== seat.id || effect.membershipId !== member.id || ["chapterId", "environment", "runtime"].some((key3) => effect[key3] !== seat[key3]) || !
|
|
16423
|
+
if (!effect || effect.kind !== input.kind || effect.seatId !== seat.id || effect.membershipId !== member.id || ["chapterId", "environment", "runtime"].some((key3) => effect[key3] !== seat[key3]) || !MEMBERSHIP_CHAPTER_EFFECT_KINDS.includes(input.kind)) throw new Error("invalid chapter effect");
|
|
16424
|
+
const released = input.kind === "release_named_recipient";
|
|
16425
|
+
const recipientUserId = released ? effect.recipientUserId : seat.recipientUserId;
|
|
16426
|
+
const recipientApplicationId = released ? effect.recipientApplicationId : seat.recipientApplicationId;
|
|
16034
16427
|
let applicable;
|
|
16035
|
-
if (input.kind === "invite_named_recipient") applicable = seat.status === "pending_acceptance" && seat.acceptedAt === void 0 && now < Number(member.termEndsAt) &&
|
|
16428
|
+
if (input.kind === "invite_named_recipient") applicable = seat.status === "pending_acceptance" && seat.acceptedAt === void 0 && now < Number(member.termEndsAt) && namedSeatInvitationOpen(member);
|
|
16036
16429
|
else if (input.kind === "revoke_named_recipient_access") applicable = ["refund_pending", "refunded"].includes(String(seat.status));
|
|
16430
|
+
else if (released) applicable = typeof recipientApplicationId === "string" && seat.recipientApplicationId !== recipientApplicationId;
|
|
16037
16431
|
else applicable = (await readNamedSeatAccess(db, {
|
|
16038
16432
|
...input,
|
|
16039
16433
|
recipientUserId: String(seat.recipientUserId),
|
|
@@ -16043,8 +16437,8 @@ async function readMembershipChapterEffect(db, input, now) {
|
|
|
16043
16437
|
...input,
|
|
16044
16438
|
recipientName: String(seat.recipientName),
|
|
16045
16439
|
recipientEmail: String(seat.recipientEmailNormalized),
|
|
16046
|
-
recipientUserId: typeof
|
|
16047
|
-
recipientApplicationId: typeof
|
|
16440
|
+
recipientUserId: typeof recipientUserId === "string" ? recipientUserId : null,
|
|
16441
|
+
recipientApplicationId: typeof recipientApplicationId === "string" ? recipientApplicationId : null,
|
|
16048
16442
|
acceptanceEndsAt: Number(seat.acceptanceEndsAt),
|
|
16049
16443
|
termEndsAt: Number(seat.termEndsAt),
|
|
16050
16444
|
applicable
|
|
@@ -16065,7 +16459,8 @@ var MEMBERSHIP_SEAT_ACTION_FIELDS = {
|
|
|
16065
16459
|
"seat-accept": recipient,
|
|
16066
16460
|
"seat-approve": recipient,
|
|
16067
16461
|
"seat-decline": recipient,
|
|
16068
|
-
"seat-effect": ["seatId", "effectId", "kind"]
|
|
16462
|
+
"seat-effect": ["seatId", "effectId", "kind"],
|
|
16463
|
+
"seat-regift": ["seatId", "recipientName", "recipientEmail"]
|
|
16069
16464
|
};
|
|
16070
16465
|
async function handleMembershipSeatAction(action, db, peer, input, now, stripeSecretName) {
|
|
16071
16466
|
if (!peer.seatPriceId) throw new Error("named seats are not configured");
|
|
@@ -16090,6 +16485,12 @@ async function handleMembershipSeatAction(action, db, peer, input, now, stripeSe
|
|
|
16090
16485
|
kind: String(input.kind)
|
|
16091
16486
|
}, now);
|
|
16092
16487
|
if (action === "seat-offer") return readNamedSeatOffer(db, target, now);
|
|
16488
|
+
if (action === "seat-regift") return regiftNamedSeat(db, {
|
|
16489
|
+
...target,
|
|
16490
|
+
seatId: String(input.seatId),
|
|
16491
|
+
recipientName: String(input.recipientName),
|
|
16492
|
+
recipientEmail: String(input.recipientEmail)
|
|
16493
|
+
}, now);
|
|
16093
16494
|
if (action === "seat-reservations") {
|
|
16094
16495
|
const seat = await reserveNamedMembershipSeat(db, {
|
|
16095
16496
|
...target,
|
|
@@ -16471,7 +16872,9 @@ var membershipAuthorityEntities = {
|
|
|
16471
16872
|
...times(),
|
|
16472
16873
|
refundReason: attr("string", true),
|
|
16473
16874
|
refundedCents: attr("number", true),
|
|
16474
|
-
providerRefundIds: attr("json", true)
|
|
16875
|
+
providerRefundIds: attr("json", true),
|
|
16876
|
+
regiftedAt: attr("number", true),
|
|
16877
|
+
formerRecipients: attr("json", true)
|
|
16475
16878
|
} },
|
|
16476
16879
|
networkSeatRecipient: { attrs: { id: id5(), appId: key(), chapterId: key(), environment: key(), runtime: key(), seatId: key(), recipientEmailNormalized: key(), createdAt: attr("number") } },
|
|
16477
16880
|
membershipEffect: { attrs: {
|
|
@@ -16486,6 +16889,8 @@ var membershipAuthorityEntities = {
|
|
|
16486
16889
|
attempts: attr("number"),
|
|
16487
16890
|
providerObjectId: key(true),
|
|
16488
16891
|
error: attr("string", true),
|
|
16892
|
+
recipientUserId: key(true),
|
|
16893
|
+
recipientApplicationId: key(true),
|
|
16489
16894
|
createdAt: attr("number"),
|
|
16490
16895
|
updatedAt: attr("number", false, false, true)
|
|
16491
16896
|
} }
|
|
@@ -16662,7 +17067,7 @@ async function drainMembershipEffects(db, options, now) {
|
|
|
16662
17067
|
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100 || !options.stripeSecretKey) throw new TypeError("invalid membership recovery configuration");
|
|
16663
17068
|
await loadMembershipAuthorityState(db, scope2);
|
|
16664
17069
|
const matches2 = (row2) => Object.entries(scope2).every(([key3, value]) => row2[key3] === value);
|
|
16665
|
-
let pending = 0, complete = 0, more = false;
|
|
17070
|
+
let pending = 0, complete = 0, held = 0, more = false;
|
|
16666
17071
|
for (const status2 of ["payment_pending", "payment_processing", "payment_canceling"]) {
|
|
16667
17072
|
const seats = (await db.query({ networkSeat: { $: { where: { ...scope2, status: status2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkSeat ?? [];
|
|
16668
17073
|
more ||= seats.length > limit;
|
|
@@ -16698,6 +17103,10 @@ async function drainMembershipEffects(db, options, now) {
|
|
|
16698
17103
|
requireMembershipScope(seat, target);
|
|
16699
17104
|
if (seat.primaryMembershipId !== member.id) throw new Error("membership seat scope mismatch");
|
|
16700
17105
|
}
|
|
17106
|
+
if (effect.kind === "invite_named_recipient" && seat?.acceptedAt === void 0 && !namedSeatInvitationOpen(member) && !["refund_pending", "refunded"].includes(String(seat?.status))) {
|
|
17107
|
+
held++;
|
|
17108
|
+
continue;
|
|
17109
|
+
}
|
|
16701
17110
|
try {
|
|
16702
17111
|
const attempts = Number(effect.attempts);
|
|
16703
17112
|
if (!Number.isSafeInteger(attempts) || attempts < 0) throw new Error("invalid membership effect attempt");
|
|
@@ -16721,9 +17130,10 @@ async function drainMembershipEffects(db, options, now) {
|
|
|
16721
17130
|
break;
|
|
16722
17131
|
case "invite_named_recipient":
|
|
16723
17132
|
case "grant_named_recipient_access":
|
|
16724
|
-
case "revoke_named_recipient_access":
|
|
17133
|
+
case "revoke_named_recipient_access":
|
|
17134
|
+
case "release_named_recipient": {
|
|
16725
17135
|
if (!seat) throw new Error("named seat effect missing seat");
|
|
16726
|
-
if (effect.kind
|
|
17136
|
+
if (["invite_named_recipient", "grant_named_recipient_access"].includes(String(effect.kind)) && ["refund_pending", "refunded"].includes(String(seat.status))) break;
|
|
16727
17137
|
if (!options.deliverChapterEffect) throw new Error("chapter effect receiver unavailable");
|
|
16728
17138
|
const receipt = await options.deliverChapterEffect({ ...target, effectId: String(effect.id), seatId: String(effect.seatId), kind: effect.kind });
|
|
16729
17139
|
if (receipt.effectId !== effect.id || receipt.complete !== true) throw new Error("chapter effect receipt unconfirmed");
|
|
@@ -16741,7 +17151,7 @@ async function drainMembershipEffects(db, options, now) {
|
|
|
16741
17151
|
pending++;
|
|
16742
17152
|
}
|
|
16743
17153
|
}
|
|
16744
|
-
return { complete, pending, more };
|
|
17154
|
+
return { complete, pending, held, more };
|
|
16745
17155
|
}
|
|
16746
17156
|
|
|
16747
17157
|
// src/approval-effects-retry.ts
|
|
@@ -16921,6 +17331,14 @@ function createMembershipChapterEffectRoute(options) {
|
|
|
16921
17331
|
tierId: effect.skuId
|
|
16922
17332
|
});
|
|
16923
17333
|
if (!result.sent) throw new Error("invitation delivery unconfirmed");
|
|
17334
|
+
} else if (effect.kind === "release_named_recipient") {
|
|
17335
|
+
stage = "release_recipient";
|
|
17336
|
+
if (!effect.recipientApplicationId || !effect.recipientUserId) throw new Error("recipient not bound");
|
|
17337
|
+
await releaseNamedSeatRecipientApplications(db, env, ctx, {
|
|
17338
|
+
seatId: effect.seatId,
|
|
17339
|
+
recipientApplicationId: effect.recipientApplicationId,
|
|
17340
|
+
recipientUserId: effect.recipientUserId
|
|
17341
|
+
});
|
|
16924
17342
|
} else {
|
|
16925
17343
|
stage = "recipient_load";
|
|
16926
17344
|
if (!effect.recipientApplicationId || !effect.recipientUserId) throw new Error("recipient not bound");
|
|
@@ -17016,6 +17434,18 @@ async function requestNamedSeatOperatorRefund(options) {
|
|
|
17016
17434
|
if (!seat || seat.seatId !== options.seatId || seat.status !== "refunded" || seat.subscriptionCanceled !== false || !Number.isSafeInteger(seat.refundedCents) || seat.refundedCents < 0) throw new Error("invalid named seat refund receipt");
|
|
17017
17435
|
return seat;
|
|
17018
17436
|
}
|
|
17437
|
+
async function requestNamedSeatRegift(options) {
|
|
17438
|
+
const result = await requestAuthority(options, "seat-regift", {
|
|
17439
|
+
applicationId: options.applicationId,
|
|
17440
|
+
skuId: options.skuId,
|
|
17441
|
+
seatId: options.seatId,
|
|
17442
|
+
recipientName: options.recipientName,
|
|
17443
|
+
recipientEmail: options.recipientEmail
|
|
17444
|
+
});
|
|
17445
|
+
const seat = result.seat;
|
|
17446
|
+
if (!seat || seat.seatId !== options.seatId || seat.status !== "pending_acceptance" || typeof seat.recipientName !== "string" || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(seat.recipientEmail)) || !Number.isSafeInteger(seat.regiftedAt) || seat.releasedRecipientApplicationId !== null && typeof seat.releasedRecipientApplicationId !== "string") throw new Error("invalid named seat regift receipt");
|
|
17447
|
+
return seat;
|
|
17448
|
+
}
|
|
17019
17449
|
async function requestNamedSeatReservation(options) {
|
|
17020
17450
|
const result = await requestAuthority(options, "seat-reservations", {
|
|
17021
17451
|
applicationId: options.applicationId,
|
|
@@ -17222,7 +17652,8 @@ function createMembershipWorkerAdapter(options) {
|
|
|
17222
17652
|
accept: async (input, env) => requestNamedSeatAcceptance({ ...input, ...await transport(input, env) }),
|
|
17223
17653
|
approve: async (input, env) => requestNamedSeatApproval({ ...input, ...await transport(input, env) }),
|
|
17224
17654
|
decline: async (input, env) => requestNamedSeatDecline({ ...input, ...await transport(input, env) }),
|
|
17225
|
-
operatorRefund: async (input, env) => requestNamedSeatOperatorRefund({ ...input, ...await transport(input, env) })
|
|
17655
|
+
operatorRefund: async (input, env) => requestNamedSeatOperatorRefund({ ...input, ...await transport(input, env) }),
|
|
17656
|
+
regift: async (input, env) => requestNamedSeatRegift({ ...input, ...await transport(input, env) })
|
|
17226
17657
|
} } : {}
|
|
17227
17658
|
},
|
|
17228
17659
|
readEffect: async (input, env) => requestMembershipChapterEffect({ ...input, ...await transport(input, env) })
|
|
@@ -17470,6 +17901,7 @@ var BUILTIN_ROUTES = [
|
|
|
17470
17901
|
handleMemberContent,
|
|
17471
17902
|
handleGifts,
|
|
17472
17903
|
handleNamedSeat,
|
|
17904
|
+
handleNamedSeatRegift,
|
|
17473
17905
|
handleNamedSeatRecipient,
|
|
17474
17906
|
handleEmailDeliveryResolution,
|
|
17475
17907
|
handleSchedule,
|
|
@@ -17500,6 +17932,7 @@ var BUILTIN_ROUTES = [
|
|
|
17500
17932
|
handleAdminMeetingReschedule,
|
|
17501
17933
|
handleAdminMeetingCancel,
|
|
17502
17934
|
handleAdminApprove,
|
|
17935
|
+
handleAdminDecline,
|
|
17503
17936
|
handleAdminRefund,
|
|
17504
17937
|
handleAdminApplicationPatch,
|
|
17505
17938
|
handleAdminApplicationActions,
|