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