@odla-ai/chapter 0.56.2 → 0.58.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.
@@ -4036,912 +4036,887 @@ var handlePaymentRecovery = async (req, url, env, ctx) => {
4036
4036
  }
4037
4037
  };
4038
4038
 
4039
- // src/worker-routes-admin-named-seat.ts
4040
- var PATH2 = /^\/api\/admin\/applications\/([0-9a-f-]+)\/named-seat(\/refund)?$/;
4041
- var REFUNDABLE = /* @__PURE__ */ new Set(["renewal_pending", "pending_acceptance", "pending_approval", "pending_primary", "active"]);
4042
- var handleAdminNamedSeat = async (req, url, env, ctx) => {
4043
- const match = url.pathname.match(PATH2);
4044
- if (!match) return null;
4045
- const seats = ctx.membershipAuthority?.namedSeats;
4046
- if (ctx.chapter.mode !== "chapter" || !seats) return json({ error: "named seats are not configured" }, 404);
4047
- const refund = Boolean(match[2]);
4048
- if (req.method !== (refund ? "POST" : "GET")) return json({ error: "method not allowed" }, 405);
4049
- const raw2 = ctx.makeDb(env), user = await ctx.verifyUser(req, env);
4050
- if (!user) return json({ error: "unauthorized" }, 401);
4051
- if (!await ctx.isSuperAdminEmail(raw2, user.email, user.userId)) return json({ error: "super-admin required", code: "super_admin_required" }, 403);
4052
- const db = raw2, app = await membershipLoad(db, "applications", match[1]);
4053
- if (!app || !chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME) || app.namedSeatId) return json({ error: "not found" }, 404);
4054
- const intent = await membershipLoad(db, "subscriptionCheckoutIntents", String(app.id));
4055
- if (!intent) return json({ error: "paid membership required" }, 409);
4039
+ // src/scheduling.ts
4040
+ var SCHEDULING_DEFAULTS = {
4041
+ slotMinutes: 45,
4042
+ days: [1, 2, 3, 4, 5],
4043
+ startHour: 9,
4044
+ endHour: 17,
4045
+ timezone: "America/Los_Angeles",
4046
+ minNoticeHours: 24,
4047
+ windowDays: 21,
4048
+ summaryTemplate: "Introduction call with {{firstName}} {{lastName}}"
4049
+ };
4050
+ function isValidTimeZone(tz) {
4056
4051
  try {
4057
- const target = await membershipTargetForIntent(intent, app, env, ctx);
4058
- const offer = await seats.offer(target, env);
4059
- const seat = offer.seat ?? null, refundable = Boolean(seat && REFUNDABLE.has(seat.status));
4060
- if (!refund) {
4061
- return json({
4062
- seat,
4063
- amountCents: offer.amountCents,
4064
- currency: offer.currency,
4065
- termEndsAt: offer.termEndsAt,
4066
- purchaseEndsAt: offer.purchaseEndsAt,
4067
- refundable
4068
- });
4069
- }
4070
- if (!seat) return json({ error: "no purchased seat", code: "seat_not_found" }, 409);
4071
- if (seat.status === "refunded") {
4072
- await closeRecipientApplication(db, env, ctx, seat.seatId);
4073
- return json({ ok: true, seatId: seat.seatId, status: "refunded", replayed: true });
4074
- }
4075
- if (!refundable) return json({ error: `seat is ${seat.status.replaceAll("_", " ")}`, code: "seat_not_refundable" }, 409);
4076
- const receipt = await seats.operatorRefund({ ...target, seatId: seat.seatId }, env);
4077
- await closeRecipientApplication(db, env, ctx, seat.seatId);
4078
- return json({ ok: true, ...receipt });
4052
+ new Intl.DateTimeFormat(void 0, { timeZone: tz });
4053
+ return true;
4079
4054
  } catch {
4080
- return json({ error: "named seat service unavailable; retry to confirm" }, 503);
4055
+ return false;
4081
4056
  }
4082
- };
4083
- async function closeRecipientApplication(db, env, ctx, seatId) {
4084
- const recipient2 = (await db.query({ applications: { $: { where: { namedSeatId: seatId }, limit: 1 } } })).applications?.[0];
4085
- if (!recipient2 || !chapterBillingApplication(recipient2, ctx.chapter.id, env.ODLA_RUNTIME) || recipient2.status === "refunded") return;
4086
- const attrs = { status: "refunded", renewalAt: 0, canceled: true };
4087
- await db.transact(
4088
- [{ t: "update", ns: "applications", id: String(recipient2.id), attrs }],
4089
- { mutationId: `verified-refund:${String(recipient2.id)}:${seatId}` }
4090
- );
4091
- await syncApplicationToCrm(crmDeps(db, ctx), { app: { ...recipient2, ...attrs }, stage: "refunded" }).catch(() => void 0);
4092
4057
  }
