@odla-ai/chapter 0.56.1 → 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.
@@ -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/worker-routes-admin-named-seat.ts
4089
- var PATH2 = /^\/api\/admin\/applications\/([0-9a-f-]+)\/named-seat(\/refund)?$/;
4090
- var REFUNDABLE = /* @__PURE__ */ new Set(["renewal_pending", "pending_acceptance", "pending_approval", "pending_primary", "active"]);
4091
- var handleAdminNamedSeat = async (req, url, env, ctx) => {
4092
- const match = url.pathname.match(PATH2);
4093
- if (!match) return null;
4094
- const seats = ctx.membershipAuthority?.namedSeats;
4095
- if (ctx.chapter.mode !== "chapter" || !seats) return json({ error: "named seats are not configured" }, 404);
4096
- const refund = Boolean(match[2]);
4097
- if (req.method !== (refund ? "POST" : "GET")) return json({ error: "method not allowed" }, 405);
4098
- const raw2 = ctx.makeDb(env), user = await ctx.verifyUser(req, env);
4099
- if (!user) return json({ error: "unauthorized" }, 401);
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
- const target = await membershipTargetForIntent(intent, app, env, ctx);
4107
- const offer = await seats.offer(target, env);
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 json({ error: "named seat service unavailable; retry to confirm" }, 503);
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
- // src/membership-seat-refund-stripe.ts
4144
- var import_stripe7 = require("@odla-ai/stripe");
4145
-
4146
- // src/membership-checkout-seat-refund.ts
4147
- var import_stripe6 = require("@odla-ai/stripe");
4148
- var fail4 = (code = "named_seat_refund_mismatch") => {
4149
- throw Object.assign(new Error(code), { code });
4150
- };
4151
- var id2 = (value) => typeof value === "string" ? value : value?.id ?? "";
4152
- async function refundCheckoutNamedSeat(secretKey, seat) {
4153
- const invoiceRef = seat.combinedInvoice;
4154
- 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();
4155
- const call = async (method, path, params, key3) => {
4156
- const result = await (0, import_stripe6.stripeRequest)({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
4157
- if (!result.ok || !result.body) return fail4("named_seat_refund_provider_unavailable");
4158
- return result.body;
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 metadata = { ...stripePartitionMetadata(seat), applicationId: seat.applicationId };
4161
- const sub = await call("GET", `/v1/subscriptions/${encodeURIComponent(seat.providerSubscriptionId)}`);
4162
- 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();
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 (charge.amount_refunded === charge.amount && charge.refunded === true) return { refundedCents: seat.amountCents, refundIds: [] };
4191
- const list = await call("GET", "/v1/refunds", { charge: chargeId, limit: 100 });
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 (refundedCents !== charge.amount_refunded || refundedCents > seat.amountCents) fail4();
4205
- if (refundedCents < seat.amountCents) {
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
- return { refundedCents, refundIds };
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 list = await call("GET", "/v1/refunds", { payment_intent: seat.providerPaymentIntentId, limit: 100 });
4247
- if (!Array.isArray(list.data) || list.has_more !== false || list.data.length >= 100) fail5("named_seat_refund_history_incomplete");
4248
- let refundedCents = 0, pendingCents = 0;
4249
- const refundIds = [];
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 (refundedCents + pendingCents > seat.amountCents) fail5();
4258
- if (pendingCents) fail5("named_seat_refund_pending");
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
- return { refundedCents, refundIds };
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
- // src/membership-seat-reservation.ts
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
- async function reserveNamedMembershipSeat(db, input, now) {
4281
- membershipClock(now);
4282
- const name = typeof input.recipientName === "string" ? input.recipientName.trim().replace(/\s+/g, " ") : "";
4283
- const email = normalizeChapterEmail(typeof input.recipientEmail === "string" ? input.recipientEmail : "");
4284
- const purchaser = normalizeChapterEmail(typeof input.purchaserEmail === "string" ? input.purchaserEmail : "");
4285
- 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");
4286
- const state = await loadMembershipAuthorityState(db, input);
4287
- const memberId = await networkMembershipId(input), member = await membershipLoad(db, "networkMembership", memberId);
4288
- requireMembershipScope(member, input);
4289
- if (!Number.isSafeInteger(member.revision) || !/^price_[A-Za-z0-9_]+$/.test(String(member.providerPriceId)) || member.providerPriceId === input.priceId) membershipFail("named_seat_purchase_unavailable");
4290
- const id7 = (await membershipOfferIdForRequest({ ...input, requestId: "named_seat" })).replace(/^offer_/, "seat_");
4291
- let seat = await membershipLoad(db, "networkSeat", id7);
4292
- if (!seat) {
4293
- const offering = membershipOfferingVersion(member.offeringSnapshot), claimId = await recipientKey(input, email);
4294
- const purchaseEndsAt = seatPurchaseDeadline(offering, member);
4295
- 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) {
4296
- membershipFail("named_seat_purchase_unavailable");
4297
- }
4298
- const attrs = {
4299
- id: id7,
4300
- ...membershipScope(input),
4301
- primaryMembershipId: memberId,
4302
- recipientName: name,
4303
- recipientEmailNormalized: email,
4304
- purchaserUserId: input.purchaserUserId,
4305
- recipientClaimId: claimId,
4306
- priceId: input.priceId,
4307
- primaryPriceId: member.providerPriceId,
4308
- amountCents: offering.seatPriceCents,
4309
- currency: offering.currency,
4310
- interval: offering.interval,
4311
- providerCustomerId: member.providerCustomerId,
4312
- providerSubscriptionId: member.providerSubscriptionId,
4313
- status: "payment_pending",
4314
- preparedAt: now,
4315
- termEndsAt: member.termEndsAt,
4316
- seatPurchaseEndsAt: purchaseEndsAt,
4317
- offeringSnapshot: offering,
4318
- revision: 1,
4319
- createdAt: now,
4320
- updatedAt: now
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/membership-seat-included.ts
4354
- var namedSeatIncluded = (seat) => Number(seat.amountCents) === 0;
4355
- var namedSeatSettled = (seat) => namedSeatIncluded(seat) ? seat.purchasedAt !== void 0 : Boolean((seat.providerPaymentIntentId || seat.combinedInvoice) && seat.providerSubscriptionItemId);
4356
- function namedSeatRecipientState(seat) {
4357
- switch (String(seat.status)) {
4358
- case "renewal_pending":
4359
- case "pending_acceptance":
4360
- return "invited";
4361
- case "pending_approval":
4362
- case "pending_primary":
4363
- return "accepted";
4364
- case "active":
4365
- return "joined";
4366
- case "refund_pending":
4367
- case "refunded":
4368
- return seat.refundReason === "declined" ? "declined" : "refunded";
4369
- case "canceled":
4370
- return "canceled";
4371
- default:
4372
- return "payment_pending";
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
- var OPEN = ["pending_acceptance", "pending_approval", "pending_primary", "active"];
4376
- async function grantIncludedNamedSeat(db, input, now) {
4377
- membershipClock(now);
4378
- const seat = await reserveNamedMembershipSeat(db, input, now);
4379
- if (!namedSeatIncluded(seat)) membershipFail("named_seat_not_included");
4380
- const state = await loadMembershipAuthorityState(db, input);
4381
- const member = await membershipLoad(db, "networkMembership", seat.primaryMembershipId);
4382
- requireMembershipScope(member, input);
4383
- 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)) {
4384
- membershipFail("named_seat_purchase_unavailable");
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
- if (seat.status === "payment_pending") {
4387
- const entitlementId = `${seat.id}:entitlement`, inviteId = `${seat.id}:invite`, source = `included:${seat.id}`;
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
- membershipUpdate("networkSeat", seat.id, {
4391
- status: "pending_acceptance",
4392
- purchasedAt: now,
4393
- termStartedAt: now,
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: { status: "payment_pending", revision: seat.revision, amountCents: 0 },
4434
- absent: ["purchasedAt", "providerPaymentIntentId"]
4435
- },
4436
- { ns: "networkEntitlement", id: entitlementId, exists: false },
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 current = await membershipLoad(db, "networkSeat", seat.id);
4445
- requireMembershipScope(current, input);
4446
- if (!namedSeatIncluded(current) || current.purchasedAt === void 0 || !OPEN.includes(String(current.status))) membershipFail("named_seat_purchase_unavailable");
4447
- return { seatId: seat.id, amountCents: 0, currency: seat.currency, termEndsAt: Number(current.termEndsAt), status: String(current.status), included: true };
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/membership-seat-refund.ts
4451
- async function requestNamedSeatRefund(db, input, now) {
4452
- membershipClock(now);
4453
- const state = await loadMembershipAuthorityState(db, input);
4454
- const seat = await membershipLoad(db, "networkSeat", input.seatId);
4455
- requireMembershipScope(seat, input);
4456
- const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
4457
- requireMembershipScope(member, input);
4458
- if (seat.status === "refunded" || seat.status === "refund_pending") return;
4459
- 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");
4460
- const entitlement = seat.entitlementId ? await membershipLoad(db, "networkEntitlement", String(seat.entitlementId)) : void 0;
4461
- await db.transact([
4462
- membershipUpdate("networkSeat", input.seatId, { status: "refund_pending", refundReason: input.reason, revision: Number(seat.revision) + 1, updatedAt: now }),
4463
- ...entitlement ? [membershipUpdate("networkEntitlement", String(entitlement.id), { status: "closed", revision: Number(entitlement.revision) + 1, updatedAt: now })] : [],
4464
- membershipUpdate("membershipEffect", `${seat.id}:refund`, {
4465
- id: `${seat.id}:refund`,
4466
- chapterId: input.chapterId,
4467
- environment: input.environment,
4468
- runtime: input.runtime,
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
- return { seatId: String(seat.id), status: "refunded", refundedCents: Number(refunded.refundedCents), subscriptionCanceled: false };
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/membership-seat-window.ts
4533
- async function liftSeatPurchaseWindow(db, scope2, now, limit = 100) {
4534
- membershipClock(now);
4535
- if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200) throw new TypeError("invalid lift limit");
4536
- const state = await loadMembershipAuthorityState(db, scope2);
4537
- const rows5 = (await db.query({ networkMembership: { $: { where: { ...scope2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkMembership ?? [];
4538
- let lifted = 0;
4539
- for (const member of rows5.slice(0, limit)) {
4540
- if (Object.entries(scope2).some(([key3, value]) => member[key3] !== value)) throw new Error("membership lift scope mismatch");
4541
- const offering = membershipOfferingVersion(member.offeringSnapshot);
4542
- if (offering.seatPurchaseWindowCalendarMonths === void 0) continue;
4543
- const { seatPurchaseWindowCalendarMonths: _window, ...windowless } = offering;
4544
- await db.transact([membershipUpdate("networkMembership", String(member.id), {
4545
- offeringSnapshot: windowless,
4546
- seatPurchaseEndsAt: Number(member.termEndsAt),
4547
- revision: Number(member.revision) + 1,
4548
- updatedAt: now
4549
- })], { mutationId: `seat-window-lift:${member.id}:${member.revision}`, guards: [
4550
- { ns: "networkMembership", id: String(member.id), exists: true, equals: { revision: member.revision } },
4551
- { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative" } }
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
- lifted++;
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
- return { lifted, more: rows5.length > limit };
4556
- }
4557
-
4558
- // src/membership-seat-reprice.ts
4559
- async function lowerSeatPrice(db, scope2, now, limit = 100) {
4560
- membershipClock(now);
4561
- if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200) throw new TypeError("invalid reprice limit");
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
- return receipt;
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-admin-membership-seats.ts
4605
- var LIST = "/api/admin/membership-seats";
4606
- var LIFT = "/api/admin/membership-seats/lift-purchase-window";
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
- db = operator.makeDb(env);
4375
+ body = await namedSeatRequest(req);
4645
4376
  } catch {
4646
- return json({ error: "the membership authority is not hosted by this deployment" }, 404);
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 (!one) {
4669
- const rows5 = (await db.query({ networkSeat: { $: { where: { environment: env.ODLA_ENV }, order: { updatedAt: "desc" }, limit: 201 } } })).networkSeat ?? [];
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 seat = await membershipLoad(db, "networkSeat", one[1]);
4673
- if (!seat || seat.environment !== env.ODLA_ENV) return json({ error: "not found" }, 404);
4674
- if (seat.status === "refunded") {
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
- if (!seat.purchasedAt) return json({ error: "seat has no completed payment to refund", code: "seat_not_refundable" }, 409);
4678
- const secret = await getVaultSecret(db, operator.stripeSecretName ?? "stripe_secret_key");
4679
- if (!secret) return json({ error: "network payment provider unavailable" }, 503);
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 receipt = await refundNamedSeatByOperator(db, {
4682
- appId: String(seat.appId),
4683
- chapterId: String(seat.chapterId),
4684
- environment: String(seat.environment),
4685
- runtime: String(seat.runtime),
4686
- applicationId: String(seat.applicationId),
4687
- skuId: String(seat.skuId),
4688
- seatId: String(seat.id)
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/membership-policy-versions.ts
4698
- var LIMIT = 200;
4699
- var text2 = (value) => typeof value === "string" && value.length > 0 && value.length <= 200 ? value : void 0;
4700
- var instant = (value) => {
4701
- const at2 = typeof value === "number" ? value : typeof value === "string" ? Date.parse(value) : NaN;
4702
- return Number.isSafeInteger(at2) && at2 >= 0 ? at2 : void 0;
4703
- };
4704
- function provenance(row2) {
4705
- const source = text2(row2.source), actor = text2(row2.actor ?? row2.$createdBy), recordedAt = instant(row2.recordedAt ?? row2.createdAt ?? row2.$createdAt);
4706
- return { ...source ? { source } : {}, ...actor ? { actor } : {}, ...recordedAt !== void 0 ? { recordedAt } : {} };
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 chapterId = url.searchParams.get("chapterId");
4788
- if (chapterId !== null && !SCOPE2.test(chapterId)) return json({ error: "chapterId is invalid" }, 400);
4789
- let db;
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
- db = operator.makeDb(env);
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: "the membership authority is not hosted by this deployment" }, 404);
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
- // src/membership-discount-reterm.ts
4799
- var import_stripe9 = require("@odla-ai/stripe");
4800
-
4801
- // src/subscription-provider-contract.ts
4802
- var import_stripe8 = require("@odla-ai/stripe");
4803
- function subscriptionContractMismatch() {
4804
- throw Object.assign(new Error("Stripe subscription does not match the checkout contract"), { code: "stripe_contract_mismatch" });
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-discount-reterm.ts
4825
- var SAFE = /^[A-Za-z0-9][A-Za-z0-9_.:-]{2,159}$/;
4826
- var RETERMABLE = /* @__PURE__ */ new Set(["active", "paid_pending_approval", "cancel_at_term_end", "payment_grace"]);
4827
- var PROVIDER_LIVE = /* @__PURE__ */ new Set(["active", "trialing", "past_due"]);
4828
- var couponOf = (discount) => {
4829
- const ref = discount?.source?.coupon ?? discount?.coupon;
4830
- return typeof ref === "string" ? ref : typeof ref?.id === "string" ? ref.id : void 0;
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
- async function retermPolicy(db, scope2, id7) {
4833
- const row2 = await membershipLoad(db, "discountPolicyVersion", id7);
4834
- if (!row2) membershipFail("membership_reterm_policy_unknown");
4835
- let policy2;
4836
- try {
4837
- policy2 = membershipDiscountPolicyVersion(row2.snapshot);
4838
- } catch {
4839
- return membershipFail("membership_reterm_policy_ineligible");
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 (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");
4842
- return policy2;
4843
- }
4844
- async function attachProviderDiscount(sk, scope2, member, policy2, couponId, productId) {
4845
- const subId = String(member.providerSubscriptionId), path = `/v1/subscriptions/${encodeURIComponent(subId)}`;
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
- const discounts = sub.discounts ?? (sub.discount ? [sub.discount] : []);
4853
- if (!Array.isArray(discounts)) throw Object.assign(new Error("provider discounts unreadable"), { code: "provider_mismatch" });
4854
- const present = discounts.map(couponOf);
4855
- if (present.length === 1 && present[0] === couponId) return false;
4856
- if (present.length > 0) throw Object.assign(new Error("provider subscription carries an unrelated discount"), { code: "provider_mismatch" });
4857
- const written = await (0, import_stripe9.stripeRequest)({ secretKey: sk }, "POST", path, {
4858
- "discounts[0][coupon]": couponId,
4859
- "expand[0]": "discounts",
4860
- "metadata[discountPolicyVersionId]": policy2.id
4861
- }, { idempotencyKey: `membership-reterm:${String(member.id)}:${policy2.id}` });
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 true;
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 retermMembershipDiscount(db, stripeSecretKey, scope2, input, now) {
4838
+ async function completeNamedSeatRefund(db, seatId, secretKey, now) {
4869
4839
  membershipClock(now);
4870
- const limit = input.limit ?? 100;
4871
- if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200) throw new TypeError("invalid reterm limit");
4872
- if (typeof input.policyVersionId !== "string" || !SAFE.test(input.policyVersionId)) throw new TypeError("invalid policy version id");
4873
- 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");
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 policy2 = await retermPolicy(db, scope2, input.policyVersionId);
4876
- const receipt = { policyVersionId: policy2.id, retermed: [], unchanged: [], skipped: [], more: false };
4877
- const skip = (membershipId, reason) => {
4878
- receipt.skipped.push({ membershipId, reason });
4879
- };
4880
- let rows5;
4881
- if (input.membershipIds) {
4882
- rows5 = [];
4883
- for (const id7 of input.membershipIds) {
4884
- const member = await membershipLoad(db, "networkMembership", id7);
4885
- if (!member) skip(id7, "not_found");
4886
- else if (Object.entries(scope2).some(([key3, value]) => member[key3] !== value)) skip(id7, "scope_mismatch");
4887
- else rows5.push(member);
4888
- }
4889
- } else {
4890
- const found = (await db.query({ networkMembership: { $: { where: { ...scope2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkMembership ?? [];
4891
- if (found.some((member) => Object.entries(scope2).some(([key3, value]) => member[key3] !== value))) throw new Error("membership reterm scope mismatch");
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
- for (const member of rows5) {
4896
- const id7 = String(member.id);
4897
- if (member.discountPolicyVersionId === policy2.id) {
4898
- receipt.unchanged.push(id7);
4899
- continue;
4900
- }
4901
- if (!RETERMABLE.has(String(member.status))) {
4902
- skip(id7, "status");
4903
- continue;
4904
- }
4905
- if (member.founding !== true) {
4906
- skip(id7, "not_founding");
4907
- continue;
4908
- }
4909
- if (member.cohort !== policy2.cohortLabel) {
4910
- skip(id7, "cohort_mismatch");
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
- const skuId = String(member.baseSkuId);
4914
- if (!policy2.discountEligibleSkuIds.includes(skuId)) {
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 base = Number(member.baseAmountCents), current = Number(member.renewalAmountCents);
4919
- const renewal = Math.round(base * (1e4 - policy2.percentOffBps) / 1e4);
4920
- if (!Number.isSafeInteger(base) || base <= 0 || !Number.isSafeInteger(current) || renewal > current) {
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 couponId = await membershipDiscountCouponId({ ...scope2, skuId }, policy2.id);
4925
- let providerUpdated;
4926
- try {
4927
- const productId = await verifySubscriptionPrice(stripeSecretKey, {
4928
- priceId: String(member.providerPriceId),
4929
- priceCents: base,
4930
- currency: String(member.currency),
4931
- interval: member.interval === "month" ? "month" : "year",
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
- receipt.retermed.push({
4974
- membershipId: id7,
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-reterm.ts
4985
- var PATH4 = "/api/admin/membership-discount/reterm";
4986
- var SCOPE3 = /^[a-z0-9][a-z0-9-]{1,62}$/;
4987
- var ID = /^[A-Za-z0-9][A-Za-z0-9_.:-]{2,159}$/;
4988
- var BAD = "chapterId and policyVersionId are required; runtime, appId, membershipIds and limit are optional";
4989
- var handleAdminMembershipReterm = async (req, url, env, ctx) => {
4990
- if (url.pathname !== PATH4) return null;
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
- let body;
5004
- try {
5005
- body = await req.json();
5006
- } catch {
5007
- return json({ error: BAD }, 400);
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
- const appId = body.appId ?? body.chapterId, runtime = body.runtime ?? env.ODLA_RUNTIME;
5010
- 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)) {
5011
- return json({ error: BAD }, 400);
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 retermMembershipDiscount(
5017
- db,
5018
- secret,
5019
- { appId, chapterId: body.chapterId, environment: env.ODLA_ENV, runtime },
5020
- {
5021
- policyVersionId: body.policyVersionId,
5022
- ...body.membershipIds ? { membershipIds: body.membershipIds } : {},
5023
- ...body.limit !== void 0 ? { limit: Number(body.limit) } : {}
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: "re-term is pending; retry to confirm", ...code ? { code } : {} }, code ? 409 : 503);
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/scheduling.ts
5070
- var SCHEDULING_DEFAULTS = {
5071
- slotMinutes: 45,
5072
- days: [1, 2, 3, 4, 5],
5073
- startHour: 9,
5074
- endHour: 17,
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 isValidTimeZone(tz) {
5081
- try {
5082
- new Intl.DateTimeFormat(void 0, { timeZone: tz });
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
- function resolveScheduling(config) {
5089
- const result = validateScheduling(config);
5090
- if (result.ok) return result.value;
5091
- const detail = Object.entries(result.errors).map(([field, message]) => `${field}: ${message}`).join(" ");
5092
- throw new Error(`scheduling: ${detail}`);
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
- function validateScheduling(config) {
5095
- const d = config ?? {};
5096
- const c = {
5097
- slotMinutes: d.slotMinutes ?? SCHEDULING_DEFAULTS.slotMinutes,
5098
- days: d.days ?? SCHEDULING_DEFAULTS.days,
5099
- startHour: d.startHour ?? SCHEDULING_DEFAULTS.startHour,
5100
- endHour: d.endHour ?? SCHEDULING_DEFAULTS.endHour,
5101
- timezone: d.timezone ?? SCHEDULING_DEFAULTS.timezone,
5102
- minNoticeHours: d.minNoticeHours ?? SCHEDULING_DEFAULTS.minNoticeHours,
5103
- windowDays: d.windowDays ?? SCHEDULING_DEFAULTS.windowDays,
5104
- summaryTemplate: d.summaryTemplate ?? SCHEDULING_DEFAULTS.summaryTemplate
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
- if (typeof c.summaryTemplate !== "string") errors.summaryTemplate = "Calendar summary template must be text.";
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 meetingCreateRow(i) {
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
- id: i.meetingId,
5152
- applicationId: i.applicationId,
5153
- groupId: i.groupId,
5154
- startAt: i.startAt,
5155
- endAt: i.endAt,
5156
- timezone: i.timezone,
5157
- status: "scheduled",
5158
- googleEventId: i.googleEventId,
5159
- ...i.meetUrl ? { meetUrl: i.meetUrl } : {},
5160
- ...i.htmlLink ? { htmlLink: i.htmlLink } : {},
5161
- drift: "none",
5162
- createdAt: i.createdAt
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
- function meetingRescheduleUpdate(startAt, endAt) {
5166
- return { startAt, endAt, drift: "none" };
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 applicationBookingUpdate(currentStatus, startAt, htmlLink) {
5169
- return {
5170
- meetingAt: startAt,
5171
- ...htmlLink ? { meetingLink: htmlLink } : {},
5172
- ...currentStatus !== "call_scheduled" ? { status: "call_scheduled" } : {}
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/session.ts
5177
- function applicationSummary(app) {
5178
- return {
5179
- id: app.id,
5180
- firstName: app.firstName ?? null,
5181
- lastName: app.lastName ?? null,
5182
- email: app.email ?? null,
5183
- status: app.status,
5184
- createdAt: app.createdAt ?? null,
5185
- meetingLink: app.meetingLink ?? null,
5186
- paid: Boolean(app.stripeSubscriptionId) && app.status !== "refunded" && app.canceled !== true && !["lapsed", "refunded", "refund_pending", "canceled", "restarting"].includes(app.membershipStatus ?? ""),
5187
- ...app.membershipStatus ? { membershipStatus: app.membershipStatus, membershipGraceEndsAt: app.membershipGraceEndsAt, membershipAutoRenew: app.membershipAutoRenew } : {},
5188
- renewalAt: app.renewalAt ?? null,
5189
- canceled: app.canceled === true,
5190
- tierId: app.tierId ?? null
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 memberApplication(app, meeting, defaultTimezone) {
5194
- const summary = applicationSummary(app);
5195
- let meetingAt = app.meetingAt ?? null;
5196
- let meetUrl = null;
5197
- let timezone = defaultTimezone;
5198
- if (meeting) {
5199
- timezone = meeting.timezone ?? timezone;
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
- return { ...summary, meetingAt, meetUrl, timezone };
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
- // src/member-session-application.ts
5211
- var local = (row2, chapterId, runtime) => row2.groupId === chapterId && (runtime ? row2.stripeRuntime === runtime : row2.stripeRuntime === void 0 || row2.stripeRuntime === "default");
5212
- async function memberSessionApplication(db, chapterId, runtime, identity2) {
5213
- const lookup = async (where) => {
5214
- const rows5 = (await db.query({ applications: { $: { where: { groupId: chapterId, ...where }, limit: 100 } } })).applications;
5215
- if (!Array.isArray(rows5) || rows5.length >= 100) throw new Error("member identity lookup incomplete");
5216
- return rows5.filter((row2) => local(row2, chapterId, runtime));
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 apps = await lookup({ clerkUserId: identity2.userId });
5219
- let emailFallback = false;
5220
- if (!apps.length && identity2.email) {
5221
- const normalized = normalizeChapterEmail(identity2.email);
5222
- const normalizedRows = await lookup({ emailNormalized: normalized });
5223
- const legacyRows = await lookup({ email: identity2.email });
5224
- apps = [...new Map([...normalizedRows, ...legacyRows].map((row2) => [row2.id, row2])).values()];
5225
- if (apps.length !== 1 || apps[0]?.clerkUserId !== void 0 || apps[0]?.clerkAccountPending === true || ["declined", "canceled", "refunded"].includes(String(apps[0]?.status))) return null;
5226
- emailFallback = true;
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
- let app = apps.sort((a, b) => Number(b.createdAt ?? 0) - Number(a.createdAt ?? 0))[0];
5229
- if (!app) return null;
5230
- if (emailFallback) {
5231
- const keys3 = ["groupId", "stripeRuntime", "email", "emailNormalized", "status", "clerkAccountPending"];
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
- await db.transact([{ t: "update", ns: "applications", id: String(app.id), attrs: { clerkUserId: identity2.userId } }], {
5234
- mutationId: `member-account-link:${app.id}:${identity2.userId}`,
5235
- guards: [{
5236
- ns: "applications",
5237
- id: String(app.id),
5238
- exists: true,
5239
- equals: Object.fromEntries(keys3.filter((key3) => app[key3] !== void 0).map((key3) => [key3, app[key3]])),
5240
- absent: ["clerkUserId", ...keys3.filter((key3) => app[key3] === void 0)]
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
- const saved = (await db.query({ applications: { $: { where: { id: app.id }, limit: 1 } } })).applications?.[0];
5247
- if (!saved || !local(saved, chapterId, runtime) || saved.clerkUserId !== identity2.userId || keys3.some((key3) => saved[key3] !== app[key3])) return null;
5248
- app = saved;
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
- const meeting = (await db.query({ meetings: { $: {
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
- async function checkSignupEligibility(db, chapter, fields, submissionId) {
5557
- if (typeof fields.email !== "string" || !isValidEmail(fields.email.trim())) return null;
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: 2 } },
5713
+ applications: { $: { where: { emailNormalized: email }, limit: 10 } },
5564
5714
  $users: { $: { where: { email }, limit: 2 } }
5565
5715
  });
5566
- if ((data.applications ?? []).some((row2) => row2.id !== applicationId)) return conflict();
5567
- const original = (data.applications ?? []).find((row2) => row2.id === applicationId && row2.groupId === chapter.id);
5568
- if ((data.$users ?? []).some((row2) => row2.deleted !== true && row2.id !== original?.clerkUserId)) return conflict();
5569
- if (chapter.account === "none") return null;
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 && !(original && (original.clerkUserId === account.id || belongsToSignup(account, original.id)))) return conflict();
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 apps = (await db.query({ applications: { $: { where: { emailNormalized: normalizeChapterEmail(email) }, limit: 2 } } })).applications ?? [];
6743
- if (!apps.length) return null;
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 signupFailure = named ? null : await checkSignupEligibility(db, chapter, parsed, submissionId);
7260
- if (signupFailure) return signupFailure;
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 result = existing ?? await submitApplication(db, effectiveChapter, parsed, {
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 && ["submitted", "paid_pending_vetting", "declined", "canceled"].includes(to)) return false;
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
- return refundApplication(db, env, ctx, app);
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]) || !["invite_named_recipient", "grant_named_recipient_access", "revoke_named_recipient_access"].includes(input.kind)) throw new Error("invalid chapter effect");
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) && ["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status));
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 seat.recipientUserId === "string" ? seat.recipientUserId : null,
16047
- recipientApplicationId: typeof seat.recipientApplicationId === "string" ? seat.recipientApplicationId : null,
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 !== "revoke_named_recipient_access" && ["refund_pending", "refunded"].includes(String(seat.status))) break;
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,