@odla-ai/chapter 0.49.0 → 0.50.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 +29 -2
- package/dist/{chunk-KXIGZWG5.js → chunk-4EA5V4JE.js} +855 -614
- package/dist/chunk-4EA5V4JE.js.map +1 -0
- package/dist/{chunk-N2JIKINY.js → chunk-JXX4BFDP.js} +5 -2
- package/dist/chunk-JXX4BFDP.js.map +1 -0
- package/dist/{chunk-KHDO2QFA.js → chunk-OS46ZE46.js} +40 -24
- package/dist/chunk-OS46ZE46.js.map +1 -0
- package/dist/{copy-context-By4Nq7yB.d.ts → copy-context-Ddat__H5.d.ts} +10 -2
- package/dist/index.cjs +106 -43
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +52 -6
- package/dist/index.d.ts +52 -6
- package/dist/index.js +106 -43
- package/dist/index.js.map +1 -1
- package/dist/ui/admin/index.d.ts +54 -6
- package/dist/ui/admin/index.js +10 -2
- package/dist/ui/index.d.ts +2 -2
- package/dist/ui/index.js +11 -3
- package/dist/ui/member/index.d.ts +10 -2
- package/dist/ui/member/index.js +2 -2
- package/dist/worker/index.cjs +520 -261
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +81 -5
- package/dist/worker/index.d.ts +81 -5
- package/dist/worker/index.js +514 -255
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-KHDO2QFA.js.map +0 -1
- package/dist/chunk-KXIGZWG5.js.map +0 -1
- package/dist/chunk-N2JIKINY.js.map +0 -1
package/dist/worker/index.js
CHANGED
|
@@ -329,6 +329,7 @@ function createWorkerContext(options) {
|
|
|
329
329
|
crmBase,
|
|
330
330
|
requirePaymentQuote: options.requirePaymentQuote === true,
|
|
331
331
|
membershipAuthority: options.membershipAuthority,
|
|
332
|
+
membershipAuthorityOperator: options.membershipAuthorityOperator,
|
|
332
333
|
getPublicConfig,
|
|
333
334
|
verifyUser,
|
|
334
335
|
makeDb,
|
|
@@ -1842,24 +1843,63 @@ async function verifyIssuedMembershipOffer(value, expected) {
|
|
|
1842
1843
|
if (!value || typeof value !== "object" || Array.isArray(value)) return reject("invalid_offer_snapshot");
|
|
1843
1844
|
return restore({ ...value, approvedFoundingCount: 0 }, expected);
|
|
1844
1845
|
}
|
|
1845
|
-
async function
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1846
|
+
async function previewMembershipCommercialOffer(db, input) {
|
|
1847
|
+
for (const key3 of ["appId", "chapterId", "environment", "runtime", "skuId"]) {
|
|
1848
|
+
if (typeof input[key3] !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_.:-]{1,159}$/.test(input[key3])) reject("invalid_offer_identity");
|
|
1849
|
+
}
|
|
1850
|
+
if (!Number.isSafeInteger(input.now) || input.now < 0) reject("invalid_offer_window");
|
|
1851
|
+
const state = await authoritativeState(db, input);
|
|
1852
|
+
const { offering, policy } = await currentPolicy(db, input, false, state);
|
|
1853
|
+
const approvedFoundingCount = policy ? state.cohortCounts[policy.cohortLabel] ?? 0 : 0;
|
|
1854
|
+
if (!Number.isSafeInteger(approvedFoundingCount) || Number(approvedFoundingCount) < 0) reject("invalid_cohort_count");
|
|
1855
|
+
const contract = createMembershipCommercialOffer({
|
|
1856
|
+
offering,
|
|
1857
|
+
discountPolicy: policy,
|
|
1858
|
+
approvedFoundingCount: Number(approvedFoundingCount),
|
|
1859
|
+
now: input.now,
|
|
1860
|
+
couponId: await couponId(input, policy?.id)
|
|
1861
|
+
});
|
|
1862
|
+
const discount = contract.billingTerms.discount;
|
|
1863
|
+
return {
|
|
1864
|
+
skuId: input.skuId,
|
|
1865
|
+
offeringVersionId: offering.id,
|
|
1866
|
+
basePriceCents: offering.basePriceCents,
|
|
1867
|
+
currency: offering.currency,
|
|
1868
|
+
interval: offering.interval,
|
|
1869
|
+
founding: contract.founding,
|
|
1870
|
+
...contract.founding && contract.discountPolicy ? { cohortLabel: contract.discountPolicy.cohortLabel } : {},
|
|
1871
|
+
dueTodayCents: contract.dueTodayCents,
|
|
1872
|
+
renewalCents: contract.renewalCents,
|
|
1873
|
+
...discount ? { discountDuration: discount.duration === "once" ? "first_term" : "while_active" } : {},
|
|
1874
|
+
...contract.initialTermEndsAt !== void 0 ? { initialTermEndsAt: contract.initialTermEndsAt } : {}
|
|
1875
|
+
};
|
|
1876
|
+
}
|
|
1877
|
+
async function authoritativeState(db, input) {
|
|
1878
|
+
const state = await load(db, "membershipAuthorityState", `${input.chapterId}:${input.environment}`);
|
|
1849
1879
|
if (!state || state.mode !== "authoritative" || state.chapterId !== input.chapterId || state.environment !== input.environment || !Number.isSafeInteger(state.revision) || Number(state.revision) < 1 || !state.cohortCounts || typeof state.cohortCounts !== "object" || Array.isArray(state.cohortCounts)) return reject("membership_authority_unavailable");
|
|
1850
|
-
|
|
1851
|
-
|
|
1880
|
+
return state;
|
|
1881
|
+
}
|
|
1882
|
+
async function currentPolicy(db, input, restartAware, state) {
|
|
1852
1883
|
const rows5 = await db.query({
|
|
1853
1884
|
membershipOfferingVersion: { $: { where: { chapterId: input.chapterId }, limit: 200 } },
|
|
1854
1885
|
discountPolicyVersion: { $: { where: { chapterId: input.chapterId }, limit: 200 } }
|
|
1855
1886
|
});
|
|
1856
1887
|
if (!Array.isArray(rows5.membershipOfferingVersion) || !Array.isArray(rows5.discountPolicyVersion) || rows5.membershipOfferingVersion.length >= 200 || rows5.discountPolicyVersion.length >= 200) return reject("policy_history_incomplete");
|
|
1857
|
-
const offerings = rows5.membershipOfferingVersion.map((
|
|
1858
|
-
const policies = rows5.discountPolicyVersion.map((
|
|
1888
|
+
const offerings = rows5.membershipOfferingVersion.map((row2) => membershipOfferingVersion(row2.snapshot)).filter((offer) => offer.chapterId === input.chapterId && offer.baseSkuId === input.skuId && offer.status === "published" && input.now >= offer.effectiveFrom && (offer.effectiveUntil === void 0 || input.now < offer.effectiveUntil));
|
|
1889
|
+
const policies = rows5.discountPolicyVersion.map((row2) => membershipDiscountPolicyVersion(row2.snapshot)).filter((policy2) => policy2.chapterId === input.chapterId && policy2.status === "published" && policy2.cohortEligibleSkuIds.includes(input.skuId) && input.now >= policy2.qualifyingStartsAt && input.now < policy2.qualifyingEndsAt);
|
|
1859
1890
|
const offering = offerings.sort((a, b) => b.version - a.version)[0];
|
|
1860
|
-
const restart = await load(db, "membershipRestart", (await membershipOfferIdForRequest({ ...input, requestId: "checkout_binding" })).replace(/^offer_/, "membership_"));
|
|
1891
|
+
const restart = restartAware && input.applicationId ? await load(db, "membershipRestart", (await membershipOfferIdForRequest({ ...input, applicationId: input.applicationId, requestId: "checkout_binding" })).replace(/^offer_/, "membership_")) : void 0;
|
|
1861
1892
|
const policy = restart ? void 0 : policies.sort((a, b) => b.version - a.version)[0];
|
|
1862
1893
|
if (!offering || offerings.filter((item) => item.version === offering.version).length !== 1 || policy && policies.filter((item) => item.version === policy.version).length !== 1) reject("ambiguous_membership_policy");
|
|
1894
|
+
return { offering, policy };
|
|
1895
|
+
}
|
|
1896
|
+
async function issueMembershipCommercialOffer(db, input) {
|
|
1897
|
+
validate(input);
|
|
1898
|
+
const stateId = `${input.chapterId}:${input.environment}`;
|
|
1899
|
+
const state = await authoritativeState(db, input);
|
|
1900
|
+
const existing = await load(db, "membershipCheckoutOffer", input.offerId);
|
|
1901
|
+
if (existing) return restore(existing, input);
|
|
1902
|
+
const { offering, policy } = await currentPolicy(db, input, true, state);
|
|
1863
1903
|
const approvedFoundingCount = policy ? state.cohortCounts[policy.cohortLabel] ?? 0 : 0;
|
|
1864
1904
|
if (!Number.isSafeInteger(approvedFoundingCount) || Number(approvedFoundingCount) < 0) reject("invalid_cohort_count");
|
|
1865
1905
|
const contract = createMembershipCommercialOffer({
|
|
@@ -2167,7 +2207,7 @@ function findTier(tiers, tierId) {
|
|
|
2167
2207
|
if (!tierId) return tiers.length === 1 ? tiers[0] : null;
|
|
2168
2208
|
return tiers.find((tier) => tier.id === tierId) ?? null;
|
|
2169
2209
|
}
|
|
2170
|
-
function joinConfigTiers(tiers, defaults = {}) {
|
|
2210
|
+
function joinConfigTiers(tiers, defaults = {}, founding = /* @__PURE__ */ new Map()) {
|
|
2171
2211
|
return tiers.map((tier) => ({
|
|
2172
2212
|
id: tier.id,
|
|
2173
2213
|
name: tier.name,
|
|
@@ -2175,7 +2215,8 @@ function joinConfigTiers(tiers, defaults = {}) {
|
|
|
2175
2215
|
currency: tier.currency ?? defaults.currency ?? "usd",
|
|
2176
2216
|
interval: tier.interval ?? defaults.interval ?? "year",
|
|
2177
2217
|
blurb: tier.blurb ?? "",
|
|
2178
|
-
free: tierIsFree(tier)
|
|
2218
|
+
free: tierIsFree(tier),
|
|
2219
|
+
...founding.has(tier.id) ? { founding: founding.get(tier.id) } : {}
|
|
2179
2220
|
}));
|
|
2180
2221
|
}
|
|
2181
2222
|
|
|
@@ -3410,6 +3451,267 @@ var handlePaymentRecovery = async (req, url, env, ctx) => {
|
|
|
3410
3451
|
}
|
|
3411
3452
|
};
|
|
3412
3453
|
|
|
3454
|
+
// src/worker-routes-admin-named-seat.ts
|
|
3455
|
+
var PATH = /^\/api\/admin\/applications\/([0-9a-f-]+)\/named-seat(\/refund)?$/;
|
|
3456
|
+
var REFUNDABLE = /* @__PURE__ */ new Set(["renewal_pending", "pending_acceptance", "pending_approval", "pending_primary", "active"]);
|
|
3457
|
+
var handleAdminNamedSeat = async (req, url, env, ctx) => {
|
|
3458
|
+
const match = url.pathname.match(PATH);
|
|
3459
|
+
if (!match) return null;
|
|
3460
|
+
const seats = ctx.membershipAuthority?.namedSeats;
|
|
3461
|
+
if (ctx.chapter.mode !== "chapter" || !seats) return json({ error: "named seats are not configured" }, 404);
|
|
3462
|
+
const refund = Boolean(match[2]);
|
|
3463
|
+
if (req.method !== (refund ? "POST" : "GET")) return json({ error: "method not allowed" }, 405);
|
|
3464
|
+
const raw2 = ctx.makeDb(env), user = await ctx.verifyUser(req, env);
|
|
3465
|
+
if (!user) return json({ error: "unauthorized" }, 401);
|
|
3466
|
+
if (!await ctx.isSuperAdminEmail(raw2, user.email, user.userId)) return json({ error: "super-admin required", code: "super_admin_required" }, 403);
|
|
3467
|
+
const db = raw2, app = await membershipLoad(db, "applications", match[1]);
|
|
3468
|
+
if (!app || !chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME) || app.namedSeatId) return json({ error: "not found" }, 404);
|
|
3469
|
+
const intent = await membershipLoad(db, "subscriptionCheckoutIntents", String(app.id));
|
|
3470
|
+
if (!intent) return json({ error: "paid membership required" }, 409);
|
|
3471
|
+
try {
|
|
3472
|
+
const target = await membershipTargetForIntent(intent, app, env, ctx);
|
|
3473
|
+
const offer = await seats.offer(target, env);
|
|
3474
|
+
const seat = offer.seat ?? null, refundable = Boolean(seat && REFUNDABLE.has(seat.status));
|
|
3475
|
+
if (!refund) {
|
|
3476
|
+
return json({
|
|
3477
|
+
seat,
|
|
3478
|
+
amountCents: offer.amountCents,
|
|
3479
|
+
currency: offer.currency,
|
|
3480
|
+
termEndsAt: offer.termEndsAt,
|
|
3481
|
+
purchaseEndsAt: offer.purchaseEndsAt,
|
|
3482
|
+
refundable
|
|
3483
|
+
});
|
|
3484
|
+
}
|
|
3485
|
+
if (!seat) return json({ error: "no purchased seat", code: "seat_not_found" }, 409);
|
|
3486
|
+
if (seat.status === "refunded") {
|
|
3487
|
+
await closeRecipientApplication(db, env, ctx, seat.seatId);
|
|
3488
|
+
return json({ ok: true, seatId: seat.seatId, status: "refunded", replayed: true });
|
|
3489
|
+
}
|
|
3490
|
+
if (!refundable) return json({ error: `seat is ${seat.status.replaceAll("_", " ")}`, code: "seat_not_refundable" }, 409);
|
|
3491
|
+
const receipt = await seats.operatorRefund({ ...target, seatId: seat.seatId }, env);
|
|
3492
|
+
await closeRecipientApplication(db, env, ctx, seat.seatId);
|
|
3493
|
+
return json({ ok: true, ...receipt });
|
|
3494
|
+
} catch {
|
|
3495
|
+
return json({ error: "named seat service unavailable; retry to confirm" }, 503);
|
|
3496
|
+
}
|
|
3497
|
+
};
|
|
3498
|
+
async function closeRecipientApplication(db, env, ctx, seatId) {
|
|
3499
|
+
const recipient2 = (await db.query({ applications: { $: { where: { namedSeatId: seatId }, limit: 1 } } })).applications?.[0];
|
|
3500
|
+
if (!recipient2 || !chapterBillingApplication(recipient2, ctx.chapter.id, env.ODLA_RUNTIME) || recipient2.status === "refunded") return;
|
|
3501
|
+
const attrs = { status: "refunded", renewalAt: 0, canceled: true };
|
|
3502
|
+
await db.transact(
|
|
3503
|
+
[{ t: "update", ns: "applications", id: String(recipient2.id), attrs }],
|
|
3504
|
+
{ mutationId: `verified-refund:${String(recipient2.id)}:${seatId}` }
|
|
3505
|
+
);
|
|
3506
|
+
await syncApplicationToCrm(crmDeps(db, ctx), { app: { ...recipient2, ...attrs }, stage: "refunded" }).catch(() => void 0);
|
|
3507
|
+
}
|
|
3508
|
+
|
|
3509
|
+
// src/membership-seat-refund-stripe.ts
|
|
3510
|
+
import { stripeRequest as stripeRequest4 } from "@odla-ai/stripe";
|
|
3511
|
+
var fail3 = (code = "named_seat_refund_mismatch") => {
|
|
3512
|
+
throw Object.assign(new Error(code), { code });
|
|
3513
|
+
};
|
|
3514
|
+
async function refundNamedSeatStripePayment(secretKey, seat) {
|
|
3515
|
+
if (!seat.providerPaymentIntentId || seat.status !== "refund_pending") fail3();
|
|
3516
|
+
const call = async (method, path, params, idempotencyKey) => {
|
|
3517
|
+
const result = await stripeRequest4({ secretKey }, method, path, params, idempotencyKey ? { idempotencyKey } : {});
|
|
3518
|
+
if (!result.ok || !result.body) return fail3("named_seat_refund_provider_unavailable");
|
|
3519
|
+
return result.body;
|
|
3520
|
+
};
|
|
3521
|
+
const metadata = { ...stripePartitionMetadata(seat), applicationId: seat.applicationId, membershipId: seat.primaryMembershipId, seatId: seat.id };
|
|
3522
|
+
const payment = await call("GET", `/v1/payment_intents/${encodeURIComponent(seat.providerPaymentIntentId)}`);
|
|
3523
|
+
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)) fail3();
|
|
3524
|
+
const sub = await call("GET", `/v1/subscriptions/${encodeURIComponent(seat.providerSubscriptionId)}`);
|
|
3525
|
+
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)) fail3();
|
|
3526
|
+
const items = sub.items.data.filter((item) => item.metadata?.seatId === seat.id);
|
|
3527
|
+
if (items.length > 1) fail3();
|
|
3528
|
+
if (items.length && sub.status !== "canceled") {
|
|
3529
|
+
const item = items[0];
|
|
3530
|
+
if (!/^si_[A-Za-z0-9_]+$/.test(item.id) || item.price?.id !== seat.priceId || item.quantity !== 1) fail3();
|
|
3531
|
+
const removed = await call("DELETE", `/v1/subscription_items/${encodeURIComponent(item.id)}`, { proration_behavior: "none" });
|
|
3532
|
+
if (removed.id !== item.id || removed.deleted !== true) fail3("named_seat_renewal_removal_unconfirmed");
|
|
3533
|
+
}
|
|
3534
|
+
const list = await call("GET", "/v1/refunds", { payment_intent: seat.providerPaymentIntentId, limit: 100 });
|
|
3535
|
+
if (!Array.isArray(list.data) || list.has_more !== false || list.data.length >= 100) fail3("named_seat_refund_history_incomplete");
|
|
3536
|
+
let refundedCents = 0, pendingCents = 0;
|
|
3537
|
+
const refundIds = [];
|
|
3538
|
+
for (const refund of list.data) {
|
|
3539
|
+
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)) fail3();
|
|
3540
|
+
if (refund.status === "succeeded") {
|
|
3541
|
+
refundedCents += refund.amount;
|
|
3542
|
+
refundIds.push(refund.id);
|
|
3543
|
+
} else if (refund.status === "pending" || refund.status === "requires_action") pendingCents += refund.amount;
|
|
3544
|
+
}
|
|
3545
|
+
if (refundedCents + pendingCents > seat.amountCents) fail3();
|
|
3546
|
+
if (pendingCents) fail3("named_seat_refund_pending");
|
|
3547
|
+
if (refundedCents < seat.amountCents) {
|
|
3548
|
+
const remaining = seat.amountCents - refundedCents;
|
|
3549
|
+
const refund = await call("POST", "/v1/refunds", {
|
|
3550
|
+
payment_intent: seat.providerPaymentIntentId,
|
|
3551
|
+
amount: remaining,
|
|
3552
|
+
reason: "requested_by_customer",
|
|
3553
|
+
metadata
|
|
3554
|
+
}, `named-seat-refund:${seat.id}:${remaining}`);
|
|
3555
|
+
if (refund.payment_intent !== seat.providerPaymentIntentId || refund.amount !== remaining || refund.currency !== seat.currency || !/^re_[A-Za-z0-9_]+$/.test(refund.id)) fail3();
|
|
3556
|
+
if (refund.status !== "succeeded") fail3("named_seat_refund_pending");
|
|
3557
|
+
refundedCents += remaining;
|
|
3558
|
+
refundIds.push(refund.id);
|
|
3559
|
+
}
|
|
3560
|
+
return { refundedCents, refundIds };
|
|
3561
|
+
}
|
|
3562
|
+
|
|
3563
|
+
// src/membership-seat-refund.ts
|
|
3564
|
+
async function requestNamedSeatRefund(db, input, now) {
|
|
3565
|
+
membershipClock(now);
|
|
3566
|
+
const state = await loadMembershipAuthorityState(db, input);
|
|
3567
|
+
const seat = await membershipLoad(db, "networkSeat", input.seatId);
|
|
3568
|
+
requireMembershipScope(seat, input);
|
|
3569
|
+
const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
|
|
3570
|
+
requireMembershipScope(member, input);
|
|
3571
|
+
if (seat.status === "refunded" || seat.status === "refund_pending") return;
|
|
3572
|
+
if (!seat.purchasedAt || !seat.providerPaymentIntentId || input.reason === "declined" && seat.journeyApprovedAt !== void 0 || input.reason === "primary_closed" && !["refunded", "declined"].includes(String(member.status))) membershipFail("named_seat_refund_not_eligible");
|
|
3573
|
+
const entitlement = seat.entitlementId ? await membershipLoad(db, "networkEntitlement", String(seat.entitlementId)) : void 0;
|
|
3574
|
+
await db.transact([
|
|
3575
|
+
membershipUpdate("networkSeat", input.seatId, { status: "refund_pending", refundReason: input.reason, revision: Number(seat.revision) + 1, updatedAt: now }),
|
|
3576
|
+
...entitlement ? [membershipUpdate("networkEntitlement", String(entitlement.id), { status: "closed", revision: Number(entitlement.revision) + 1, updatedAt: now })] : [],
|
|
3577
|
+
membershipUpdate("membershipEffect", `${seat.id}:refund`, {
|
|
3578
|
+
id: `${seat.id}:refund`,
|
|
3579
|
+
chapterId: input.chapterId,
|
|
3580
|
+
environment: input.environment,
|
|
3581
|
+
runtime: input.runtime,
|
|
3582
|
+
membershipId: member.id,
|
|
3583
|
+
seatId: seat.id,
|
|
3584
|
+
kind: "refund_seat",
|
|
3585
|
+
status: "pending",
|
|
3586
|
+
attempts: 0,
|
|
3587
|
+
createdAt: now,
|
|
3588
|
+
updatedAt: now
|
|
3589
|
+
}),
|
|
3590
|
+
...seat.recipientUserId ? [membershipUpdate("membershipEffect", `${seat.id}:revoke`, {
|
|
3591
|
+
id: `${seat.id}:revoke`,
|
|
3592
|
+
chapterId: input.chapterId,
|
|
3593
|
+
environment: input.environment,
|
|
3594
|
+
runtime: input.runtime,
|
|
3595
|
+
membershipId: member.id,
|
|
3596
|
+
seatId: seat.id,
|
|
3597
|
+
kind: "revoke_named_recipient_access",
|
|
3598
|
+
status: "pending",
|
|
3599
|
+
attempts: 0,
|
|
3600
|
+
createdAt: now,
|
|
3601
|
+
updatedAt: now
|
|
3602
|
+
})] : []
|
|
3603
|
+
], { mutationId: `named-seat-refund-request:${seat.id}`, guards: [
|
|
3604
|
+
{ ns: "networkSeat", id: input.seatId, exists: true, equals: { revision: seat.revision } },
|
|
3605
|
+
{ ns: "networkMembership", id: String(member.id), exists: true, equals: { revision: member.revision } },
|
|
3606
|
+
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
|
|
3607
|
+
...entitlement ? [{ ns: "networkEntitlement", id: String(entitlement.id), exists: true, equals: { revision: entitlement.revision } }] : []
|
|
3608
|
+
] });
|
|
3609
|
+
}
|
|
3610
|
+
async function completeNamedSeatRefund(db, seatId, secretKey, now) {
|
|
3611
|
+
membershipClock(now);
|
|
3612
|
+
const seat = await membershipLoad(db, "networkSeat", seatId);
|
|
3613
|
+
if (!seat) return membershipFail("named_seat_not_found");
|
|
3614
|
+
await loadMembershipAuthorityState(db, seat);
|
|
3615
|
+
if (seat.status === "refunded") return;
|
|
3616
|
+
if (seat.status !== "refund_pending") membershipFail("named_seat_refund_not_ready");
|
|
3617
|
+
const result = await refundNamedSeatStripePayment(secretKey, seat);
|
|
3618
|
+
await db.transact([
|
|
3619
|
+
membershipUpdate("networkSeat", seatId, {
|
|
3620
|
+
status: "refunded",
|
|
3621
|
+
refundedCents: result.refundedCents,
|
|
3622
|
+
providerRefundIds: result.refundIds,
|
|
3623
|
+
revision: Number(seat.revision) + 1,
|
|
3624
|
+
updatedAt: now
|
|
3625
|
+
}),
|
|
3626
|
+
membershipUpdate("membershipEffect", `${seatId}:refund`, { status: "complete", updatedAt: now })
|
|
3627
|
+
], { mutationId: `named-seat-refunded:${seatId}`, guards: [
|
|
3628
|
+
{ ns: "networkSeat", id: seatId, exists: true, equals: { status: "refund_pending", revision: seat.revision } }
|
|
3629
|
+
] });
|
|
3630
|
+
}
|
|
3631
|
+
|
|
3632
|
+
// src/membership-seat-operator-refund.ts
|
|
3633
|
+
async function refundNamedSeatByOperator(db, input, stripeSecretKey, now) {
|
|
3634
|
+
const seat = await membershipLoad(db, "networkSeat", input.seatId);
|
|
3635
|
+
requireMembershipScope(seat, input);
|
|
3636
|
+
await requestNamedSeatRefund(db, { ...input, reason: "operator" }, now);
|
|
3637
|
+
await completeNamedSeatRefund(db, input.seatId, stripeSecretKey, now);
|
|
3638
|
+
const refunded = await membershipLoad(db, "networkSeat", input.seatId);
|
|
3639
|
+
if (refunded?.status !== "refunded" || !Number.isSafeInteger(refunded.refundedCents) || Number(refunded.refundedCents) <= 0) {
|
|
3640
|
+
throw new Error("named seat refund unconfirmed");
|
|
3641
|
+
}
|
|
3642
|
+
return { seatId: String(seat.id), status: "refunded", refundedCents: Number(refunded.refundedCents), subscriptionCanceled: false };
|
|
3643
|
+
}
|
|
3644
|
+
|
|
3645
|
+
// src/worker-routes-admin-membership-seats.ts
|
|
3646
|
+
var LIST = "/api/admin/membership-seats";
|
|
3647
|
+
var ONE = /^\/api\/admin\/membership-seats\/([A-Za-z0-9][A-Za-z0-9_.:-]{2,159})\/refund$/;
|
|
3648
|
+
var PUBLIC = [
|
|
3649
|
+
"id",
|
|
3650
|
+
"appId",
|
|
3651
|
+
"chapterId",
|
|
3652
|
+
"environment",
|
|
3653
|
+
"runtime",
|
|
3654
|
+
"applicationId",
|
|
3655
|
+
"skuId",
|
|
3656
|
+
"status",
|
|
3657
|
+
"amountCents",
|
|
3658
|
+
"currency",
|
|
3659
|
+
"recipientName",
|
|
3660
|
+
"recipientEmailNormalized",
|
|
3661
|
+
"purchaserUserId",
|
|
3662
|
+
"purchasedAt",
|
|
3663
|
+
"acceptedAt",
|
|
3664
|
+
"journeyApprovedAt",
|
|
3665
|
+
"refundedCents",
|
|
3666
|
+
"refundReason",
|
|
3667
|
+
"termEndsAt",
|
|
3668
|
+
"updatedAt"
|
|
3669
|
+
];
|
|
3670
|
+
var project = (row2) => Object.fromEntries(PUBLIC.filter((key3) => row2[key3] !== void 0).map((key3) => [key3, row2[key3]]));
|
|
3671
|
+
var handleAdminMembershipSeats = async (req, url, env, ctx) => {
|
|
3672
|
+
const one = url.pathname.match(ONE);
|
|
3673
|
+
if (url.pathname !== LIST && !one) return null;
|
|
3674
|
+
const operator = ctx.membershipAuthorityOperator;
|
|
3675
|
+
if (!operator) return json({ error: "not found" }, 404);
|
|
3676
|
+
if (req.method !== (one ? "POST" : "GET")) return json({ error: "method not allowed" }, 405);
|
|
3677
|
+
const raw2 = ctx.makeDb(env), user = await ctx.verifyUser(req, env);
|
|
3678
|
+
if (!user) return json({ error: "unauthorized" }, 401);
|
|
3679
|
+
if (!await ctx.isSuperAdminEmail(raw2, user.email, user.userId)) return json({ error: "super-admin required", code: "super_admin_required" }, 403);
|
|
3680
|
+
let db;
|
|
3681
|
+
try {
|
|
3682
|
+
db = operator.makeDb(env);
|
|
3683
|
+
} catch {
|
|
3684
|
+
return json({ error: "the membership authority is not hosted by this deployment" }, 404);
|
|
3685
|
+
}
|
|
3686
|
+
if (!one) {
|
|
3687
|
+
const rows5 = (await db.query({ networkSeat: { $: { where: { environment: env.ODLA_ENV }, order: { updatedAt: "desc" }, limit: 201 } } })).networkSeat ?? [];
|
|
3688
|
+
return json({ seats: rows5.slice(0, 200).map(project), truncated: rows5.length > 200 });
|
|
3689
|
+
}
|
|
3690
|
+
const seat = await membershipLoad(db, "networkSeat", one[1]);
|
|
3691
|
+
if (!seat || seat.environment !== env.ODLA_ENV) return json({ error: "not found" }, 404);
|
|
3692
|
+
if (seat.status === "refunded") {
|
|
3693
|
+
return json({ ok: true, seatId: seat.id, status: "refunded", refundedCents: seat.refundedCents, subscriptionCanceled: false, replayed: true });
|
|
3694
|
+
}
|
|
3695
|
+
if (!seat.purchasedAt) return json({ error: "seat has no completed payment to refund", code: "seat_not_refundable" }, 409);
|
|
3696
|
+
const secret = await getVaultSecret(db, operator.stripeSecretName ?? "stripe_secret_key");
|
|
3697
|
+
if (!secret) return json({ error: "network payment provider unavailable" }, 503);
|
|
3698
|
+
try {
|
|
3699
|
+
const receipt = await refundNamedSeatByOperator(db, {
|
|
3700
|
+
appId: String(seat.appId),
|
|
3701
|
+
chapterId: String(seat.chapterId),
|
|
3702
|
+
environment: String(seat.environment),
|
|
3703
|
+
runtime: String(seat.runtime),
|
|
3704
|
+
applicationId: String(seat.applicationId),
|
|
3705
|
+
skuId: String(seat.skuId),
|
|
3706
|
+
seatId: String(seat.id)
|
|
3707
|
+
}, secret, Date.now());
|
|
3708
|
+
return json({ ok: true, ...receipt });
|
|
3709
|
+
} catch (error) {
|
|
3710
|
+
const code = error.code;
|
|
3711
|
+
return json({ error: "seat refund is pending; retry to confirm", ...code ? { code } : {} }, code ? 409 : 503);
|
|
3712
|
+
}
|
|
3713
|
+
};
|
|
3714
|
+
|
|
3413
3715
|
// src/worker-routes-membership-cancel.ts
|
|
3414
3716
|
var handleMembershipCancellation = async (req, url, env, ctx) => {
|
|
3415
3717
|
const match = url.pathname.match(/^\/api\/admin\/applications\/([0-9a-f-]+)\/cancel-renewal$/);
|
|
@@ -4006,6 +4308,29 @@ async function authorizeCrmDiscussionReference(req, env, fetcher = fetch) {
|
|
|
4006
4308
|
}
|
|
4007
4309
|
}
|
|
4008
4310
|
|
|
4311
|
+
// src/join-founding-preview.ts
|
|
4312
|
+
async function foundingPreviews(tiers, env, ctx) {
|
|
4313
|
+
const result = /* @__PURE__ */ new Map();
|
|
4314
|
+
const preview = ctx.membershipAuthority?.previewOffer;
|
|
4315
|
+
if (!preview) return result;
|
|
4316
|
+
const scope2 = { appId: env.ODLA_APP_ID, chapterId: ctx.chapter.id, environment: env.ODLA_ENV, runtime: stripeRuntimeIdentity(env.ODLA_RUNTIME) };
|
|
4317
|
+
await Promise.all(tiers.filter((tier) => !tierIsFree(tier) && tier.stripePriceId).map(async (tier) => {
|
|
4318
|
+
try {
|
|
4319
|
+
const offer = await preview({ ...scope2, skuId: tier.id }, env);
|
|
4320
|
+
if (!offer.founding || !offer.cohortLabel || offer.basePriceCents !== tier.priceCents || offer.dueTodayCents >= offer.basePriceCents) return;
|
|
4321
|
+
result.set(tier.id, {
|
|
4322
|
+
cohortLabel: offer.cohortLabel,
|
|
4323
|
+
dueTodayCents: offer.dueTodayCents,
|
|
4324
|
+
discountCents: offer.basePriceCents - offer.dueTodayCents,
|
|
4325
|
+
renewalCents: offer.renewalCents,
|
|
4326
|
+
...offer.discountDuration ? { discountDuration: offer.discountDuration } : {}
|
|
4327
|
+
});
|
|
4328
|
+
} catch {
|
|
4329
|
+
}
|
|
4330
|
+
}));
|
|
4331
|
+
return result;
|
|
4332
|
+
}
|
|
4333
|
+
|
|
4009
4334
|
// src/admission-grants-contract.ts
|
|
4010
4335
|
var ADMISSION_GRANTS_SCHEMA_VERSION = 1;
|
|
4011
4336
|
var ADMISSION_GRANTS_PATH = "/api/admission-grants";
|
|
@@ -4532,10 +4857,24 @@ async function materializeSignupControl(db, envelope, previous, receivedAt) {
|
|
|
4532
4857
|
}
|
|
4533
4858
|
|
|
4534
4859
|
// src/payment-quote.ts
|
|
4860
|
+
function tierPresentationFor(document, tierId) {
|
|
4861
|
+
const compareAt = document?.membershipPage?.tiers?.find((tier) => tier.tierId === tierId)?.compareAtPriceCents;
|
|
4862
|
+
return Number.isSafeInteger(compareAt) && Number(compareAt) > 0 ? { compareAtPriceCents: Number(compareAt) } : void 0;
|
|
4863
|
+
}
|
|
4864
|
+
function foundingOf(offer) {
|
|
4865
|
+
return offer?.contract?.founding === true && offer.contract.discountPolicy ? { cohortLabel: offer.contract.discountPolicy.cohortLabel } : void 0;
|
|
4866
|
+
}
|
|
4535
4867
|
async function createPaymentQuote(input) {
|
|
4536
4868
|
const { group, tier, policy } = input;
|
|
4537
4869
|
const terms2 = input.billingTerms ? subscriptionBillingTerms(input.billingTerms) : void 0;
|
|
4870
|
+
const compareAt = input.presentation?.compareAtPriceCents;
|
|
4871
|
+
const presentation = Number.isSafeInteger(compareAt) && Number(compareAt) > tier.priceCents ? { compareAtPriceCents: Number(compareAt) } : void 0;
|
|
4538
4872
|
const dueToday = terms2 ? subscriptionAmountDue({ priceCents: tier.priceCents, billingTerms: terms2 }) : void 0;
|
|
4873
|
+
const lines = dueToday === void 0 ? paymentLineItems(group, tier) : {
|
|
4874
|
+
standardCents: tier.priceCents,
|
|
4875
|
+
discountCents: tier.priceCents - dueToday,
|
|
4876
|
+
dueTodayCents: dueToday
|
|
4877
|
+
};
|
|
4539
4878
|
const quote = {
|
|
4540
4879
|
applicationId: input.applicationId,
|
|
4541
4880
|
chapterId: input.chapterId,
|
|
@@ -4543,11 +4882,12 @@ async function createPaymentQuote(input) {
|
|
|
4543
4882
|
tierName: tier.name,
|
|
4544
4883
|
currency: tier.currency ?? String(group.currency ?? "usd"),
|
|
4545
4884
|
interval: tier.interval ?? (group.interval === "month" ? "month" : "year"),
|
|
4546
|
-
lineItems:
|
|
4547
|
-
standardCents:
|
|
4548
|
-
discountCents:
|
|
4549
|
-
dueTodayCents:
|
|
4550
|
-
},
|
|
4885
|
+
lineItems: presentation ? {
|
|
4886
|
+
standardCents: presentation.compareAtPriceCents,
|
|
4887
|
+
discountCents: presentation.compareAtPriceCents - lines.dueTodayCents,
|
|
4888
|
+
dueTodayCents: lines.dueTodayCents
|
|
4889
|
+
} : lines,
|
|
4890
|
+
...presentation ? { presentation } : {},
|
|
4551
4891
|
refundPolicyText: policy?.refundPolicyText ?? String(group.refundPolicyText ?? ""),
|
|
4552
4892
|
merchantDisclosureText: String(group.merchantDisclosureText ?? ""),
|
|
4553
4893
|
trustCopy: policy ? policy.trustCopy ?? "" : String(group.trustCopy ?? ""),
|
|
@@ -4572,37 +4912,39 @@ async function createPaymentQuote(input) {
|
|
|
4572
4912
|
});
|
|
4573
4913
|
const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(contract));
|
|
4574
4914
|
const digest = [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
4575
|
-
return { ...quote, digest };
|
|
4915
|
+
return { ...quote, digest, ...input.founding ? { founding: input.founding } : {} };
|
|
4576
4916
|
}
|
|
4577
4917
|
|
|
4578
4918
|
// src/membership-quote-projection.ts
|
|
4579
4919
|
var MEMBERSHIP_QUOTE_NAMESPACE = "membershipQuoteProjections";
|
|
4580
4920
|
var keys2 = ["appId", "chapterId", "environment", "runtime", "applicationId", "skuId"];
|
|
4581
|
-
var
|
|
4921
|
+
var fail4 = (code) => {
|
|
4582
4922
|
throw Object.assign(new Error(code), { code });
|
|
4583
4923
|
};
|
|
4584
4924
|
var load3 = async (db, ns, id6) => (await db.query({ [ns]: { $: { where: { id: id6 }, limit: 1 } } }))[ns]?.[0];
|
|
4585
4925
|
async function restore2(row2, target, now, allowExpired = false) {
|
|
4586
|
-
if (!row2 || keys2.some((key3) => row2[key3] !== target[key3]) || !Number.isSafeInteger(row2.revision) || Number(row2.revision) < 1 || typeof row2.requestId !== "string") return
|
|
4587
|
-
if (row2.status !== "ready") return
|
|
4926
|
+
if (!row2 || keys2.some((key3) => row2[key3] !== target[key3]) || !Number.isSafeInteger(row2.revision) || Number(row2.revision) < 1 || typeof row2.requestId !== "string") return fail4("membership_quote_unavailable");
|
|
4927
|
+
if (row2.status !== "ready") return fail4("membership_quote_pending");
|
|
4588
4928
|
const offer = await verifyIssuedMembershipOffer(row2.offer, { ...target, offerId: await membershipOfferIdForRequest({
|
|
4589
4929
|
...target,
|
|
4590
4930
|
requestId: row2.requestId
|
|
4591
4931
|
}), now, lifetimeMs: 6e4 });
|
|
4592
|
-
if (offer.issuedAt > now + 3e4 || !allowExpired && offer.expiresAt <= now) return
|
|
4932
|
+
if (offer.issuedAt > now + 3e4 || !allowExpired && offer.expiresAt <= now) return fail4("membership_quote_expired");
|
|
4593
4933
|
const tier = row2.tier, quote = row2.quote;
|
|
4594
4934
|
const offering = offer.contract.offering;
|
|
4595
4935
|
if (!tier || tier.id !== target.skuId || tier.groupId !== target.chapterId || !tier.stripePriceId || tier.priceCents !== offering.basePriceCents || tier.currency !== offering.currency || tier.interval !== offering.interval || !quote) {
|
|
4596
|
-
return
|
|
4936
|
+
return fail4("membership_catalog_mismatch");
|
|
4597
4937
|
}
|
|
4598
4938
|
const expected = await createPaymentQuote({
|
|
4599
4939
|
...target,
|
|
4600
4940
|
tier,
|
|
4601
4941
|
group: { merchantDisclosureText: quote.merchantDisclosureText },
|
|
4602
4942
|
policy: { refundPolicyText: quote.refundPolicyText, trustCopy: quote.trustCopy },
|
|
4603
|
-
billingTerms: offer.contract.billingTerms
|
|
4943
|
+
billingTerms: offer.contract.billingTerms,
|
|
4944
|
+
presentation: quote.presentation,
|
|
4945
|
+
founding: foundingOf(offer)
|
|
4604
4946
|
});
|
|
4605
|
-
if (quote.digest !== expected.digest) return
|
|
4947
|
+
if (quote.digest !== expected.digest) return fail4("membership_quote_conflict");
|
|
4606
4948
|
return { tier, quote: expected, billingTerms: offer.contract.billingTerms, membershipOffer: offer, projectionRevision: Number(row2.revision) };
|
|
4607
4949
|
}
|
|
4608
4950
|
async function readMembershipQuote(db, target, now) {
|
|
@@ -4610,9 +4952,9 @@ async function readMembershipQuote(db, target, now) {
|
|
|
4610
4952
|
}
|
|
4611
4953
|
async function prepareMembershipQuote(db, input, issue) {
|
|
4612
4954
|
const { applicationId: id6, now, app } = input;
|
|
4613
|
-
if (app.id !== id6 || app.groupId !== input.chapterId || app.tierId !== input.skuId || app.stripeRuntime !== input.runtime || app.status !== "submitted" || app.canceled === true || app.giftOfferId || app.giftClaimId || app.namedSeatId || app.namedSeatClaimPending) return
|
|
4955
|
+
if (app.id !== id6 || app.groupId !== input.chapterId || app.tierId !== input.skuId || app.stripeRuntime !== input.runtime || app.status !== "submitted" || app.canceled === true || app.giftOfferId || app.giftClaimId || app.namedSeatId || app.namedSeatClaimPending) return fail4("membership_quote_unavailable");
|
|
4614
4956
|
let row2 = await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, id6);
|
|
4615
|
-
if (row2 && keys2.some((key3) => row2[key3] !== input[key3])) return
|
|
4957
|
+
if (row2 && keys2.some((key3) => row2[key3] !== input[key3])) return fail4("membership_quote_conflict");
|
|
4616
4958
|
if (row2?.status === "ready") {
|
|
4617
4959
|
try {
|
|
4618
4960
|
return await restore2(row2, input, now);
|
|
@@ -4657,7 +4999,7 @@ async function prepareMembershipQuote(db, input, issue) {
|
|
|
4657
4999
|
}
|
|
4658
5000
|
if (row2?.status === "ready") return restore2(row2, input, now);
|
|
4659
5001
|
if (!row2 || row2.status !== "pending" || keys2.some((key3) => row2[key3] !== input[key3]) || typeof row2.requestId !== "string") {
|
|
4660
|
-
return
|
|
5002
|
+
return fail4("membership_quote_pending");
|
|
4661
5003
|
}
|
|
4662
5004
|
const target = Object.fromEntries(keys2.map((key3) => [key3, input[key3]]));
|
|
4663
5005
|
const offer = await verifyIssuedMembershipOffer(await issue({ ...target, requestId: row2.requestId }), {
|
|
@@ -4671,7 +5013,15 @@ async function prepareMembershipQuote(db, input, issue) {
|
|
|
4671
5013
|
currency: input.tier.currency ?? String(input.group.currency ?? "usd"),
|
|
4672
5014
|
interval: input.tier.interval ?? (input.group.interval === "month" ? "month" : "year")
|
|
4673
5015
|
};
|
|
4674
|
-
const quote = await createPaymentQuote({
|
|
5016
|
+
const quote = await createPaymentQuote({
|
|
5017
|
+
...target,
|
|
5018
|
+
tier,
|
|
5019
|
+
group: input.group,
|
|
5020
|
+
policy: input.policy,
|
|
5021
|
+
billingTerms: offer.contract.billingTerms,
|
|
5022
|
+
presentation: input.presentation,
|
|
5023
|
+
founding: foundingOf(offer)
|
|
5024
|
+
});
|
|
4675
5025
|
const ready = { ...row2, offer, tier, quote, status: "ready", updatedAt: now };
|
|
4676
5026
|
await restore2(ready, target, now, true);
|
|
4677
5027
|
try {
|
|
@@ -4726,7 +5076,9 @@ async function checkoutQuoteForApplication(db, app, group, env, ctx, prepare = f
|
|
|
4726
5076
|
group: { merchantDisclosureText: quote2.merchantDisclosureText },
|
|
4727
5077
|
tier: tier2,
|
|
4728
5078
|
policy: { refundPolicyText: quote2.refundPolicyText, trustCopy: quote2.trustCopy },
|
|
4729
|
-
billingTerms
|
|
5079
|
+
billingTerms,
|
|
5080
|
+
presentation: quote2.presentation,
|
|
5081
|
+
founding: foundingOf(intent.membershipOffer)
|
|
4730
5082
|
});
|
|
4731
5083
|
if (expected.digest !== quote2.digest) throw new Error("invalid commercial snapshot");
|
|
4732
5084
|
return { tier: tier2, quote: expected, billingTerms, membershipOffer: intent.membershipOffer };
|
|
@@ -4753,9 +5105,10 @@ async function checkoutQuoteForApplication(db, app, group, env, ctx, prepare = f
|
|
|
4753
5105
|
if (!tier || tierIsFree(tier) || !tier.stripePriceId) throw Object.assign(new Error("tier unavailable"), { code: "tier_unresolved" });
|
|
4754
5106
|
const control3 = await loadActiveSignupControl(db, ctx.chapter.id, env.ODLA_ENV);
|
|
4755
5107
|
const policy = control3?.document.policy?.tierPolicies?.[tier.id];
|
|
5108
|
+
const presentation = tierPresentationFor(control3?.document, tier.id);
|
|
4756
5109
|
if (ctx.membershipAuthority) return prepareMembershipQuote(
|
|
4757
5110
|
db,
|
|
4758
|
-
{ ...target, app, group, tier, policy, now: Date.now() },
|
|
5111
|
+
{ ...target, app, group, tier, policy, presentation, now: Date.now() },
|
|
4759
5112
|
(input) => ctx.membershipAuthority.issueOffer(input, env)
|
|
4760
5113
|
);
|
|
4761
5114
|
const quote = await createPaymentQuote({
|
|
@@ -4766,7 +5119,8 @@ async function checkoutQuoteForApplication(db, app, group, env, ctx, prepare = f
|
|
|
4766
5119
|
runtime: stripeRuntimeIdentity(env.ODLA_RUNTIME),
|
|
4767
5120
|
group,
|
|
4768
5121
|
tier,
|
|
4769
|
-
policy
|
|
5122
|
+
policy,
|
|
5123
|
+
presentation
|
|
4770
5124
|
});
|
|
4771
5125
|
return { tier, quote, billingTerms: void 0 };
|
|
4772
5126
|
}
|
|
@@ -5420,10 +5774,11 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
5420
5774
|
group,
|
|
5421
5775
|
Boolean(stripeKey)
|
|
5422
5776
|
);
|
|
5777
|
+
const founding = await foundingPreviews(offered, env, ctx);
|
|
5423
5778
|
const activeControl = chapter.signupControl ? await loadActiveSignupControl(db, chapter.id, env.ODLA_ENV) : null;
|
|
5424
5779
|
const application = controlledApplication(chapter.application, activeControl?.document);
|
|
5425
5780
|
return json({
|
|
5426
|
-
...joinConfig(group, paymentsReady, joinConfigTiers(offered, { currency: typeof group.currency === "string" ? group.currency : void 0, interval: group.interval === "month" ? "month" : "year" }), application.conditions),
|
|
5781
|
+
...joinConfig(group, paymentsReady, joinConfigTiers(offered, { currency: typeof group.currency === "string" ? group.currency : void 0, interval: group.interval === "month" ? "month" : "year" }, founding), application.conditions),
|
|
5427
5782
|
fieldContract: chapterFlowFieldContract(chapter, "join", application),
|
|
5428
5783
|
copy: { ...chapter.copy.join, ...activeControl?.document.application?.copy ?? {} },
|
|
5429
5784
|
...activeControl ? { signupControl: {
|
|
@@ -7116,7 +7471,7 @@ async function deliverAdmissionGrants(options) {
|
|
|
7116
7471
|
}
|
|
7117
7472
|
|
|
7118
7473
|
// src/worker-routes-network-admission.ts
|
|
7119
|
-
var
|
|
7474
|
+
var PATH2 = "/api/admin/network/admissions";
|
|
7120
7475
|
var RETRY_PATH = "/api/admin/network/admissions/retry";
|
|
7121
7476
|
var REVOKE_PATH = "/api/admin/network/admissions/revoke";
|
|
7122
7477
|
var MUTATION_ID2 = /^[A-Za-z0-9_-]{16,128}$/;
|
|
@@ -7196,11 +7551,11 @@ async function parseBody(req) {
|
|
|
7196
7551
|
}
|
|
7197
7552
|
}
|
|
7198
7553
|
var handleAdminNetworkAdmissions = async (req, url, env, ctx) => {
|
|
7199
|
-
if (![
|
|
7554
|
+
if (![PATH2, RETRY_PATH, REVOKE_PATH].includes(url.pathname)) return null;
|
|
7200
7555
|
const auth = await gate2(req, env, ctx);
|
|
7201
7556
|
if ("response" in auth) return auth.response;
|
|
7202
7557
|
const db = auth.db;
|
|
7203
|
-
if (url.pathname ===
|
|
7558
|
+
if (url.pathname === PATH2 && req.method === "GET") {
|
|
7204
7559
|
const targetId2 = url.searchParams.get("targetId") ?? "";
|
|
7205
7560
|
const target2 = targetFor(ctx, targetId2);
|
|
7206
7561
|
if (!target2) return reject6(404, "unknown_target", "admission target is not configured");
|
|
@@ -7444,28 +7799,28 @@ var handleSchedule = async (req, url, env, ctx) => {
|
|
|
7444
7799
|
};
|
|
7445
7800
|
|
|
7446
7801
|
// src/payments-stripe.ts
|
|
7447
|
-
import { stripeRequest as
|
|
7802
|
+
import { stripeRequest as stripeRequest8, verifyStripeSignature as verifyStripeSignature2 } from "@odla-ai/stripe";
|
|
7448
7803
|
|
|
7449
7804
|
// src/payments-refund.ts
|
|
7450
|
-
import { stripeRequest as
|
|
7805
|
+
import { stripeRequest as stripeRequest5 } from "@odla-ai/stripe";
|
|
7451
7806
|
var row = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
7452
7807
|
var id2 = (value) => typeof value === "string" ? value : String(row(value).id ?? "");
|
|
7453
7808
|
var providerId2 = (value, prefix) => new RegExp(`^${prefix}_[A-Za-z0-9]+$`).test(value);
|
|
7454
|
-
function
|
|
7809
|
+
function fail5(code = "refund_target_mismatch") {
|
|
7455
7810
|
throw Object.assign(new Error(code), { code });
|
|
7456
7811
|
}
|
|
7457
7812
|
async function refundSubscriptionPayment(secretKey, input) {
|
|
7458
|
-
if (!providerId2(input.customerId, "cus") || !providerId2(input.subscriptionId, "sub"))
|
|
7813
|
+
if (!providerId2(input.customerId, "cus") || !providerId2(input.subscriptionId, "sub")) fail5();
|
|
7459
7814
|
const call = async (method, path, params, key3) => {
|
|
7460
|
-
const result = await
|
|
7461
|
-
if (!result.ok)
|
|
7462
|
-
if (!result.body || typeof result.body !== "object" || Array.isArray(result.body))
|
|
7815
|
+
const result = await stripeRequest5({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
|
|
7816
|
+
if (!result.ok) fail5("stripe_error");
|
|
7817
|
+
if (!result.body || typeof result.body !== "object" || Array.isArray(result.body)) fail5("stripe_invalid_response");
|
|
7463
7818
|
return result.body;
|
|
7464
7819
|
};
|
|
7465
7820
|
const sub = await call("GET", `/v1/subscriptions/${input.subscriptionId}`);
|
|
7466
|
-
if (id2(sub) !== input.subscriptionId || id2(sub.customer) !== input.customerId)
|
|
7821
|
+
if (id2(sub) !== input.subscriptionId || id2(sub.customer) !== input.customerId) fail5();
|
|
7467
7822
|
for (const [key3, value] of Object.entries(input.metadata ?? {})) {
|
|
7468
|
-
if (row(sub.metadata)[key3] !== value)
|
|
7823
|
+
if (row(sub.metadata)[key3] !== value) fail5();
|
|
7469
7824
|
}
|
|
7470
7825
|
let cursor;
|
|
7471
7826
|
let earliest;
|
|
@@ -7478,69 +7833,69 @@ async function refundSubscriptionPayment(secretKey, input) {
|
|
|
7478
7833
|
limit: 100,
|
|
7479
7834
|
...cursor ? { starting_after: cursor } : {}
|
|
7480
7835
|
});
|
|
7481
|
-
if (!Array.isArray(list.data) || typeof list.has_more !== "boolean")
|
|
7836
|
+
if (!Array.isArray(list.data) || typeof list.has_more !== "boolean") fail5("stripe_invalid_response");
|
|
7482
7837
|
for (const value of list.data) {
|
|
7483
7838
|
const invoice2 = row(value);
|
|
7484
7839
|
const subscription = invoice2.subscription ?? row(row(invoice2.parent).subscription_details).subscription;
|
|
7485
|
-
if (!providerId2(id2(invoice2), "in") || seen.has(id2(invoice2)) || id2(subscription) !== input.subscriptionId || id2(invoice2.customer) !== input.customerId || invoice2.status !== "paid" || !Number.isSafeInteger(invoice2.created) || !Number.isSafeInteger(invoice2.amount_paid) || Number(invoice2.amount_paid) < 0)
|
|
7840
|
+
if (!providerId2(id2(invoice2), "in") || seen.has(id2(invoice2)) || id2(subscription) !== input.subscriptionId || id2(invoice2.customer) !== input.customerId || invoice2.status !== "paid" || !Number.isSafeInteger(invoice2.created) || !Number.isSafeInteger(invoice2.amount_paid) || Number(invoice2.amount_paid) < 0) fail5();
|
|
7486
7841
|
seen.add(id2(invoice2));
|
|
7487
7842
|
if (Number(invoice2.amount_paid) > 0 && (!earliest || Number(invoice2.created) < Number(earliest.created))) earliest = invoice2;
|
|
7488
7843
|
}
|
|
7489
7844
|
if (!list.has_more) break;
|
|
7490
|
-
if (!list.data.length || page === 9)
|
|
7845
|
+
if (!list.data.length || page === 9) fail5("refund_history_incomplete");
|
|
7491
7846
|
cursor = id2(list.data[list.data.length - 1]);
|
|
7492
7847
|
}
|
|
7493
|
-
if (!earliest)
|
|
7848
|
+
if (!earliest) fail5("no_charge");
|
|
7494
7849
|
const invoice = earliest;
|
|
7495
|
-
if (input.expectedInvoiceId !== void 0 && id2(invoice) !== input.expectedInvoiceId || input.expectedAmountCents !== void 0 && invoice.amount_paid !== input.expectedAmountCents)
|
|
7850
|
+
if (input.expectedInvoiceId !== void 0 && id2(invoice) !== input.expectedInvoiceId || input.expectedAmountCents !== void 0 && invoice.amount_paid !== input.expectedAmountCents) fail5();
|
|
7496
7851
|
let chargeId = id2(invoice.charge);
|
|
7497
7852
|
let paymentIntentId = id2(invoice.payment_intent);
|
|
7498
7853
|
if (!chargeId && !paymentIntentId) {
|
|
7499
7854
|
const payments = await call("GET", "/v1/invoice_payments", { invoice: id2(invoice), status: "paid", limit: 100 });
|
|
7500
|
-
if (payments.has_more !== false || !Array.isArray(payments.data) || payments.data.length !== 1)
|
|
7855
|
+
if (payments.has_more !== false || !Array.isArray(payments.data) || payments.data.length !== 1) fail5("refund_payment_ambiguous");
|
|
7501
7856
|
const payment = row(payments.data[0]);
|
|
7502
|
-
if (id2(payment.invoice) !== id2(invoice) || payment.status !== "paid" || row(payment.payment).type !== "payment_intent")
|
|
7857
|
+
if (id2(payment.invoice) !== id2(invoice) || payment.status !== "paid" || row(payment.payment).type !== "payment_intent") fail5();
|
|
7503
7858
|
paymentIntentId = id2(row(payment.payment).payment_intent);
|
|
7504
7859
|
}
|
|
7505
7860
|
if (!chargeId) {
|
|
7506
|
-
if (!providerId2(paymentIntentId, "pi"))
|
|
7861
|
+
if (!providerId2(paymentIntentId, "pi")) fail5();
|
|
7507
7862
|
const payment = await call("GET", `/v1/payment_intents/${paymentIntentId}`);
|
|
7508
|
-
if (id2(payment) !== paymentIntentId || id2(payment.customer) !== input.customerId || payment.status !== "succeeded")
|
|
7863
|
+
if (id2(payment) !== paymentIntentId || id2(payment.customer) !== input.customerId || payment.status !== "succeeded") fail5();
|
|
7509
7864
|
chargeId = id2(payment.latest_charge);
|
|
7510
7865
|
}
|
|
7511
|
-
if (!providerId2(chargeId, "ch"))
|
|
7866
|
+
if (!providerId2(chargeId, "ch")) fail5();
|
|
7512
7867
|
const charge = await call("GET", `/v1/charges/${chargeId}`);
|
|
7513
|
-
if (id2(charge) !== chargeId || id2(charge.customer) !== input.customerId || charge.status !== "succeeded" || charge.invoice != null && id2(charge.invoice) !== id2(invoice) || paymentIntentId && id2(charge.payment_intent) !== paymentIntentId || !Number.isSafeInteger(charge.amount) || Number(charge.amount) <= 0 || !Number.isSafeInteger(charge.amount_refunded) || Number(charge.amount_refunded) < 0 || Number(charge.amount_refunded) > Number(charge.amount) || charge.currency !== invoice.currency || Number(charge.amount) !== Number(invoice.amount_paid))
|
|
7514
|
-
if (input.alreadyRefundedChargeId !== void 0 && (chargeId !== input.alreadyRefundedChargeId || charge.amount_refunded !== charge.amount || charge.refunded !== true))
|
|
7868
|
+
if (id2(charge) !== chargeId || id2(charge.customer) !== input.customerId || charge.status !== "succeeded" || charge.invoice != null && id2(charge.invoice) !== id2(invoice) || paymentIntentId && id2(charge.payment_intent) !== paymentIntentId || !Number.isSafeInteger(charge.amount) || Number(charge.amount) <= 0 || !Number.isSafeInteger(charge.amount_refunded) || Number(charge.amount_refunded) < 0 || Number(charge.amount_refunded) > Number(charge.amount) || charge.currency !== invoice.currency || Number(charge.amount) !== Number(invoice.amount_paid)) fail5();
|
|
7869
|
+
if (input.alreadyRefundedChargeId !== void 0 && (chargeId !== input.alreadyRefundedChargeId || charge.amount_refunded !== charge.amount || charge.refunded !== true)) fail5();
|
|
7515
7870
|
if (charge.amount_refunded !== charge.amount) {
|
|
7516
7871
|
const refund = await call("POST", "/v1/refunds", {
|
|
7517
7872
|
charge: chargeId,
|
|
7518
7873
|
...input.metadata ? { metadata: input.metadata } : {}
|
|
7519
7874
|
}, `chapter-initial-refund:${input.subscriptionId}:${chargeId}`);
|
|
7520
|
-
if (id2(refund.charge) !== chargeId || !["succeeded", "pending"].includes(String(refund.status)) || !Number.isSafeInteger(refund.amount) || Number(refund.amount) !== Number(charge.amount) - Number(charge.amount_refunded))
|
|
7875
|
+
if (id2(refund.charge) !== chargeId || !["succeeded", "pending"].includes(String(refund.status)) || !Number.isSafeInteger(refund.amount) || Number(refund.amount) !== Number(charge.amount) - Number(charge.amount_refunded)) fail5("refund_not_confirmed");
|
|
7521
7876
|
}
|
|
7522
7877
|
let subscriptionCanceled = sub.status === "canceled";
|
|
7523
7878
|
if (input.cancelSubscription !== false && !subscriptionCanceled) {
|
|
7524
7879
|
const canceled = await call("DELETE", `/v1/subscriptions/${input.subscriptionId}`);
|
|
7525
|
-
if (id2(canceled) !== input.subscriptionId || canceled.status !== "canceled")
|
|
7880
|
+
if (id2(canceled) !== input.subscriptionId || canceled.status !== "canceled") fail5("refund_cancel_incomplete");
|
|
7526
7881
|
subscriptionCanceled = true;
|
|
7527
7882
|
}
|
|
7528
7883
|
const confirmed = await call("GET", `/v1/charges/${chargeId}`);
|
|
7529
|
-
if (id2(confirmed) !== chargeId || id2(confirmed.customer) !== input.customerId || confirmed.amount !== charge.amount || confirmed.currency !== charge.currency || confirmed.amount_refunded !== charge.amount || confirmed.refunded !== true)
|
|
7884
|
+
if (id2(confirmed) !== chargeId || id2(confirmed.customer) !== input.customerId || confirmed.amount !== charge.amount || confirmed.currency !== charge.currency || confirmed.amount_refunded !== charge.amount || confirmed.refunded !== true) fail5("refund_not_confirmed");
|
|
7530
7885
|
return { refundedCents: Number(charge.amount), subscriptionCanceled };
|
|
7531
7886
|
}
|
|
7532
7887
|
|
|
7533
7888
|
// src/subscription-payment-identity.ts
|
|
7534
|
-
import { stripeRequest as
|
|
7889
|
+
import { stripeRequest as stripeRequest7 } from "@odla-ai/stripe";
|
|
7535
7890
|
|
|
7536
7891
|
// src/subscription-provider-contract.ts
|
|
7537
|
-
import { stripeRequest as
|
|
7892
|
+
import { stripeRequest as stripeRequest6 } from "@odla-ai/stripe";
|
|
7538
7893
|
function subscriptionContractMismatch() {
|
|
7539
7894
|
throw Object.assign(new Error("Stripe subscription does not match the checkout contract"), { code: "stripe_contract_mismatch" });
|
|
7540
7895
|
}
|
|
7541
7896
|
async function verifySubscriptionPrice(secretKey, input, resume = false) {
|
|
7542
7897
|
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();
|
|
7543
|
-
const result = await
|
|
7898
|
+
const result = await stripeRequest6({ secretKey }, "GET", `/v1/prices/${encodeURIComponent(input.priceId)}`);
|
|
7544
7899
|
const price = result.body;
|
|
7545
7900
|
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();
|
|
7546
7901
|
return typeof price.product === "string" ? price.product : void 0;
|
|
@@ -7559,7 +7914,7 @@ async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata,
|
|
|
7559
7914
|
let paymentId = id3(invoice.payment_intent);
|
|
7560
7915
|
if (!paymentId) {
|
|
7561
7916
|
if (!/^in_[A-Za-z0-9]+$/.test(id3(invoice))) subscriptionContractMismatch();
|
|
7562
|
-
const result2 = await
|
|
7917
|
+
const result2 = await stripeRequest7({ secretKey }, "GET", "/v1/invoice_payments", { invoice: id3(invoice), limit: 100 });
|
|
7563
7918
|
const list = result2.body;
|
|
7564
7919
|
if (!result2.ok || list?.has_more !== false || !Array.isArray(list.data) || list.data.length !== 1) subscriptionContractMismatch();
|
|
7565
7920
|
const link = list.data[0];
|
|
@@ -7568,11 +7923,11 @@ async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata,
|
|
|
7568
7923
|
}
|
|
7569
7924
|
if (!/^pi_[A-Za-z0-9]+$/.test(paymentId)) subscriptionContractMismatch();
|
|
7570
7925
|
const path = `/v1/payment_intents/${paymentId}`;
|
|
7571
|
-
const result = await
|
|
7926
|
+
const result = await stripeRequest7({ secretKey }, "GET", path);
|
|
7572
7927
|
const payment = result.body;
|
|
7573
7928
|
if (!result.ok || payment?.id !== paymentId || id3(payment.customer) !== customerId || payment.amount !== invoice.total || payment.currency !== invoice.currency || payment.client_secret !== clientSecret2 || payment.livemode !== (metadata.odlaEnvironment === "prod") || !Object.entries(metadata).every(([key3, value]) => payment.metadata?.[key3] === void 0 || payment.metadata[key3] === value)) subscriptionContractMismatch();
|
|
7574
7929
|
if (Object.entries(metadata).every(([key3, value]) => payment.metadata?.[key3] === value)) return;
|
|
7575
|
-
const tagged = await
|
|
7930
|
+
const tagged = await stripeRequest7(
|
|
7576
7931
|
{ secretKey },
|
|
7577
7932
|
"POST",
|
|
7578
7933
|
path,
|
|
@@ -7584,7 +7939,7 @@ async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata,
|
|
|
7584
7939
|
|
|
7585
7940
|
// src/payments-stripe.ts
|
|
7586
7941
|
async function stripeCall(sk, method, path, params, idempotencyKey) {
|
|
7587
|
-
return
|
|
7942
|
+
return stripeRequest8(
|
|
7588
7943
|
{ secretKey: sk },
|
|
7589
7944
|
method,
|
|
7590
7945
|
path,
|
|
@@ -7592,7 +7947,7 @@ async function stripeCall(sk, method, path, params, idempotencyKey) {
|
|
|
7592
7947
|
idempotencyKey ? { idempotencyKey } : {}
|
|
7593
7948
|
);
|
|
7594
7949
|
}
|
|
7595
|
-
function
|
|
7950
|
+
function fail6(op, r) {
|
|
7596
7951
|
const err = new Error(`stripe ${op} failed: ${r.status}`);
|
|
7597
7952
|
err.code = "stripe_error";
|
|
7598
7953
|
throw err;
|
|
@@ -7642,7 +7997,7 @@ function createStripeProvider(config) {
|
|
|
7642
7997
|
{ email: input.email, name: input.name, metadata: meta },
|
|
7643
7998
|
`cus:${input.applicationId}`
|
|
7644
7999
|
);
|
|
7645
|
-
if (!cust.ok)
|
|
8000
|
+
if (!cust.ok) fail6("customer create", cust);
|
|
7646
8001
|
if (typeof cust.body.id !== "string" || !/^cus_[A-Za-z0-9]+$/.test(cust.body.id)) subscriptionContractMismatch();
|
|
7647
8002
|
customerId = cust.body.id;
|
|
7648
8003
|
}
|
|
@@ -7674,7 +8029,7 @@ function createStripeProvider(config) {
|
|
|
7674
8029
|
// retry between create and the db write can't orphan a second one.
|
|
7675
8030
|
`sub:${input.applicationId}${input.purchaseAttemptId ? `:${input.purchaseAttemptId}` : ""}`
|
|
7676
8031
|
);
|
|
7677
|
-
if (!sub.ok)
|
|
8032
|
+
if (!sub.ok) fail6("subscription create", sub);
|
|
7678
8033
|
if (input.billingTerms?.discount?.duration === "once" && Array.isArray(sub.body.discounts) && sub.body.discounts.length === 0) {
|
|
7679
8034
|
const subscriptionId = sub.body.id;
|
|
7680
8035
|
if (typeof subscriptionId !== "string" || !/^sub_[A-Za-z0-9]+$/.test(subscriptionId)) subscriptionContractMismatch();
|
|
@@ -7684,14 +8039,14 @@ function createStripeProvider(config) {
|
|
|
7684
8039
|
"expand[2]": "discounts",
|
|
7685
8040
|
"expand[3]": "latest_invoice.discounts"
|
|
7686
8041
|
});
|
|
7687
|
-
if (!expanded.ok)
|
|
8042
|
+
if (!expanded.ok) fail6("subscription invoice evidence", expanded);
|
|
7688
8043
|
if (expanded.body.id !== subscriptionId) subscriptionContractMismatch();
|
|
7689
8044
|
sub = expanded;
|
|
7690
8045
|
}
|
|
7691
8046
|
verifySubscriptionResult(sub.body, input, customerId, meta);
|
|
7692
8047
|
const invoice = sub.body.latest_invoice;
|
|
7693
8048
|
const clientSecret2 = clientSecretOf(sub.body);
|
|
7694
|
-
if (!clientSecret2)
|
|
8049
|
+
if (!clientSecret2) fail6("subscription confirmation-secret missing", sub);
|
|
7695
8050
|
await tagSubscriptionPayment(
|
|
7696
8051
|
sk,
|
|
7697
8052
|
invoice,
|
|
@@ -8363,7 +8718,7 @@ async function namedSeatOfferDigest(offer) {
|
|
|
8363
8718
|
"named-only",
|
|
8364
8719
|
"full-upfront",
|
|
8365
8720
|
"auto-renew-coterm",
|
|
8366
|
-
"
|
|
8721
|
+
"no-automatic-refund"
|
|
8367
8722
|
])));
|
|
8368
8723
|
return [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
8369
8724
|
}
|
|
@@ -8768,6 +9123,23 @@ async function updateChapterPerson(opts) {
|
|
|
8768
9123
|
});
|
|
8769
9124
|
}
|
|
8770
9125
|
|
|
9126
|
+
// src/application-actions.ts
|
|
9127
|
+
function isApplicationStage(ctx, stage) {
|
|
9128
|
+
return ctx.chapter.pipeline.stages.includes(stage);
|
|
9129
|
+
}
|
|
9130
|
+
function applicationActionStages(app, env, ctx) {
|
|
9131
|
+
if (!chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME)) return [];
|
|
9132
|
+
const from = String(app.status ?? "");
|
|
9133
|
+
if (app.canceled === true || ["refunded", "canceled"].includes(from)) return [];
|
|
9134
|
+
return ctx.chapter.pipeline.stages.filter((to) => {
|
|
9135
|
+
if (to === from) return false;
|
|
9136
|
+
if (to === "approved") return canApprove(from, ctx.chapter.pipeline) && from !== "declined";
|
|
9137
|
+
if (to === "refunded") return Boolean(app.namedSeatId || app.stripeCustomerId && app.stripeSubscriptionId) && (!ctx.chapter.operations.refund.allowedFrom || ctx.chapter.operations.refund.allowedFrom.includes(from));
|
|
9138
|
+
if (ctx.membershipAuthority && ["submitted", "paid_pending_vetting", "declined", "canceled"].includes(to)) return false;
|
|
9139
|
+
return canTransition(from, to, ctx.chapter.pipeline);
|
|
9140
|
+
});
|
|
9141
|
+
}
|
|
9142
|
+
|
|
8771
9143
|
// src/worker-routes-people-profile.ts
|
|
8772
9144
|
var handlePeopleProfile = async (req, url, env, ctx) => {
|
|
8773
9145
|
if (ctx.chapter.mode !== "chapter") return null;
|
|
@@ -8789,7 +9161,16 @@ var handlePeopleProfile = async (req, url, env, ctx) => {
|
|
|
8789
9161
|
const record4 = await getRecord3({ crm: ctx.chapter.crm, db }, id6);
|
|
8790
9162
|
if (!record4) return json({ error: "not found" }, 404);
|
|
8791
9163
|
if (record4.type !== "person") return dedicated ? json({ error: "person required" }, 422) : null;
|
|
8792
|
-
if (match?.[2])
|
|
9164
|
+
if (match?.[2]) {
|
|
9165
|
+
const linked = typeof record4.fields.applicationId === "string" && record4.fields.applicationId !== "";
|
|
9166
|
+
let to = "";
|
|
9167
|
+
try {
|
|
9168
|
+
to = String((await req.clone().json()).to ?? "");
|
|
9169
|
+
} catch {
|
|
9170
|
+
}
|
|
9171
|
+
if (!linked && to && !isApplicationStage(ctx, to)) return null;
|
|
9172
|
+
return json({ error: "use the application lifecycle workflow", code: "application_workflow_required" }, 409);
|
|
9173
|
+
}
|
|
8793
9174
|
if (req.method === "GET" && dedicated) {
|
|
8794
9175
|
const excluded = profileReadOnlyFields(record4, ctx);
|
|
8795
9176
|
const fields = Object.entries(ctx.chapter.crm.type(record4.type).fields).filter(([id7]) => !excluded.includes(id7)).map(([id7, field]) => ({
|
|
@@ -9480,20 +9861,6 @@ var handleAdminBilling = async (req, url, env, ctx) => {
|
|
|
9480
9861
|
// src/worker-routes-admin-lifecycle.ts
|
|
9481
9862
|
import { computeBookableSlots as computeBookableSlots2, initCalendar as initCalendar3 } from "@odla-ai/calendar";
|
|
9482
9863
|
|
|
9483
|
-
// src/application-actions.ts
|
|
9484
|
-
function applicationActionStages(app, env, ctx) {
|
|
9485
|
-
if (!chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME)) return [];
|
|
9486
|
-
const from = String(app.status ?? "");
|
|
9487
|
-
if (app.canceled === true || ["refunded", "canceled"].includes(from)) return [];
|
|
9488
|
-
return ctx.chapter.pipeline.stages.filter((to) => {
|
|
9489
|
-
if (to === from) return false;
|
|
9490
|
-
if (to === "approved") return canApprove(from, ctx.chapter.pipeline) && from !== "declined";
|
|
9491
|
-
if (to === "refunded") return Boolean(app.namedSeatId || app.stripeCustomerId && app.stripeSubscriptionId) && (!ctx.chapter.operations.refund.allowedFrom || ctx.chapter.operations.refund.allowedFrom.includes(from));
|
|
9492
|
-
if (ctx.membershipAuthority && ["submitted", "paid_pending_vetting", "declined", "canceled"].includes(to)) return false;
|
|
9493
|
-
return canTransition(from, to, ctx.chapter.pipeline);
|
|
9494
|
-
});
|
|
9495
|
-
}
|
|
9496
|
-
|
|
9497
9864
|
// src/refund-application.ts
|
|
9498
9865
|
async function refundApplication(db, env, ctx, app) {
|
|
9499
9866
|
if (!chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME)) return json({ error: "not found" }, 404);
|
|
@@ -9684,6 +10051,12 @@ var handleAdminRefund = async (req, url, env, ctx) => {
|
|
|
9684
10051
|
const app = await loadApp(db, m[1]);
|
|
9685
10052
|
if (!app) return json({ error: "not found" }, 404);
|
|
9686
10053
|
if (!chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME)) return json({ error: "not found" }, 404);
|
|
10054
|
+
if (app.namedSeatId) {
|
|
10055
|
+
const actor = await ctx.verifyUser(req, env);
|
|
10056
|
+
if (!actor || !await ctx.isSuperAdminEmail(db, actor.email, actor.userId)) {
|
|
10057
|
+
return json({ error: "super-admin required", code: "super_admin_required" }, 403);
|
|
10058
|
+
}
|
|
10059
|
+
}
|
|
9687
10060
|
return refundApplication(db, env, ctx, app);
|
|
9688
10061
|
};
|
|
9689
10062
|
var handleAdminApplicationPatch = async (req, url, env, ctx) => {
|
|
@@ -10298,7 +10671,7 @@ async function operationalReaderRecord(record4, reader, db, env, ctx) {
|
|
|
10298
10671
|
const appId = record4.fields.applicationId;
|
|
10299
10672
|
const app = typeof appId === "string" ? (await db.query({ applications: { $: { where: { id: appId }, limit: 1 } } })).applications?.[0] : void 0;
|
|
10300
10673
|
if (reader.stageTransitions.includes(record4.type)) {
|
|
10301
|
-
result.edit.allowedStages = app ? applicationActionStages(app, env, ctx).map((id6) => ({ id: id6, label: id6.replaceAll("_", " ") })) :
|
|
10674
|
+
result.edit.allowedStages = app ? applicationActionStages(app, env, ctx).map((id6) => ({ id: id6, label: id6.replaceAll("_", " ") })) : result.edit.allowedStages.filter((stage) => !isApplicationStage(ctx, stage.id));
|
|
10302
10675
|
const lock = (await db.query({ crm_activity: { $: { where: { id: `network-action-lock:${record4.id}` }, limit: 1 } } })).crm_activity?.[0];
|
|
10303
10676
|
const meta = lock?.meta;
|
|
10304
10677
|
if (meta?.state === "pending" && typeof meta.activityId === "string") {
|
|
@@ -10516,7 +10889,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
|
10516
10889
|
}
|
|
10517
10890
|
};
|
|
10518
10891
|
try {
|
|
10519
|
-
if (hasStage && ctx.chapter.mode === "chapter" && record4.type === "person") {
|
|
10892
|
+
if (hasStage && ctx.chapter.mode === "chapter" && record4.type === "person" && (record4.fields.applicationId || isApplicationStage(ctx, input.stage))) {
|
|
10520
10893
|
const updated2 = await networkApplicationAction({
|
|
10521
10894
|
db,
|
|
10522
10895
|
env,
|
|
@@ -11876,7 +12249,7 @@ async function revealNetworkPersonRegistryRecord(db, input, mutationId, actor, n
|
|
|
11876
12249
|
}
|
|
11877
12250
|
|
|
11878
12251
|
// src/worker-routes-network-person-registry.ts
|
|
11879
|
-
var
|
|
12252
|
+
var PATH3 = "/api/admin/network-person-registry";
|
|
11880
12253
|
var MAX_BODY2 = 8 * 1024 * 1024;
|
|
11881
12254
|
var MUTATION_ID4 = /^[A-Za-z0-9_-]{16,128}$/;
|
|
11882
12255
|
var positiveRevision = (value) => {
|
|
@@ -11901,7 +12274,7 @@ function registryError(error) {
|
|
|
11901
12274
|
return null;
|
|
11902
12275
|
}
|
|
11903
12276
|
var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
11904
|
-
if (url.pathname !==
|
|
12277
|
+
if (url.pathname !== PATH3) return null;
|
|
11905
12278
|
if (!ctx.chapter.networkPersonRegistry) return json({ error: "not found" }, 404);
|
|
11906
12279
|
if (req.method !== "GET" && req.method !== "POST") return json({ error: "method not allowed" }, 405);
|
|
11907
12280
|
if (req.method === "POST" && Number(req.headers.get("content-length") ?? 0) > MAX_BODY2) {
|
|
@@ -13097,7 +13470,7 @@ var handleAdminBuilderLibrary = async (req, url, env, ctx) => {
|
|
|
13097
13470
|
};
|
|
13098
13471
|
|
|
13099
13472
|
// src/membership-cancel-authority.ts
|
|
13100
|
-
import { stripeRequest as
|
|
13473
|
+
import { stripeRequest as stripeRequest9 } from "@odla-ai/stripe";
|
|
13101
13474
|
async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
|
|
13102
13475
|
membershipClock(now);
|
|
13103
13476
|
const id6 = await networkMembershipId(target);
|
|
@@ -13108,14 +13481,14 @@ async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
|
|
|
13108
13481
|
membershipFail("membership_cancellation_unavailable");
|
|
13109
13482
|
}
|
|
13110
13483
|
const path = `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`;
|
|
13111
|
-
const read2 = await
|
|
13484
|
+
const read2 = await stripeRequest9({ secretKey: stripeSecret }, "GET", path);
|
|
13112
13485
|
const sub = read2.body;
|
|
13113
13486
|
if (!read2.ok || sub.id !== member.providerSubscriptionId || sub.customer !== member.providerCustomerId || sub.livemode !== (target.environment === "prod") || !Object.entries({
|
|
13114
13487
|
...stripePartitionMetadata(target),
|
|
13115
13488
|
applicationId: target.applicationId
|
|
13116
13489
|
}).every(([key3, value]) => sub.metadata?.[key3] === value)) membershipFail("membership_cancellation_provider_mismatch");
|
|
13117
13490
|
if (sub.status !== "canceled" && sub.cancel_at_period_end !== true) {
|
|
13118
|
-
const write = await
|
|
13491
|
+
const write = await stripeRequest9(
|
|
13119
13492
|
{ secretKey: stripeSecret },
|
|
13120
13493
|
"POST",
|
|
13121
13494
|
path,
|
|
@@ -13337,7 +13710,7 @@ async function refundNetworkMembership(db, input, secretKey, now) {
|
|
|
13337
13710
|
}
|
|
13338
13711
|
|
|
13339
13712
|
// src/membership-provider-lifecycle.ts
|
|
13340
|
-
import { stripeRequest as
|
|
13713
|
+
import { stripeRequest as stripeRequest10 } from "@odla-ai/stripe";
|
|
13341
13714
|
|
|
13342
13715
|
// src/membership-renewal-authority.ts
|
|
13343
13716
|
async function reconcileMembershipRenewal(db, input, now) {
|
|
@@ -13495,7 +13868,7 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
13495
13868
|
}
|
|
13496
13869
|
if (fact.subscriptionId !== member.providerSubscriptionId || fact.objectKind !== "subscription") membershipFail("membership_billing_scope_mismatch");
|
|
13497
13870
|
if (["refund_pending", "refunded", "declined", "canceled", "restarting"].includes(String(member.status))) return receipt();
|
|
13498
|
-
const result = await
|
|
13871
|
+
const result = await stripeRequest10({ secretKey: sk }, "GET", `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`);
|
|
13499
13872
|
const sub = result.body;
|
|
13500
13873
|
if (!result.ok || sub.id !== member.providerSubscriptionId || sub.customer !== member.providerCustomerId || sub.livemode !== (input.environment === "prod") || typeof sub.cancel_at_period_end !== "boolean" || !["active", "trialing", "past_due", "unpaid", "canceled"].includes(sub.status) || !Object.entries({ ...stripePartitionMetadata(input), applicationId: input.applicationId }).every(([key3, value]) => sub.metadata?.[key3] === value)) {
|
|
13501
13874
|
membershipFail("membership_billing_provider_mismatch");
|
|
@@ -13610,7 +13983,7 @@ async function reserveNamedMembershipSeat(db, input, now) {
|
|
|
13610
13983
|
}
|
|
13611
13984
|
|
|
13612
13985
|
// src/membership-seat-stripe.ts
|
|
13613
|
-
import { stripeRequest as
|
|
13986
|
+
import { stripeRequest as stripeRequest11 } from "@odla-ai/stripe";
|
|
13614
13987
|
var reject7 = () => {
|
|
13615
13988
|
throw Object.assign(new Error("named seat provider contract mismatch"), { code: "named_seat_provider_mismatch" });
|
|
13616
13989
|
};
|
|
@@ -13622,7 +13995,7 @@ var metadataFor = (seat) => ({
|
|
|
13622
13995
|
});
|
|
13623
13996
|
var matches = (value, expected) => value && Object.entries(expected).every(([key3, item]) => value[key3] === item);
|
|
13624
13997
|
async function read(sk, path, params) {
|
|
13625
|
-
const response = await
|
|
13998
|
+
const response = await stripeRequest11({ secretKey: sk }, "GET", path, params);
|
|
13626
13999
|
if (!response.ok) return reject7();
|
|
13627
14000
|
return response.body;
|
|
13628
14001
|
}
|
|
@@ -13659,11 +14032,11 @@ async function createNamedSeatStripePayment(sk, seat, now) {
|
|
|
13659
14032
|
}
|
|
13660
14033
|
await subscriptionForSeat(sk, seat);
|
|
13661
14034
|
const metadata = metadataFor(seat);
|
|
13662
|
-
const result = seat.providerPaymentIntentId ? await
|
|
14035
|
+
const result = seat.providerPaymentIntentId ? await stripeRequest11(
|
|
13663
14036
|
{ secretKey: sk },
|
|
13664
14037
|
"GET",
|
|
13665
14038
|
`/v1/payment_intents/${encodeURIComponent(seat.providerPaymentIntentId)}`
|
|
13666
|
-
) : await
|
|
14039
|
+
) : await stripeRequest11({ secretKey: sk }, "POST", "/v1/payment_intents", {
|
|
13667
14040
|
amount: seat.amountCents,
|
|
13668
14041
|
currency: seat.currency,
|
|
13669
14042
|
customer: seat.providerCustomerId,
|
|
@@ -13681,7 +14054,7 @@ async function attachNamedSeatStripeRenewal(sk, seat) {
|
|
|
13681
14054
|
const sub = await subscriptionForSeat(sk, seat);
|
|
13682
14055
|
const existing = sub.items.data.find((item2) => item2.metadata?.seatId === seat.id);
|
|
13683
14056
|
if (existing) return String(existing.id);
|
|
13684
|
-
const result = await
|
|
14057
|
+
const result = await stripeRequest11({ secretKey: sk }, "POST", "/v1/subscription_items", {
|
|
13685
14058
|
subscription: seat.providerSubscriptionId,
|
|
13686
14059
|
price: seat.priceId,
|
|
13687
14060
|
quantity: 1,
|
|
@@ -13781,7 +14154,7 @@ async function confirmNamedMembershipSeatPayment(db, input, now) {
|
|
|
13781
14154
|
membershipUpdate("networkSeat", input.seatId, {
|
|
13782
14155
|
status: status2,
|
|
13783
14156
|
purchasedAt: paidAt,
|
|
13784
|
-
acceptanceEndsAt:
|
|
14157
|
+
acceptanceEndsAt: Number(member.termEndsAt),
|
|
13785
14158
|
termStartedAt: paidAt,
|
|
13786
14159
|
termEndsAt: member.termEndsAt,
|
|
13787
14160
|
paymentEventId: input.eventId,
|
|
@@ -13829,7 +14202,7 @@ async function completeNamedSeatRenewal(db, seatId, secretKey, now) {
|
|
|
13829
14202
|
if (seat.status !== "renewal_pending" || !seat.purchasedAt) membershipFail("named_seat_renewal_not_ready");
|
|
13830
14203
|
const member = await membershipLoad(db, "networkMembership", target.primaryMembershipId);
|
|
13831
14204
|
requireMembershipScope(member, target);
|
|
13832
|
-
if (now >= Number(
|
|
14205
|
+
if (now >= Number(member.termEndsAt) || Number(member.refundedCents) !== 0 || !["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status))) membershipFail("named_seat_refund_required");
|
|
13833
14206
|
const itemId = await attachNamedSeatStripeRenewal(secretKey, target);
|
|
13834
14207
|
const entitlementId = `${seatId}:entitlement`, effectId = `${seatId}:attach-renewal`;
|
|
13835
14208
|
try {
|
|
@@ -13904,7 +14277,7 @@ async function recordRecipient(db, input, now, approve) {
|
|
|
13904
14277
|
requireMembershipScope(entitlement, input);
|
|
13905
14278
|
if (!["pending_acceptance", "pending_approval", "pending_primary", "active"].includes(String(seat.status)) || !["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) || Number(member.refundedCents) !== 0 || now >= Number(member.termEndsAt) || !seat.providerPaymentIntentId || !seat.providerSubscriptionItemId) membershipFail("named_seat_acceptance_unavailable");
|
|
13906
14279
|
if (seat.acceptedAt === void 0) {
|
|
13907
|
-
if (approve ||
|
|
14280
|
+
if (approve || normalizeChapterEmail(input.verifiedRecipientEmail) !== seat.recipientEmailNormalized) {
|
|
13908
14281
|
membershipFail("named_seat_recipient_verification_required");
|
|
13909
14282
|
}
|
|
13910
14283
|
} else if (seat.recipientUserId !== input.recipientUserId || seat.recipientApplicationId !== input.recipientApplicationId) {
|
|
@@ -13954,129 +14327,6 @@ async function recordRecipient(db, input, now, approve) {
|
|
|
13954
14327
|
return membershipFail("named_seat_recipient_retry_required");
|
|
13955
14328
|
}
|
|
13956
14329
|
|
|
13957
|
-
// src/membership-seat-refund-stripe.ts
|
|
13958
|
-
import { stripeRequest as stripeRequest11 } from "@odla-ai/stripe";
|
|
13959
|
-
var fail6 = (code = "named_seat_refund_mismatch") => {
|
|
13960
|
-
throw Object.assign(new Error(code), { code });
|
|
13961
|
-
};
|
|
13962
|
-
async function refundNamedSeatStripePayment(secretKey, seat) {
|
|
13963
|
-
if (!seat.providerPaymentIntentId || seat.status !== "refund_pending") fail6();
|
|
13964
|
-
const call = async (method, path, params, idempotencyKey) => {
|
|
13965
|
-
const result = await stripeRequest11({ secretKey }, method, path, params, idempotencyKey ? { idempotencyKey } : {});
|
|
13966
|
-
if (!result.ok || !result.body) return fail6("named_seat_refund_provider_unavailable");
|
|
13967
|
-
return result.body;
|
|
13968
|
-
};
|
|
13969
|
-
const metadata = { ...stripePartitionMetadata(seat), applicationId: seat.applicationId, membershipId: seat.primaryMembershipId, seatId: seat.id };
|
|
13970
|
-
const payment = await call("GET", `/v1/payment_intents/${encodeURIComponent(seat.providerPaymentIntentId)}`);
|
|
13971
|
-
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)) fail6();
|
|
13972
|
-
const sub = await call("GET", `/v1/subscriptions/${encodeURIComponent(seat.providerSubscriptionId)}`);
|
|
13973
|
-
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)) fail6();
|
|
13974
|
-
const items = sub.items.data.filter((item) => item.metadata?.seatId === seat.id);
|
|
13975
|
-
if (items.length > 1) fail6();
|
|
13976
|
-
if (items.length && sub.status !== "canceled") {
|
|
13977
|
-
const item = items[0];
|
|
13978
|
-
if (!/^si_[A-Za-z0-9_]+$/.test(item.id) || item.price?.id !== seat.priceId || item.quantity !== 1) fail6();
|
|
13979
|
-
const removed = await call("DELETE", `/v1/subscription_items/${encodeURIComponent(item.id)}`, { proration_behavior: "none" });
|
|
13980
|
-
if (removed.id !== item.id || removed.deleted !== true) fail6("named_seat_renewal_removal_unconfirmed");
|
|
13981
|
-
}
|
|
13982
|
-
const list = await call("GET", "/v1/refunds", { payment_intent: seat.providerPaymentIntentId, limit: 100 });
|
|
13983
|
-
if (!Array.isArray(list.data) || list.has_more !== false || list.data.length >= 100) fail6("named_seat_refund_history_incomplete");
|
|
13984
|
-
let refundedCents = 0, pendingCents = 0;
|
|
13985
|
-
const refundIds = [];
|
|
13986
|
-
for (const refund of list.data) {
|
|
13987
|
-
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)) fail6();
|
|
13988
|
-
if (refund.status === "succeeded") {
|
|
13989
|
-
refundedCents += refund.amount;
|
|
13990
|
-
refundIds.push(refund.id);
|
|
13991
|
-
} else if (refund.status === "pending" || refund.status === "requires_action") pendingCents += refund.amount;
|
|
13992
|
-
}
|
|
13993
|
-
if (refundedCents + pendingCents > seat.amountCents) fail6();
|
|
13994
|
-
if (pendingCents) fail6("named_seat_refund_pending");
|
|
13995
|
-
if (refundedCents < seat.amountCents) {
|
|
13996
|
-
const remaining = seat.amountCents - refundedCents;
|
|
13997
|
-
const refund = await call("POST", "/v1/refunds", {
|
|
13998
|
-
payment_intent: seat.providerPaymentIntentId,
|
|
13999
|
-
amount: remaining,
|
|
14000
|
-
reason: "requested_by_customer",
|
|
14001
|
-
metadata
|
|
14002
|
-
}, `named-seat-refund:${seat.id}:${remaining}`);
|
|
14003
|
-
if (refund.payment_intent !== seat.providerPaymentIntentId || refund.amount !== remaining || refund.currency !== seat.currency || !/^re_[A-Za-z0-9_]+$/.test(refund.id)) fail6();
|
|
14004
|
-
if (refund.status !== "succeeded") fail6("named_seat_refund_pending");
|
|
14005
|
-
refundedCents += remaining;
|
|
14006
|
-
refundIds.push(refund.id);
|
|
14007
|
-
}
|
|
14008
|
-
return { refundedCents, refundIds };
|
|
14009
|
-
}
|
|
14010
|
-
|
|
14011
|
-
// src/membership-seat-refund.ts
|
|
14012
|
-
async function requestNamedSeatRefund(db, input, now) {
|
|
14013
|
-
membershipClock(now);
|
|
14014
|
-
const state = await loadMembershipAuthorityState(db, input);
|
|
14015
|
-
const seat = await membershipLoad(db, "networkSeat", input.seatId);
|
|
14016
|
-
requireMembershipScope(seat, input);
|
|
14017
|
-
const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
|
|
14018
|
-
requireMembershipScope(member, input);
|
|
14019
|
-
if (seat.status === "refunded" || seat.status === "refund_pending") return;
|
|
14020
|
-
if (!seat.purchasedAt || !seat.providerPaymentIntentId || input.reason === "declined" && seat.journeyApprovedAt !== void 0 || input.reason === "expired" && (seat.acceptedAt !== void 0 || !Number.isSafeInteger(seat.acceptanceEndsAt) || now < Number(seat.acceptanceEndsAt)) || input.reason === "primary_closed" && !["refunded", "declined"].includes(String(member.status))) membershipFail("named_seat_refund_not_eligible");
|
|
14021
|
-
const entitlement = seat.entitlementId ? await membershipLoad(db, "networkEntitlement", String(seat.entitlementId)) : void 0;
|
|
14022
|
-
await db.transact([
|
|
14023
|
-
membershipUpdate("networkSeat", input.seatId, { status: "refund_pending", refundReason: input.reason, revision: Number(seat.revision) + 1, updatedAt: now }),
|
|
14024
|
-
...entitlement ? [membershipUpdate("networkEntitlement", String(entitlement.id), { status: "closed", revision: Number(entitlement.revision) + 1, updatedAt: now })] : [],
|
|
14025
|
-
membershipUpdate("membershipEffect", `${seat.id}:refund`, {
|
|
14026
|
-
id: `${seat.id}:refund`,
|
|
14027
|
-
chapterId: input.chapterId,
|
|
14028
|
-
environment: input.environment,
|
|
14029
|
-
runtime: input.runtime,
|
|
14030
|
-
membershipId: member.id,
|
|
14031
|
-
seatId: seat.id,
|
|
14032
|
-
kind: "refund_seat",
|
|
14033
|
-
status: "pending",
|
|
14034
|
-
attempts: 0,
|
|
14035
|
-
createdAt: now,
|
|
14036
|
-
updatedAt: now
|
|
14037
|
-
}),
|
|
14038
|
-
...seat.recipientUserId ? [membershipUpdate("membershipEffect", `${seat.id}:revoke`, {
|
|
14039
|
-
id: `${seat.id}:revoke`,
|
|
14040
|
-
chapterId: input.chapterId,
|
|
14041
|
-
environment: input.environment,
|
|
14042
|
-
runtime: input.runtime,
|
|
14043
|
-
membershipId: member.id,
|
|
14044
|
-
seatId: seat.id,
|
|
14045
|
-
kind: "revoke_named_recipient_access",
|
|
14046
|
-
status: "pending",
|
|
14047
|
-
attempts: 0,
|
|
14048
|
-
createdAt: now,
|
|
14049
|
-
updatedAt: now
|
|
14050
|
-
})] : []
|
|
14051
|
-
], { mutationId: `named-seat-refund-request:${seat.id}`, guards: [
|
|
14052
|
-
{ ns: "networkSeat", id: input.seatId, exists: true, equals: { revision: seat.revision } },
|
|
14053
|
-
{ ns: "networkMembership", id: String(member.id), exists: true, equals: { revision: member.revision } },
|
|
14054
|
-
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
|
|
14055
|
-
...entitlement ? [{ ns: "networkEntitlement", id: String(entitlement.id), exists: true, equals: { revision: entitlement.revision } }] : []
|
|
14056
|
-
] });
|
|
14057
|
-
}
|
|
14058
|
-
async function completeNamedSeatRefund(db, seatId, secretKey, now) {
|
|
14059
|
-
membershipClock(now);
|
|
14060
|
-
const seat = await membershipLoad(db, "networkSeat", seatId);
|
|
14061
|
-
if (!seat) return membershipFail("named_seat_not_found");
|
|
14062
|
-
await loadMembershipAuthorityState(db, seat);
|
|
14063
|
-
if (seat.status === "refunded") return;
|
|
14064
|
-
if (seat.status !== "refund_pending") membershipFail("named_seat_refund_not_ready");
|
|
14065
|
-
const result = await refundNamedSeatStripePayment(secretKey, seat);
|
|
14066
|
-
await db.transact([
|
|
14067
|
-
membershipUpdate("networkSeat", seatId, {
|
|
14068
|
-
status: "refunded",
|
|
14069
|
-
refundedCents: result.refundedCents,
|
|
14070
|
-
providerRefundIds: result.refundIds,
|
|
14071
|
-
revision: Number(seat.revision) + 1,
|
|
14072
|
-
updatedAt: now
|
|
14073
|
-
}),
|
|
14074
|
-
membershipUpdate("membershipEffect", `${seatId}:refund`, { status: "complete", updatedAt: now })
|
|
14075
|
-
], { mutationId: `named-seat-refunded:${seatId}`, guards: [
|
|
14076
|
-
{ ns: "networkSeat", id: seatId, exists: true, equals: { status: "refund_pending", revision: seat.revision } }
|
|
14077
|
-
] });
|
|
14078
|
-
}
|
|
14079
|
-
|
|
14080
14330
|
// src/membership-seat-provider-refund.ts
|
|
14081
14331
|
async function confirmNamedSeatProviderRefund(db, input, now) {
|
|
14082
14332
|
const seat = await membershipLoad(db, "networkSeat", input.seatId);
|
|
@@ -14098,7 +14348,7 @@ async function readNamedSeatInvitation(db, input, now) {
|
|
|
14098
14348
|
await loadMembershipAuthorityState(db, target);
|
|
14099
14349
|
const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
|
|
14100
14350
|
requireMembershipScope(member, target);
|
|
14101
|
-
if (!["pending_acceptance", "pending_approval", "pending_primary", "active"].includes(String(seat.status)) || !["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) || now >= Number(member.termEndsAt)
|
|
14351
|
+
if (!["pending_acceptance", "pending_approval", "pending_primary", "active"].includes(String(seat.status)) || !["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) || now >= Number(member.termEndsAt)) membershipFail("named_seat_invitation_unavailable");
|
|
14102
14352
|
return {
|
|
14103
14353
|
seatId: input.seatId,
|
|
14104
14354
|
primaryApplicationId: target.applicationId,
|
|
@@ -14128,7 +14378,7 @@ async function readMembershipChapterEffect(db, input, now) {
|
|
|
14128
14378
|
requireMembershipScope(member, input);
|
|
14129
14379
|
if (!effect || effect.kind !== input.kind || effect.seatId !== seat.id || effect.membershipId !== member.id || ["chapterId", "environment", "runtime"].some((key3) => effect[key3] !== seat[key3]) || !["invite_named_recipient", "grant_named_recipient_access", "revoke_named_recipient_access"].includes(input.kind)) throw new Error("invalid chapter effect");
|
|
14130
14380
|
let applicable;
|
|
14131
|
-
if (input.kind === "invite_named_recipient") applicable = seat.status === "pending_acceptance" && seat.acceptedAt === void 0 && now < Number(
|
|
14381
|
+
if (input.kind === "invite_named_recipient") applicable = seat.status === "pending_acceptance" && seat.acceptedAt === void 0 && now < Number(member.termEndsAt) && ["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status));
|
|
14132
14382
|
else if (input.kind === "revoke_named_recipient_access") applicable = ["refund_pending", "refunded"].includes(String(seat.status));
|
|
14133
14383
|
else applicable = (await readNamedSeatAccess(db, {
|
|
14134
14384
|
...input,
|
|
@@ -14157,6 +14407,7 @@ var MEMBERSHIP_SEAT_ACTION_FIELDS = {
|
|
|
14157
14407
|
"seat-reservations": ["recipientName", "recipientEmail", "purchaserEmail", "purchaserUserId"],
|
|
14158
14408
|
"seat-payments": ["seatId", "eventId"],
|
|
14159
14409
|
"seat-refunds": ["seatId", "eventId"],
|
|
14410
|
+
"seat-operator-refund": ["seatId"],
|
|
14160
14411
|
"seat-accept": recipient,
|
|
14161
14412
|
"seat-approve": recipient,
|
|
14162
14413
|
"seat-decline": recipient,
|
|
@@ -14225,6 +14476,7 @@ async function handleMembershipSeatAction(action, db, peer, input, now, stripeSe
|
|
|
14225
14476
|
if (refunded?.status !== "refunded" || refunded.refundedCents !== seat.amountCents) throw new Error("named seat refund unconfirmed");
|
|
14226
14477
|
return { seatId: seat.id, status: "refunded", refundedCents: refunded.refundedCents, subscriptionCanceled: false };
|
|
14227
14478
|
}
|
|
14479
|
+
if (action === "seat-operator-refund") return refundNamedSeatByOperator(db, { ...target, seatId: String(input.seatId) }, secret, now);
|
|
14228
14480
|
if (action === "seat-checkouts") return prepareNamedMembershipSeatPayment(db, {
|
|
14229
14481
|
...target,
|
|
14230
14482
|
priceId: peer.seatPriceId,
|
|
@@ -14313,7 +14565,7 @@ function createMembershipAuthorityHandler(options) {
|
|
|
14313
14565
|
if (req.method !== "POST") return respond({ error: "method_not_allowed" }, 405);
|
|
14314
14566
|
const sender = req.headers.get(FEDERATION_HEADERS2.sender);
|
|
14315
14567
|
const action = url.pathname.split("/").at(-1);
|
|
14316
|
-
if (!["offers", "checkouts", "payments", "approvals", "refunds", "renewals", "access", "restarts", "cancel-renewal", ...Object.keys(MEMBERSHIP_SEAT_ACTION_FIELDS)].includes(action ?? "")) return respond({ error: "unauthorized" }, 401);
|
|
14568
|
+
if (!["offers", "offer-preview", "checkouts", "payments", "approvals", "refunds", "renewals", "access", "restarts", "cancel-renewal", ...Object.keys(MEMBERSHIP_SEAT_ACTION_FIELDS)].includes(action ?? "")) return respond({ error: "unauthorized" }, 401);
|
|
14317
14569
|
const peer = peers.find((candidate) => candidate.appId === sender && url.pathname === `${MEMBERSHIP_AUTHORITY_PATH}/${candidate.runtime}/${action}` && !url.search);
|
|
14318
14570
|
if (!peer) return respond({ error: "unauthorized" }, 401);
|
|
14319
14571
|
let body;
|
|
@@ -14335,13 +14587,23 @@ function createMembershipAuthorityHandler(options) {
|
|
|
14335
14587
|
let input;
|
|
14336
14588
|
try {
|
|
14337
14589
|
input = JSON.parse(verified.body);
|
|
14338
|
-
const fields = action === "seat-invitation" ? MEMBERSHIP_SEAT_ACTION_FIELDS[action] : ["applicationId", "skuId", ...action === "offers" ? ["requestId"] : action === "checkouts" ? ["offerId", "offerDigest", "quoteDigest", "priceId"] : action === "payments" ? ["eventId", "providerSubscriptionId", "providerCustomerId"] : action === "renewals" ? ["eventId"] : action === "restarts" ? ["previousSubscriptionId"] : MEMBERSHIP_SEAT_ACTION_FIELDS[action] ?? []];
|
|
14590
|
+
const fields = action === "seat-invitation" ? MEMBERSHIP_SEAT_ACTION_FIELDS[action] : action === "offer-preview" ? ["skuId"] : ["applicationId", "skuId", ...action === "offers" ? ["requestId"] : action === "checkouts" ? ["offerId", "offerDigest", "quoteDigest", "priceId"] : action === "payments" ? ["eventId", "providerSubscriptionId", "providerCustomerId"] : action === "renewals" ? ["eventId"] : action === "restarts" ? ["previousSubscriptionId"] : MEMBERSHIP_SEAT_ACTION_FIELDS[action] ?? []];
|
|
14339
14591
|
if (!input || typeof input !== "object" || Array.isArray(input) || Object.keys(input).some((key3) => !fields.includes(key3)) || !fields.every((key3) => typeof input[key3] === "string" && (["recipientName", "recipientEmail", "purchaserEmail", "verifiedRecipientEmail"].includes(key3) ? input[key3].length > 0 && input[key3].length <= 254 && !/[\u0000-\u001f\u007f]/.test(input[key3]) : SAFE.test(input[key3])))) {
|
|
14340
14592
|
throw new TypeError("invalid request");
|
|
14341
14593
|
}
|
|
14342
14594
|
} catch {
|
|
14343
14595
|
return respond({ error: "invalid_offer_request" }, 400);
|
|
14344
14596
|
}
|
|
14597
|
+
if (action === "offer-preview") {
|
|
14598
|
+
return respond({ ok: true, preview: await previewMembershipCommercialOffer(db, {
|
|
14599
|
+
appId: peer.appId,
|
|
14600
|
+
chapterId: peer.chapterId,
|
|
14601
|
+
environment: peer.environment,
|
|
14602
|
+
runtime: peer.runtime,
|
|
14603
|
+
skuId: String(input.skuId),
|
|
14604
|
+
now: options.now?.() ?? Date.now()
|
|
14605
|
+
}) });
|
|
14606
|
+
}
|
|
14345
14607
|
if (Object.hasOwn(MEMBERSHIP_SEAT_ACTION_FIELDS, action)) {
|
|
14346
14608
|
return respond({ ok: true, seat: await handleMembershipSeatAction(
|
|
14347
14609
|
action,
|
|
@@ -14735,7 +14997,7 @@ async function drainMembershipEffects(db, options, now) {
|
|
|
14735
14997
|
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100 || !options.stripeSecretKey) throw new TypeError("invalid membership recovery configuration");
|
|
14736
14998
|
await loadMembershipAuthorityState(db, scope2);
|
|
14737
14999
|
const matches2 = (row2) => Object.entries(scope2).every(([key3, value]) => row2[key3] === value);
|
|
14738
|
-
let
|
|
15000
|
+
let pending = 0, complete = 0, more = false;
|
|
14739
15001
|
for (const status2 of ["payment_pending", "payment_processing", "payment_canceling"]) {
|
|
14740
15002
|
const seats = (await db.query({ networkSeat: { $: { where: { ...scope2, status: status2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkSeat ?? [];
|
|
14741
15003
|
more ||= seats.length > limit;
|
|
@@ -14753,26 +15015,6 @@ async function drainMembershipEffects(db, options, now) {
|
|
|
14753
15015
|
}
|
|
14754
15016
|
}
|
|
14755
15017
|
}
|
|
14756
|
-
for (const status2 of ["renewal_pending", "pending_acceptance"]) {
|
|
14757
|
-
const seats = (await db.query({ networkSeat: { $: { where: { ...scope2, status: status2 }, order: { acceptanceEndsAt: "asc" }, limit: limit + 1 } } })).networkSeat ?? [];
|
|
14758
|
-
more ||= seats.length > limit;
|
|
14759
|
-
for (const seat of seats.slice(0, limit)) {
|
|
14760
|
-
if (!matches2(seat)) throw new Error("membership recovery scope mismatch");
|
|
14761
|
-
if (seat.acceptedAt !== void 0 || now < Number(seat.acceptanceEndsAt)) continue;
|
|
14762
|
-
try {
|
|
14763
|
-
await requestNamedSeatRefund(db, {
|
|
14764
|
-
...scope2,
|
|
14765
|
-
applicationId: String(seat.applicationId),
|
|
14766
|
-
skuId: String(seat.skuId),
|
|
14767
|
-
seatId: String(seat.id),
|
|
14768
|
-
reason: "expired"
|
|
14769
|
-
}, now);
|
|
14770
|
-
expired++;
|
|
14771
|
-
} catch {
|
|
14772
|
-
pending++;
|
|
14773
|
-
}
|
|
14774
|
-
}
|
|
14775
|
-
}
|
|
14776
15018
|
const effects = (await db.query({ membershipEffect: { $: { where: {
|
|
14777
15019
|
chapterId: scope2.chapterId,
|
|
14778
15020
|
environment: scope2.environment,
|
|
@@ -14834,7 +15076,7 @@ async function drainMembershipEffects(db, options, now) {
|
|
|
14834
15076
|
pending++;
|
|
14835
15077
|
}
|
|
14836
15078
|
}
|
|
14837
|
-
return { complete, pending,
|
|
15079
|
+
return { complete, pending, more };
|
|
14838
15080
|
}
|
|
14839
15081
|
|
|
14840
15082
|
// src/approval-effects-retry.ts
|
|
@@ -14984,7 +15226,7 @@ function createMembershipChapterEffectRoute(options) {
|
|
|
14984
15226
|
config.emailTemplates = { ...config.emailTemplates, namedSeatInvitation: config.emailTemplates?.namedSeatInvitation ?? {
|
|
14985
15227
|
enabled: true,
|
|
14986
15228
|
subject: "{{purchaserName}} invited you to {{chapterName}}",
|
|
14987
|
-
text: "Hello {{firstName}},\n\n{{purchaserName}} has purchased a membership for you at {{chapterName}}. Your seat is already paid for.\n\nAccept your invitation
|
|
15229
|
+
text: "Hello {{firstName}},\n\n{{purchaserName}} has purchased a membership for you at {{chapterName}}. Your seat is already paid for.\n\nAccept your invitation here:\n{{joinUrl}}\n\nYou will complete your own application and membership requirements. You will not be asked to pay for this seat."
|
|
14988
15230
|
} };
|
|
14989
15231
|
const template = config.emailTemplates.namedSeatInvitation;
|
|
14990
15232
|
if (!template.text.includes("{{purchaserName}}")) config.emailTemplates.namedSeatInvitation = {
|
|
@@ -15057,6 +15299,12 @@ async function requestMembershipCommercialOffer(options) {
|
|
|
15057
15299
|
lifetimeMs: 6e4
|
|
15058
15300
|
});
|
|
15059
15301
|
}
|
|
15302
|
+
async function requestMembershipOfferPreview(options) {
|
|
15303
|
+
const result = await requestAuthority(options, "offer-preview", { skuId: options.skuId });
|
|
15304
|
+
const preview = result.preview;
|
|
15305
|
+
if (!preview || preview.skuId !== options.skuId || typeof preview.founding !== "boolean" || !Number.isSafeInteger(preview.basePriceCents) || !Number.isSafeInteger(preview.dueTodayCents) || preview.dueTodayCents < 0 || preview.dueTodayCents > preview.basePriceCents || !Number.isSafeInteger(preview.renewalCents) || !/^[a-z]{3}$/.test(preview.currency) || !["month", "year"].includes(preview.interval) || preview.cohortLabel !== void 0 && typeof preview.cohortLabel !== "string" || preview.discountDuration !== void 0 && !["first_term", "while_active"].includes(preview.discountDuration)) throw new Error("invalid offer preview");
|
|
15306
|
+
return preview;
|
|
15307
|
+
}
|
|
15060
15308
|
async function requestMembershipCheckoutBinding(options) {
|
|
15061
15309
|
const result = await requestAuthority(options, "checkouts", {
|
|
15062
15310
|
applicationId: options.applicationId,
|
|
@@ -15095,6 +15343,12 @@ async function requestNamedSeatDecline(options) {
|
|
|
15095
15343
|
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");
|
|
15096
15344
|
return seat;
|
|
15097
15345
|
}
|
|
15346
|
+
async function requestNamedSeatOperatorRefund(options) {
|
|
15347
|
+
const result = await requestAuthority(options, "seat-operator-refund", { applicationId: options.applicationId, skuId: options.skuId, seatId: options.seatId });
|
|
15348
|
+
const seat = result.seat;
|
|
15349
|
+
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");
|
|
15350
|
+
return seat;
|
|
15351
|
+
}
|
|
15098
15352
|
async function requestNamedSeatReservation(options) {
|
|
15099
15353
|
const result = await requestAuthority(options, "seat-reservations", {
|
|
15100
15354
|
applicationId: options.applicationId,
|
|
@@ -15280,6 +15534,7 @@ function createMembershipWorkerAdapter(options) {
|
|
|
15280
15534
|
return {
|
|
15281
15535
|
authority: {
|
|
15282
15536
|
issueOffer: async (input, env) => requestMembershipCommercialOffer({ ...input, ...await transport(input, env) }),
|
|
15537
|
+
previewOffer: async (input, env) => requestMembershipOfferPreview({ ...input, ...await transport(input, env) }),
|
|
15283
15538
|
bindCheckout: async (input, env) => requestMembershipCheckoutBinding({ ...input, ...await transport(input, env) }),
|
|
15284
15539
|
confirmPayment: async (input, env) => requestMembershipPayment({ ...input, ...await transport(input, env) }),
|
|
15285
15540
|
approveMembership: async (input, env) => requestMembershipApproval({ ...input, ...await transport(input, env) }),
|
|
@@ -15298,7 +15553,8 @@ function createMembershipWorkerAdapter(options) {
|
|
|
15298
15553
|
confirmRefund: async (input, env) => requestNamedSeatRefund2({ ...input, ...await transport(input, env) }),
|
|
15299
15554
|
accept: async (input, env) => requestNamedSeatAcceptance({ ...input, ...await transport(input, env) }),
|
|
15300
15555
|
approve: async (input, env) => requestNamedSeatApproval({ ...input, ...await transport(input, env) }),
|
|
15301
|
-
decline: async (input, env) => requestNamedSeatDecline({ ...input, ...await transport(input, env) })
|
|
15556
|
+
decline: async (input, env) => requestNamedSeatDecline({ ...input, ...await transport(input, env) }),
|
|
15557
|
+
operatorRefund: async (input, env) => requestNamedSeatOperatorRefund({ ...input, ...await transport(input, env) })
|
|
15302
15558
|
} } : {}
|
|
15303
15559
|
},
|
|
15304
15560
|
readEffect: async (input, env) => requestMembershipChapterEffect({ ...input, ...await transport(input, env) })
|
|
@@ -15553,6 +15809,8 @@ var BUILTIN_ROUTES = [
|
|
|
15553
15809
|
handleMembershipRestart,
|
|
15554
15810
|
handleMembershipCancellation,
|
|
15555
15811
|
handlePaymentRecovery,
|
|
15812
|
+
handleAdminNamedSeat,
|
|
15813
|
+
handleAdminMembershipSeats,
|
|
15556
15814
|
handleAdminMeetings,
|
|
15557
15815
|
handleAdminScheduling,
|
|
15558
15816
|
// Roster + identity
|
|
@@ -15633,6 +15891,7 @@ export {
|
|
|
15633
15891
|
deliverMembershipChapterEffect,
|
|
15634
15892
|
deliverSignupControl,
|
|
15635
15893
|
drainMembershipEffects,
|
|
15894
|
+
refundNamedSeatByOperator,
|
|
15636
15895
|
renderActiveSignupControlHtml,
|
|
15637
15896
|
resolveNetworkTargetTransport,
|
|
15638
15897
|
retryChapterApprovalEffects,
|