4093
-
4094
- // src/membership-seat-refund-stripe.ts
4095
- import { stripeRequest as stripeRequest5 } from "@odla-ai/stripe";
4096
-
4097
- // src/membership-checkout-seat-refund.ts
4098
- import { stripeRequest as stripeRequest4 } from "@odla-ai/stripe";
4099
- var fail4 = (code = "named_seat_refund_mismatch") => {
4100
- throw Object.assign(new Error(code), { code });
4101
- };
4102
- var id2 = (value) => typeof value === "string" ? value : value?.id ?? "";
4103
- async function refundCheckoutNamedSeat(secretKey, seat) {
4104
- const invoiceRef = seat.combinedInvoice;
4105
- 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();
4106
- const call = async (method, path, params, key3) => {
4107
- const result = await stripeRequest4({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
4108
- if (!result.ok || !result.body) return fail4("named_seat_refund_provider_unavailable");
4109
- return result.body;
4058
+ function resolveScheduling(config) {
4059
+ const result = validateScheduling(config);
4060
+ if (result.ok) return result.value;
4061
+ const detail = Object.entries(result.errors).map(([field, message]) => `${field}: ${message}`).join(" ");
4062
+ throw new Error(`scheduling: ${detail}`);
4063
+ }
4064
+ function validateScheduling(config) {
4065
+ const d = config ?? {};
4066
+ const c = {
4067
+ slotMinutes: d.slotMinutes ?? SCHEDULING_DEFAULTS.slotMinutes,
4068
+ days: d.days ?? SCHEDULING_DEFAULTS.days,
4069
+ startHour: d.startHour ?? SCHEDULING_DEFAULTS.startHour,
4070
+ endHour: d.endHour ?? SCHEDULING_DEFAULTS.endHour,
4071
+ timezone: d.timezone ?? SCHEDULING_DEFAULTS.timezone,
4072
+ minNoticeHours: d.minNoticeHours ?? SCHEDULING_DEFAULTS.minNoticeHours,
4073
+ windowDays: d.windowDays ?? SCHEDULING_DEFAULTS.windowDays,
4074
+ summaryTemplate: d.summaryTemplate ?? SCHEDULING_DEFAULTS.summaryTemplate
4110
4075
  };
4111
- const metadata = { ...stripePartitionMetadata(seat), applicationId: seat.applicationId };
4112
- const sub = await call("GET", `/v1/subscriptions/${encodeURIComponent(seat.providerSubscriptionId)}`);
4113
- 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();
4114
- const invoice = await call("GET", `/v1/invoices/${encodeURIComponent(invoiceRef.invoiceId)}`);
4115
- 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();
4116
- let chargeId = id2(invoice.charge), paymentId = id2(invoice.payment_intent);
4117
- if (!chargeId && !paymentId) {
4118
- const payments = await call("GET", "/v1/invoice_payments", { invoice: invoice.id, status: "paid", limit: 100 });
4119
- if (!Array.isArray(payments.data) || payments.data.length !== 1 || payments.has_more !== false) fail4();
4120
- const payment = payments.data[0];
4121
- if (id2(payment.invoice) !== invoice.id || payment.status !== "paid" || payment.payment?.type !== "payment_intent") fail4();
4122
- paymentId = id2(payment.payment.payment_intent);
4123
- }
4124
- if (!chargeId) {
4125
- if (!/^pi_[A-Za-z0-9_]+$/.test(paymentId)) fail4();
4126
- const payment = await call("GET", `/v1/payment_intents/${encodeURIComponent(paymentId)}`);
4127
- 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();
4128
- chargeId = id2(payment.latest_charge);
4129
- }
4130
- if (!/^ch_[A-Za-z0-9_]+$/.test(chargeId)) fail4();
4131
- const charge = await call("GET", `/v1/charges/${encodeURIComponent(chargeId)}`);
4132
- 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();
4133
- const items = sub.items.data.filter((item) => item.metadata?.seatId === seat.id);
4134
- if (items.length > 1) fail4();
4135
- if (items.length && sub.status !== "canceled") {
4136
- const item = items[0];
4137
- if (!/^si_[A-Za-z0-9_]+$/.test(item.id) || item.quantity !== 1 || item.price?.id !== seat.priceId) fail4();
4138
- const removed = await call("DELETE", `/v1/subscription_items/${encodeURIComponent(item.id)}`, { proration_behavior: "none" });
4139
- if (removed.id !== item.id || removed.deleted !== true) fail4("named_seat_renewal_removal_unconfirmed");
4076
+ const errors = {};
4077
+ if (!(c.slotMinutes >= 15 && c.slotMinutes <= 240)) {
4078
+ errors.slotMinutes = "Slot length must be between 15 and 240 minutes.";
4140
4079
  }
4141
- if (charge.amount_refunded === charge.amount && charge.refunded === true) return { refundedCents: seat.amountCents, refundIds: [] };
4142
- const list = await call("GET", "/v1/refunds", { charge: chargeId, limit: 100 });
4143
- if (!Array.isArray(list.data) || list.has_more !== false || list.data.length >= 100) fail4("named_seat_refund_history_incomplete");
4144
- let refundedCents = 0;
4145
- const refundIds = [];
4146
- for (const refund of list.data) {
4147
- 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();
4148
- if (["pending", "requires_action"].includes(refund.status)) fail4("named_seat_refund_pending");
4149
- if (refund.status === "succeeded") {
4150
- if (refund.metadata?.seatId !== seat.id || refund.metadata?.membershipId !== seat.primaryMembershipId) fail4("named_seat_refund_allocation_ambiguous");
4151
- refundedCents += refund.amount;
4152
- refundIds.push(refund.id);
4153
- }
4080
+ if (!(c.windowDays >= 1 && c.windowDays <= 62)) {
4081
+ errors.windowDays = "Booking window must be between 1 and 62 days (the calendar caps look-ahead at 62).";
4154
4082
  }
4155
- if (refundedCents !== charge.amount_refunded || refundedCents > seat.amountCents) fail4();
4156
- if (refundedCents < seat.amountCents) {
4157
- const remaining = seat.amountCents - refundedCents;
4158
- const refund = await call("POST", "/v1/refunds", {
4159
- charge: chargeId,
4160
- amount: remaining,
4161
- reason: "requested_by_customer",
4162
- metadata: { ...metadata, membershipId: seat.primaryMembershipId, seatId: seat.id }
4163
- }, `combined-seat-refund:${seat.id}:${remaining}`);
4164
- if (id2(refund.charge) !== chargeId || refund.amount !== remaining || refund.currency !== seat.currency || !/^re_[A-Za-z0-9_]+$/.test(refund.id)) fail4();
4165
- if (refund.status !== "succeeded") fail4("named_seat_refund_pending");
4166
- refundedCents += remaining;
4167
- refundIds.push(refund.id);
4083
+ if (!(c.minNoticeHours >= 0 && c.minNoticeHours <= 336)) {
4084
+ errors.minNoticeHours = "Minimum notice must be between 0 and 336 hours.";
4168
4085
  }
4169
- return { refundedCents, refundIds };
4170
- }
4171
-
4172
- // src/membership-seat-refund-stripe.ts
4173
- var fail5 = (code = "named_seat_refund_mismatch") => {
4174
- throw Object.assign(new Error(code), { code });
4175
- };
4176
- async function refundNamedSeatStripePayment(secretKey, seat) {
4177
- if (seat.combinedInvoice) return refundCheckoutNamedSeat(secretKey, seat);
4178
- if (!seat.providerPaymentIntentId || seat.status !== "refund_pending") fail5();
4179
- const call = async (method, path, params, idempotencyKey) => {
4180
- const result = await stripeRequest5({ secretKey }, method, path, params, idempotencyKey ? { idempotencyKey } : {});
4181
- if (!result.ok || !result.body) return fail5("named_seat_refund_provider_unavailable");
4182
- return result.body;
4183
- };
4184
- const metadata = { ...stripePartitionMetadata(seat), applicationId: seat.applicationId, membershipId: seat.primaryMembershipId, seatId: seat.id };
4185
- const payment = await call("GET", `/v1/payment_intents/${encodeURIComponent(seat.providerPaymentIntentId)}`);
4186
- 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();
4187
- const sub = await call("GET", `/v1/subscriptions/${encodeURIComponent(seat.providerSubscriptionId)}`);
4188
- 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();
4189
- const items = sub.items.data.filter((item) => item.metadata?.seatId === seat.id);
4190
- if (items.length > 1) fail5();
4191
- if (items.length && sub.status !== "canceled") {
4192
- const item = items[0];
4193
- if (!/^si_[A-Za-z0-9_]+$/.test(item.id) || item.price?.id !== seat.priceId || item.quantity !== 1) fail5();
4194
- const removed = await call("DELETE", `/v1/subscription_items/${encodeURIComponent(item.id)}`, { proration_behavior: "none" });
4195
- if (removed.id !== item.id || removed.deleted !== true) fail5("named_seat_renewal_removal_unconfirmed");
4086
+ if (!(c.startHour >= 0 && c.startHour < c.endHour && c.endHour <= 24)) {
4087
+ errors.hours = "Hours must satisfy 0 \u2264 start < end \u2264 24.";
4196
4088
  }
4197
- const list = await call("GET", "/v1/refunds", { payment_intent: seat.providerPaymentIntentId, limit: 100 });
4198
- if (!Array.isArray(list.data) || list.has_more !== false || list.data.length >= 100) fail5("named_seat_refund_history_incomplete");
4199
- let refundedCents = 0, pendingCents = 0;
4200
- const refundIds = [];
4201
- for (const refund of list.data) {
4202
- 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();
4203
- if (refund.status === "succeeded") {
4204
- refundedCents += refund.amount;
4205
- refundIds.push(refund.id);
4206
- } else if (refund.status === "pending" || refund.status === "requires_action") pendingCents += refund.amount;
4089
+ const days = [...c.days];
4090
+ if (!days.length) errors.days = "Pick at least one day.";
4091
+ else if (!days.every((n) => Number.isInteger(n) && n >= 0 && n <= 6)) {
4092
+ errors.days = "Days must be weekday numbers, 0 (Sunday) through 6 (Saturday).";
4207
4093
  }
4208
- if (refundedCents + pendingCents > seat.amountCents) fail5();
4209
- if (pendingCents) fail5("named_seat_refund_pending");
4210
- if (refundedCents < seat.amountCents) {
4211
- const remaining = seat.amountCents - refundedCents;
4212
- const refund = await call("POST", "/v1/refunds", {
4213
- payment_intent: seat.providerPaymentIntentId,
4214
- amount: remaining,
4215
- reason: "requested_by_customer",
4216
- metadata
4217
- }, `named-seat-refund:${seat.id}:${remaining}`);
4218
- if (refund.payment_intent !== seat.providerPaymentIntentId || refund.amount !== remaining || refund.currency !== seat.currency || !/^re_[A-Za-z0-9_]+$/.test(refund.id)) fail5();
4219
- if (refund.status !== "succeeded") fail5("named_seat_refund_pending");
4220
- refundedCents += remaining;
4221
- refundIds.push(refund.id);
4094
+ if (typeof c.timezone !== "string" || !isValidTimeZone(c.timezone)) {
4095
+ errors.timezone = `"${String(c.timezone)}" is not a valid IANA timezone (for example "America/Los_Angeles").`;
4222
4096
  }
4223
- return { refundedCents, refundIds };
4097
+ if (typeof c.summaryTemplate !== "string") errors.summaryTemplate = "Calendar summary template must be text.";
4098
+ return Object.keys(errors).length > 0 ? { ok: false, errors } : { ok: true, value: { ...c, days } };
4224
4099
  }
4225
-
4226
- // src/membership-seat-reservation.ts
4227
- async function recipientKey(input, email) {
4228
- const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify([input.appId, input.chapterId, input.environment, input.runtime, email])));
4229
- return `seat-recipient:${[...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
4100
+ function slotWindow(now, windowDays) {
4101
+ return { from: now, to: now + windowDays * 864e5 };
4230
4102
  }
4231
- async function reserveNamedMembershipSeat(db, input, now) {
4232
- membershipClock(now);
4233
- const name = typeof input.recipientName === "string" ? input.recipientName.trim().replace(/\s+/g, " ") : "";
4234
- const email = normalizeChapterEmail(typeof input.recipientEmail === "string" ? input.recipientEmail : "");
4235
- const purchaser = normalizeChapterEmail(typeof input.purchaserEmail === "string" ? input.purchaserEmail : "");
4236
- 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");
4237
- const state = await loadMembershipAuthorityState(db, input);
4238
- const memberId = await networkMembershipId(input), member = await membershipLoad(db, "networkMembership", memberId);
4239
- requireMembershipScope(member, input);
4240
- if (!Number.isSafeInteger(member.revision) || !/^price_[A-Za-z0-9_]+$/.test(String(member.providerPriceId)) || member.providerPriceId === input.priceId) membershipFail("named_seat_purchase_unavailable");
4241
- const id7 = (await membershipOfferIdForRequest({ ...input, requestId: "named_seat" })).replace(/^offer_/, "seat_");
4242
- let seat = await membershipLoad(db, "networkSeat", id7);
4243
- if (!seat) {
4244
- const offering = membershipOfferingVersion(member.offeringSnapshot), claimId = await recipientKey(input, email);
4245
- const purchaseEndsAt = seatPurchaseDeadline(offering, member);
4246
- 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) {
4247
- membershipFail("named_seat_purchase_unavailable");
4248
- }
4249
- const attrs = {
4250
- id: id7,
4251
- ...membershipScope(input),
4252
- primaryMembershipId: memberId,
4253
- recipientName: name,
4254
- recipientEmailNormalized: email,
4255
- purchaserUserId: input.purchaserUserId,
4256
- recipientClaimId: claimId,
4257
- priceId: input.priceId,
4258
- primaryPriceId: member.providerPriceId,
4259
- amountCents: offering.seatPriceCents,
4260
- currency: offering.currency,
4261
- interval: offering.interval,
4262
- providerCustomerId: member.providerCustomerId,
4263
- providerSubscriptionId: member.providerSubscriptionId,
4264
- status: "payment_pending",
4265
- preparedAt: now,
4266
- termEndsAt: member.termEndsAt,
4267
- seatPurchaseEndsAt: purchaseEndsAt,
4268
- offeringSnapshot: offering,
4269
- revision: 1,
4270
- createdAt: now,
4271
- updatedAt: now
4272
- };
4273
- try {
4274
- await db.transact([
4275
- membershipUpdate("networkSeat", id7, attrs),
4276
- membershipUpdate("networkSeatRecipient", claimId, {
4277
- id: claimId,
4278
- appId: input.appId,
4279
- chapterId: input.chapterId,
4280
- environment: input.environment,
4281
- runtime: input.runtime,
4282
- seatId: id7,
4283
- recipientEmailNormalized: email,
4284
- createdAt: now
4285
- }),
4286
- membershipUpdate("networkMembership", memberId, { namedSeatId: id7, revision: Number(member.revision) + 1, updatedAt: now })
4287
- ], { mutationId: `named-seat-reserve:${id7}`, guards: [
4288
- { ns: "networkSeat", id: id7, exists: false },
4289
- { ns: "networkSeatRecipient", id: claimId, exists: false },
4290
- { ns: "networkMembership", id: memberId, exists: true, equals: { revision: member.revision }, absent: ["namedSeatId"] },
4291
- { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
4292
- ] });
4293
- } catch (error) {
4294
- if (error.code !== "transact_guard_failed") throw error;
4295
- }
4296
- seat = await membershipLoad(db, "networkSeat", id7);
4297
- if (!seat && await membershipLoad(db, "networkSeatRecipient", claimId)) membershipFail("named_seat_recipient_unavailable");
4298
- }
4299
- requireMembershipScope(seat, input);
4300
- if (seat.primaryMembershipId !== memberId || seat.recipientName !== name || seat.recipientEmailNormalized !== email || seat.priceId !== input.priceId || seat.purchaserUserId !== input.purchaserUserId) membershipFail("named_seat_purchase_conflict");
4301
- return seat;
4103
+ function endForSlot(startAt, slotMinutes) {
4104
+ return startAt + slotMinutes * 6e4;
4105
+ }
4106
+ function isSlotAvailable(slots, startAt) {
4107
+ return slots.some((s) => s.startAt === startAt);
4108
+ }
4109
+ function renderSummary(template, app) {
4110
+ return template.replace("{{firstName}}", app.firstName ?? "").replace("{{lastName}}", app.lastName ?? "");
4111
+ }
4112
+ function bookingDecision(existing) {
4113
+ const eventId = existing?.googleEventId ?? null;
4114
+ return { reschedule: Boolean(eventId), eventId };
4115
+ }
4116
+ function introIdempotencyKey(applicationId) {
4117
+ return `application:${applicationId}:intro`;
4118
+ }
4119
+ function meetingCreateRow(i) {
4120
+ return {
4121
+ id: i.meetingId,
4122
+ applicationId: i.applicationId,
4123
+ groupId: i.groupId,
4124
+ startAt: i.startAt,
4125
+ endAt: i.endAt,
4126
+ timezone: i.timezone,
4127
+ status: "scheduled",
4128
+ googleEventId: i.googleEventId,
4129
+ ...i.meetUrl ? { meetUrl: i.meetUrl } : {},
4130
+ ...i.htmlLink ? { htmlLink: i.htmlLink } : {},
4131
+ drift: "none",
4132
+ createdAt: i.createdAt
4133
+ };
4134
+ }
4135
+ function meetingRescheduleUpdate(startAt, endAt) {
4136
+ return { startAt, endAt, drift: "none" };
4137
+ }
4138
+ function applicationBookingUpdate(currentStatus, startAt, htmlLink) {
4139
+ return {
4140
+ meetingAt: startAt,
4141
+ ...htmlLink ? { meetingLink: htmlLink } : {},
4142
+ ...currentStatus !== "call_scheduled" ? { status: "call_scheduled" } : {}
4143
+ };
4302
4144
  }
4303
4145
 
4304
- // src/membership-seat-included.ts
4305
- var namedSeatIncluded = (seat) => Number(seat.amountCents) === 0;
4306
- var namedSeatSettled = (seat) => namedSeatIncluded(seat) ? seat.purchasedAt !== void 0 : Boolean((seat.providerPaymentIntentId || seat.combinedInvoice) && seat.providerSubscriptionItemId);
4307
- function namedSeatRecipientState(seat) {
4308
- switch (String(seat.status)) {
4309
- case "renewal_pending":
4310
- case "pending_acceptance":
4311
- return "invited";
4312
- case "pending_approval":
4313
- case "pending_primary":
4314
- return "accepted";
4315
- case "active":
4316
- return "joined";
4317
- case "refund_pending":
4318
- case "refunded":
4319
- return seat.refundReason === "declined" ? "declined" : "refunded";
4320
- case "canceled":
4321
- return "canceled";
4322
- default:
4323
- return "payment_pending";
4146
+ // src/session.ts
4147
+ function applicationSummary(app) {
4148
+ return {
4149
+ id: app.id,
4150
+ firstName: app.firstName ?? null,
4151
+ lastName: app.lastName ?? null,
4152
+ email: app.email ?? null,
4153
+ status: app.status,
4154
+ createdAt: app.createdAt ?? null,
4155
+ meetingLink: app.meetingLink ?? null,
4156
+ paid: Boolean(app.stripeSubscriptionId) && app.status !== "refunded" && app.canceled !== true && !["lapsed", "refunded", "refund_pending", "canceled", "restarting"].includes(app.membershipStatus ?? ""),
4157
+ ...app.membershipStatus ? { membershipStatus: app.membershipStatus, membershipGraceEndsAt: app.membershipGraceEndsAt, membershipAutoRenew: app.membershipAutoRenew } : {},
4158
+ renewalAt: app.renewalAt ?? null,
4159
+ canceled: app.canceled === true,
4160
+ tierId: app.tierId ?? null
4161
+ };
4162
+ }
4163
+ function memberApplication(app, meeting, defaultTimezone) {
4164
+ const summary = applicationSummary(app);
4165
+ let meetingAt = app.meetingAt ?? null;
4166
+ let meetUrl = null;
4167
+ let timezone = defaultTimezone;
4168
+ if (meeting) {
4169
+ timezone = meeting.timezone ?? timezone;
4170
+ if (meeting.status === "scheduled") {
4171
+ meetingAt = meeting.startAt ?? null;
4172
+ meetUrl = meeting.meetUrl ?? null;
4173
+ } else {
4174
+ meetingAt = null;
4175
+ }
4324
4176
  }
4177
+ return { ...summary, meetingAt, meetUrl, timezone };
4325
4178
  }
4326
- var OPEN = ["pending_acceptance", "pending_approval", "pending_primary", "active"];
4327
- async function grantIncludedNamedSeat(db, input, now) {
4328
- membershipClock(now);
4329
- const seat = await reserveNamedMembershipSeat(db, input, now);
4330
- if (!namedSeatIncluded(seat)) membershipFail("named_seat_not_included");
4331
- const state = await loadMembershipAuthorityState(db, input);
4332
- const member = await membershipLoad(db, "networkMembership", seat.primaryMembershipId);
4333
- requireMembershipScope(member, input);
4334
- 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)) {
4335
- membershipFail("named_seat_purchase_unavailable");
4179
+
4180
+ // src/member-session-application.ts
4181
+ var local = (row2, chapterId, runtime) => row2.groupId === chapterId && (runtime ? row2.stripeRuntime === runtime : row2.stripeRuntime === void 0 || row2.stripeRuntime === "default");
4182
+ async function memberSessionApplication(db, chapterId, runtime, identity2) {
4183
+ const lookup = async (where) => {
4184
+ const rows5 = (await db.query({ applications: { $: { where: { groupId: chapterId, ...where }, limit: 100 } } })).applications;
4185
+ if (!Array.isArray(rows5) || rows5.length >= 100) throw new Error("member identity lookup incomplete");
4186
+ return rows5.filter((row2) => local(row2, chapterId, runtime));
4187
+ };
4188
+ let apps = await lookup({ clerkUserId: identity2.userId });
4189
+ let emailFallback = false;
4190
+ if (!apps.length && identity2.email) {
4191
+ const normalized = normalizeChapterEmail(identity2.email);
4192
+ const normalizedRows = await lookup({ emailNormalized: normalized });
4193
+ const legacyRows = await lookup({ email: identity2.email });
4194
+ apps = [...new Map([...normalizedRows, ...legacyRows].map((row2) => [row2.id, row2])).values()];
4195
+ if (apps.length !== 1 || apps[0]?.clerkUserId !== void 0 || apps[0]?.clerkAccountPending === true || ["declined", "canceled", "refunded"].includes(String(apps[0]?.status))) return null;
4196
+ emailFallback = true;
4336
4197
  }
4337
- if (seat.status === "payment_pending") {
4338
- const entitlementId = `${seat.id}:entitlement`, inviteId = `${seat.id}:invite`, source = `included:${seat.id}`;
4198
+ let app = apps.sort((a, b) => Number(b.createdAt ?? 0) - Number(a.createdAt ?? 0))[0];
4199
+ if (!app) return null;
4200
+ if (emailFallback) {
4201
+ const keys3 = ["groupId", "stripeRuntime", "email", "emailNormalized", "status", "clerkAccountPending"];
4339
4202
  try {
4340
- await db.transact([
4341
- membershipUpdate("networkSeat", seat.id, {
4342
- status: "pending_acceptance",
4343
- purchasedAt: now,
4344
- termStartedAt: now,
4345
- termEndsAt: member.termEndsAt,
4346
- acceptanceEndsAt: Number(member.termEndsAt),
4347
- entitlementId,
4348
- revision: Number(seat.revision) + 1,
4349
- updatedAt: now
4350
- }),
4351
- membershipUpdate("networkEntitlement", entitlementId, {
4352
- id: entitlementId,
4353
- ...membershipScope(input),
4354
- membershipId: member.id,
4355
- seatId: seat.id,
4356
- kind: "named_seat",
4357
- status: "pending_acceptance",
4358
- termStartedAt: now,
4359
- termEndsAt: member.termEndsAt,
4360
- offeringSnapshot: member.offeringSnapshot,
4361
- sourceEventId: source,
4362
- revision: 1,
4363
- createdAt: now,
4364
- updatedAt: now
4365
- }),
4366
- membershipUpdate("membershipEffect", inviteId, {
4367
- id: inviteId,
4368
- chapterId: input.chapterId,
4369
- environment: input.environment,
4370
- runtime: input.runtime,
4371
- membershipId: member.id,
4372
- seatId: seat.id,
4373
- kind: "invite_named_recipient",
4374
- status: "pending",
4375
- attempts: 0,
4376
- createdAt: now,
4377
- updatedAt: now
4378
- })
4379
- ], { mutationId: `named-seat-included:${seat.id}`, guards: [
4380
- {
4381
- ns: "networkSeat",
4382
- id: seat.id,
4203
+ await db.transact([{ t: "update", ns: "applications", id: String(app.id), attrs: { clerkUserId: identity2.userId } }], {
4204
+ mutationId: `member-account-link:${app.id}:${identity2.userId}`,
4205
+ guards: [{
4206
+ ns: "applications",
4207
+ id: String(app.id),
4383
4208
  exists: true,
4384
- equals: { status: "payment_pending", revision: seat.revision, amountCents: 0 },
4385
- absent: ["purchasedAt", "providerPaymentIntentId"]
4386
- },
4387
- { ns: "networkEntitlement", id: entitlementId, exists: false },
4388
- { ns: "networkMembership", id: String(member.id), exists: true, equals: { revision: member.revision, namedSeatId: seat.id } },
4389
- { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
4390
- ] });
4209
+ equals: Object.fromEntries(keys3.filter((key3) => app[key3] !== void 0).map((key3) => [key3, app[key3]])),
4210
+ absent: ["clerkUserId", ...keys3.filter((key3) => app[key3] === void 0)]
4211
+ }]
4212
+ });
4391
4213
  } catch (error) {
4392
4214
  if (error.code !== "transact_guard_failed") throw error;
4393
4215
  }
4216
+ const saved = (await db.query({ applications: { $: { where: { id: app.id }, limit: 1 } } })).applications?.[0];
4217
+ if (!saved || !local(saved, chapterId, runtime) || saved.clerkUserId !== identity2.userId || keys3.some((key3) => saved[key3] !== app[key3])) return null;
4218
+ app = saved;
4394
4219
  }
4395
- const current = await membershipLoad(db, "networkSeat", seat.id);
4396
- requireMembershipScope(current, input);
4397
- if (!namedSeatIncluded(current) || current.purchasedAt === void 0 || !OPEN.includes(String(current.status))) membershipFail("named_seat_purchase_unavailable");
4398
- return { seatId: seat.id, amountCents: 0, currency: seat.currency, termEndsAt: Number(current.termEndsAt), status: String(current.status), included: true };
4220
+ const meeting = (await db.query({ meetings: { $: {
4221
+ where: { applicationId: app.id, status: "scheduled" },
4222
+ order: { createdAt: "desc" },
4223
+ limit: 1
4224
+ } } })).meetings?.[0];
4225
+ const group = (await db.query({ groups: { $: { where: { id: chapterId }, limit: 1 } } })).groups?.[0];
4226
+ const timezone = resolveScheduling(group?.schedulingJson).timezone;
4227
+ return { source: app, application: memberApplication(
4228
+ app,
4229
+ meeting,
4230
+ timezone
4231
+ ) };
4399
4232
  }
4400
4233
 
4401
- // src/membership-seat-refund.ts
4402
- async function requestNamedSeatRefund(db, input, now) {
4403
- membershipClock(now);
4404
- const state = await loadMembershipAuthorityState(db, input);
4405
- const seat = await membershipLoad(db, "networkSeat", input.seatId);
4406
- requireMembershipScope(seat, input);
4407
- const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
4408
- requireMembershipScope(member, input);
4409
- if (seat.status === "refunded" || seat.status === "refund_pending") return;
4410
- 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");
4411
- const entitlement = seat.entitlementId ? await membershipLoad(db, "networkEntitlement", String(seat.entitlementId)) : void 0;
4412
- await db.transact([
4413
- membershipUpdate("networkSeat", input.seatId, { status: "refund_pending", refundReason: input.reason, revision: Number(seat.revision) + 1, updatedAt: now }),
4414
- ...entitlement ? [membershipUpdate("networkEntitlement", String(entitlement.id), { status: "closed", revision: Number(entitlement.revision) + 1, updatedAt: now })] : [],
4415
- membershipUpdate("membershipEffect", `${seat.id}:refund`, {
4416
- id: `${seat.id}:refund`,
4417
- chapterId: input.chapterId,
4418
- environment: input.environment,
4419
- runtime: input.runtime,
4420
- membershipId: member.id,
4421
- seatId: seat.id,
4422
- kind: "refund_seat",
4423
- status: "pending",
4424
- attempts: 0,
4425
- createdAt: now,
4426
- updatedAt: now
4427
- }),
4428
- ...seat.recipientUserId ? [membershipUpdate("membershipEffect", `${seat.id}:revoke`, {
4429
- id: `${seat.id}:revoke`,
4430
- chapterId: input.chapterId,
4431
- environment: input.environment,
4432
- runtime: input.runtime,
4433
- membershipId: member.id,
4434
- seatId: seat.id,
4435
- kind: "revoke_named_recipient_access",
4436
- status: "pending",
4437
- attempts: 0,
4438
- createdAt: now,
4439
- updatedAt: now
4440
- })] : []
4441
- ], { mutationId: `named-seat-refund-request:${seat.id}`, guards: [
4442
- { ns: "networkSeat", id: input.seatId, exists: true, equals: { revision: seat.revision } },
4443
- { ns: "networkMembership", id: String(member.id), exists: true, equals: { revision: member.revision } },
4444
- { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
4445
- ...entitlement ? [{ ns: "networkEntitlement", id: String(entitlement.id), exists: true, equals: { revision: entitlement.revision } }] : []
4446
- ] });
4447
- }
4448
- async function completeNamedSeatRefund(db, seatId, secretKey, now) {
4449
- membershipClock(now);
4450
- const seat = await membershipLoad(db, "networkSeat", seatId);
4451
- if (!seat) return membershipFail("named_seat_not_found");
4452
- await loadMembershipAuthorityState(db, seat);
4453
- if (seat.status === "refunded") return;
4454
- if (seat.status !== "refund_pending") membershipFail("named_seat_refund_not_ready");
4455
- const result = namedSeatIncluded(seat) ? { refundedCents: 0, refundIds: [] } : await refundNamedSeatStripePayment(secretKey, seat);
4456
- await db.transact([
4457
- membershipUpdate("networkSeat", seatId, {
4458
- status: "refunded",
4459
- refundedCents: result.refundedCents,
4460
- providerRefundIds: result.refundIds,
4461
- revision: Number(seat.revision) + 1,
4462
- updatedAt: now
4463
- }),
4464
- membershipUpdate("membershipEffect", `${seatId}:refund`, { status: "complete", updatedAt: now })
4465
- ], { mutationId: `named-seat-refunded:${seatId}`, guards: [
4466
- { ns: "networkSeat", id: seatId, exists: true, equals: { status: "refund_pending", revision: seat.revision } }
4467
- ] });
4468
- }
4469
-
4470
- // src/membership-seat-operator-refund.ts
4471
- async function refundNamedSeatByOperator(db, input, stripeSecretKey, now) {
4472
- const seat = await membershipLoad(db, "networkSeat", input.seatId);
4473
- requireMembershipScope(seat, input);
4474
- await requestNamedSeatRefund(db, { ...input, reason: "operator" }, now);
4475
- await completeNamedSeatRefund(db, input.seatId, stripeSecretKey, now);
4476
- const refunded = await membershipLoad(db, "networkSeat", input.seatId);
4477
- if (refunded?.status !== "refunded" || !Number.isSafeInteger(refunded.refundedCents) || Number(refunded.refundedCents) !== Number(seat.amountCents)) {
4478
- throw new Error("named seat refund unconfirmed");
4234
+ // src/named-seat-request.ts
4235
+ async function smallRequestBody(req) {
4236
+ const reader = req.body?.getReader();
4237
+ if (!reader) throw new Error("request body required");
4238
+ const chunks = [];
4239
+ let size = 0;
4240
+ try {
4241
+ while (true) {
4242
+ const chunk = await reader.read();
4243
+ if (chunk.done) break;
4244
+ size += chunk.value.byteLength;
4245
+ if (size > 4096) {
4246
+ await reader.cancel();
4247
+ throw new Error("request too large");
4248
+ }
4249
+ chunks.push(chunk.value);
4250
+ }
4251
+ } finally {
4252
+ reader.releaseLock();
4479
4253
  }
4480
- return { seatId: String(seat.id), status: "refunded", refundedCents: Number(refunded.refundedCents), subscriptionCanceled: false };
4254
+ const bytes = new Uint8Array(size);
4255
+ let offset = 0;
4256
+ for (const chunk of chunks) {
4257
+ bytes.set(chunk, offset);
4258
+ offset += chunk.byteLength;
4259
+ }
4260
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
4261
+ }
4262
+ async function namedSeatRequest(req) {
4263
+ const body = JSON.parse(await smallRequestBody(req));
4264
+ if (!body || typeof body !== "object" || Array.isArray(body)) throw new Error("object required");
4265
+ return body;
4481
4266
  }
4482
4267
 
4483
- // src/membership-seat-window.ts
4484
- async function liftSeatPurchaseWindow(db, scope2, now, limit = 100) {
4485
- membershipClock(now);
4486
- if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200) throw new TypeError("invalid lift limit");
4487
- const state = await loadMembershipAuthorityState(db, scope2);
4488
- const rows5 = (await db.query({ networkMembership: { $: { where: { ...scope2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkMembership ?? [];
4489
- let lifted = 0;
4490
- for (const member of rows5.slice(0, limit)) {
4491
- if (Object.entries(scope2).some(([key3, value]) => member[key3] !== value)) throw new Error("membership lift scope mismatch");
4492
- const offering = membershipOfferingVersion(member.offeringSnapshot);
4493
- if (offering.seatPurchaseWindowCalendarMonths === void 0) continue;
4494
- const { seatPurchaseWindowCalendarMonths: _window, ...windowless } = offering;
4495
- await db.transact([membershipUpdate("networkMembership", String(member.id), {
4496
- offeringSnapshot: windowless,
4497
- seatPurchaseEndsAt: Number(member.termEndsAt),
4498
- revision: Number(member.revision) + 1,
4499
- updatedAt: now
4500
- })], { mutationId: `seat-window-lift:${member.id}:${member.revision}`, guards: [
4501
- { ns: "networkMembership", id: String(member.id), exists: true, equals: { revision: member.revision } },
4502
- { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative" } }
4268
+ // src/named-seat-release.ts
4269
+ var SEAT_FIELDS = ["namedSeatId", "namedSeatPrimaryApplicationId", "namedSeatClaimPending", "namedSeatAcceptedAt"];
4270
+ async function releaseNamedSeatRecipientApplications(db, env, ctx, input) {
4271
+ const bound = (await db.query({ applications: { $: { where: { namedSeatId: input.seatId }, limit: 5 } } })).applications ?? [];
4272
+ const keep = input.keepRecipientEmail ? normalizeChapterEmail(input.keepRecipientEmail) : null;
4273
+ const released = [];
4274
+ for (const app of bound) {
4275
+ if (!chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME)) continue;
4276
+ if (input.recipientApplicationId && app.id !== input.recipientApplicationId) continue;
4277
+ if (input.recipientUserId && app.clerkUserId !== input.recipientUserId) continue;
4278
+ if (keep && normalizeChapterEmail(String(app.emailNormalized ?? app.email ?? "")) === keep) continue;
4279
+ await releaseOne(db, env, ctx, app);
4280
+ released.push(String(app.id));
4281
+ }
4282
+ return { released };
4283
+ }
4284
+ async function releaseOne(db, env, ctx, app) {
4285
+ const id7 = String(app.id), closed = app.status === "declined" && app.canceled === true;
4286
+ if (!closed) {
4287
+ await db.transact([
4288
+ { t: "update", ns: "applications", id: id7, attrs: { status: "declined", canceled: true } },
4289
+ { t: "retract", ns: "applications", id: id7, attrs: SEAT_FIELDS.filter((key3) => app[key3] !== void 0) }
4290
+ ], { mutationId: `named-seat-release:${id7}:${String(app.namedSeatId)}`, guards: [
4291
+ { ns: "applications", id: id7, exists: true, equals: { namedSeatId: app.namedSeatId, status: app.status } }
4503
4292
  ] });
4504
- lifted++;
4293
+ } else if (SEAT_FIELDS.some((key3) => app[key3] !== void 0)) {
4294
+ await db.transact(
4295
+ [{ t: "retract", ns: "applications", id: id7, attrs: SEAT_FIELDS.filter((key3) => app[key3] !== void 0) }],
4296
+ { guards: [{ ns: "applications", id: id7, exists: true, equals: { namedSeatId: app.namedSeatId } }] }
4297
+ );
4505
4298
  }
4506
- return { lifted, more: rows5.length > limit };
4507
- }
4508
-
4509
- // src/membership-seat-reprice.ts
4510
- async function lowerSeatPrice(db, scope2, now, limit = 100) {
4511
- membershipClock(now);
4512
- if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200) throw new TypeError("invalid reprice limit");
4513
- const state = await loadMembershipAuthorityState(db, scope2);
4514
- const versions = (await db.query({ membershipOfferingVersion: { $: { where: { chapterId: scope2.chapterId }, limit: 200 } } })).membershipOfferingVersion ?? [];
4515
- if (versions.length >= 200) throw Object.assign(new Error("policy_history_incomplete"), { code: "policy_history_incomplete" });
4516
- 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];
4517
- if (!current) throw Object.assign(new Error("ambiguous_membership_policy"), { code: "ambiguous_membership_policy" });
4518
- const target = current.seatPriceCents;
4519
- const rows5 = (await db.query({ networkMembership: { $: { where: { ...scope2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkMembership ?? [];
4520
- const receipt = { seatPriceCents: target, repriced: 0, skipped: [], more: rows5.length > limit };
4521
- for (const member of rows5.slice(0, limit)) {
4522
- if (Object.entries(scope2).some(([key3, value]) => member[key3] !== value)) throw new Error("membership reprice scope mismatch");
4523
- const id7 = String(member.id), skip = (reason) => {
4524
- receipt.skipped.push({ membershipId: id7, reason });
4525
- };
4526
- const offering = membershipOfferingVersion(member.offeringSnapshot);
4527
- if (offering.seatPriceCents <= target) {
4528
- skip("seat price already at or below the current offering");
4529
- continue;
4530
- }
4531
- if (!["paid_pending_approval", "active", "cancel_at_term_end", "payment_grace"].includes(String(member.status))) {
4532
- skip(`membership is ${String(member.status)}`);
4533
- continue;
4534
- }
4535
- const seat = member.namedSeatId ? await membershipLoad(db, "networkSeat", String(member.namedSeatId)) : void 0;
4536
- if (seat && (seat.status !== "payment_pending" || seat.providerPaymentIntentId !== void 0)) {
4537
- skip(`seat is ${String(seat.status)}`);
4538
- continue;
4539
- }
4540
- const ops = [membershipUpdate("networkMembership", id7, { offeringSnapshot: { ...offering, seatPriceCents: target }, revision: Number(member.revision) + 1, updatedAt: now })];
4541
- const guards = [
4542
- { ns: "networkMembership", id: id7, exists: true, equals: { revision: member.revision } },
4543
- { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative" } }
4544
- ];
4545
- if (seat) {
4546
- ops.push(membershipUpdate("networkSeat", String(seat.id), { amountCents: target, offeringSnapshot: { ...offering, seatPriceCents: target }, revision: Number(seat.revision) + 1, updatedAt: now }));
4547
- guards.push({ ns: "networkSeat", id: String(seat.id), exists: true, equals: { revision: seat.revision, status: "payment_pending" } });
4548
- }
4549
- await db.transact(ops, { mutationId: `seat-reprice:${id7}:${member.revision}`, guards });
4550
- receipt.repriced++;
4299
+ const promoteTo = ctx.chapter.operations.onApprove.promoteTo;
4300
+ if (promoteTo !== false && typeof app.clerkUserId === "string" && app.clerkUserId) {
4301
+ const secret = await getVaultSecret(db, "clerk_secret_key");
4302
+ if (!secret) throw new Error("named seat release provider unavailable");
4303
+ const user = await clerkGetUser(secret, app.clerkUserId, fetch, clerkRoleMetadata(ctx.chapter.auth));
4304
+ if (!user) throw new Error("named seat release provider unavailable");
4305
+ 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");
4551
4306
  }
4552
- return receipt;
4307
+ const seatless = Object.fromEntries(Object.entries({ ...app, status: "declined", canceled: true }).filter(([key3]) => !SEAT_FIELDS.includes(key3)));
4308
+ await syncApplicationToCrm(crmDeps(db, ctx), { app: seatless, stage: "declined" }).catch(() => void 0);
4553
4309
  }
4554
4310
 
4555
- // src/worker-routes-admin-membership-seats.ts
4556
- var LIST = "/api/admin/membership-seats";
4557
- var LIFT = "/api/admin/membership-seats/lift-purchase-window";
4558
- var REPRICE = "/api/admin/membership-seats/reprice";
4559
- var ONE = /^\/api\/admin\/membership-seats\/([A-Za-z0-9][A-Za-z0-9_.:-]{2,159})\/refund$/;
4560
- var SCOPE = /^[a-z0-9][a-z0-9-]{1,62}$/;
4561
- var PUBLIC = [
4562
- "id",
4563
- "appId",
4564
- "chapterId",
4565
- "environment",
4566
- "runtime",
4567
- "applicationId",
4568
- "skuId",
4569
- "status",
4570
- "amountCents",
4571
- "currency",
4572
- "recipientName",
4573
- "recipientEmailNormalized",
4574
- "purchaserUserId",
4575
- "purchasedAt",
4576
- "acceptedAt",
4577
- "journeyApprovedAt",
4578
- "refundedCents",
4579
- "refundReason",
4580
- "termEndsAt",
4581
- "updatedAt"
4582
- ];
4583
- var project = (row2) => Object.fromEntries(PUBLIC.filter((key3) => row2[key3] !== void 0).map((key3) => [key3, row2[key3]]));
4584
- var handleAdminMembershipSeats = async (req, url, env, ctx) => {
4585
- const one = url.pathname.match(ONE), lift = url.pathname === LIFT, reprice = url.pathname === REPRICE;
4586
- if (url.pathname !== LIST && !one && !lift && !reprice) return null;
4587
- const operator = ctx.membershipAuthorityOperator;
4588
- if (!operator) return json({ error: "not found" }, 404);
4589
- if (req.method !== (one || lift || reprice ? "POST" : "GET")) return json({ error: "method not allowed" }, 405);
4590
- const raw2 = ctx.makeDb(env), user = await ctx.verifyUser(req, env);
4591
- if (!user) return json({ error: "unauthorized" }, 401);
4592
- if (!await ctx.isSuperAdminEmail(raw2, user.email, user.userId)) return json({ error: "super-admin required", code: "super_admin_required" }, 403);
4593
- let db;
4311
+ // src/worker-routes-named-seat-regift.ts
4312
+ async function recipientFrom(req, purchaserEmail) {
4313
+ let body;
4594
4314
  try {
4595
- db = operator.makeDb(env);
4315
+ body = await namedSeatRequest(req);
4596
4316
  } catch {
4597
- return json({ error: "the membership authority is not hosted by this deployment" }, 404);
4598
- }
4599
- if (lift || reprice) {
4600
- let body;
4601
- try {
4602
- body = await req.json();
4603
- } catch {
4604
- return json({ error: "chapterId and runtime are required" }, 400);
4605
- }
4606
- const appId = body.appId ?? body.chapterId;
4607
- 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))) {
4608
- return json({ error: reprice ? "chapterId, runtime and skuId are required" : "chapterId and runtime are required" }, 400);
4609
- }
4610
- const scope2 = { appId, chapterId: body.chapterId, environment: env.ODLA_ENV, runtime: body.runtime };
4611
- try {
4612
- if (reprice) return json({ ok: true, ...await lowerSeatPrice(db, { ...scope2, skuId: String(body.skuId) }, Date.now()) });
4613
- return json({ ok: true, ...await liftSeatPurchaseWindow(db, scope2, Date.now()) });
4614
- } catch (error) {
4615
- const code = error.code;
4616
- return json({ error: `${reprice ? "seat reprice" : "purchase window lift"} is pending; retry to confirm`, ...code ? { code } : {} }, code ? 409 : 503);
4617
- }
4317
+ return json({ error: "invalid request" }, 400);
4618
4318
  }
4619
- if (!one) {
4620
- const rows5 = (await db.query({ networkSeat: { $: { where: { environment: env.ODLA_ENV }, order: { updatedAt: "desc" }, limit: 201 } } })).networkSeat ?? [];
4621
- return json({ seats: rows5.slice(0, 200).map(project), truncated: rows5.length > 200 });
4319
+ 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) {
4320
+ return json({ error: "a recipient name and email are required" }, 400);
4622
4321
  }
4623
- const seat = await membershipLoad(db, "networkSeat", one[1]);
4624
- if (!seat || seat.environment !== env.ODLA_ENV) return json({ error: "not found" }, 404);
4625
- if (seat.status === "refunded") {
4626
- return json({ ok: true, seatId: seat.id, status: "refunded", refundedCents: seat.refundedCents, subscriptionCanceled: false, replayed: true });
4322
+ const recipientName = body.recipientName.trim().replace(/\s+/g, " "), recipientEmail = normalizeChapterEmail(body.recipientEmail);
4323
+ if (recipientName.length < 2 || /[\u0000-\u001f\u007f]/.test(body.recipientName) || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(recipientEmail) || purchaserEmail && recipientEmail === normalizeChapterEmail(purchaserEmail)) {
4324
+ return json({ error: "a valid name and email for someone other than the purchaser are required" }, 400);
4627
4325
  }
4628
- if (!seat.purchasedAt) return json({ error: "seat has no completed payment to refund", code: "seat_not_refundable" }, 409);
4629
- const secret = await getVaultSecret(db, operator.stripeSecretName ?? "stripe_secret_key");
4630
- if (!secret) return json({ error: "network payment provider unavailable" }, 503);
4326
+ return { recipientName, recipientEmail };
4327
+ }
4328
+ async function regiftChapterNamedSeat(db, env, ctx, req, target, purchaserEmail) {
4329
+ const seats = ctx.membershipAuthority?.namedSeats;
4330
+ if (!seats?.regift) return json({ error: "naming a different recipient is not available on this network authority" }, 503);
4331
+ const recipient2 = await recipientFrom(req, purchaserEmail);
4332
+ if (recipient2 instanceof Response) return recipient2;
4333
+ const offer = await seats.offer(target, env), seat = offer.seat;
4334
+ if (!seat) return json({ error: "no purchased seat", code: "seat_not_found" }, 409);
4335
+ 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);
4336
+ const receipt = await seats.regift({ ...target, seatId: seat.seatId, ...recipient2 }, env);
4337
+ if (receipt.seatId !== seat.seatId || receipt.recipientEmail !== recipient2.recipientEmail) throw new Error("named seat regift differs from the request");
4338
+ const released = await releaseNamedSeatRecipientApplications(db, env, ctx, { seatId: seat.seatId, keepRecipientEmail: receipt.recipientEmail });
4339
+ const consentId = `${offer.membershipId}:named-seat-consent`, consent = await membershipLoad(db, "namedSeatConsents", consentId);
4340
+ if (consent) await db.transact([{ t: "update", ns: "namedSeatConsents", id: consentId, attrs: {
4341
+ recipientName: receipt.recipientName,
4342
+ recipientEmail: receipt.recipientEmail,
4343
+ acceptedAt: Date.now()
4344
+ } }], { guards: [{ ns: "namedSeatConsents", id: consentId, exists: true }] });
4345
+ return json({ ok: true, seat: receipt, releasedApplicationIds: released.released });
4346
+ }
4347
+ var handleNamedSeatRegift = async (req, url, env, ctx) => {
4348
+ if (url.pathname !== "/api/named-seat/regift") return null;
4349
+ const seats = ctx.membershipAuthority?.namedSeats;
4350
+ if (ctx.chapter.mode !== "chapter" || !seats) return json({ error: "named seats are not configured" }, 404);
4351
+ if (req.method !== "POST") return json({ error: "method not allowed" }, 405);
4352
+ const user = await ctx.verifyUser(req, env);
4353
+ if (!user?.email) return json({ error: "unauthorized" }, 401);
4354
+ const db = ctx.makeDb(env);
4631
4355
  try {
4632
- const receipt = await refundNamedSeatByOperator(db, {
4633
- appId: String(seat.appId),
4634
- chapterId: String(seat.chapterId),
4635
- environment: String(seat.environment),
4636
- runtime: String(seat.runtime),
4637
- applicationId: String(seat.applicationId),
4638
- skuId: String(seat.skuId),
4639
- seatId: String(seat.id)
4640
- }, secret, Date.now());
4641
- return json({ ok: true, ...receipt });
4642
- } catch (error) {
4643
- const code = error.code;
4644
- return json({ error: "seat refund is pending; retry to confirm", ...code ? { code } : {} }, code ? 409 : 503);
4356
+ const member = await memberSessionApplication(db, ctx.chapter.id, env.ODLA_RUNTIME, { userId: user.userId, email: user.email });
4357
+ if (!member || member.source.clerkUserId !== user.userId) return json({ error: "membership required" }, 403);
4358
+ const intent = await membershipLoad(db, "subscriptionCheckoutIntents", String(member.source.id));
4359
+ if (!intent) return json({ error: "paid membership required" }, 409);
4360
+ const target = await membershipTargetForIntent(intent, member.source, env, ctx);
4361
+ return await regiftChapterNamedSeat(db, env, ctx, req, target, user.email);
4362
+ } catch {
4363
+ return json({ error: "named seat service unavailable; refresh to check the seat before trying again" }, 503);
4645
4364
  }
4646
4365
  };
4647
4366
 
4648
- // src/membership-policy-versions.ts
4649
- var LIMIT = 200;
4650
- var text2 = (value) => typeof value === "string" && value.length > 0 && value.length <= 200 ? value : void 0;
4651
- var instant = (value) => {
4652
- const at2 = typeof value === "number" ? value : typeof value === "string" ? Date.parse(value) : NaN;
4653
- return Number.isSafeInteger(at2) && at2 >= 0 ? at2 : void 0;
4654
- };
4655
- function provenance(row2) {
4656
- const source = text2(row2.source), actor = text2(row2.actor ?? row2.$createdBy), recordedAt = instant(row2.recordedAt ?? row2.createdAt ?? row2.$createdAt);
4657
- return { ...source ? { source } : {}, ...actor ? { actor } : {}, ...recordedAt !== void 0 ? { recordedAt } : {} };
4658
- }
4659
- var live = (w, now) => w.from <= now && (w.until === void 0 || now < w.until);
4660
- function stateOf(item, all, now, window, rivals) {
4661
- if (item.status !== "published") return { state: item.status };
4662
- const mine = window(item);
4663
- if (now < mine.from) return { state: "scheduled" };
4664
- const closed = mine.until !== void 0 && mine.until <= now;
4665
- 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));
4666
- if (!closed && !higher.some((w) => live(w, now))) return { state: "active" };
4667
- const dates = higher.map((w) => Math.max(w.from, mine.from));
4668
- if (closed) dates.push(mine.until);
4669
- return { state: "superseded", supersededAt: Math.min(...dates) };
4670
- }
4671
- 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";
4672
- var sharesSku = (a, b) => a.cohortEligibleSkuIds.some((sku) => b.cohortEligibleSkuIds.includes(sku));
4673
- var byChapter = (a, b) => a.chapterId.localeCompare(b.chapterId);
4674
- function normalize(rows5, collection, parse, invalid2) {
4675
- const out = [];
4676
- for (const row2 of rows5) {
4677
- try {
4678
- out.push({ value: parse(row2.snapshot), row: row2 });
4679
- } catch (error) {
4680
- invalid2.push({ collection, id: String(row2.id ?? ""), error: error instanceof Error ? error.message : String(error) });
4681
- }
4682
- }
4683
- return out;
4684
- }
4685
- async function listMembershipPolicyVersions(db, input) {
4686
- const where = input.chapterId ? { chapterId: input.chapterId } : {};
4687
- const rows5 = await db.query({
4688
- membershipOfferingVersion: { $: { where, limit: LIMIT } },
4689
- discountPolicyVersion: { $: { where, limit: LIMIT } }
4690
- });
4691
- const offeringRows = rows5.membershipOfferingVersion ?? [], policyRows = rows5.discountPolicyVersion ?? [];
4692
- const invalid2 = [];
4693
- const offerings = normalize(offeringRows, "membershipOfferingVersion", membershipOfferingVersion, invalid2);
4694
- const policies = normalize(policyRows, "discountPolicyVersion", membershipDiscountPolicyVersion, invalid2);
4695
- const offeringValues = offerings.map((item) => item.value), policyValues = policies.map((item) => item.value);
4696
- return {
4697
- now: input.now,
4698
- chapterId: input.chapterId ?? null,
4699
- offerings: offerings.map(({ value, row: row2 }) => ({
4700
- ...value,
4701
- ...provenance(row2),
4702
- ...stateOf(
4703
- value,
4704
- offeringValues,
4705
- input.now,
4706
- (v) => ({ from: v.effectiveFrom, ...v.effectiveUntil !== void 0 ? { until: v.effectiveUntil } : {} }),
4707
- (a, b) => a.chapterId === b.chapterId && a.baseSkuId === b.baseSkuId
4708
- )
4709
- })).sort((a, b) => byChapter(a, b) || a.baseSkuId.localeCompare(b.baseSkuId) || b.version - a.version),
4710
- discountPolicies: policies.map(({ value, row: row2 }) => ({
4711
- ...value,
4712
- ...provenance(row2),
4713
- mechanism: mechanism(value),
4714
- ...stateOf(
4715
- value,
4716
- policyValues,
4717
- input.now,
4718
- (v) => ({ from: v.qualifyingStartsAt, until: v.qualifyingEndsAt }),
4719
- (a, b) => a.chapterId === b.chapterId && sharesSku(a, b)
4720
- )
4721
- })).sort((a, b) => byChapter(a, b) || b.version - a.version),
4722
- invalid: invalid2,
4723
- truncated: offeringRows.length >= LIMIT || policyRows.length >= LIMIT
4724
- };
4725
- }
4726
-
4727
- // src/worker-routes-admin-membership-policy.ts
4728
- var PATH3 = "/api/admin/membership-policy-versions";
4729
- var SCOPE2 = /^[a-z0-9][a-z0-9-]{1,62}$/;
4730
- var handleAdminMembershipPolicyVersions = async (req, url, env, ctx) => {
4731
- if (url.pathname !== PATH3) return null;
4732
- const operator = ctx.membershipAuthorityOperator;
4733
- if (!operator) return json({ error: "not found" }, 404);
4734
- if (req.method !== "GET") return json({ error: "method not allowed" }, 405);
4367
+ // src/worker-routes-admin-named-seat.ts
4368
+ var PATH2 = /^\/api\/admin\/applications\/([0-9a-f-]+)\/named-seat(\/refund|\/regift)?$/;
4369
+ var REFUNDABLE = /* @__PURE__ */ new Set(["renewal_pending", "pending_acceptance", "pending_approval", "pending_primary", "active"]);
4370
+ var handleAdminNamedSeat = async (req, url, env, ctx) => {
4371
+ const match = url.pathname.match(PATH2);
4372
+ if (!match) return null;
4373
+ const seats = ctx.membershipAuthority?.namedSeats;
4374
+ if (ctx.chapter.mode !== "chapter" || !seats) return json({ error: "named seats are not configured" }, 404);
4375
+ const refund = match[2] === "/refund", regift = match[2] === "/regift";
4376
+ if (req.method !== (refund || regift ? "POST" : "GET")) return json({ error: "method not allowed" }, 405);
4735
4377
  const raw2 = ctx.makeDb(env), user = await ctx.verifyUser(req, env);
4736
4378
  if (!user) return json({ error: "unauthorized" }, 401);
4737
4379
  if (!await ctx.isSuperAdminEmail(raw2, user.email, user.userId)) return json({ error: "super-admin required", code: "super_admin_required" }, 403);
4738
- const chapterId = url.searchParams.get("chapterId");
4739
- if (chapterId !== null && !SCOPE2.test(chapterId)) return json({ error: "chapterId is invalid" }, 400);
4740
- let db;
4380
+ const db = raw2, app = await membershipLoad(db, "applications", match[1]);
4381
+ if (!app || !chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME) || app.namedSeatId) return json({ error: "not found" }, 404);
4382
+ const intent = await membershipLoad(db, "subscriptionCheckoutIntents", String(app.id));
4383
+ if (!intent) return json({ error: "paid membership required" }, 409);
4741
4384
  try {
4742
- db = operator.makeDb(env);
4385
+ const target = await membershipTargetForIntent(intent, app, env, ctx);
4386
+ if (regift) return await regiftChapterNamedSeat(db, env, ctx, req, target, typeof app.email === "string" ? app.email : void 0);
4387
+ const offer = await seats.offer(target, env);
4388
+ const seat = offer.seat ?? null, refundable = Boolean(seat && REFUNDABLE.has(seat.status));
4389
+ if (!refund) {
4390
+ return json({
4391
+ seat,
4392
+ amountCents: offer.amountCents,
4393
+ currency: offer.currency,
4394
+ termEndsAt: offer.termEndsAt,
4395
+ purchaseEndsAt: offer.purchaseEndsAt,
4396
+ refundable,
4397
+ regiftable: seat?.regiftable === true
4398
+ });
4399
+ }
4400
+ if (!seat) return json({ error: "no purchased seat", code: "seat_not_found" }, 409);
4401
+ if (seat.status === "refunded") {
4402
+ await closeRecipientApplication(db, env, ctx, seat.seatId);
4403
+ return json({ ok: true, seatId: seat.seatId, status: "refunded", replayed: true });
4404
+ }
4405
+ if (!refundable) return json({ error: `seat is ${seat.status.replaceAll("_", " ")}`, code: "seat_not_refundable" }, 409);
4406
+ const receipt = await seats.operatorRefund({ ...target, seatId: seat.seatId }, env);
4407
+ await closeRecipientApplication(db, env, ctx, seat.seatId);
4408
+ return json({ ok: true, ...receipt });
4743
4409
  } catch {
4744
- return json({ error: "the membership authority is not hosted by this deployment" }, 404);
4410
+ return json({ error: "named seat service unavailable; retry to confirm" }, 503);
4745
4411
  }
4746
- return json(await listMembershipPolicyVersions(db, { chapterId, now: Date.now() }));
4747
4412
  };
4748
-
4749
- // src/membership-discount-reterm.ts
4750
- import { stripeRequest as stripeRequest7 } from "@odla-ai/stripe";
4751
-
4752
- // src/subscription-provider-contract.ts
4753
- import { stripeRequest as stripeRequest6 } from "@odla-ai/stripe";
4754
- function subscriptionContractMismatch() {
4755
- throw Object.assign(new Error("Stripe subscription does not match the checkout contract"), { code: "stripe_contract_mismatch" });
4756
- }
4757
- async function verifySubscriptionPrice(secretKey, input, resume = false) {
4758
- 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();
4759
- const result = await stripeRequest6({ secretKey }, "GET", `/v1/prices/${encodeURIComponent(input.priceId)}`);
4760
- const price = result.body;
4761
- 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();
4762
- return typeof price.product === "string" ? price.product : void 0;
4763
- }
4764
- function verifySubscriptionResult(sub, input, customerId, metadata) {
4765
- const items = sub?.items;
4766
- const seat = input.namedSeat?.amountCents ? input.namedSeat : void 0;
4767
- const primary = items?.data?.find((item) => item.price?.id === input.priceId);
4768
- const seatItem = seat && items?.data?.find((item) => item.price?.id === seat.priceId);
4769
- 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();
4770
- const invoice = sub.latest_invoice;
4771
- 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();
4772
- verifySubscriptionBillingResult(sub, input);
4413
+ async function closeRecipientApplication(db, env, ctx, seatId) {
4414
+ const recipient2 = (await db.query({ applications: { $: { where: { namedSeatId: seatId }, limit: 1 } } })).applications?.[0];
4415
+ if (!recipient2 || !chapterBillingApplication(recipient2, ctx.chapter.id, env.ODLA_RUNTIME) || recipient2.status === "refunded") return;
4416
+ const attrs = { status: "refunded", renewalAt: 0, canceled: true };
4417
+ await db.transact(
4418
+ [{ t: "update", ns: "applications", id: String(recipient2.id), attrs }],
4419
+ { mutationId: `verified-refund:${String(recipient2.id)}:${seatId}` }
4420
+ );
4421
+ await syncApplicationToCrm(crmDeps(db, ctx), { app: { ...recipient2, ...attrs }, stage: "refunded" }).catch(() => void 0);
4773
4422
  }
4774
4423
 
4775
- // src/membership-discount-reterm.ts
4776
- var SAFE = /^[A-Za-z0-9][A-Za-z0-9_.:-]{2,159}$/;
4777
- var RETERMABLE = /* @__PURE__ */ new Set(["active", "paid_pending_approval", "cancel_at_term_end", "payment_grace"]);
4778
- var PROVIDER_LIVE = /* @__PURE__ */ new Set(["active", "trialing", "past_due"]);
4779
- var couponOf = (discount) => {
4780
- const ref = discount?.source?.coupon ?? discount?.coupon;
4781
- return typeof ref === "string" ? ref : typeof ref?.id === "string" ? ref.id : void 0;
4424
+ // src/membership-seat-refund-stripe.ts
4425
+ import { stripeRequest as stripeRequest5 } from "@odla-ai/stripe";
4426
+
4427
+ // src/membership-checkout-seat-refund.ts
4428
+ import { stripeRequest as stripeRequest4 } from "@odla-ai/stripe";
4429
+ var fail4 = (code = "named_seat_refund_mismatch") => {
4430
+ throw Object.assign(new Error(code), { code });
4782
4431
  };
4783
- async function retermPolicy(db, scope2, id7) {
4784
- const row2 = await membershipLoad(db, "discountPolicyVersion", id7);
4785
- if (!row2) membershipFail("membership_reterm_policy_unknown");
4786
- let policy2;
4787
- try {
4788
- policy2 = membershipDiscountPolicyVersion(row2.snapshot);
4789
- } catch {
4790
- return membershipFail("membership_reterm_policy_ineligible");
4432
+ var id2 = (value) => typeof value === "string" ? value : value?.id ?? "";
4433
+ async function refundCheckoutNamedSeat(secretKey, seat) {
4434
+ const invoiceRef = seat.combinedInvoice;
4435
+ 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();
4436
+ const call = async (method, path, params, key3) => {
4437
+ const result = await stripeRequest4({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
4438
+ if (!result.ok || !result.body) return fail4("named_seat_refund_provider_unavailable");
4439
+ return result.body;
4440
+ };
4441
+ const metadata = { ...stripePartitionMetadata(seat), applicationId: seat.applicationId };
4442
+ const sub = await call("GET", `/v1/subscriptions/${encodeURIComponent(seat.providerSubscriptionId)}`);
4443
+ 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();
4444
+ const invoice = await call("GET", `/v1/invoices/${encodeURIComponent(invoiceRef.invoiceId)}`);
4445
+ 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();
4446
+ let chargeId = id2(invoice.charge), paymentId = id2(invoice.payment_intent);
4447
+ if (!chargeId && !paymentId) {
4448
+ const payments = await call("GET", "/v1/invoice_payments", { invoice: invoice.id, status: "paid", limit: 100 });
4449
+ if (!Array.isArray(payments.data) || payments.data.length !== 1 || payments.has_more !== false) fail4();
4450
+ const payment = payments.data[0];
4451
+ if (id2(payment.invoice) !== invoice.id || payment.status !== "paid" || payment.payment?.type !== "payment_intent") fail4();
4452
+ paymentId = id2(payment.payment.payment_intent);
4791
4453
  }
4792
- 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");
4793
- return policy2;
4794
- }
4795
- async function attachProviderDiscount(sk, scope2, member, policy2, couponId, productId) {
4796
- const subId = String(member.providerSubscriptionId), path = `/v1/subscriptions/${encodeURIComponent(subId)}`;
4797
- const read2 = await stripeRequest7({ secretKey: sk }, "GET", path, { "expand[0]": "discounts" });
4798
- const sub = read2.body, items = sub?.items?.data;
4799
- const metadata = { ...stripePartitionMetadata(scope2), applicationId: String(member.applicationId) };
4800
- 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) {
4801
- throw Object.assign(new Error("provider subscription does not match the frozen membership"), { code: "provider_mismatch" });
4454
+ if (!chargeId) {
4455
+ if (!/^pi_[A-Za-z0-9_]+$/.test(paymentId)) fail4();
4456
+ const payment = await call("GET", `/v1/payment_intents/${encodeURIComponent(paymentId)}`);
4457
+ 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();
4458
+ chargeId = id2(payment.latest_charge);
4802
4459
  }
4803
- const discounts = sub.discounts ?? (sub.discount ? [sub.discount] : []);
4804
- if (!Array.isArray(discounts)) throw Object.assign(new Error("provider discounts unreadable"), { code: "provider_mismatch" });
4805
- const present = discounts.map(couponOf);
4806
- if (present.length === 1 && present[0] === couponId) return false;
4807
- if (present.length > 0) throw Object.assign(new Error("provider subscription carries an unrelated discount"), { code: "provider_mismatch" });
4808
- const written = await stripeRequest7({ secretKey: sk }, "POST", path, {
4809
- "discounts[0][coupon]": couponId,
4810
- "expand[0]": "discounts",
4811
- "metadata[discountPolicyVersionId]": policy2.id
4812
- }, { idempotencyKey: `membership-reterm:${String(member.id)}:${policy2.id}` });
4813
- const after = written.body?.discounts;
4814
- if (!written.ok || written.body?.id !== subId || !Array.isArray(after) || after.length !== 1 || couponOf(after[0]) !== couponId) {
4815
- membershipFail("membership_reterm_provider_unconfirmed");
4460
+ if (!/^ch_[A-Za-z0-9_]+$/.test(chargeId)) fail4();
4461
+ const charge = await call("GET", `/v1/charges/${encodeURIComponent(chargeId)}`);
4462
+ 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();
4463
+ const items = sub.items.data.filter((item) => item.metadata?.seatId === seat.id);
4464
+ if (items.length > 1) fail4();
4465
+ if (items.length && sub.status !== "canceled") {
4466
+ const item = items[0];
4467
+ if (!/^si_[A-Za-z0-9_]+$/.test(item.id) || item.quantity !== 1 || item.price?.id !== seat.priceId) fail4();
4468
+ const removed = await call("DELETE", `/v1/subscription_items/${encodeURIComponent(item.id)}`, { proration_behavior: "none" });
4469
+ if (removed.id !== item.id || removed.deleted !== true) fail4("named_seat_renewal_removal_unconfirmed");
4816
4470
  }
4817
- return true;
4818
- }
4819
- async function retermMembershipDiscount(db, stripeSecretKey, scope2, input, now) {
4471
+ if (charge.amount_refunded === charge.amount && charge.refunded === true) return { refundedCents: seat.amountCents, refundIds: [] };
4472
+ const list = await call("GET", "/v1/refunds", { charge: chargeId, limit: 100 });
4473
+ if (!Array.isArray(list.data) || list.has_more !== false || list.data.length >= 100) fail4("named_seat_refund_history_incomplete");
4474
+ let refundedCents = 0;
4475
+ const refundIds = [];
4476
+ for (const refund of list.data) {
4477
+ 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();
4478
+ if (["pending", "requires_action"].includes(refund.status)) fail4("named_seat_refund_pending");
4479
+ if (refund.status === "succeeded") {
4480
+ if (refund.metadata?.seatId !== seat.id || refund.metadata?.membershipId !== seat.primaryMembershipId) fail4("named_seat_refund_allocation_ambiguous");
4481
+ refundedCents += refund.amount;
4482
+ refundIds.push(refund.id);
4483
+ }
4484
+ }
4485
+ if (refundedCents !== charge.amount_refunded || refundedCents > seat.amountCents) fail4();
4486
+ if (refundedCents < seat.amountCents) {
4487
+ const remaining = seat.amountCents - refundedCents;
4488
+ const refund = await call("POST", "/v1/refunds", {
4489
+ charge: chargeId,
4490
+ amount: remaining,
4491
+ reason: "requested_by_customer",
4492
+ metadata: { ...metadata, membershipId: seat.primaryMembershipId, seatId: seat.id }
4493
+ }, `combined-seat-refund:${seat.id}:${remaining}`);
4494
+ if (id2(refund.charge) !== chargeId || refund.amount !== remaining || refund.currency !== seat.currency || !/^re_[A-Za-z0-9_]+$/.test(refund.id)) fail4();
4495
+ if (refund.status !== "succeeded") fail4("named_seat_refund_pending");
4496
+ refundedCents += remaining;
4497
+ refundIds.push(refund.id);
4498
+ }
4499
+ return { refundedCents, refundIds };
4500
+ }
4501
+
4502
+ // src/membership-seat-refund-stripe.ts
4503
+ var fail5 = (code = "named_seat_refund_mismatch") => {
4504
+ throw Object.assign(new Error(code), { code });
4505
+ };
4506
+ async function refundNamedSeatStripePayment(secretKey, seat) {
4507
+ if (seat.combinedInvoice) return refundCheckoutNamedSeat(secretKey, seat);
4508
+ if (!seat.providerPaymentIntentId || seat.status !== "refund_pending") fail5();
4509
+ const call = async (method, path, params, idempotencyKey) => {
4510
+ const result = await stripeRequest5({ secretKey }, method, path, params, idempotencyKey ? { idempotencyKey } : {});
4511
+ if (!result.ok || !result.body) return fail5("named_seat_refund_provider_unavailable");
4512
+ return result.body;
4513
+ };
4514
+ const metadata = { ...stripePartitionMetadata(seat), applicationId: seat.applicationId, membershipId: seat.primaryMembershipId, seatId: seat.id };
4515
+ const payment = await call("GET", `/v1/payment_intents/${encodeURIComponent(seat.providerPaymentIntentId)}`);
4516
+ 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();
4517
+ const sub = await call("GET", `/v1/subscriptions/${encodeURIComponent(seat.providerSubscriptionId)}`);
4518
+ 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();
4519
+ const items = sub.items.data.filter((item) => item.metadata?.seatId === seat.id);
4520
+ if (items.length > 1) fail5();
4521
+ if (items.length && sub.status !== "canceled") {
4522
+ const item = items[0];
4523
+ if (!/^si_[A-Za-z0-9_]+$/.test(item.id) || item.price?.id !== seat.priceId || item.quantity !== 1) fail5();
4524
+ const removed = await call("DELETE", `/v1/subscription_items/${encodeURIComponent(item.id)}`, { proration_behavior: "none" });
4525
+ if (removed.id !== item.id || removed.deleted !== true) fail5("named_seat_renewal_removal_unconfirmed");
4526
+ }
4527
+ const list = await call("GET", "/v1/refunds", { payment_intent: seat.providerPaymentIntentId, limit: 100 });
4528
+ if (!Array.isArray(list.data) || list.has_more !== false || list.data.length >= 100) fail5("named_seat_refund_history_incomplete");
4529
+ let refundedCents = 0, pendingCents = 0;
4530
+ const refundIds = [];
4531
+ for (const refund of list.data) {
4532
+ 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();
4533
+ if (refund.status === "succeeded") {
4534
+ refundedCents += refund.amount;
4535
+ refundIds.push(refund.id);
4536
+ } else if (refund.status === "pending" || refund.status === "requires_action") pendingCents += refund.amount;
4537
+ }
4538
+ if (refundedCents + pendingCents > seat.amountCents) fail5();
4539
+ if (pendingCents) fail5("named_seat_refund_pending");
4540
+ if (refundedCents < seat.amountCents) {
4541
+ const remaining = seat.amountCents - refundedCents;
4542
+ const refund = await call("POST", "/v1/refunds", {
4543
+ payment_intent: seat.providerPaymentIntentId,
4544
+ amount: remaining,
4545
+ reason: "requested_by_customer",
4546
+ metadata
4547
+ }, `named-seat-refund:${seat.id}:${remaining}`);
4548
+ if (refund.payment_intent !== seat.providerPaymentIntentId || refund.amount !== remaining || refund.currency !== seat.currency || !/^re_[A-Za-z0-9_]+$/.test(refund.id)) fail5();
4549
+ if (refund.status !== "succeeded") fail5("named_seat_refund_pending");
4550
+ refundedCents += remaining;
4551
+ refundIds.push(refund.id);
4552
+ }
4553
+ return { refundedCents, refundIds };
4554
+ }
4555
+
4556
+ // src/membership-seat-reservation.ts
4557
+ async function namedSeatRecipientClaimId(input, email) {
4558
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify([input.appId, input.chapterId, input.environment, input.runtime, email])));
4559
+ return `seat-recipient:${[...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
4560
+ }
4561
+ async function reserveNamedMembershipSeat(db, input, now) {
4562
+ membershipClock(now);
4563
+ const name = typeof input.recipientName === "string" ? input.recipientName.trim().replace(/\s+/g, " ") : "";
4564
+ const email = normalizeChapterEmail(typeof input.recipientEmail === "string" ? input.recipientEmail : "");
4565
+ const purchaser = normalizeChapterEmail(typeof input.purchaserEmail === "string" ? input.purchaserEmail : "");
4566
+ 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");
4567
+ const state = await loadMembershipAuthorityState(db, input);
4568
+ const memberId = await networkMembershipId(input), member = await membershipLoad(db, "networkMembership", memberId);
4569
+ requireMembershipScope(member, input);
4570
+ if (!Number.isSafeInteger(member.revision) || !/^price_[A-Za-z0-9_]+$/.test(String(member.providerPriceId)) || member.providerPriceId === input.priceId) membershipFail("named_seat_purchase_unavailable");
4571
+ const id7 = (await membershipOfferIdForRequest({ ...input, requestId: "named_seat" })).replace(/^offer_/, "seat_");
4572
+ let seat = await membershipLoad(db, "networkSeat", id7);
4573
+ if (!seat) {
4574
+ const offering = membershipOfferingVersion(member.offeringSnapshot), claimId = await namedSeatRecipientClaimId(input, email);
4575
+ const purchaseEndsAt = seatPurchaseDeadline(offering, member);
4576
+ 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) {
4577
+ membershipFail("named_seat_purchase_unavailable");
4578
+ }
4579
+ const attrs = {
4580
+ id: id7,
4581
+ ...membershipScope(input),
4582
+ primaryMembershipId: memberId,
4583
+ recipientName: name,
4584
+ recipientEmailNormalized: email,
4585
+ purchaserUserId: input.purchaserUserId,
4586
+ recipientClaimId: claimId,
4587
+ priceId: input.priceId,
4588
+ primaryPriceId: member.providerPriceId,
4589
+ amountCents: offering.seatPriceCents,
4590
+ currency: offering.currency,
4591
+ interval: offering.interval,
4592
+ providerCustomerId: member.providerCustomerId,
4593
+ providerSubscriptionId: member.providerSubscriptionId,
4594
+ status: "payment_pending",
4595
+ preparedAt: now,
4596
+ termEndsAt: member.termEndsAt,
4597
+ seatPurchaseEndsAt: purchaseEndsAt,
4598
+ offeringSnapshot: offering,
4599
+ revision: 1,
4600
+ createdAt: now,
4601
+ updatedAt: now
4602
+ };
4603
+ try {
4604
+ await db.transact([
4605
+ membershipUpdate("networkSeat", id7, attrs),
4606
+ membershipUpdate("networkSeatRecipient", claimId, {
4607
+ id: claimId,
4608
+ appId: input.appId,
4609
+ chapterId: input.chapterId,
4610
+ environment: input.environment,
4611
+ runtime: input.runtime,
4612
+ seatId: id7,
4613
+ recipientEmailNormalized: email,
4614
+ createdAt: now
4615
+ }),
4616
+ membershipUpdate("networkMembership", memberId, { namedSeatId: id7, revision: Number(member.revision) + 1, updatedAt: now })
4617
+ ], { mutationId: `named-seat-reserve:${id7}`, guards: [
4618
+ { ns: "networkSeat", id: id7, exists: false },
4619
+ { ns: "networkSeatRecipient", id: claimId, exists: false },
4620
+ { ns: "networkMembership", id: memberId, exists: true, equals: { revision: member.revision }, absent: ["namedSeatId"] },
4621
+ { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
4622
+ ] });
4623
+ } catch (error) {
4624
+ if (error.code !== "transact_guard_failed") throw error;
4625
+ }
4626
+ seat = await membershipLoad(db, "networkSeat", id7);
4627
+ if (!seat && await membershipLoad(db, "networkSeatRecipient", claimId)) membershipFail("named_seat_recipient_unavailable");
4628
+ }
4629
+ requireMembershipScope(seat, input);
4630
+ if (seat.primaryMembershipId !== memberId || seat.recipientName !== name || seat.recipientEmailNormalized !== email || seat.priceId !== input.priceId || seat.purchaserUserId !== input.purchaserUserId) membershipFail("named_seat_purchase_conflict");
4631
+ return seat;
4632
+ }
4633
+
4634
+ // src/membership-seat-included.ts
4635
+ var namedSeatIncluded = (seat) => Number(seat.amountCents) === 0;
4636
+ var namedSeatSettled = (seat) => namedSeatIncluded(seat) ? seat.purchasedAt !== void 0 : Boolean((seat.providerPaymentIntentId || seat.combinedInvoice) && seat.providerSubscriptionItemId);
4637
+ function namedSeatRecipientState(seat) {
4638
+ switch (String(seat.status)) {
4639
+ case "renewal_pending":
4640
+ case "pending_acceptance":
4641
+ return "invited";
4642
+ case "pending_approval":
4643
+ case "pending_primary":
4644
+ return "accepted";
4645
+ case "active":
4646
+ return "joined";
4647
+ case "refund_pending":
4648
+ case "refunded":
4649
+ return seat.refundReason === "declined" ? "declined" : "refunded";
4650
+ case "canceled":
4651
+ return "canceled";
4652
+ default:
4653
+ return "payment_pending";
4654
+ }
4655
+ }
4656
+ var OPEN = ["pending_acceptance", "pending_approval", "pending_primary", "active"];
4657
+ async function grantIncludedNamedSeat(db, input, now) {
4658
+ membershipClock(now);
4659
+ const seat = await reserveNamedMembershipSeat(db, input, now);
4660
+ if (!namedSeatIncluded(seat)) membershipFail("named_seat_not_included");
4661
+ const state = await loadMembershipAuthorityState(db, input);
4662
+ const member = await membershipLoad(db, "networkMembership", seat.primaryMembershipId);
4663
+ requireMembershipScope(member, input);
4664
+ 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)) {
4665
+ membershipFail("named_seat_purchase_unavailable");
4666
+ }
4667
+ if (seat.status === "payment_pending") {
4668
+ const entitlementId = `${seat.id}:entitlement`, inviteId = `${seat.id}:invite`, source = `included:${seat.id}`;
4669
+ try {
4670
+ await db.transact([
4671
+ membershipUpdate("networkSeat", seat.id, {
4672
+ status: "pending_acceptance",
4673
+ purchasedAt: now,
4674
+ termStartedAt: now,
4675
+ termEndsAt: member.termEndsAt,
4676
+ acceptanceEndsAt: Number(member.termEndsAt),
4677
+ entitlementId,
4678
+ revision: Number(seat.revision) + 1,
4679
+ updatedAt: now
4680
+ }),
4681
+ membershipUpdate("networkEntitlement", entitlementId, {
4682
+ id: entitlementId,
4683
+ ...membershipScope(input),
4684
+ membershipId: member.id,
4685
+ seatId: seat.id,
4686
+ kind: "named_seat",
4687
+ status: "pending_acceptance",
4688
+ termStartedAt: now,
4689
+ termEndsAt: member.termEndsAt,
4690
+ offeringSnapshot: member.offeringSnapshot,
4691
+ sourceEventId: source,
4692
+ revision: 1,
4693
+ createdAt: now,
4694
+ updatedAt: now
4695
+ }),
4696
+ membershipUpdate("membershipEffect", inviteId, {
4697
+ id: inviteId,
4698
+ chapterId: input.chapterId,
4699
+ environment: input.environment,
4700
+ runtime: input.runtime,
4701
+ membershipId: member.id,
4702
+ seatId: seat.id,
4703
+ kind: "invite_named_recipient",
4704
+ status: "pending",
4705
+ attempts: 0,
4706
+ createdAt: now,
4707
+ updatedAt: now
4708
+ })
4709
+ ], { mutationId: `named-seat-included:${seat.id}`, guards: [
4710
+ {
4711
+ ns: "networkSeat",
4712
+ id: seat.id,
4713
+ exists: true,
4714
+ equals: { status: "payment_pending", revision: seat.revision, amountCents: 0 },
4715
+ absent: ["purchasedAt", "providerPaymentIntentId"]
4716
+ },
4717
+ { ns: "networkEntitlement", id: entitlementId, exists: false },
4718
+ { ns: "networkMembership", id: String(member.id), exists: true, equals: { revision: member.revision, namedSeatId: seat.id } },
4719
+ { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
4720
+ ] });
4721
+ } catch (error) {
4722
+ if (error.code !== "transact_guard_failed") throw error;
4723
+ }
4724
+ }
4725
+ const current = await membershipLoad(db, "networkSeat", seat.id);
4726
+ requireMembershipScope(current, input);
4727
+ if (!namedSeatIncluded(current) || current.purchasedAt === void 0 || !OPEN.includes(String(current.status))) membershipFail("named_seat_purchase_unavailable");
4728
+ return { seatId: seat.id, amountCents: 0, currency: seat.currency, termEndsAt: Number(current.termEndsAt), status: String(current.status), included: true };
4729
+ }
4730
+
4731
+ // src/membership-seat-refund.ts
4732
+ async function requestNamedSeatRefund(db, input, now) {
4733
+ membershipClock(now);
4734
+ const state = await loadMembershipAuthorityState(db, input);
4735
+ const seat = await membershipLoad(db, "networkSeat", input.seatId);
4736
+ requireMembershipScope(seat, input);
4737
+ const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
4738
+ requireMembershipScope(member, input);
4739
+ if (seat.status === "refunded" || seat.status === "refund_pending") return;
4740
+ 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");
4741
+ const entitlement = seat.entitlementId ? await membershipLoad(db, "networkEntitlement", String(seat.entitlementId)) : void 0;
4742
+ await db.transact([
4743
+ membershipUpdate("networkSeat", input.seatId, { status: "refund_pending", refundReason: input.reason, revision: Number(seat.revision) + 1, updatedAt: now }),
4744
+ ...entitlement ? [membershipUpdate("networkEntitlement", String(entitlement.id), { status: "closed", revision: Number(entitlement.revision) + 1, updatedAt: now })] : [],
4745
+ membershipUpdate("membershipEffect", `${seat.id}:refund`, {
4746
+ id: `${seat.id}:refund`,
4747
+ chapterId: input.chapterId,
4748
+ environment: input.environment,
4749
+ runtime: input.runtime,
4750
+ membershipId: member.id,
4751
+ seatId: seat.id,
4752
+ kind: "refund_seat",
4753
+ status: "pending",
4754
+ attempts: 0,
4755
+ createdAt: now,
4756
+ updatedAt: now
4757
+ }),
4758
+ ...seat.recipientUserId ? [membershipUpdate("membershipEffect", `${seat.id}:revoke`, {
4759
+ id: `${seat.id}:revoke`,
4760
+ chapterId: input.chapterId,
4761
+ environment: input.environment,
4762
+ runtime: input.runtime,
4763
+ membershipId: member.id,
4764
+ seatId: seat.id,
4765
+ kind: "revoke_named_recipient_access",
4766
+ status: "pending",
4767
+ attempts: 0,
4768
+ createdAt: now,
4769
+ updatedAt: now
4770
+ })] : []
4771
+ ], { mutationId: `named-seat-refund-request:${seat.id}`, guards: [
4772
+ { ns: "networkSeat", id: input.seatId, exists: true, equals: { revision: seat.revision } },
4773
+ { ns: "networkMembership", id: String(member.id), exists: true, equals: { revision: member.revision } },
4774
+ { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
4775
+ ...entitlement ? [{ ns: "networkEntitlement", id: String(entitlement.id), exists: true, equals: { revision: entitlement.revision } }] : []
4776
+ ] });
4777
+ }
4778
+ async function completeNamedSeatRefund(db, seatId, secretKey, now) {
4820
4779
  membershipClock(now);
4821
- const limit = input.limit ?? 100;
4822
- if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200) throw new TypeError("invalid reterm limit");
4823
- if (typeof input.policyVersionId !== "string" || !SAFE.test(input.policyVersionId)) throw new TypeError("invalid policy version id");
4824
- 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");
4780
+ const seat = await membershipLoad(db, "networkSeat", seatId);
4781
+ if (!seat) return membershipFail("named_seat_not_found");
4782
+ await loadMembershipAuthorityState(db, seat);
4783
+ if (seat.status === "refunded") return;
4784
+ if (seat.status !== "refund_pending") membershipFail("named_seat_refund_not_ready");
4785
+ const result = namedSeatIncluded(seat) ? { refundedCents: 0, refundIds: [] } : await refundNamedSeatStripePayment(secretKey, seat);
4786
+ await db.transact([
4787
+ membershipUpdate("networkSeat", seatId, {
4788
+ status: "refunded",
4789
+ refundedCents: result.refundedCents,
4790
+ providerRefundIds: result.refundIds,
4791
+ revision: Number(seat.revision) + 1,
4792
+ updatedAt: now
4793
+ }),
4794
+ membershipUpdate("membershipEffect", `${seatId}:refund`, { status: "complete", updatedAt: now })
4795
+ ], { mutationId: `named-seat-refunded:${seatId}`, guards: [
4796
+ { ns: "networkSeat", id: seatId, exists: true, equals: { status: "refund_pending", revision: seat.revision } }
4797
+ ] });
4798
+ }
4799
+
4800
+ // src/membership-seat-operator-refund.ts
4801
+ async function refundNamedSeatByOperator(db, input, stripeSecretKey, now) {
4802
+ const seat = await membershipLoad(db, "networkSeat", input.seatId);
4803
+ requireMembershipScope(seat, input);
4804
+ await requestNamedSeatRefund(db, { ...input, reason: "operator" }, now);
4805
+ await completeNamedSeatRefund(db, input.seatId, stripeSecretKey, now);
4806
+ const refunded = await membershipLoad(db, "networkSeat", input.seatId);
4807
+ if (refunded?.status !== "refunded" || !Number.isSafeInteger(refunded.refundedCents) || Number(refunded.refundedCents) !== Number(seat.amountCents)) {
4808
+ throw new Error("named seat refund unconfirmed");
4809
+ }
4810
+ return { seatId: String(seat.id), status: "refunded", refundedCents: Number(refunded.refundedCents), subscriptionCanceled: false };
4811
+ }
4812
+
4813
+ // src/membership-seat-window.ts
4814
+ async function liftSeatPurchaseWindow(db, scope2, now, limit = 100) {
4815
+ membershipClock(now);
4816
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200) throw new TypeError("invalid lift limit");
4825
4817
  const state = await loadMembershipAuthorityState(db, scope2);
4826
- const policy2 = await retermPolicy(db, scope2, input.policyVersionId);
4827
- const receipt = { policyVersionId: policy2.id, retermed: [], unchanged: [], skipped: [], more: false };
4828
- const skip = (membershipId, reason) => {
4829
- receipt.skipped.push({ membershipId, reason });
4830
- };
4831
- let rows5;
4832
- if (input.membershipIds) {
4833
- rows5 = [];
4834
- for (const id7 of input.membershipIds) {
4835
- const member = await membershipLoad(db, "networkMembership", id7);
4836
- if (!member) skip(id7, "not_found");
4837
- else if (Object.entries(scope2).some(([key3, value]) => member[key3] !== value)) skip(id7, "scope_mismatch");
4838
- else rows5.push(member);
4839
- }
4840
- } else {
4841
- const found = (await db.query({ networkMembership: { $: { where: { ...scope2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkMembership ?? [];
4842
- if (found.some((member) => Object.entries(scope2).some(([key3, value]) => member[key3] !== value))) throw new Error("membership reterm scope mismatch");
4843
- receipt.more = found.length > limit;
4844
- rows5 = found.slice(0, limit);
4818
+ const rows5 = (await db.query({ networkMembership: { $: { where: { ...scope2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkMembership ?? [];
4819
+ let lifted = 0;
4820
+ for (const member of rows5.slice(0, limit)) {
4821
+ if (Object.entries(scope2).some(([key3, value]) => member[key3] !== value)) throw new Error("membership lift scope mismatch");
4822
+ const offering = membershipOfferingVersion(member.offeringSnapshot);
4823
+ if (offering.seatPurchaseWindowCalendarMonths === void 0) continue;
4824
+ const { seatPurchaseWindowCalendarMonths: _window, ...windowless } = offering;
4825
+ await db.transact([membershipUpdate("networkMembership", String(member.id), {
4826
+ offeringSnapshot: windowless,
4827
+ seatPurchaseEndsAt: Number(member.termEndsAt),
4828
+ revision: Number(member.revision) + 1,
4829
+ updatedAt: now
4830
+ })], { mutationId: `seat-window-lift:${member.id}:${member.revision}`, guards: [
4831
+ { ns: "networkMembership", id: String(member.id), exists: true, equals: { revision: member.revision } },
4832
+ { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative" } }
4833
+ ] });
4834
+ lifted++;
4845
4835
  }
4846
- for (const member of rows5) {
4847
- const id7 = String(member.id);
4848
- if (member.discountPolicyVersionId === policy2.id) {
4849
- receipt.unchanged.push(id7);
4850
- continue;
4851
- }
4852
- if (!RETERMABLE.has(String(member.status))) {
4853
- skip(id7, "status");
4854
- continue;
4855
- }
4856
- if (member.founding !== true) {
4857
- skip(id7, "not_founding");
4858
- continue;
4859
- }
4860
- if (member.cohort !== policy2.cohortLabel) {
4861
- skip(id7, "cohort_mismatch");
4836
+ return { lifted, more: rows5.length > limit };
4837
+ }
4838
+
4839
+ // src/membership-seat-reprice.ts
4840
+ async function lowerSeatPrice(db, scope2, now, limit = 100) {
4841
+ membershipClock(now);
4842
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200) throw new TypeError("invalid reprice limit");
4843
+ const state = await loadMembershipAuthorityState(db, scope2);
4844
+ const versions = (await db.query({ membershipOfferingVersion: { $: { where: { chapterId: scope2.chapterId }, limit: 200 } } })).membershipOfferingVersion ?? [];
4845
+ if (versions.length >= 200) throw Object.assign(new Error("policy_history_incomplete"), { code: "policy_history_incomplete" });
4846
+ 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];
4847
+ if (!current) throw Object.assign(new Error("ambiguous_membership_policy"), { code: "ambiguous_membership_policy" });
4848
+ const target = current.seatPriceCents;
4849
+ const rows5 = (await db.query({ networkMembership: { $: { where: { ...scope2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkMembership ?? [];
4850
+ const receipt = { seatPriceCents: target, repriced: 0, skipped: [], more: rows5.length > limit };
4851
+ for (const member of rows5.slice(0, limit)) {
4852
+ if (Object.entries(scope2).some(([key3, value]) => member[key3] !== value)) throw new Error("membership reprice scope mismatch");
4853
+ const id7 = String(member.id), skip = (reason) => {
4854
+ receipt.skipped.push({ membershipId: id7, reason });
4855
+ };
4856
+ const offering = membershipOfferingVersion(member.offeringSnapshot);
4857
+ if (offering.seatPriceCents <= target) {
4858
+ skip("seat price already at or below the current offering");
4862
4859
  continue;
4863
4860
  }
4864
- const skuId = String(member.baseSkuId);
4865
- if (!policy2.discountEligibleSkuIds.includes(skuId)) {
4866
- skip(id7, "sku_not_discounted");
4861
+ if (!["paid_pending_approval", "active", "cancel_at_term_end", "payment_grace"].includes(String(member.status))) {
4862
+ skip(`membership is ${String(member.status)}`);
4867
4863
  continue;
4868
4864
  }
4869
- const base = Number(member.baseAmountCents), current = Number(member.renewalAmountCents);
4870
- const renewal = Math.round(base * (1e4 - policy2.percentOffBps) / 1e4);
4871
- if (!Number.isSafeInteger(base) || base <= 0 || !Number.isSafeInteger(current) || renewal > current) {
4872
- skip(id7, "would_not_widen");
4865
+ const seat = member.namedSeatId ? await membershipLoad(db, "networkSeat", String(member.namedSeatId)) : void 0;
4866
+ if (seat && (seat.status !== "payment_pending" || seat.providerPaymentIntentId !== void 0)) {
4867
+ skip(`seat is ${String(seat.status)}`);
4873
4868
  continue;
4874
4869
  }
4875
- const couponId = await membershipDiscountCouponId({ ...scope2, skuId }, policy2.id);
4876
- let providerUpdated;
4877
- try {
4878
- const productId = await verifySubscriptionPrice(stripeSecretKey, {
4879
- priceId: String(member.providerPriceId),
4880
- priceCents: base,
4881
- currency: String(member.currency),
4882
- interval: member.interval === "month" ? "month" : "year",
4883
- environment: scope2.environment
4884
- }, true);
4885
- if (!productId) throw Object.assign(new Error("price has no product"), { code: "provider_mismatch" });
4886
- await prepareSubscriptionBillingTerms(stripeSecretKey, {
4887
- ...scope2,
4888
- priceCents: base,
4889
- currency: String(member.currency),
4890
- discountProductId: productId,
4891
- billingTerms: {
4892
- offeringVersionId: String(member.offeringVersionId),
4893
- discount: { policyVersionId: policy2.id, couponId, percentOffBps: policy2.percentOffBps, duration: "forever" }
4894
- }
4895
- }, false);
4896
- providerUpdated = await attachProviderDiscount(stripeSecretKey, scope2, member, policy2, couponId, productId);
4897
- } catch (error) {
4898
- const code = error.code;
4899
- if (code === "provider_mismatch" || code === "stripe_contract_mismatch") {
4900
- skip(id7, "provider_mismatch");
4901
- continue;
4902
- }
4903
- throw error;
4904
- }
4905
- try {
4906
- await db.transact([membershipUpdate("networkMembership", id7, {
4907
- discountPolicyVersionId: policy2.id,
4908
- discountSnapshot: policy2,
4909
- renewalAmountCents: renewal,
4910
- revision: Number(member.revision) + 1,
4911
- updatedAt: now
4912
- })], {
4913
- mutationId: `membership-reterm:${id7}:${policy2.id}:${member.revision}`,
4914
- guards: [
4915
- { ns: "networkMembership", id: id7, exists: true, equals: { revision: member.revision, status: member.status } },
4916
- { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative" } },
4917
- { ns: "discountPolicyVersion", id: policy2.id, exists: true }
4918
- ]
4919
- });
4920
- } catch (error) {
4921
- if (error.code !== "transact_guard_failed") throw error;
4922
- return membershipFail("membership_reterm_retry_required");
4870
+ const ops = [membershipUpdate("networkMembership", id7, { offeringSnapshot: { ...offering, seatPriceCents: target }, revision: Number(member.revision) + 1, updatedAt: now })];
4871
+ const guards = [
4872
+ { ns: "networkMembership", id: id7, exists: true, equals: { revision: member.revision } },
4873
+ { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative" } }
4874
+ ];
4875
+ if (seat) {
4876
+ ops.push(membershipUpdate("networkSeat", String(seat.id), { amountCents: target, offeringSnapshot: { ...offering, seatPriceCents: target }, revision: Number(seat.revision) + 1, updatedAt: now }));
4877
+ guards.push({ ns: "networkSeat", id: String(seat.id), exists: true, equals: { revision: seat.revision, status: "payment_pending" } });
4923
4878
  }
4924
- receipt.retermed.push({
4925
- membershipId: id7,
4926
- ...typeof member.discountPolicyVersionId === "string" ? { previousPolicyVersionId: member.discountPolicyVersionId } : {},
4927
- previousRenewalAmountCents: current,
4928
- renewalAmountCents: renewal,
4929
- providerUpdated
4930
- });
4879
+ await db.transact(ops, { mutationId: `seat-reprice:${id7}:${member.revision}`, guards });
4880
+ receipt.repriced++;
4931
4881
  }
4932
4882
  return receipt;
4933
4883
  }
4934
4884
 
4935
- // src/worker-routes-admin-membership-reterm.ts
4936
- var PATH4 = "/api/admin/membership-discount/reterm";
4937
- var SCOPE3 = /^[a-z0-9][a-z0-9-]{1,62}$/;
4938
- var ID = /^[A-Za-z0-9][A-Za-z0-9_.:-]{2,159}$/;
4939
- var BAD = "chapterId and policyVersionId are required; runtime, appId, membershipIds and limit are optional";
4940
- var handleAdminMembershipReterm = async (req, url, env, ctx) => {
4941
- if (url.pathname !== PATH4) return null;
4885
+ // src/worker-routes-admin-membership-seats.ts
4886
+ var LIST = "/api/admin/membership-seats";
4887
+ var LIFT = "/api/admin/membership-seats/lift-purchase-window";
4888
+ var REPRICE = "/api/admin/membership-seats/reprice";
4889
+ var ONE = /^\/api\/admin\/membership-seats\/([A-Za-z0-9][A-Za-z0-9_.:-]{2,159})\/refund$/;
4890
+ var SCOPE = /^[a-z0-9][a-z0-9-]{1,62}$/;
4891
+ var PUBLIC = [
4892
+ "id",
4893
+ "appId",
4894
+ "chapterId",
4895
+ "environment",
4896
+ "runtime",
4897
+ "applicationId",
4898
+ "skuId",
4899
+ "status",
4900
+ "amountCents",
4901
+ "currency",
4902
+ "recipientName",
4903
+ "recipientEmailNormalized",
4904
+ "purchaserUserId",
4905
+ "purchasedAt",
4906
+ "acceptedAt",
4907
+ "journeyApprovedAt",
4908
+ "refundedCents",
4909
+ "refundReason",
4910
+ "termEndsAt",
4911
+ "updatedAt"
4912
+ ];
4913
+ var project = (row2) => Object.fromEntries(PUBLIC.filter((key3) => row2[key3] !== void 0).map((key3) => [key3, row2[key3]]));
4914
+ var handleAdminMembershipSeats = async (req, url, env, ctx) => {
4915
+ const one = url.pathname.match(ONE), lift = url.pathname === LIFT, reprice = url.pathname === REPRICE;
4916
+ if (url.pathname !== LIST && !one && !lift && !reprice) return null;
4942
4917
  const operator = ctx.membershipAuthorityOperator;
4943
4918
  if (!operator) return json({ error: "not found" }, 404);
4944
- if (req.method !== "POST") return json({ error: "method not allowed" }, 405);
4919
+ if (req.method !== (one || lift || reprice ? "POST" : "GET")) return json({ error: "method not allowed" }, 405);
4945
4920
  const raw2 = ctx.makeDb(env), user = await ctx.verifyUser(req, env);
4946
4921
  if (!user) return json({ error: "unauthorized" }, 401);
4947
4922
  if (!await ctx.isSuperAdminEmail(raw2, user.email, user.userId)) return json({ error: "super-admin required", code: "super_admin_required" }, 403);
@@ -4951,267 +4926,427 @@ var handleAdminMembershipReterm = async (req, url, env, ctx) => {
4951
4926
  } catch {
4952
4927
  return json({ error: "the membership authority is not hosted by this deployment" }, 404);
4953
4928
  }
4954
- let body;
4955
- try {
4956
- body = await req.json();
4957
- } catch {
4958
- return json({ error: BAD }, 400);
4929
+ if (lift || reprice) {
4930
+ let body;
4931
+ try {
4932
+ body = await req.json();
4933
+ } catch {
4934
+ return json({ error: "chapterId and runtime are required" }, 400);
4935
+ }
4936
+ const appId = body.appId ?? body.chapterId;
4937
+ 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))) {
4938
+ return json({ error: reprice ? "chapterId, runtime and skuId are required" : "chapterId and runtime are required" }, 400);
4939
+ }
4940
+ const scope2 = { appId, chapterId: body.chapterId, environment: env.ODLA_ENV, runtime: body.runtime };
4941
+ try {
4942
+ if (reprice) return json({ ok: true, ...await lowerSeatPrice(db, { ...scope2, skuId: String(body.skuId) }, Date.now()) });
4943
+ return json({ ok: true, ...await liftSeatPurchaseWindow(db, scope2, Date.now()) });
4944
+ } catch (error) {
4945
+ const code = error.code;
4946
+ return json({ error: `${reprice ? "seat reprice" : "purchase window lift"} is pending; retry to confirm`, ...code ? { code } : {} }, code ? 409 : 503);
4947
+ }
4959
4948
  }
4960
- const appId = body.appId ?? body.chapterId, runtime = body.runtime ?? env.ODLA_RUNTIME;
4961
- 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)) {
4962
- return json({ error: BAD }, 400);
4949
+ if (!one) {
4950
+ const rows5 = (await db.query({ networkSeat: { $: { where: { environment: env.ODLA_ENV }, order: { updatedAt: "desc" }, limit: 201 } } })).networkSeat ?? [];
4951
+ return json({ seats: rows5.slice(0, 200).map(project), truncated: rows5.length > 200 });
4952
+ }
4953
+ const seat = await membershipLoad(db, "networkSeat", one[1]);
4954
+ if (!seat || seat.environment !== env.ODLA_ENV) return json({ error: "not found" }, 404);
4955
+ if (seat.status === "refunded") {
4956
+ return json({ ok: true, seatId: seat.id, status: "refunded", refundedCents: seat.refundedCents, subscriptionCanceled: false, replayed: true });
4963
4957
  }
4958
+ if (!seat.purchasedAt) return json({ error: "seat has no completed payment to refund", code: "seat_not_refundable" }, 409);
4964
4959
  const secret = await getVaultSecret(db, operator.stripeSecretName ?? "stripe_secret_key");
4965
4960
  if (!secret) return json({ error: "network payment provider unavailable" }, 503);
4966
4961
  try {
4967
- const receipt = await retermMembershipDiscount(
4968
- db,
4969
- secret,
4970
- { appId, chapterId: body.chapterId, environment: env.ODLA_ENV, runtime },
4971
- {
4972
- policyVersionId: body.policyVersionId,
4973
- ...body.membershipIds ? { membershipIds: body.membershipIds } : {},
4974
- ...body.limit !== void 0 ? { limit: Number(body.limit) } : {}
4975
- },
4976
- Date.now()
4977
- );
4962
+ const receipt = await refundNamedSeatByOperator(db, {
4963
+ appId: String(seat.appId),
4964
+ chapterId: String(seat.chapterId),
4965
+ environment: String(seat.environment),
4966
+ runtime: String(seat.runtime),
4967
+ applicationId: String(seat.applicationId),
4968
+ skuId: String(seat.skuId),
4969
+ seatId: String(seat.id)
4970
+ }, secret, Date.now());
4978
4971
  return json({ ok: true, ...receipt });
4979
4972
  } catch (error) {
4980
4973
  const code = error.code;
4981
- return json({ error: "re-term is pending; retry to confirm", ...code ? { code } : {} }, code ? 409 : 503);
4982
- }
4983
- };
4984
-
4985
- // src/worker-routes-membership-cancel.ts
4986
- var handleMembershipCancellation = async (req, url, env, ctx) => {
4987
- const match = url.pathname.match(/^\/api\/admin\/applications\/([0-9a-f-]+)\/cancel-renewal$/);
4988
- if (!match) return null;
4989
- if (req.method !== "POST") return json({ error: "method not allowed" }, 405);
4990
- const user = await ctx.verifyUser(req, env), raw2 = ctx.makeDb(env);
4991
- if (!user) return json({ error: "unauthorized" }, 401);
4992
- if (!await ctx.isAdmin(raw2, user)) return json({ error: "forbidden" }, 403);
4993
- if (!ctx.membershipAuthority?.cancelRenewal) return json({ error: "renewal cancellation unavailable" }, 503);
4994
- const db = raw2, app = await membershipLoad(db, "applications", match[1]);
4995
- if (!app || !chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME)) return json({ error: "not found" }, 404);
4996
- if (app.namedSeatId || app.giftClaimId) return json({ error: "this membership has no independently renewing subscription" }, 409);
4997
- const intent = await membershipLoad(db, "subscriptionCheckoutIntents", String(app.id));
4998
- if (!intent) return json({ error: "membership history needs reconciliation" }, 409);
4999
- try {
5000
- const target = await membershipTargetForIntent(intent, app, env, ctx);
5001
- const receipt = await ctx.membershipAuthority.cancelRenewal(target, env);
5002
- 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");
5003
- await db.transact([{ t: "update", ns: "applications", id: String(app.id), attrs: {
5004
- membershipStatus: receipt.status,
5005
- membershipAutoRenew: false,
5006
- membershipGraceEndsAt: 0,
5007
- renewalAt: receipt.termEndsAt
5008
- } }], { guards: [{
5009
- ns: "applications",
5010
- id: String(app.id),
5011
- exists: true,
5012
- equals: { status: app.status, stripeSubscriptionId: app.stripeSubscriptionId }
5013
- }] });
5014
- return json({ ok: true, accessEndsAt: receipt.termEndsAt, status: receipt.status });
5015
- } catch {
5016
- return json({ error: "renewal cancellation is pending; retry to confirm" }, 503);
4974
+ return json({ error: "seat refund is pending; retry to confirm", ...code ? { code } : {} }, code ? 409 : 503);
5017
4975
  }
5018
4976
  };
5019
4977
 
5020
- // src/scheduling.ts
5021
- var SCHEDULING_DEFAULTS = {
5022
- slotMinutes: 45,
5023
- days: [1, 2, 3, 4, 5],
5024
- startHour: 9,
5025
- endHour: 17,
5026
- timezone: "America/Los_Angeles",
5027
- minNoticeHours: 24,
5028
- windowDays: 14,
5029
- summaryTemplate: "Introduction call with {{firstName}} {{lastName}}"
4978
+ // src/membership-policy-versions.ts
4979
+ var LIMIT = 200;
4980
+ var text2 = (value) => typeof value === "string" && value.length > 0 && value.length <= 200 ? value : void 0;
4981
+ var instant = (value) => {
4982
+ const at2 = typeof value === "number" ? value : typeof value === "string" ? Date.parse(value) : NaN;
4983
+ return Number.isSafeInteger(at2) && at2 >= 0 ? at2 : void 0;
5030
4984
  };
5031
- function isValidTimeZone(tz) {
5032
- try {
5033
- new Intl.DateTimeFormat(void 0, { timeZone: tz });
5034
- return true;
5035
- } catch {
5036
- return false;
5037
- }
4985
+ function provenance(row2) {
4986
+ const source = text2(row2.source), actor = text2(row2.actor ?? row2.$createdBy), recordedAt = instant(row2.recordedAt ?? row2.createdAt ?? row2.$createdAt);
4987
+ return { ...source ? { source } : {}, ...actor ? { actor } : {}, ...recordedAt !== void 0 ? { recordedAt } : {} };
5038
4988
  }
5039
- function resolveScheduling(config) {
5040
- const result = validateScheduling(config);
5041
- if (result.ok) return result.value;
5042
- const detail = Object.entries(result.errors).map(([field, message]) => `${field}: ${message}`).join(" ");
5043
- throw new Error(`scheduling: ${detail}`);
4989
+ var live = (w, now) => w.from <= now && (w.until === void 0 || now < w.until);
4990
+ function stateOf(item, all, now, window, rivals) {
4991
+ if (item.status !== "published") return { state: item.status };
4992
+ const mine = window(item);
4993
+ if (now < mine.from) return { state: "scheduled" };
4994
+ const closed = mine.until !== void 0 && mine.until <= now;
4995
+ 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));
4996
+ if (!closed && !higher.some((w) => live(w, now))) return { state: "active" };
4997
+ const dates = higher.map((w) => Math.max(w.from, mine.from));
4998
+ if (closed) dates.push(mine.until);
4999
+ return { state: "superseded", supersededAt: Math.min(...dates) };
5044
5000
  }
5045
- function validateScheduling(config) {
5046
- const d = config ?? {};
5047
- const c = {
5048
- slotMinutes: d.slotMinutes ?? SCHEDULING_DEFAULTS.slotMinutes,
5049
- days: d.days ?? SCHEDULING_DEFAULTS.days,
5050
- startHour: d.startHour ?? SCHEDULING_DEFAULTS.startHour,
5051
- endHour: d.endHour ?? SCHEDULING_DEFAULTS.endHour,
5052
- timezone: d.timezone ?? SCHEDULING_DEFAULTS.timezone,
5053
- minNoticeHours: d.minNoticeHours ?? SCHEDULING_DEFAULTS.minNoticeHours,
5054
- windowDays: d.windowDays ?? SCHEDULING_DEFAULTS.windowDays,
5055
- summaryTemplate: d.summaryTemplate ?? SCHEDULING_DEFAULTS.summaryTemplate
5056
- };
5057
- const errors = {};
5058
- if (!(c.slotMinutes >= 15 && c.slotMinutes <= 240)) {
5059
- errors.slotMinutes = "Slot length must be between 15 and 240 minutes.";
5060
- }
5061
- if (!(c.windowDays >= 1 && c.windowDays <= 62)) {
5062
- errors.windowDays = "Booking window must be between 1 and 62 days (the calendar caps look-ahead at 62).";
5063
- }
5064
- if (!(c.minNoticeHours >= 0 && c.minNoticeHours <= 336)) {
5065
- errors.minNoticeHours = "Minimum notice must be between 0 and 336 hours.";
5066
- }
5067
- if (!(c.startHour >= 0 && c.startHour < c.endHour && c.endHour <= 24)) {
5068
- errors.hours = "Hours must satisfy 0 \u2264 start < end \u2264 24.";
5069
- }
5070
- const days = [...c.days];
5071
- if (!days.length) errors.days = "Pick at least one day.";
5072
- else if (!days.every((n) => Number.isInteger(n) && n >= 0 && n <= 6)) {
5073
- errors.days = "Days must be weekday numbers, 0 (Sunday) through 6 (Saturday).";
5074
- }
5075
- if (typeof c.timezone !== "string" || !isValidTimeZone(c.timezone)) {
5076
- errors.timezone = `"${String(c.timezone)}" is not a valid IANA timezone (for example "America/Los_Angeles").`;
5001
+ 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";
5002
+ var sharesSku = (a, b) => a.cohortEligibleSkuIds.some((sku) => b.cohortEligibleSkuIds.includes(sku));
5003
+ var byChapter = (a, b) => a.chapterId.localeCompare(b.chapterId);
5004
+ function normalize(rows5, collection, parse, invalid2) {
5005
+ const out = [];
5006
+ for (const row2 of rows5) {
5007
+ try {
5008
+ out.push({ value: parse(row2.snapshot), row: row2 });
5009
+ } catch (error) {
5010
+ invalid2.push({ collection, id: String(row2.id ?? ""), error: error instanceof Error ? error.message : String(error) });
5011
+ }
5077
5012
  }
5078
- if (typeof c.summaryTemplate !== "string") errors.summaryTemplate = "Calendar summary template must be text.";
5079
- return Object.keys(errors).length > 0 ? { ok: false, errors } : { ok: true, value: { ...c, days } };
5080
- }
5081
- function slotWindow(now, windowDays) {
5082
- return { from: now, to: now + windowDays * 864e5 };
5083
- }
5084
- function endForSlot(startAt, slotMinutes) {
5085
- return startAt + slotMinutes * 6e4;
5086
- }
5087
- function isSlotAvailable(slots, startAt) {
5088
- return slots.some((s) => s.startAt === startAt);
5089
- }
5090
- function renderSummary(template, app) {
5091
- return template.replace("{{firstName}}", app.firstName ?? "").replace("{{lastName}}", app.lastName ?? "");
5092
- }
5093
- function bookingDecision(existing) {
5094
- const eventId = existing?.googleEventId ?? null;
5095
- return { reschedule: Boolean(eventId), eventId };
5096
- }
5097
- function introIdempotencyKey(applicationId) {
5098
- return `application:${applicationId}:intro`;
5013
+ return out;
5099
5014
  }
5100
- function meetingCreateRow(i) {
5015
+ async function listMembershipPolicyVersions(db, input) {
5016
+ const where = input.chapterId ? { chapterId: input.chapterId } : {};
5017
+ const rows5 = await db.query({
5018
+ membershipOfferingVersion: { $: { where, limit: LIMIT } },
5019
+ discountPolicyVersion: { $: { where, limit: LIMIT } }
5020
+ });
5021
+ const offeringRows = rows5.membershipOfferingVersion ?? [], policyRows = rows5.discountPolicyVersion ?? [];
5022
+ const invalid2 = [];
5023
+ const offerings = normalize(offeringRows, "membershipOfferingVersion", membershipOfferingVersion, invalid2);
5024
+ const policies = normalize(policyRows, "discountPolicyVersion", membershipDiscountPolicyVersion, invalid2);
5025
+ const offeringValues = offerings.map((item) => item.value), policyValues = policies.map((item) => item.value);
5101
5026
  return {
5102
- id: i.meetingId,
5103
- applicationId: i.applicationId,
5104
- groupId: i.groupId,
5105
- startAt: i.startAt,
5106
- endAt: i.endAt,
5107
- timezone: i.timezone,
5108
- status: "scheduled",
5109
- googleEventId: i.googleEventId,
5110
- ...i.meetUrl ? { meetUrl: i.meetUrl } : {},
5111
- ...i.htmlLink ? { htmlLink: i.htmlLink } : {},
5112
- drift: "none",
5113
- createdAt: i.createdAt
5027
+ now: input.now,
5028
+ chapterId: input.chapterId ?? null,
5029
+ offerings: offerings.map(({ value, row: row2 }) => ({
5030
+ ...value,
5031
+ ...provenance(row2),
5032
+ ...stateOf(
5033
+ value,
5034
+ offeringValues,
5035
+ input.now,
5036
+ (v) => ({ from: v.effectiveFrom, ...v.effectiveUntil !== void 0 ? { until: v.effectiveUntil } : {} }),
5037
+ (a, b) => a.chapterId === b.chapterId && a.baseSkuId === b.baseSkuId
5038
+ )
5039
+ })).sort((a, b) => byChapter(a, b) || a.baseSkuId.localeCompare(b.baseSkuId) || b.version - a.version),
5040
+ discountPolicies: policies.map(({ value, row: row2 }) => ({
5041
+ ...value,
5042
+ ...provenance(row2),
5043
+ mechanism: mechanism(value),
5044
+ ...stateOf(
5045
+ value,
5046
+ policyValues,
5047
+ input.now,
5048
+ (v) => ({ from: v.qualifyingStartsAt, until: v.qualifyingEndsAt }),
5049
+ (a, b) => a.chapterId === b.chapterId && sharesSku(a, b)
5050
+ )
5051
+ })).sort((a, b) => byChapter(a, b) || b.version - a.version),
5052
+ invalid: invalid2,
5053
+ truncated: offeringRows.length >= LIMIT || policyRows.length >= LIMIT
5114
5054
  };
5115
5055
  }
5116
- function meetingRescheduleUpdate(startAt, endAt) {
5117
- return { startAt, endAt, drift: "none" };
5056
+
5057
+ // src/worker-routes-admin-membership-policy.ts
5058
+ var PATH3 = "/api/admin/membership-policy-versions";
5059
+ var SCOPE2 = /^[a-z0-9][a-z0-9-]{1,62}$/;
5060
+ var handleAdminMembershipPolicyVersions = async (req, url, env, ctx) => {
5061
+ if (url.pathname !== PATH3) return null;
5062
+ const operator = ctx.membershipAuthorityOperator;
5063
+ if (!operator) return json({ error: "not found" }, 404);
5064
+ if (req.method !== "GET") return json({ error: "method not allowed" }, 405);
5065
+ const raw2 = ctx.makeDb(env), user = await ctx.verifyUser(req, env);
5066
+ if (!user) return json({ error: "unauthorized" }, 401);
5067
+ if (!await ctx.isSuperAdminEmail(raw2, user.email, user.userId)) return json({ error: "super-admin required", code: "super_admin_required" }, 403);
5068
+ const chapterId = url.searchParams.get("chapterId");
5069
+ if (chapterId !== null && !SCOPE2.test(chapterId)) return json({ error: "chapterId is invalid" }, 400);
5070
+ let db;
5071
+ try {
5072
+ db = operator.makeDb(env);
5073
+ } catch {
5074
+ return json({ error: "the membership authority is not hosted by this deployment" }, 404);
5075
+ }
5076
+ return json(await listMembershipPolicyVersions(db, { chapterId, now: Date.now() }));
5077
+ };
5078
+
5079
+ // src/membership-discount-reterm.ts
5080
+ import { stripeRequest as stripeRequest7 } from "@odla-ai/stripe";
5081
+
5082
+ // src/subscription-provider-contract.ts
5083
+ import { stripeRequest as stripeRequest6 } from "@odla-ai/stripe";
5084
+ function subscriptionContractMismatch() {
5085
+ throw Object.assign(new Error("Stripe subscription does not match the checkout contract"), { code: "stripe_contract_mismatch" });
5118
5086
  }
5119
- function applicationBookingUpdate(currentStatus, startAt, htmlLink) {
5120
- return {
5121
- meetingAt: startAt,
5122
- ...htmlLink ? { meetingLink: htmlLink } : {},
5123
- ...currentStatus !== "call_scheduled" ? { status: "call_scheduled" } : {}
5124
- };
5087
+ async function verifySubscriptionPrice(secretKey, input, resume = false) {
5088
+ 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();
5089
+ const result = await stripeRequest6({ secretKey }, "GET", `/v1/prices/${encodeURIComponent(input.priceId)}`);
5090
+ const price = result.body;
5091
+ 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();
5092
+ return typeof price.product === "string" ? price.product : void 0;
5093
+ }
5094
+ function verifySubscriptionResult(sub, input, customerId, metadata) {
5095
+ const items = sub?.items;
5096
+ const seat = input.namedSeat?.amountCents ? input.namedSeat : void 0;
5097
+ const primary = items?.data?.find((item) => item.price?.id === input.priceId);
5098
+ const seatItem = seat && items?.data?.find((item) => item.price?.id === seat.priceId);
5099
+ 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();
5100
+ const invoice = sub.latest_invoice;
5101
+ 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();
5102
+ verifySubscriptionBillingResult(sub, input);
5125
5103
  }
5126
5104
 
5127
- // src/session.ts
5128
- function applicationSummary(app) {
5129
- return {
5130
- id: app.id,
5131
- firstName: app.firstName ?? null,
5132
- lastName: app.lastName ?? null,
5133
- email: app.email ?? null,
5134
- status: app.status,
5135
- createdAt: app.createdAt ?? null,
5136
- meetingLink: app.meetingLink ?? null,
5137
- paid: Boolean(app.stripeSubscriptionId) && app.status !== "refunded" && app.canceled !== true && !["lapsed", "refunded", "refund_pending", "canceled", "restarting"].includes(app.membershipStatus ?? ""),
5138
- ...app.membershipStatus ? { membershipStatus: app.membershipStatus, membershipGraceEndsAt: app.membershipGraceEndsAt, membershipAutoRenew: app.membershipAutoRenew } : {},
5139
- renewalAt: app.renewalAt ?? null,
5140
- canceled: app.canceled === true,
5141
- tierId: app.tierId ?? null
5142
- };
5105
+ // src/membership-discount-reterm.ts
5106
+ var SAFE = /^[A-Za-z0-9][A-Za-z0-9_.:-]{2,159}$/;
5107
+ var RETERMABLE = /* @__PURE__ */ new Set(["active", "paid_pending_approval", "cancel_at_term_end", "payment_grace"]);
5108
+ var PROVIDER_LIVE = /* @__PURE__ */ new Set(["active", "trialing", "past_due"]);
5109
+ var couponOf = (discount) => {
5110
+ const ref = discount?.source?.coupon ?? discount?.coupon;
5111
+ return typeof ref === "string" ? ref : typeof ref?.id === "string" ? ref.id : void 0;
5112
+ };
5113
+ async function retermPolicy(db, scope2, id7) {
5114
+ const row2 = await membershipLoad(db, "discountPolicyVersion", id7);
5115
+ if (!row2) membershipFail("membership_reterm_policy_unknown");
5116
+ let policy2;
5117
+ try {
5118
+ policy2 = membershipDiscountPolicyVersion(row2.snapshot);
5119
+ } catch {
5120
+ return membershipFail("membership_reterm_policy_ineligible");
5121
+ }
5122
+ 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");
5123
+ return policy2;
5143
5124
  }
5144
- function memberApplication(app, meeting, defaultTimezone) {
5145
- const summary = applicationSummary(app);
5146
- let meetingAt = app.meetingAt ?? null;
5147
- let meetUrl = null;
5148
- let timezone = defaultTimezone;
5149
- if (meeting) {
5150
- timezone = meeting.timezone ?? timezone;
5151
- if (meeting.status === "scheduled") {
5152
- meetingAt = meeting.startAt ?? null;
5153
- meetUrl = meeting.meetUrl ?? null;
5154
- } else {
5155
- meetingAt = null;
5156
- }
5125
+ async function attachProviderDiscount(sk, scope2, member, policy2, couponId, productId) {
5126
+ const subId = String(member.providerSubscriptionId), path = `/v1/subscriptions/${encodeURIComponent(subId)}`;
5127
+ const read2 = await stripeRequest7({ secretKey: sk }, "GET", path, { "expand[0]": "discounts" });
5128
+ const sub = read2.body, items = sub?.items?.data;
5129
+ const metadata = { ...stripePartitionMetadata(scope2), applicationId: String(member.applicationId) };
5130
+ 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) {
5131
+ throw Object.assign(new Error("provider subscription does not match the frozen membership"), { code: "provider_mismatch" });
5157
5132
  }
5158
- return { ...summary, meetingAt, meetUrl, timezone };
5133
+ const discounts = sub.discounts ?? (sub.discount ? [sub.discount] : []);
5134
+ if (!Array.isArray(discounts)) throw Object.assign(new Error("provider discounts unreadable"), { code: "provider_mismatch" });
5135
+ const present = discounts.map(couponOf);
5136
+ if (present.length === 1 && present[0] === couponId) return false;
5137
+ if (present.length > 0) throw Object.assign(new Error("provider subscription carries an unrelated discount"), { code: "provider_mismatch" });
5138
+ const written = await stripeRequest7({ secretKey: sk }, "POST", path, {
5139
+ "discounts[0][coupon]": couponId,
5140
+ "expand[0]": "discounts",
5141
+ "metadata[discountPolicyVersionId]": policy2.id
5142
+ }, { idempotencyKey: `membership-reterm:${String(member.id)}:${policy2.id}` });
5143
+ const after = written.body?.discounts;
5144
+ if (!written.ok || written.body?.id !== subId || !Array.isArray(after) || after.length !== 1 || couponOf(after[0]) !== couponId) {
5145
+ membershipFail("membership_reterm_provider_unconfirmed");
5146
+ }
5147
+ return true;
5159
5148
  }
5160
-
5161
- // src/member-session-application.ts
5162
- var local = (row2, chapterId, runtime) => row2.groupId === chapterId && (runtime ? row2.stripeRuntime === runtime : row2.stripeRuntime === void 0 || row2.stripeRuntime === "default");
5163
- async function memberSessionApplication(db, chapterId, runtime, identity2) {
5164
- const lookup = async (where) => {
5165
- const rows5 = (await db.query({ applications: { $: { where: { groupId: chapterId, ...where }, limit: 100 } } })).applications;
5166
- if (!Array.isArray(rows5) || rows5.length >= 100) throw new Error("member identity lookup incomplete");
5167
- return rows5.filter((row2) => local(row2, chapterId, runtime));
5149
+ async function retermMembershipDiscount(db, stripeSecretKey, scope2, input, now) {
5150
+ membershipClock(now);
5151
+ const limit = input.limit ?? 100;
5152
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200) throw new TypeError("invalid reterm limit");
5153
+ if (typeof input.policyVersionId !== "string" || !SAFE.test(input.policyVersionId)) throw new TypeError("invalid policy version id");
5154
+ 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");
5155
+ const state = await loadMembershipAuthorityState(db, scope2);
5156
+ const policy2 = await retermPolicy(db, scope2, input.policyVersionId);
5157
+ const receipt = { policyVersionId: policy2.id, retermed: [], unchanged: [], skipped: [], more: false };
5158
+ const skip = (membershipId, reason) => {
5159
+ receipt.skipped.push({ membershipId, reason });
5168
5160
  };
5169
- let apps = await lookup({ clerkUserId: identity2.userId });
5170
- let emailFallback = false;
5171
- if (!apps.length && identity2.email) {
5172
- const normalized = normalizeChapterEmail(identity2.email);
5173
- const normalizedRows = await lookup({ emailNormalized: normalized });
5174
- const legacyRows = await lookup({ email: identity2.email });
5175
- apps = [...new Map([...normalizedRows, ...legacyRows].map((row2) => [row2.id, row2])).values()];
5176
- if (apps.length !== 1 || apps[0]?.clerkUserId !== void 0 || apps[0]?.clerkAccountPending === true || ["declined", "canceled", "refunded"].includes(String(apps[0]?.status))) return null;
5177
- emailFallback = true;
5161
+ let rows5;
5162
+ if (input.membershipIds) {
5163
+ rows5 = [];
5164
+ for (const id7 of input.membershipIds) {
5165
+ const member = await membershipLoad(db, "networkMembership", id7);
5166
+ if (!member) skip(id7, "not_found");
5167
+ else if (Object.entries(scope2).some(([key3, value]) => member[key3] !== value)) skip(id7, "scope_mismatch");
5168
+ else rows5.push(member);
5169
+ }
5170
+ } else {
5171
+ const found = (await db.query({ networkMembership: { $: { where: { ...scope2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkMembership ?? [];
5172
+ if (found.some((member) => Object.entries(scope2).some(([key3, value]) => member[key3] !== value))) throw new Error("membership reterm scope mismatch");
5173
+ receipt.more = found.length > limit;
5174
+ rows5 = found.slice(0, limit);
5178
5175
  }
5179
- let app = apps.sort((a, b) => Number(b.createdAt ?? 0) - Number(a.createdAt ?? 0))[0];
5180
- if (!app) return null;
5181
- if (emailFallback) {
5182
- const keys3 = ["groupId", "stripeRuntime", "email", "emailNormalized", "status", "clerkAccountPending"];
5176
+ for (const member of rows5) {
5177
+ const id7 = String(member.id);
5178
+ if (member.discountPolicyVersionId === policy2.id) {
5179
+ receipt.unchanged.push(id7);
5180
+ continue;
5181
+ }
5182
+ if (!RETERMABLE.has(String(member.status))) {
5183
+ skip(id7, "status");
5184
+ continue;
5185
+ }
5186
+ if (member.founding !== true) {
5187
+ skip(id7, "not_founding");
5188
+ continue;
5189
+ }
5190
+ if (member.cohort !== policy2.cohortLabel) {
5191
+ skip(id7, "cohort_mismatch");
5192
+ continue;
5193
+ }
5194
+ const skuId = String(member.baseSkuId);
5195
+ if (!policy2.discountEligibleSkuIds.includes(skuId)) {
5196
+ skip(id7, "sku_not_discounted");
5197
+ continue;
5198
+ }
5199
+ const base = Number(member.baseAmountCents), current = Number(member.renewalAmountCents);
5200
+ const renewal = Math.round(base * (1e4 - policy2.percentOffBps) / 1e4);
5201
+ if (!Number.isSafeInteger(base) || base <= 0 || !Number.isSafeInteger(current) || renewal > current) {
5202
+ skip(id7, "would_not_widen");
5203
+ continue;
5204
+ }
5205
+ const couponId = await membershipDiscountCouponId({ ...scope2, skuId }, policy2.id);
5206
+ let providerUpdated;
5183
5207
  try {
5184
- await db.transact([{ t: "update", ns: "applications", id: String(app.id), attrs: { clerkUserId: identity2.userId } }], {
5185
- mutationId: `member-account-link:${app.id}:${identity2.userId}`,
5186
- guards: [{
5187
- ns: "applications",
5188
- id: String(app.id),
5189
- exists: true,
5190
- equals: Object.fromEntries(keys3.filter((key3) => app[key3] !== void 0).map((key3) => [key3, app[key3]])),
5191
- absent: ["clerkUserId", ...keys3.filter((key3) => app[key3] === void 0)]
5192
- }]
5208
+ const productId = await verifySubscriptionPrice(stripeSecretKey, {
5209
+ priceId: String(member.providerPriceId),
5210
+ priceCents: base,
5211
+ currency: String(member.currency),
5212
+ interval: member.interval === "month" ? "month" : "year",
5213
+ environment: scope2.environment
5214
+ }, true);
5215
+ if (!productId) throw Object.assign(new Error("price has no product"), { code: "provider_mismatch" });
5216
+ await prepareSubscriptionBillingTerms(stripeSecretKey, {
5217
+ ...scope2,
5218
+ priceCents: base,
5219
+ currency: String(member.currency),
5220
+ discountProductId: productId,
5221
+ billingTerms: {
5222
+ offeringVersionId: String(member.offeringVersionId),
5223
+ discount: { policyVersionId: policy2.id, couponId, percentOffBps: policy2.percentOffBps, duration: "forever" }
5224
+ }
5225
+ }, false);
5226
+ providerUpdated = await attachProviderDiscount(stripeSecretKey, scope2, member, policy2, couponId, productId);
5227
+ } catch (error) {
5228
+ const code = error.code;
5229
+ if (code === "provider_mismatch" || code === "stripe_contract_mismatch") {
5230
+ skip(id7, "provider_mismatch");
5231
+ continue;
5232
+ }
5233
+ throw error;
5234
+ }
5235
+ try {
5236
+ await db.transact([membershipUpdate("networkMembership", id7, {
5237
+ discountPolicyVersionId: policy2.id,
5238
+ discountSnapshot: policy2,
5239
+ renewalAmountCents: renewal,
5240
+ revision: Number(member.revision) + 1,
5241
+ updatedAt: now
5242
+ })], {
5243
+ mutationId: `membership-reterm:${id7}:${policy2.id}:${member.revision}`,
5244
+ guards: [
5245
+ { ns: "networkMembership", id: id7, exists: true, equals: { revision: member.revision, status: member.status } },
5246
+ { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative" } },
5247
+ { ns: "discountPolicyVersion", id: policy2.id, exists: true }
5248
+ ]
5193
5249
  });
5194
5250
  } catch (error) {
5195
5251
  if (error.code !== "transact_guard_failed") throw error;
5252
+ return membershipFail("membership_reterm_retry_required");
5196
5253
  }
5197
- const saved = (await db.query({ applications: { $: { where: { id: app.id }, limit: 1 } } })).applications?.[0];
5198
- if (!saved || !local(saved, chapterId, runtime) || saved.clerkUserId !== identity2.userId || keys3.some((key3) => saved[key3] !== app[key3])) return null;
5199
- app = saved;
5254
+ receipt.retermed.push({
5255
+ membershipId: id7,
5256
+ ...typeof member.discountPolicyVersionId === "string" ? { previousPolicyVersionId: member.discountPolicyVersionId } : {},
5257
+ previousRenewalAmountCents: current,
5258
+ renewalAmountCents: renewal,
5259
+ providerUpdated
5260
+ });
5200
5261
  }
5201
- const meeting = (await db.query({ meetings: { $: {
5202
- where: { applicationId: app.id, status: "scheduled" },
5203
- order: { createdAt: "desc" },
5204
- limit: 1
5205
- } } })).meetings?.[0];
5206
- const group = (await db.query({ groups: { $: { where: { id: chapterId }, limit: 1 } } })).groups?.[0];
5207
- const timezone = resolveScheduling(group?.schedulingJson).timezone;
5208
- return { source: app, application: memberApplication(
5209
- app,
5210
- meeting,
5211
- timezone
5212
- ) };
5262
+ return receipt;
5213
5263
  }
5214
5264
 
5265
+ // src/worker-routes-admin-membership-reterm.ts
5266
+ var PATH4 = "/api/admin/membership-discount/reterm";
5267
+ var SCOPE3 = /^[a-z0-9][a-z0-9-]{1,62}$/;
5268
+ var ID = /^[A-Za-z0-9][A-Za-z0-9_.:-]{2,159}$/;
5269
+ var BAD = "chapterId and policyVersionId are required; runtime, appId, membershipIds and limit are optional";
5270
+ var handleAdminMembershipReterm = async (req, url, env, ctx) => {
5271
+ if (url.pathname !== PATH4) return null;
5272
+ const operator = ctx.membershipAuthorityOperator;
5273
+ if (!operator) return json({ error: "not found" }, 404);
5274
+ if (req.method !== "POST") return json({ error: "method not allowed" }, 405);
5275
+ const raw2 = ctx.makeDb(env), user = await ctx.verifyUser(req, env);
5276
+ if (!user) return json({ error: "unauthorized" }, 401);
5277
+ if (!await ctx.isSuperAdminEmail(raw2, user.email, user.userId)) return json({ error: "super-admin required", code: "super_admin_required" }, 403);
5278
+ let db;
5279
+ try {
5280
+ db = operator.makeDb(env);
5281
+ } catch {
5282
+ return json({ error: "the membership authority is not hosted by this deployment" }, 404);
5283
+ }
5284
+ let body;
5285
+ try {
5286
+ body = await req.json();
5287
+ } catch {
5288
+ return json({ error: BAD }, 400);
5289
+ }
5290
+ const appId = body.appId ?? body.chapterId, runtime = body.runtime ?? env.ODLA_RUNTIME;
5291
+ 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)) {
5292
+ return json({ error: BAD }, 400);
5293
+ }
5294
+ const secret = await getVaultSecret(db, operator.stripeSecretName ?? "stripe_secret_key");
5295
+ if (!secret) return json({ error: "network payment provider unavailable" }, 503);
5296
+ try {
5297
+ const receipt = await retermMembershipDiscount(
5298
+ db,
5299
+ secret,
5300
+ { appId, chapterId: body.chapterId, environment: env.ODLA_ENV, runtime },
5301
+ {
5302
+ policyVersionId: body.policyVersionId,
5303
+ ...body.membershipIds ? { membershipIds: body.membershipIds } : {},
5304
+ ...body.limit !== void 0 ? { limit: Number(body.limit) } : {}
5305
+ },
5306
+ Date.now()
5307
+ );
5308
+ return json({ ok: true, ...receipt });
5309
+ } catch (error) {
5310
+ const code = error.code;
5311
+ return json({ error: "re-term is pending; retry to confirm", ...code ? { code } : {} }, code ? 409 : 503);
5312
+ }
5313
+ };
5314
+
5315
+ // src/worker-routes-membership-cancel.ts
5316
+ var handleMembershipCancellation = async (req, url, env, ctx) => {
5317
+ const match = url.pathname.match(/^\/api\/admin\/applications\/([0-9a-f-]+)\/cancel-renewal$/);
5318
+ if (!match) return null;
5319
+ if (req.method !== "POST") return json({ error: "method not allowed" }, 405);
5320
+ const user = await ctx.verifyUser(req, env), raw2 = ctx.makeDb(env);
5321
+ if (!user) return json({ error: "unauthorized" }, 401);
5322
+ if (!await ctx.isAdmin(raw2, user)) return json({ error: "forbidden" }, 403);
5323
+ if (!ctx.membershipAuthority?.cancelRenewal) return json({ error: "renewal cancellation unavailable" }, 503);
5324
+ const db = raw2, app = await membershipLoad(db, "applications", match[1]);
5325
+ if (!app || !chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME)) return json({ error: "not found" }, 404);
5326
+ if (app.namedSeatId || app.giftClaimId) return json({ error: "this membership has no independently renewing subscription" }, 409);
5327
+ const intent = await membershipLoad(db, "subscriptionCheckoutIntents", String(app.id));
5328
+ if (!intent) return json({ error: "membership history needs reconciliation" }, 409);
5329
+ try {
5330
+ const target = await membershipTargetForIntent(intent, app, env, ctx);
5331
+ const receipt = await ctx.membershipAuthority.cancelRenewal(target, env);
5332
+ 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");
5333
+ await db.transact([{ t: "update", ns: "applications", id: String(app.id), attrs: {
5334
+ membershipStatus: receipt.status,
5335
+ membershipAutoRenew: false,
5336
+ membershipGraceEndsAt: 0,
5337
+ renewalAt: receipt.termEndsAt
5338
+ } }], { guards: [{
5339
+ ns: "applications",
5340
+ id: String(app.id),
5341
+ exists: true,
5342
+ equals: { status: app.status, stripeSubscriptionId: app.stripeSubscriptionId }
5343
+ }] });
5344
+ return json({ ok: true, accessEndsAt: receipt.termEndsAt, status: receipt.status });
5345
+ } catch {
5346
+ return json({ error: "renewal cancellation is pending; retry to confirm" }, 503);
5347
+ }
5348
+ };
5349
+
5215
5350
  // src/worker-routes-membership-restart.ts
5216
5351
  async function memberRestartState(db, env, ctx, app) {
5217
5352
  if (!ctx.membershipAuthority?.restart || app.namedSeatId || app.giftClaimId) return null;
@@ -5439,6 +5574,7 @@ async function submitApplication(db, chapter, fields, opts) {
5439
5574
  clerkUserId: opts.namedSeat.userId,
5440
5575
  clerkAccountPending: false
5441
5576
  });
5577
+ if (opts.predecessor?.clerkUserId && !opts.namedSeat) Object.assign(row2, { clerkUserId: opts.predecessor.clerkUserId, clerkAccountPending: false });
5442
5578
  if (typeof fields.email === "string" && isValidEmail(fields.email.trim())) {
5443
5579
  row2.emailNormalized = normalizeChapterEmail(fields.email);
5444
5580
  row2.signupEmailKey = row2.emailNormalized;
@@ -5457,6 +5593,8 @@ async function submitApplication(db, chapter, fields, opts) {
5457
5593
  }
5458
5594
  if (acked) row2.disclaimerAckAt = opts.now;
5459
5595
  const ops = [
5596
+ // The unique signup key leaves the closed predecessor before the new attempt takes it.
5597
+ ...opts.predecessor && opts.predecessor.id !== id7 ? [{ t: "retract", ns: "applications", id: opts.predecessor.id, attrs: ["signupEmailKey"] }] : [],
5460
5598
  { t: "update", ns: "applications", id: id7, attrs: row2 }
5461
5599
  ];
5462
5600
  if (opts.admissionGrant?.decision === "waive_interview") {
@@ -5504,27 +5642,35 @@ function joinConfig(group, paymentsReady, tiers = [], conditions = {}) {
5504
5642
 
5505
5643
  // src/signup-eligibility.ts
5506
5644
  var signupConflict = { code: "account_already_exists", message: "An account or application already exists for this email. Please sign in to continue." };
5507
- async function checkSignupEligibility(db, chapter, fields, submissionId) {
5508
- if (typeof fields.email !== "string" || !isValidEmail(fields.email.trim())) return null;
5645
+ var closedApplication = (row2) => row2.canceled === true || ["declined", "refunded", "canceled"].includes(String(row2.status));
5646
+ async function resolveSignupEligibility(db, chapter, fields, submissionId) {
5647
+ if (typeof fields.email !== "string" || !isValidEmail(fields.email.trim())) return { conflict: null, predecessor: null };
5509
5648
  const email = normalizeChapterEmail(fields.email);
5510
5649
  const applicationId = submissionId ? await applicationIdForSubmission(submissionId) : void 0;
5511
- const conflict = () => Response.json({ error: signupConflict }, { status: 409 });
5650
+ const conflict = () => ({ conflict: Response.json({ error: signupConflict }, { status: 409 }), predecessor: null });
5512
5651
  try {
5513
5652
  const data = await db.query({
5514
- applications: { $: { where: { emailNormalized: email }, limit: 2 } },
5653
+ applications: { $: { where: { emailNormalized: email }, limit: 10 } },
5515
5654
  $users: { $: { where: { email }, limit: 2 } }
5516
5655
  });
5517
- if ((data.applications ?? []).some((row2) => row2.id !== applicationId)) return conflict();
5518
- const original = (data.applications ?? []).find((row2) => row2.id === applicationId && row2.groupId === chapter.id);
5519
- if ((data.$users ?? []).some((row2) => row2.deleted !== true && row2.id !== original?.clerkUserId)) return conflict();
5520
- if (chapter.account === "none") return null;
5656
+ const rows5 = data.applications ?? [], others = rows5.filter((row2) => row2.id !== applicationId);
5657
+ const closed = others.filter((row2) => row2.groupId === chapter.id && closedApplication(row2)).sort((a, b) => Number(b.createdAt ?? 0) - Number(a.createdAt ?? 0));
5658
+ if (others.length !== closed.length) return conflict();
5659
+ const original = rows5.find((row2) => row2.id === applicationId && row2.groupId === chapter.id);
5660
+ const owned = (userId) => original?.clerkUserId === userId || closed.some((row2) => row2.clerkUserId === userId);
5661
+ if ((data.$users ?? []).some((row2) => row2.deleted !== true && !owned(row2.id))) return conflict();
5662
+ const predecessor = (clerkUserId) => closed.length ? { id: String(closed[0].id), ...clerkUserId ? { clerkUserId } : {} } : null;
5663
+ if (chapter.account === "none") return { conflict: null, predecessor: predecessor(typeof closed[0]?.clerkUserId === "string" ? closed[0].clerkUserId : void 0) };
5521
5664
  const secret = await getVaultSecret(db, "clerk_secret_key");
5522
- if (!secret) return Response.json({ error: { code: "clerk_secret_unavailable", message: "account provisioning credential is unavailable" } }, { status: 503 });
5665
+ if (!secret) return { conflict: Response.json({ error: { code: "clerk_secret_unavailable", message: "account provisioning credential is unavailable" } }, { status: 503 }), predecessor: null };
5523
5666
  const account = await findSignupAccount(secret, email);
5524
- if (account && !(original && (original.clerkUserId === account.id || belongsToSignup(account, original.id)))) return conflict();
5525
- return null;
5667
+ if (!account) return { conflict: null, predecessor: predecessor() };
5668
+ if (original && (original.clerkUserId === account.id || belongsToSignup(account, original.id))) return { conflict: null, predecessor: null };
5669
+ const owner = closed.find((row2) => row2.clerkUserId === account.id || belongsToSignup(account, row2.id));
5670
+ if (!owner) return conflict();
5671
+ return { conflict: null, predecessor: { id: String(owner.id), clerkUserId: account.id } };
5526
5672
  } catch {
5527
- return Response.json({ error: { code: "signup_lookup_unavailable", message: "Signup is temporarily unavailable. Please try again." } }, { status: 503 });
5673
+ return { conflict: Response.json({ error: { code: "signup_lookup_unavailable", message: "Signup is temporarily unavailable. Please try again." } }, { status: 503 }), predecessor: null };
5528
5674
  }
5529
5675
  }
5530
5676
  async function rejectUnprovisionedSignup(db, applicationId, status2) {
@@ -6690,8 +6836,12 @@ async function namedSeatApplicationContext(req, seatId, env, ctx) {
6690
6836
  }
6691
6837
  async function existingNamedSeatApplication(context, env, ctx) {
6692
6838
  const { db, userId, email, invitation } = context;
6693
- const apps = (await db.query({ applications: { $: { where: { emailNormalized: normalizeChapterEmail(email) }, limit: 2 } } })).applications ?? [];
6694
- if (!apps.length) return null;
6839
+ const rows5 = (await db.query({ applications: { $: { where: { emailNormalized: normalizeChapterEmail(email) }, limit: 10 } } })).applications ?? [];
6840
+ const apps = rows5.filter((row2) => !closedApplication(row2));
6841
+ if (!apps.length) {
6842
+ const closed = rows5.filter((row2) => row2.groupId === ctx.chapter.id).sort((a, b) => Number(b.createdAt ?? 0) - Number(a.createdAt ?? 0))[0];
6843
+ return { predecessor: closed ? { id: String(closed.id) } : null };
6844
+ }
6695
6845
  const app = apps[0];
6696
6846
  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)) {
6697
6847
  return json({ error: "Your existing application cannot accept this invitation. Contact the chapter for help.", code: "application_conflict" }, 409);
@@ -7207,8 +7357,8 @@ var handleMember = async (req, url, env, ctx) => {
7207
7357
  parsed.tierId = named.invitation.skuId;
7208
7358
  submissionId = `named-seat:${named.invitation.seatId}:${named.userId}`;
7209
7359
  }
7210
- const signupFailure = named ? null : await checkSignupEligibility(db, chapter, parsed, submissionId);
7211
- if (signupFailure) return signupFailure;
7360
+ const eligibility = named ? null : await resolveSignupEligibility(db, chapter, parsed, submissionId);
7361
+ if (eligibility?.conflict) return eligibility.conflict;
7212
7362
  const activeControl = chapter.signupControl ? await loadActiveSignupControl(db, chapter.id, env.ODLA_ENV) : null;
7213
7363
  const effectiveChapter = activeControl ? { ...chapter, application: controlledApplication(chapter.application, activeControl.document) } : chapter;
7214
7364
  const offeredTierIds = typeof parsed.tierId === "string" && parsed.tierId ? (await loadTiers(db, { id: chapter.id })).map((tier) => tier.id) : void 0;
@@ -7219,7 +7369,8 @@ var handleMember = async (req, url, env, ctx) => {
7219
7369
  const effectiveGrant = !named && !giftOffer && typeof parsed.email === "string" ? await findAdmissionGrant(db, chapter.id, parsed.email, now) : null;
7220
7370
  const existing = named ? await existingNamedSeatApplication(named, env, ctx) : null;
7221
7371
  if (existing instanceof Response) return existing;
7222
- const result = existing ?? await submitApplication(db, effectiveChapter, parsed, {
7372
+ const predecessor = existing && "predecessor" in existing ? existing.predecessor : eligibility?.predecessor ?? null;
7373
+ const result = existing && "ok" in existing ? existing : await submitApplication(db, effectiveChapter, parsed, {
7223
7374
  submissionId,
7224
7375
  groupId: chapter.id,
7225
7376
  runtime: stripeRuntimeIdentity(env.ODLA_RUNTIME),
@@ -7228,7 +7379,8 @@ var handleMember = async (req, url, env, ctx) => {
7228
7379
  ...named ? { namedSeat: { seatId: named.invitation.seatId, primaryApplicationId: named.invitation.primaryApplicationId, userId: named.userId } } : {},
7229
7380
  ...named ? { tierIds: [named.invitation.skuId] } : offeredTierIds ? { tierIds: offeredTierIds } : {},
7230
7381
  ...effectiveGrant?.decision === "waive_interview" ? { admissionGrant: effectiveGrant } : {},
7231
- ...giftOffer ? { giftOfferId: giftOffer.id } : {}
7382
+ ...giftOffer ? { giftOfferId: giftOffer.id } : {},
7383
+ ...predecessor ? { predecessor } : {}
7232
7384
  });
7233
7385
  if (!result.ok) return json({ error: result.error, ...result.code ? { code: result.code } : {} }, result.status ?? 400);
7234
7386
  const apps = (await db.query({ applications: { $: { where: { id: result.id }, limit: 1 } } })).applications;
@@ -7248,6 +7400,7 @@ var handleMember = async (req, url, env, ctx) => {
7248
7400
  if (provisioningFailure.status === 409) await rejectUnprovisionedSignup(db, result.id, canonical3.status);
7249
7401
  return json(provisioningFailure.body, provisioningFailure.status);
7250
7402
  }
7403
+ if (predecessor) await syncApplicationToCrm(crmDeps(db, ctx), { app: canonical3, stage: String(canonical3.status) }).catch(() => void 0);
7251
7404
  const groups = (await db.query({ groups: { $: { where: { id: chapter.id }, limit: 1 } } })).groups;
7252
7405
  const group = Array.isArray(groups) ? groups[0] : void 0;
7253
7406
  const notice = group ? { group, application: canonical3, origin: url.origin } : null;
@@ -10198,6 +10351,128 @@ var handleGifts = async (req, url, env, ctx) => {
10198
10351
  return null;
10199
10352
  };
10200
10353
 
10354
+ // src/membership-effect-kinds.ts
10355
+ var MEMBERSHIP_CHAPTER_EFFECT_KINDS = [
10356
+ "invite_named_recipient",
10357
+ "grant_named_recipient_access",
10358
+ "revoke_named_recipient_access",
10359
+ "release_named_recipient"
10360
+ ];
10361
+ var namedSeatInvitationOpen = (member) => ["active", "cancel_at_term_end"].includes(String(member.status)) && Number.isSafeInteger(member.journeyApprovedAt);
10362
+
10363
+ // src/membership-seat-regift.ts
10364
+ var REGIFTABLE_SEAT_STATUSES = ["pending_acceptance", "pending_approval", "pending_primary"];
10365
+ var namedSeatRegiftable = (seat) => REGIFTABLE_SEAT_STATUSES.includes(String(seat.status));
10366
+ var receiptFor2 = (seat, released) => ({
10367
+ seatId: String(seat.id),
10368
+ status: "pending_acceptance",
10369
+ recipientName: String(seat.recipientName),
10370
+ recipientEmail: String(seat.recipientEmailNormalized),
10371
+ regiftedAt: Number(seat.regiftedAt),
10372
+ releasedRecipientApplicationId: released
10373
+ });
10374
+ async function regiftNamedSeat(db, input, now) {
10375
+ membershipClock(now);
10376
+ const name = typeof input.recipientName === "string" ? input.recipientName.trim().replace(/\s+/g, " ") : "";
10377
+ const email = normalizeChapterEmail(typeof input.recipientEmail === "string" ? input.recipientEmail : "");
10378
+ 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");
10379
+ const state = await loadMembershipAuthorityState(db, input);
10380
+ const seat = await membershipLoad(db, "networkSeat", input.seatId);
10381
+ requireMembershipScope(seat, input);
10382
+ const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
10383
+ requireMembershipScope(member, input);
10384
+ if (member.namedSeatId !== seat.id) membershipFail("named_seat_primary_conflict");
10385
+ if (typeof seat.purchaserEmailNormalized === "string" && seat.purchaserEmailNormalized === email) membershipFail("invalid_named_seat_recipient");
10386
+ const history = Array.isArray(seat.formerRecipients) ? seat.formerRecipients : [];
10387
+ if (seat.recipientEmailNormalized === email && seat.recipientName === name && seat.regiftedAt !== void 0 && seat.status === "pending_acceptance") {
10388
+ return receiptFor2(seat, history.at(-1)?.recipientApplicationId ?? null);
10389
+ }
10390
+ if (!namedSeatRegiftable(seat)) membershipFail("named_seat_not_regiftable");
10391
+ if (seat.recipientEmailNormalized === email) membershipFail("named_seat_recipient_unchanged");
10392
+ const entitlement = await membershipLoad(db, "networkEntitlement", String(seat.entitlementId));
10393
+ requireMembershipScope(entitlement, input);
10394
+ const claimId = await namedSeatRecipientClaimId(input, email);
10395
+ const former = {
10396
+ recipientName: String(seat.recipientName),
10397
+ recipientEmail: String(seat.recipientEmailNormalized),
10398
+ recipientClaimId: String(seat.recipientClaimId),
10399
+ releasedAt: now,
10400
+ ...typeof seat.recipientUserId === "string" ? { recipientUserId: seat.recipientUserId } : {},
10401
+ ...typeof seat.recipientApplicationId === "string" ? { recipientApplicationId: seat.recipientApplicationId } : {},
10402
+ ...Number.isSafeInteger(seat.acceptedAt) ? { acceptedAt: Number(seat.acceptedAt) } : {},
10403
+ ...Number.isSafeInteger(seat.journeyApprovedAt) ? { journeyApprovedAt: Number(seat.journeyApprovedAt) } : {}
10404
+ };
10405
+ const generation = history.length + 1, inviteId = `${seat.id}:invite:${generation}`, releaseId = `${seat.id}:release:${generation}`;
10406
+ const bound = Boolean(former.recipientUserId && former.recipientApplicationId);
10407
+ 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);
10408
+ const cleared = ["recipientUserId", "recipientApplicationId", "acceptedAt", "journeyApprovedAt"];
10409
+ const effect = (id7, kind, extra = {}) => membershipUpdate("membershipEffect", id7, {
10410
+ id: id7,
10411
+ chapterId: input.chapterId,
10412
+ environment: input.environment,
10413
+ runtime: input.runtime,
10414
+ membershipId: member.id,
10415
+ seatId: seat.id,
10416
+ kind,
10417
+ status: "pending",
10418
+ attempts: 0,
10419
+ createdAt: now,
10420
+ updatedAt: now,
10421
+ ...extra
10422
+ });
10423
+ const ops = [
10424
+ membershipUpdate("networkSeat", String(seat.id), {
10425
+ recipientName: name,
10426
+ recipientEmailNormalized: email,
10427
+ recipientClaimId: claimId,
10428
+ status: "pending_acceptance",
10429
+ regiftedAt: now,
10430
+ formerRecipients: [...history, former],
10431
+ revision: Number(seat.revision) + 1,
10432
+ updatedAt: now
10433
+ }),
10434
+ { t: "retract", ns: "networkSeat", id: String(seat.id), attrs: cleared },
10435
+ membershipUpdate("networkEntitlement", String(entitlement.id), { status: "pending_acceptance", revision: Number(entitlement.revision) + 1, updatedAt: now }),
10436
+ { t: "retract", ns: "networkEntitlement", id: String(entitlement.id), attrs: cleared },
10437
+ { t: "delete", ns: "networkSeatRecipient", id: String(seat.recipientClaimId) },
10438
+ membershipUpdate("networkSeatRecipient", claimId, {
10439
+ id: claimId,
10440
+ appId: input.appId,
10441
+ chapterId: input.chapterId,
10442
+ environment: input.environment,
10443
+ runtime: input.runtime,
10444
+ seatId: seat.id,
10445
+ recipientEmailNormalized: email,
10446
+ createdAt: now
10447
+ }),
10448
+ ...stale.map((row2) => membershipUpdate("membershipEffect", String(row2.id), { status: "complete", updatedAt: now })),
10449
+ effect(inviteId, "invite_named_recipient"),
10450
+ ...bound ? [effect(releaseId, "release_named_recipient", { recipientUserId: former.recipientUserId, recipientApplicationId: former.recipientApplicationId })] : []
10451
+ ];
10452
+ try {
10453
+ await db.transact(ops, { mutationId: `named-seat-regift:${seat.id}:${generation}`, guards: [
10454
+ { ns: "networkSeat", id: String(seat.id), exists: true, equals: { revision: seat.revision, status: seat.status } },
10455
+ { ns: "networkEntitlement", id: String(entitlement.id), exists: true, equals: { revision: entitlement.revision } },
10456
+ { ns: "networkMembership", id: String(member.id), exists: true, equals: { revision: member.revision, namedSeatId: seat.id } },
10457
+ { ns: "networkSeatRecipient", id: claimId, exists: false },
10458
+ ...stale.map((row2) => ({ ns: "membershipEffect", id: String(row2.id), exists: true, equals: { status: "pending" } })),
10459
+ { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
10460
+ ] });
10461
+ } catch (error) {
10462
+ if (error.code !== "transact_guard_failed") throw error;
10463
+ const current = await membershipLoad(db, "networkSeat", input.seatId);
10464
+ requireMembershipScope(current, input);
10465
+ if (current.recipientEmailNormalized === email && current.status === "pending_acceptance") return receiptFor2(current, former.recipientApplicationId ?? null);
10466
+ const claim = await membershipLoad(db, "networkSeatRecipient", claimId);
10467
+ if (claim && claim.seatId !== seat.id) membershipFail("named_seat_recipient_unavailable");
10468
+ membershipFail("named_seat_regift_retry_required");
10469
+ }
10470
+ const saved = await membershipLoad(db, "networkSeat", input.seatId);
10471
+ requireMembershipScope(saved, input);
10472
+ if (saved.recipientEmailNormalized !== email || saved.status !== "pending_acceptance") membershipFail("named_seat_regift_retry_required");
10473
+ return receiptFor2(saved, former.recipientApplicationId ?? null);
10474
+ }
10475
+
10201
10476
  // src/membership-seat-offer.ts
10202
10477
  async function namedSeatOfferDigest(offer) {
10203
10478
  const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify([
@@ -10241,7 +10516,10 @@ async function readNamedSeatOffer(db, input, now) {
10241
10516
  recipientState: namedSeatRecipientState(seat),
10242
10517
  ...at(seat.purchasedAt) !== void 0 ? { purchasedAt: at(seat.purchasedAt) } : {},
10243
10518
  ...at(seat.acceptedAt) !== void 0 ? { acceptedAt: at(seat.acceptedAt) } : {},
10244
- ...seat.status === "active" && at(seat.journeyApprovedAt) !== void 0 ? { joinedAt: at(seat.journeyApprovedAt) } : {}
10519
+ ...seat.status === "active" && at(seat.journeyApprovedAt) !== void 0 ? { joinedAt: at(seat.journeyApprovedAt) } : {},
10520
+ ...seat.status === "pending_acceptance" && seat.acceptedAt === void 0 && !namedSeatInvitationOpen(member) ? { invitationHeld: true } : {},
10521
+ ...namedSeatRegiftable(seat) ? { regiftable: true } : {},
10522
+ ...at(seat.regiftedAt) !== void 0 ? { regiftedAt: at(seat.regiftedAt) } : {}
10245
10523
  } } : {}
10246
10524
  };
10247
10525
  return {
@@ -10252,40 +10530,6 @@ async function readNamedSeatOffer(db, input, now) {
10252
10530
  };
10253
10531
  }
10254
10532
 
10255
- // src/named-seat-request.ts
10256
- async function smallRequestBody(req) {
10257
- const reader = req.body?.getReader();
10258
- if (!reader) throw new Error("request body required");
10259
- const chunks = [];
10260
- let size = 0;
10261
- try {
10262
- while (true) {
10263
- const chunk = await reader.read();
10264
- if (chunk.done) break;
10265
- size += chunk.value.byteLength;
10266
- if (size > 4096) {
10267
- await reader.cancel();
10268
- throw new Error("request too large");
10269
- }
10270
- chunks.push(chunk.value);
10271
- }
10272
- } finally {
10273
- reader.releaseLock();
10274
- }
10275
- const bytes = new Uint8Array(size);
10276
- let offset = 0;
10277
- for (const chunk of chunks) {
10278
- bytes.set(chunk, offset);
10279
- offset += chunk.byteLength;
10280
- }
10281
- return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
10282
- }
10283
- async function namedSeatRequest(req) {
10284
- const body = JSON.parse(await smallRequestBody(req));
10285
- if (!body || typeof body !== "object" || Array.isArray(body)) throw new Error("object required");
10286
- return body;
10287
- }
10288
-
10289
10533
  // src/worker-routes-named-seat.ts
10290
10534
  var handleNamedSeat = async (req, url, env, ctx) => {
10291
10535
  if (!["/api/named-seat", "/api/named-seat/checkout"].includes(url.pathname)) return null;
@@ -10691,7 +10935,8 @@ function applicationActionStages(app, env, ctx) {
10691
10935
  if (to === from) return false;
10692
10936
  if (to === "approved") return canApprove(from, ctx.chapter.pipeline) && from !== "declined";
10693
10937
  if (to === "refunded") return Boolean(app.namedSeatId || app.stripeCustomerId && app.stripeSubscriptionId) && (!ctx.chapter.operations.refund.allowedFrom || ctx.chapter.operations.refund.allowedFrom.includes(from));
10694
- if (ctx.membershipAuthority && ["submitted", "paid_pending_vetting", "declined", "canceled"].includes(to)) return false;
10938
+ if (to === "declined" && ctx.membershipAuthority) return !app.namedSeatId && !app.stripeSubscriptionId && canTransition(from, to, ctx.chapter.pipeline);
10939
+ if (ctx.membershipAuthority && ["submitted", "paid_pending_vetting", "canceled"].includes(to)) return false;
10695
10940
  return canTransition(from, to, ctx.chapter.pipeline);
10696
10941
  });
10697
10942
  }
@@ -10824,18 +11069,36 @@ var handleAdminScheduling = async (req, url, env, ctx) => {
10824
11069
  const got = await adminGroup(req, env, ctx, url);
10825
11070
  if (got instanceof Response) return got;
10826
11071
  const { db, group } = got;
11072
+ const stored = group.schedulingJson && typeof group.schedulingJson === "object" && !Array.isArray(group.schedulingJson) ? group.schedulingJson : {};
10827
11073
  if (req.method === "GET") {
10828
- return json({ scheduling: resolveScheduling(group.schedulingJson) });
11074
+ const current = validateScheduling(stored);
11075
+ if (!current.ok) return json({
11076
+ error: "stored scheduling config is invalid; save the form to repair it",
11077
+ errors: current.errors,
11078
+ scheduling: { ...resolveScheduling(void 0), ...stored }
11079
+ }, 409);
11080
+ return json({ scheduling: current.value });
10829
11081
  }
10830
11082
  let body;
10831
11083
  try {
10832
11084
  body = JSON.parse(await req.text());
11085
+ if (!body || typeof body !== "object" || Array.isArray(body)) throw new TypeError("object required");
10833
11086
  } catch {
10834
11087
  return json({ error: "invalid JSON body" }, 400);
10835
11088
  }
10836
- const checked = validateScheduling(body);
11089
+ const checked = validateScheduling({ ...stored, ...body });
10837
11090
  if (!checked.ok) return json({ error: "invalid scheduling config", errors: checked.errors }, 400);
10838
- await db.transact([{ t: "update", ns: "groups", id: String(group.id), attrs: { schedulingJson: checked.value } }]);
11091
+ try {
11092
+ await db.transact([{ t: "update", ns: "groups", id: String(group.id), attrs: { schedulingJson: checked.value } }]);
11093
+ } catch (error) {
11094
+ const code = error.code;
11095
+ console.error("admin_scheduling_write_failed", { code: typeof code === "string" ? code : "unknown" });
11096
+ return json({
11097
+ error: "availability could not be saved; retry",
11098
+ code: "scheduling_write_failed",
11099
+ ...typeof code === "string" ? { cause: code } : {}
11100
+ }, 503);
11101
+ }
10839
11102
  return json({ scheduling: checked.value });
10840
11103
  };
10841
11104
  async function upcomingEvents(env) {
@@ -11461,6 +11724,116 @@ async function refundApplication(db, env, ctx, app) {
11461
11724
  }
11462
11725
  }
11463
11726
 
11727
+ // src/decline-application.ts
11728
+ var DECLINABLE_FROM = (status2) => !["approved", "declined", "refunded", "canceled"].includes(status2);
11729
+ async function paymentHeld(db, env, ctx, app) {
11730
+ const intent = await membershipLoad(db, "subscriptionCheckoutIntents", String(app.id));
11731
+ if (ctx.membershipAuthority && intent) {
11732
+ try {
11733
+ const target = await membershipTargetForIntent(intent, app, env, ctx);
11734
+ const receipt = await verifyMembershipAccessReceipt(await ctx.membershipAuthority.readAccess(target, env), target, Date.now());
11735
+ return receipt.status !== "unpaid";
11736
+ } catch {
11737
+ return json({ error: "membership authority unavailable; retry before declining", code: "authority_unavailable" }, 503);
11738
+ }
11739
+ }
11740
+ return applicationSummary(app).paid || Boolean(intent?.paymentReceipt);
11741
+ }
11742
+ async function declineApplication(db, env, ctx, app) {
11743
+ if (!chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME)) return json({ error: "not found" }, 404);
11744
+ const id7 = String(app.id), status2 = String(app.status ?? "");
11745
+ if (status2 === "declined") return json({ ok: true, status: "declined", replayed: true });
11746
+ if (app.canceled === true || !DECLINABLE_FROM(status2)) return json({ error: `cannot decline from status "${status2}"`, code: "not_declinable" }, 409);
11747
+ if (app.namedSeatId) return json({ error: "a gifted seat is released by naming someone else, or refunded", code: "named_seat_recipient" }, 409);
11748
+ if (!ctx.chapter.pipeline.stages.includes("declined")) return json({ error: "this chapter's pipeline has no declined stage" }, 409);
11749
+ const held = await paymentHeld(db, env, ctx, app);
11750
+ if (held instanceof Response) return held;
11751
+ if (held) return json({ error: "a payment is held for this application; refund it instead", code: "payment_held" }, 409);
11752
+ try {
11753
+ await db.transact(
11754
+ [{ t: "update", ns: "applications", id: id7, attrs: { status: "declined", canceled: true } }],
11755
+ { mutationId: `decline:${id7}:${status2}`, guards: [{ ns: "applications", id: id7, exists: true, equals: { status: status2 } }] }
11756
+ );
11757
+ } catch (error) {
11758
+ if (error.code !== "transact_guard_failed") throw error;
11759
+ return json({ error: "application changed; reload before declining" }, 409);
11760
+ }
11761
+ await syncApplicationToCrm(crmDeps(db, ctx), { app: { ...app, status: "declined", canceled: true }, stage: "declined" }).catch(() => void 0);
11762
+ return json({ ok: true, status: "declined" });
11763
+ }
11764
+
11765
+ // src/refund-continue.ts
11766
+ var BILLING_FIELDS = [
11767
+ "stripeSubscriptionId",
11768
+ "stripeCustomerId",
11769
+ "canceled",
11770
+ "renewalAt",
11771
+ "refundPolicyAckAt",
11772
+ "membershipStatus",
11773
+ "membershipGraceEndsAt",
11774
+ "membershipAutoRenew",
11775
+ "approvalEffectsPending",
11776
+ "approvalEffectsOrigin",
11777
+ "approvalEffectsLastAttemptAt",
11778
+ "membershipRestartId"
11779
+ ];
11780
+ async function resolveFreeTier(db, ctx, app, tierId) {
11781
+ const group = await membershipLoad(db, "groups", String(app.groupId ?? ctx.chapter.id));
11782
+ const tier = group ? findTier(await loadTiers(db, group), tierId) : null;
11783
+ 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);
11784
+ if (!ctx.chapter.pipeline.approvableFrom.length) return json({ error: "this chapter's pipeline has no approvable stage" }, 409);
11785
+ if (app.namedSeatId) return json({ error: "a gifted seat cannot continue as a free member", code: "named_seat_recipient" }, 409);
11786
+ return tier;
11787
+ }
11788
+ async function continueApplicationOnFreeTier(db, env, ctx, app, tier, origin) {
11789
+ const id7 = String(app.id), now = Date.now();
11790
+ const approvable = ctx.chapter.pipeline.approvableFrom.at(-1);
11791
+ if (!approvable) return json({ error: "this chapter's pipeline has no approvable stage" }, 409);
11792
+ const current = await membershipLoad(db, "applications", id7);
11793
+ if (!current || !chapterBillingApplication(current, ctx.chapter.id, env.ODLA_RUNTIME)) return json({ error: "not found" }, 404);
11794
+ if (current.status === "refunded") {
11795
+ const intent = await membershipLoad(db, "subscriptionCheckoutIntents", id7), quote = await membershipLoad(db, "membershipQuoteProjections", id7);
11796
+ const subscription = typeof intent?.providerSubscriptionId === "string" ? intent.providerSubscriptionId : null;
11797
+ const historyId = `${id7}:continue:${subscription ?? "none"}`;
11798
+ const ops = [
11799
+ ...intent && subscription ? [{ t: "update", ns: "membershipCheckoutHistory", id: historyId, attrs: {
11800
+ id: historyId,
11801
+ applicationId: id7,
11802
+ appId: env.ODLA_APP_ID,
11803
+ chapterId: ctx.chapter.id,
11804
+ environment: env.ODLA_ENV,
11805
+ runtime: stripeRuntimeIdentity(env.ODLA_RUNTIME),
11806
+ skuId: String(current.tierId ?? intent.tierId ?? ""),
11807
+ providerSubscriptionId: subscription,
11808
+ applicationSnapshot: current,
11809
+ intentSnapshot: intent,
11810
+ quoteSnapshot: quote ?? {},
11811
+ archivedAt: now
11812
+ } }] : [],
11813
+ ...intent ? [{ t: "delete", ns: "subscriptionCheckoutIntents", id: id7 }] : [],
11814
+ ...quote ? [{ t: "update", ns: "membershipQuoteProjections", id: id7, attrs: { status: "continued", updatedAt: now } }] : [],
11815
+ { t: "retract", ns: "applications", id: id7, attrs: BILLING_FIELDS.filter((key3) => current[key3] !== void 0) },
11816
+ { t: "update", ns: "applications", id: id7, attrs: { tierId: tier.id, status: approvable } }
11817
+ ];
11818
+ try {
11819
+ await db.transact(ops, { mutationId: `continue-free:${id7}:${subscription ?? "none"}`, guards: [
11820
+ { ns: "applications", id: id7, exists: true, equals: { status: "refunded", ...current.tierId !== void 0 ? { tierId: current.tierId } : {} } },
11821
+ ...intent && subscription ? [{ ns: "membershipCheckoutHistory", id: historyId, exists: false }] : []
11822
+ ] });
11823
+ } catch (error) {
11824
+ if (error.code !== "transact_guard_failed") throw error;
11825
+ return json({ error: "application changed; reload before continuing", code: "continue_conflict" }, 409);
11826
+ }
11827
+ } else if (current.tierId !== tier.id || ![approvable, "approved"].includes(String(current.status))) {
11828
+ return json({ error: `cannot continue as ${tier.id} from status "${String(current.status)}"; refund first`, code: "continue_conflict" }, 409);
11829
+ }
11830
+ const converted = await membershipLoad(db, "applications", id7);
11831
+ if (!converted || converted.tierId !== tier.id) return json({ error: "conversion pending; retry", code: "continue_pending" }, 503);
11832
+ const approved = await approveApplication({ db, env, ctx, app: converted, origin, reason: "manual" });
11833
+ if (!approved.ok) return json({ error: approved.error, code: "continue_pending" }, approved.status);
11834
+ return json({ ok: true, continued: { tierId: tier.id, status: "approved", rolePromoted: approved.rolePromoted, emailLogged: approved.emailLogged } });
11835
+ }
11836
+
11464
11837
  // src/patch-application.ts
11465
11838
  async function patchApplication(db, env, ctx, app, body) {
11466
11839
  const attrs = {};
@@ -11600,6 +11973,15 @@ var handleAdminApprove = async (req, url, env, ctx) => {
11600
11973
  if (!result.ok) return json({ error: result.error }, result.status);
11601
11974
  return json({ ok: true, status: "approved", rolePromoted: result.rolePromoted, emailLogged: result.emailLogged });
11602
11975
  };
11976
+ var handleAdminDecline = async (req, url, env, ctx) => {
11977
+ const m = url.pathname.match(/^\/api\/admin\/applications\/([0-9a-f-]+)\/decline$/);
11978
+ if (req.method !== "POST" || !m) return null;
11979
+ const got = await gate4(req, env, ctx);
11980
+ if (got instanceof Response) return got;
11981
+ const app = await loadApp(got, m[1]);
11982
+ if (!app) return json({ error: "not found" }, 404);
11983
+ return declineApplication(got, env, ctx, app);
11984
+ };
11603
11985
  var handleAdminRefund = async (req, url, env, ctx) => {
11604
11986
  const m = url.pathname.match(/^\/api\/admin\/applications\/([0-9a-f-]+)\/refund$/);
11605
11987
  if (req.method !== "POST" || !m) return null;
@@ -11615,7 +11997,21 @@ var handleAdminRefund = async (req, url, env, ctx) => {
11615
11997
  return json({ error: "super-admin required", code: "super_admin_required" }, 403);
11616
11998
  }
11617
11999
  }
11618
- return refundApplication(db, env, ctx, app);
12000
+ const body = req.headers.get("content-length") === "0" || !req.body ? {} : await readJson(req);
12001
+ if (body === null) return json({ error: "invalid JSON body" }, 400);
12002
+ const continueAs = typeof body.continueAs === "string" ? body.continueAs : void 0;
12003
+ if (continueAs === void 0) return refundApplication(db, env, ctx, app);
12004
+ const tier = await resolveFreeTier(db, ctx, app, continueAs);
12005
+ if (tier instanceof Response) return tier;
12006
+ let receipt = {};
12007
+ if (app.stripeSubscriptionId || app.namedSeatId) {
12008
+ const refunded = await refundApplication(db, env, ctx, app);
12009
+ if (!refunded.ok) return refunded;
12010
+ receipt = await refunded.json();
12011
+ }
12012
+ const continued = await continueApplicationOnFreeTier(db, env, ctx, app, tier, url.origin);
12013
+ if (!continued.ok) return continued;
12014
+ return json({ ...receipt, ...await continued.json() });
11619
12015
  };
11620
12016
  var handleAdminApplicationPatch = async (req, url, env, ctx) => {
11621
12017
  const m = url.pathname.match(/^\/api\/admin\/applications\/([0-9a-f-]+)$/);
@@ -12275,6 +12671,7 @@ async function networkApplicationAction(opts) {
12275
12671
  const approval = await approveApplication({ db, env, ctx, app, origin: opts.origin, reason: "manual" });
12276
12672
  result = json(approval, approval.status);
12277
12673
  } else if (input.stage === "refunded") result = await refundApplication(db, env, ctx, app);
12674
+ else if (input.stage === "declined" && ctx.membershipAuthority) result = await declineApplication(db, env, ctx, app);
12278
12675
  else result = app.status === input.stage ? json({ ok: true }) : await patchApplication(db, env, ctx, app, { status: input.stage });
12279
12676
  if (!result.ok) {
12280
12677
  const latest = (await db.query({ applications: { $: { where: { id: appId }, limit: 1 } } })).applications?.[0];
@@ -15990,10 +16387,14 @@ async function readMembershipChapterEffect(db, input, now) {
15990
16387
  requireMembershipScope(seat, input);
15991
16388
  const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
15992
16389
  requireMembershipScope(member, input);
15993
- 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");
16390
+ 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");
16391
+ const released = input.kind === "release_named_recipient";
16392
+ const recipientUserId = released ? effect.recipientUserId : seat.recipientUserId;
16393
+ const recipientApplicationId = released ? effect.recipientApplicationId : seat.recipientApplicationId;
15994
16394
  let applicable;
15995
- 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));
16395
+ if (input.kind === "invite_named_recipient") applicable = seat.status === "pending_acceptance" && seat.acceptedAt === void 0 && now < Number(member.termEndsAt) && namedSeatInvitationOpen(member);
15996
16396
  else if (input.kind === "revoke_named_recipient_access") applicable = ["refund_pending", "refunded"].includes(String(seat.status));
16397
+ else if (released) applicable = typeof recipientApplicationId === "string" && seat.recipientApplicationId !== recipientApplicationId;
15997
16398
  else applicable = (await readNamedSeatAccess(db, {
15998
16399
  ...input,
15999
16400
  recipientUserId: String(seat.recipientUserId),
@@ -16003,8 +16404,8 @@ async function readMembershipChapterEffect(db, input, now) {
16003
16404
  ...input,
16004
16405
  recipientName: String(seat.recipientName),
16005
16406
  recipientEmail: String(seat.recipientEmailNormalized),
16006
- recipientUserId: typeof seat.recipientUserId === "string" ? seat.recipientUserId : null,
16007
- recipientApplicationId: typeof seat.recipientApplicationId === "string" ? seat.recipientApplicationId : null,
16407
+ recipientUserId: typeof recipientUserId === "string" ? recipientUserId : null,
16408
+ recipientApplicationId: typeof recipientApplicationId === "string" ? recipientApplicationId : null,
16008
16409
  acceptanceEndsAt: Number(seat.acceptanceEndsAt),
16009
16410
  termEndsAt: Number(seat.termEndsAt),
16010
16411
  applicable
@@ -16025,7 +16426,8 @@ var MEMBERSHIP_SEAT_ACTION_FIELDS = {
16025
16426
  "seat-accept": recipient,
16026
16427
  "seat-approve": recipient,
16027
16428
  "seat-decline": recipient,
16028
- "seat-effect": ["seatId", "effectId", "kind"]
16429
+ "seat-effect": ["seatId", "effectId", "kind"],
16430
+ "seat-regift": ["seatId", "recipientName", "recipientEmail"]
16029
16431
  };
16030
16432
  async function handleMembershipSeatAction(action, db, peer, input, now, stripeSecretName) {
16031
16433
  if (!peer.seatPriceId) throw new Error("named seats are not configured");
@@ -16050,6 +16452,12 @@ async function handleMembershipSeatAction(action, db, peer, input, now, stripeSe
16050
16452
  kind: String(input.kind)
16051
16453
  }, now);
16052
16454
  if (action === "seat-offer") return readNamedSeatOffer(db, target, now);
16455
+ if (action === "seat-regift") return regiftNamedSeat(db, {
16456
+ ...target,
16457
+ seatId: String(input.seatId),
16458
+ recipientName: String(input.recipientName),
16459
+ recipientEmail: String(input.recipientEmail)
16460
+ }, now);
16053
16461
  if (action === "seat-reservations") {
16054
16462
  const seat = await reserveNamedMembershipSeat(db, {
16055
16463
  ...target,
@@ -16431,7 +16839,9 @@ var membershipAuthorityEntities = {
16431
16839
  ...times(),
16432
16840
  refundReason: attr("string", true),
16433
16841
  refundedCents: attr("number", true),
16434
- providerRefundIds: attr("json", true)
16842
+ providerRefundIds: attr("json", true),
16843
+ regiftedAt: attr("number", true),
16844
+ formerRecipients: attr("json", true)
16435
16845
  } },
16436
16846
  networkSeatRecipient: { attrs: { id: id5(), appId: key(), chapterId: key(), environment: key(), runtime: key(), seatId: key(), recipientEmailNormalized: key(), createdAt: attr("number") } },
16437
16847
  membershipEffect: { attrs: {
@@ -16446,6 +16856,8 @@ var membershipAuthorityEntities = {
16446
16856
  attempts: attr("number"),
16447
16857
  providerObjectId: key(true),
16448
16858
  error: attr("string", true),
16859
+ recipientUserId: key(true),
16860
+ recipientApplicationId: key(true),
16449
16861
  createdAt: attr("number"),
16450
16862
  updatedAt: attr("number", false, false, true)
16451
16863
  } }
@@ -16622,7 +17034,7 @@ async function drainMembershipEffects(db, options, now) {
16622
17034
  if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100 || !options.stripeSecretKey) throw new TypeError("invalid membership recovery configuration");
16623
17035
  await loadMembershipAuthorityState(db, scope2);
16624
17036
  const matches2 = (row2) => Object.entries(scope2).every(([key3, value]) => row2[key3] === value);
16625
- let pending = 0, complete = 0, more = false;
17037
+ let pending = 0, complete = 0, held = 0, more = false;
16626
17038
  for (const status2 of ["payment_pending", "payment_processing", "payment_canceling"]) {
16627
17039
  const seats = (await db.query({ networkSeat: { $: { where: { ...scope2, status: status2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkSeat ?? [];
16628
17040
  more ||= seats.length > limit;
@@ -16658,6 +17070,10 @@ async function drainMembershipEffects(db, options, now) {
16658
17070
  requireMembershipScope(seat, target);
16659
17071
  if (seat.primaryMembershipId !== member.id) throw new Error("membership seat scope mismatch");
16660
17072
  }
17073
+ if (effect.kind === "invite_named_recipient" && seat?.acceptedAt === void 0 && !namedSeatInvitationOpen(member) && !["refund_pending", "refunded"].includes(String(seat?.status))) {
17074
+ held++;
17075
+ continue;
17076
+ }
16661
17077
  try {
16662
17078
  const attempts = Number(effect.attempts);
16663
17079
  if (!Number.isSafeInteger(attempts) || attempts < 0) throw new Error("invalid membership effect attempt");
@@ -16681,9 +17097,10 @@ async function drainMembershipEffects(db, options, now) {
16681
17097
  break;
16682
17098
  case "invite_named_recipient":
16683
17099
  case "grant_named_recipient_access":
16684
- case "revoke_named_recipient_access": {
17100
+ case "revoke_named_recipient_access":
17101
+ case "release_named_recipient": {
16685
17102
  if (!seat) throw new Error("named seat effect missing seat");
16686
- if (effect.kind !== "revoke_named_recipient_access" && ["refund_pending", "refunded"].includes(String(seat.status))) break;
17103
+ if (["invite_named_recipient", "grant_named_recipient_access"].includes(String(effect.kind)) && ["refund_pending", "refunded"].includes(String(seat.status))) break;
16687
17104
  if (!options.deliverChapterEffect) throw new Error("chapter effect receiver unavailable");
16688
17105
  const receipt = await options.deliverChapterEffect({ ...target, effectId: String(effect.id), seatId: String(effect.seatId), kind: effect.kind });
16689
17106
  if (receipt.effectId !== effect.id || receipt.complete !== true) throw new Error("chapter effect receipt unconfirmed");
@@ -16701,7 +17118,7 @@ async function drainMembershipEffects(db, options, now) {
16701
17118
  pending++;
16702
17119
  }
16703
17120
  }
16704
- return { complete, pending, more };
17121
+ return { complete, pending, held, more };
16705
17122
  }
16706
17123
 
16707
17124
  // src/approval-effects-retry.ts
@@ -16881,6 +17298,14 @@ function createMembershipChapterEffectRoute(options) {
16881
17298
  tierId: effect.skuId
16882
17299
  });
16883
17300
  if (!result.sent) throw new Error("invitation delivery unconfirmed");
17301
+ } else if (effect.kind === "release_named_recipient") {
17302
+ stage = "release_recipient";
17303
+ if (!effect.recipientApplicationId || !effect.recipientUserId) throw new Error("recipient not bound");
17304
+ await releaseNamedSeatRecipientApplications(db, env, ctx, {
17305
+ seatId: effect.seatId,
17306
+ recipientApplicationId: effect.recipientApplicationId,
17307
+ recipientUserId: effect.recipientUserId
17308
+ });
16884
17309
  } else {
16885
17310
  stage = "recipient_load";
16886
17311
  if (!effect.recipientApplicationId || !effect.recipientUserId) throw new Error("recipient not bound");
@@ -16976,6 +17401,18 @@ async function requestNamedSeatOperatorRefund(options) {
16976
17401
  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");
16977
17402
  return seat;
16978
17403
  }
17404
+ async function requestNamedSeatRegift(options) {
17405
+ const result = await requestAuthority(options, "seat-regift", {
17406
+ applicationId: options.applicationId,
17407
+ skuId: options.skuId,
17408
+ seatId: options.seatId,
17409
+ recipientName: options.recipientName,
17410
+ recipientEmail: options.recipientEmail
17411
+ });
17412
+ const seat = result.seat;
17413
+ 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");
17414
+ return seat;
17415
+ }
16979
17416
  async function requestNamedSeatReservation(options) {
16980
17417
  const result = await requestAuthority(options, "seat-reservations", {
16981
17418
  applicationId: options.applicationId,
@@ -17182,7 +17619,8 @@ function createMembershipWorkerAdapter(options) {
17182
17619
  accept: async (input, env) => requestNamedSeatAcceptance({ ...input, ...await transport(input, env) }),
17183
17620
  approve: async (input, env) => requestNamedSeatApproval({ ...input, ...await transport(input, env) }),
17184
17621
  decline: async (input, env) => requestNamedSeatDecline({ ...input, ...await transport(input, env) }),
17185
- operatorRefund: async (input, env) => requestNamedSeatOperatorRefund({ ...input, ...await transport(input, env) })
17622
+ operatorRefund: async (input, env) => requestNamedSeatOperatorRefund({ ...input, ...await transport(input, env) }),
17623
+ regift: async (input, env) => requestNamedSeatRegift({ ...input, ...await transport(input, env) })
17186
17624
  } } : {}
17187
17625
  },
17188
17626
  readEffect: async (input, env) => requestMembershipChapterEffect({ ...input, ...await transport(input, env) })
@@ -17430,6 +17868,7 @@ var BUILTIN_ROUTES = [
17430
17868
  handleMemberContent,
17431
17869
  handleGifts,
17432
17870
  handleNamedSeat,
17871
+ handleNamedSeatRegift,
17433
17872
  handleNamedSeatRecipient,
17434
17873
  handleEmailDeliveryResolution,
17435
17874
  handleSchedule,
@@ -17460,6 +17899,7 @@ var BUILTIN_ROUTES = [
17460
17899
  handleAdminMeetingReschedule,
17461
17900
  handleAdminMeetingCancel,
17462
17901
  handleAdminApprove,
17902
+ handleAdminDecline,
17463
17903
  handleAdminRefund,
17464
17904
  handleAdminApplicationPatch,
17465
17905
  handleAdminApplicationActions,
@@ -17506,15 +17946,21 @@ function chapterWorker(options) {
17506
17946
  }
17507
17947
  export {
17508
17948
  AdmissionGrantDeliveryError,
17949
+ DECLINABLE_FROM,
17509
17950
  MEMBERSHIP_AUTHORITY_PATH,
17951
+ MEMBERSHIP_CHAPTER_EFFECT_KINDS,
17510
17952
  NetworkTargetTransportError,
17953
+ REGIFTABLE_SEAT_STATUSES,
17511
17954
  chapterMembershipAccess,
17512
17955
  chapterWorker,
17956
+ closedApplication,
17957
+ continueApplicationOnFreeTier,
17513
17958
  createMembershipAuthorityHandler,
17514
17959
  createMembershipAuthorityStorage,
17515
17960
  createMembershipChapterEffectRoute,
17516
17961
  createMembershipWorkerAdapter,
17517
17962
  createWorkerContext,
17963
+ declineApplication,
17518
17964
  deliverAdmissionGrants,
17519
17965
  deliverBuilderLibrary,
17520
17966
  deliverMemberContent,
@@ -17525,11 +17971,16 @@ export {
17525
17971
  liftSeatPurchaseWindow,
17526
17972
  lowerSeatPrice,
17527
17973
  namedSeatIncluded,
17974
+ namedSeatInvitationOpen,
17528
17975
  namedSeatRecipientState,
17976
+ namedSeatRegiftable,
17529
17977
  readNamedSeatOffer,
17530
17978
  refundNamedSeatByOperator,
17979
+ regiftNamedSeat,
17980
+ releaseNamedSeatRecipientApplications,
17531
17981
  renderActiveSignupControlHtml,
17532
17982
  resolveNetworkTargetTransport,
17983
+ resolveSignupEligibility,
17533
17984
  retermMembershipDiscount,
17534
17985
  retryChapterApprovalEffects,
17535
17986
  retryLeaderAdmissionDeliveries,