@odla-ai/chapter 0.49.0 → 0.51.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 +36 -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-UAXWA5BL.js} +36 -24
- package/dist/chunk-UAXWA5BL.js.map +1 -0
- package/dist/{copy-context-By4Nq7yB.d.ts → copy-context-Ddat__H5.d.ts} +10 -2
- package/dist/index.cjs +129 -54
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +59 -7
- package/dist/index.d.ts +59 -7
- package/dist/index.js +129 -54
- 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 +588 -272
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +86 -6
- package/dist/worker/index.d.ts +86 -6
- package/dist/worker/index.js +582 -266
- 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,
|
|
@@ -1659,6 +1660,11 @@ function canApprove(status2, p) {
|
|
|
1659
1660
|
}
|
|
1660
1661
|
|
|
1661
1662
|
// src/membership-policy.ts
|
|
1663
|
+
function seatPurchaseDeadline(offering, member) {
|
|
1664
|
+
if (offering.seatPurchaseWindowCalendarMonths === void 0) return Number(member.termEndsAt);
|
|
1665
|
+
if (Number.isSafeInteger(member.seatPurchaseEndsAt)) return Number(member.seatPurchaseEndsAt);
|
|
1666
|
+
return addMembershipCalendarMonths(Number(member.initialTermStartedAt ?? member.termStartedAt), offering.seatPurchaseWindowCalendarMonths);
|
|
1667
|
+
}
|
|
1662
1668
|
var fail2 = (field) => {
|
|
1663
1669
|
throw new TypeError(`invalid membership policy: ${field}`);
|
|
1664
1670
|
};
|
|
@@ -1715,7 +1721,7 @@ function membershipOfferingVersion(value) {
|
|
|
1715
1721
|
autoRenew: true,
|
|
1716
1722
|
paymentGraceDays: integer2(input.paymentGraceDays, 0, 60),
|
|
1717
1723
|
seatPriceCents: integer2(input.seatPriceCents, 1, Number.MAX_SAFE_INTEGER),
|
|
1718
|
-
seatPurchaseWindowCalendarMonths: integer2(input.seatPurchaseWindowCalendarMonths, 1, 120),
|
|
1724
|
+
...input.seatPurchaseWindowCalendarMonths === void 0 || input.seatPurchaseWindowCalendarMonths === null ? {} : { seatPurchaseWindowCalendarMonths: integer2(input.seatPurchaseWindowCalendarMonths, 1, 120) },
|
|
1719
1725
|
requiredDisclosures: ids(input.requiredDisclosures)
|
|
1720
1726
|
};
|
|
1721
1727
|
}
|
|
@@ -1842,24 +1848,63 @@ async function verifyIssuedMembershipOffer(value, expected) {
|
|
|
1842
1848
|
if (!value || typeof value !== "object" || Array.isArray(value)) return reject("invalid_offer_snapshot");
|
|
1843
1849
|
return restore({ ...value, approvedFoundingCount: 0 }, expected);
|
|
1844
1850
|
}
|
|
1845
|
-
async function
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1851
|
+
async function previewMembershipCommercialOffer(db, input) {
|
|
1852
|
+
for (const key3 of ["appId", "chapterId", "environment", "runtime", "skuId"]) {
|
|
1853
|
+
if (typeof input[key3] !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_.:-]{1,159}$/.test(input[key3])) reject("invalid_offer_identity");
|
|
1854
|
+
}
|
|
1855
|
+
if (!Number.isSafeInteger(input.now) || input.now < 0) reject("invalid_offer_window");
|
|
1856
|
+
const state = await authoritativeState(db, input);
|
|
1857
|
+
const { offering, policy } = await currentPolicy(db, input, false, state);
|
|
1858
|
+
const approvedFoundingCount = policy ? state.cohortCounts[policy.cohortLabel] ?? 0 : 0;
|
|
1859
|
+
if (!Number.isSafeInteger(approvedFoundingCount) || Number(approvedFoundingCount) < 0) reject("invalid_cohort_count");
|
|
1860
|
+
const contract = createMembershipCommercialOffer({
|
|
1861
|
+
offering,
|
|
1862
|
+
discountPolicy: policy,
|
|
1863
|
+
approvedFoundingCount: Number(approvedFoundingCount),
|
|
1864
|
+
now: input.now,
|
|
1865
|
+
couponId: await couponId(input, policy?.id)
|
|
1866
|
+
});
|
|
1867
|
+
const discount = contract.billingTerms.discount;
|
|
1868
|
+
return {
|
|
1869
|
+
skuId: input.skuId,
|
|
1870
|
+
offeringVersionId: offering.id,
|
|
1871
|
+
basePriceCents: offering.basePriceCents,
|
|
1872
|
+
currency: offering.currency,
|
|
1873
|
+
interval: offering.interval,
|
|
1874
|
+
founding: contract.founding,
|
|
1875
|
+
...contract.founding && contract.discountPolicy ? { cohortLabel: contract.discountPolicy.cohortLabel } : {},
|
|
1876
|
+
dueTodayCents: contract.dueTodayCents,
|
|
1877
|
+
renewalCents: contract.renewalCents,
|
|
1878
|
+
...discount ? { discountDuration: discount.duration === "once" ? "first_term" : "while_active" } : {},
|
|
1879
|
+
...contract.initialTermEndsAt !== void 0 ? { initialTermEndsAt: contract.initialTermEndsAt } : {}
|
|
1880
|
+
};
|
|
1881
|
+
}
|
|
1882
|
+
async function authoritativeState(db, input) {
|
|
1883
|
+
const state = await load(db, "membershipAuthorityState", `${input.chapterId}:${input.environment}`);
|
|
1849
1884
|
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
|
-
|
|
1885
|
+
return state;
|
|
1886
|
+
}
|
|
1887
|
+
async function currentPolicy(db, input, restartAware, state) {
|
|
1852
1888
|
const rows5 = await db.query({
|
|
1853
1889
|
membershipOfferingVersion: { $: { where: { chapterId: input.chapterId }, limit: 200 } },
|
|
1854
1890
|
discountPolicyVersion: { $: { where: { chapterId: input.chapterId }, limit: 200 } }
|
|
1855
1891
|
});
|
|
1856
1892
|
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((
|
|
1893
|
+
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));
|
|
1894
|
+
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
1895
|
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_"));
|
|
1896
|
+
const restart = restartAware && input.applicationId ? await load(db, "membershipRestart", (await membershipOfferIdForRequest({ ...input, applicationId: input.applicationId, requestId: "checkout_binding" })).replace(/^offer_/, "membership_")) : void 0;
|
|
1861
1897
|
const policy = restart ? void 0 : policies.sort((a, b) => b.version - a.version)[0];
|
|
1862
1898
|
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");
|
|
1899
|
+
return { offering, policy };
|
|
1900
|
+
}
|
|
1901
|
+
async function issueMembershipCommercialOffer(db, input) {
|
|
1902
|
+
validate(input);
|
|
1903
|
+
const stateId = `${input.chapterId}:${input.environment}`;
|
|
1904
|
+
const state = await authoritativeState(db, input);
|
|
1905
|
+
const existing = await load(db, "membershipCheckoutOffer", input.offerId);
|
|
1906
|
+
if (existing) return restore(existing, input);
|
|
1907
|
+
const { offering, policy } = await currentPolicy(db, input, true, state);
|
|
1863
1908
|
const approvedFoundingCount = policy ? state.cohortCounts[policy.cohortLabel] ?? 0 : 0;
|
|
1864
1909
|
if (!Number.isSafeInteger(approvedFoundingCount) || Number(approvedFoundingCount) < 0) reject("invalid_cohort_count");
|
|
1865
1910
|
const contract = createMembershipCommercialOffer({
|
|
@@ -2067,7 +2112,7 @@ async function confirmMembershipPayment(db, input, now) {
|
|
|
2067
2112
|
initialTermEndsAt: endsAt,
|
|
2068
2113
|
termStartedAt: paidAt,
|
|
2069
2114
|
termEndsAt: endsAt,
|
|
2070
|
-
seatPurchaseEndsAt:
|
|
2115
|
+
seatPurchaseEndsAt: seatPurchaseDeadline(offering, { initialTermStartedAt: paidAt, termEndsAt: endsAt }),
|
|
2071
2116
|
autoRenew: true,
|
|
2072
2117
|
refundedCents: 0,
|
|
2073
2118
|
revision: 1,
|
|
@@ -2167,7 +2212,7 @@ function findTier(tiers, tierId) {
|
|
|
2167
2212
|
if (!tierId) return tiers.length === 1 ? tiers[0] : null;
|
|
2168
2213
|
return tiers.find((tier) => tier.id === tierId) ?? null;
|
|
2169
2214
|
}
|
|
2170
|
-
function joinConfigTiers(tiers, defaults = {}) {
|
|
2215
|
+
function joinConfigTiers(tiers, defaults = {}, founding = /* @__PURE__ */ new Map()) {
|
|
2171
2216
|
return tiers.map((tier) => ({
|
|
2172
2217
|
id: tier.id,
|
|
2173
2218
|
name: tier.name,
|
|
@@ -2175,7 +2220,8 @@ function joinConfigTiers(tiers, defaults = {}) {
|
|
|
2175
2220
|
currency: tier.currency ?? defaults.currency ?? "usd",
|
|
2176
2221
|
interval: tier.interval ?? defaults.interval ?? "year",
|
|
2177
2222
|
blurb: tier.blurb ?? "",
|
|
2178
|
-
free: tierIsFree(tier)
|
|
2223
|
+
free: tierIsFree(tier),
|
|
2224
|
+
...founding.has(tier.id) ? { founding: founding.get(tier.id) } : {}
|
|
2179
2225
|
}));
|
|
2180
2226
|
}
|
|
2181
2227
|
|
|
@@ -3410,6 +3456,313 @@ var handlePaymentRecovery = async (req, url, env, ctx) => {
|
|
|
3410
3456
|
}
|
|
3411
3457
|
};
|
|
3412
3458
|
|
|
3459
|
+
// src/worker-routes-admin-named-seat.ts
|
|
3460
|
+
var PATH = /^\/api\/admin\/applications\/([0-9a-f-]+)\/named-seat(\/refund)?$/;
|
|
3461
|
+
var REFUNDABLE = /* @__PURE__ */ new Set(["renewal_pending", "pending_acceptance", "pending_approval", "pending_primary", "active"]);
|
|
3462
|
+
var handleAdminNamedSeat = async (req, url, env, ctx) => {
|
|
3463
|
+
const match = url.pathname.match(PATH);
|
|
3464
|
+
if (!match) return null;
|
|
3465
|
+
const seats = ctx.membershipAuthority?.namedSeats;
|
|
3466
|
+
if (ctx.chapter.mode !== "chapter" || !seats) return json({ error: "named seats are not configured" }, 404);
|
|
3467
|
+
const refund = Boolean(match[2]);
|
|
3468
|
+
if (req.method !== (refund ? "POST" : "GET")) return json({ error: "method not allowed" }, 405);
|
|
3469
|
+
const raw2 = ctx.makeDb(env), user = await ctx.verifyUser(req, env);
|
|
3470
|
+
if (!user) return json({ error: "unauthorized" }, 401);
|
|
3471
|
+
if (!await ctx.isSuperAdminEmail(raw2, user.email, user.userId)) return json({ error: "super-admin required", code: "super_admin_required" }, 403);
|
|
3472
|
+
const db = raw2, app = await membershipLoad(db, "applications", match[1]);
|
|
3473
|
+
if (!app || !chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME) || app.namedSeatId) return json({ error: "not found" }, 404);
|
|
3474
|
+
const intent = await membershipLoad(db, "subscriptionCheckoutIntents", String(app.id));
|
|
3475
|
+
if (!intent) return json({ error: "paid membership required" }, 409);
|
|
3476
|
+
try {
|
|
3477
|
+
const target = await membershipTargetForIntent(intent, app, env, ctx);
|
|
3478
|
+
const offer = await seats.offer(target, env);
|
|
3479
|
+
const seat = offer.seat ?? null, refundable = Boolean(seat && REFUNDABLE.has(seat.status));
|
|
3480
|
+
if (!refund) {
|
|
3481
|
+
return json({
|
|
3482
|
+
seat,
|
|
3483
|
+
amountCents: offer.amountCents,
|
|
3484
|
+
currency: offer.currency,
|
|
3485
|
+
termEndsAt: offer.termEndsAt,
|
|
3486
|
+
purchaseEndsAt: offer.purchaseEndsAt,
|
|
3487
|
+
refundable
|
|
3488
|
+
});
|
|
3489
|
+
}
|
|
3490
|
+
if (!seat) return json({ error: "no purchased seat", code: "seat_not_found" }, 409);
|
|
3491
|
+
if (seat.status === "refunded") {
|
|
3492
|
+
await closeRecipientApplication(db, env, ctx, seat.seatId);
|
|
3493
|
+
return json({ ok: true, seatId: seat.seatId, status: "refunded", replayed: true });
|
|
3494
|
+
}
|
|
3495
|
+
if (!refundable) return json({ error: `seat is ${seat.status.replaceAll("_", " ")}`, code: "seat_not_refundable" }, 409);
|
|
3496
|
+
const receipt = await seats.operatorRefund({ ...target, seatId: seat.seatId }, env);
|
|
3497
|
+
await closeRecipientApplication(db, env, ctx, seat.seatId);
|
|
3498
|
+
return json({ ok: true, ...receipt });
|
|
3499
|
+
} catch {
|
|
3500
|
+
return json({ error: "named seat service unavailable; retry to confirm" }, 503);
|
|
3501
|
+
}
|
|
3502
|
+
};
|
|
3503
|
+
async function closeRecipientApplication(db, env, ctx, seatId) {
|
|
3504
|
+
const recipient2 = (await db.query({ applications: { $: { where: { namedSeatId: seatId }, limit: 1 } } })).applications?.[0];
|
|
3505
|
+
if (!recipient2 || !chapterBillingApplication(recipient2, ctx.chapter.id, env.ODLA_RUNTIME) || recipient2.status === "refunded") return;
|
|
3506
|
+
const attrs = { status: "refunded", renewalAt: 0, canceled: true };
|
|
3507
|
+
await db.transact(
|
|
3508
|
+
[{ t: "update", ns: "applications", id: String(recipient2.id), attrs }],
|
|
3509
|
+
{ mutationId: `verified-refund:${String(recipient2.id)}:${seatId}` }
|
|
3510
|
+
);
|
|
3511
|
+
await syncApplicationToCrm(crmDeps(db, ctx), { app: { ...recipient2, ...attrs }, stage: "refunded" }).catch(() => void 0);
|
|
3512
|
+
}
|
|
3513
|
+
|
|
3514
|
+
// src/membership-seat-refund-stripe.ts
|
|
3515
|
+
import { stripeRequest as stripeRequest4 } from "@odla-ai/stripe";
|
|
3516
|
+
var fail3 = (code = "named_seat_refund_mismatch") => {
|
|
3517
|
+
throw Object.assign(new Error(code), { code });
|
|
3518
|
+
};
|
|
3519
|
+
async function refundNamedSeatStripePayment(secretKey, seat) {
|
|
3520
|
+
if (!seat.providerPaymentIntentId || seat.status !== "refund_pending") fail3();
|
|
3521
|
+
const call = async (method, path, params, idempotencyKey) => {
|
|
3522
|
+
const result = await stripeRequest4({ secretKey }, method, path, params, idempotencyKey ? { idempotencyKey } : {});
|
|
3523
|
+
if (!result.ok || !result.body) return fail3("named_seat_refund_provider_unavailable");
|
|
3524
|
+
return result.body;
|
|
3525
|
+
};
|
|
3526
|
+
const metadata = { ...stripePartitionMetadata(seat), applicationId: seat.applicationId, membershipId: seat.primaryMembershipId, seatId: seat.id };
|
|
3527
|
+
const payment = await call("GET", `/v1/payment_intents/${encodeURIComponent(seat.providerPaymentIntentId)}`);
|
|
3528
|
+
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();
|
|
3529
|
+
const sub = await call("GET", `/v1/subscriptions/${encodeURIComponent(seat.providerSubscriptionId)}`);
|
|
3530
|
+
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();
|
|
3531
|
+
const items = sub.items.data.filter((item) => item.metadata?.seatId === seat.id);
|
|
3532
|
+
if (items.length > 1) fail3();
|
|
3533
|
+
if (items.length && sub.status !== "canceled") {
|
|
3534
|
+
const item = items[0];
|
|
3535
|
+
if (!/^si_[A-Za-z0-9_]+$/.test(item.id) || item.price?.id !== seat.priceId || item.quantity !== 1) fail3();
|
|
3536
|
+
const removed = await call("DELETE", `/v1/subscription_items/${encodeURIComponent(item.id)}`, { proration_behavior: "none" });
|
|
3537
|
+
if (removed.id !== item.id || removed.deleted !== true) fail3("named_seat_renewal_removal_unconfirmed");
|
|
3538
|
+
}
|
|
3539
|
+
const list = await call("GET", "/v1/refunds", { payment_intent: seat.providerPaymentIntentId, limit: 100 });
|
|
3540
|
+
if (!Array.isArray(list.data) || list.has_more !== false || list.data.length >= 100) fail3("named_seat_refund_history_incomplete");
|
|
3541
|
+
let refundedCents = 0, pendingCents = 0;
|
|
3542
|
+
const refundIds = [];
|
|
3543
|
+
for (const refund of list.data) {
|
|
3544
|
+
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();
|
|
3545
|
+
if (refund.status === "succeeded") {
|
|
3546
|
+
refundedCents += refund.amount;
|
|
3547
|
+
refundIds.push(refund.id);
|
|
3548
|
+
} else if (refund.status === "pending" || refund.status === "requires_action") pendingCents += refund.amount;
|
|
3549
|
+
}
|
|
3550
|
+
if (refundedCents + pendingCents > seat.amountCents) fail3();
|
|
3551
|
+
if (pendingCents) fail3("named_seat_refund_pending");
|
|
3552
|
+
if (refundedCents < seat.amountCents) {
|
|
3553
|
+
const remaining = seat.amountCents - refundedCents;
|
|
3554
|
+
const refund = await call("POST", "/v1/refunds", {
|
|
3555
|
+
payment_intent: seat.providerPaymentIntentId,
|
|
3556
|
+
amount: remaining,
|
|
3557
|
+
reason: "requested_by_customer",
|
|
3558
|
+
metadata
|
|
3559
|
+
}, `named-seat-refund:${seat.id}:${remaining}`);
|
|
3560
|
+
if (refund.payment_intent !== seat.providerPaymentIntentId || refund.amount !== remaining || refund.currency !== seat.currency || !/^re_[A-Za-z0-9_]+$/.test(refund.id)) fail3();
|
|
3561
|
+
if (refund.status !== "succeeded") fail3("named_seat_refund_pending");
|
|
3562
|
+
refundedCents += remaining;
|
|
3563
|
+
refundIds.push(refund.id);
|
|
3564
|
+
}
|
|
3565
|
+
return { refundedCents, refundIds };
|
|
3566
|
+
}
|
|
3567
|
+
|
|
3568
|
+
// src/membership-seat-refund.ts
|
|
3569
|
+
async function requestNamedSeatRefund(db, input, now) {
|
|
3570
|
+
membershipClock(now);
|
|
3571
|
+
const state = await loadMembershipAuthorityState(db, input);
|
|
3572
|
+
const seat = await membershipLoad(db, "networkSeat", input.seatId);
|
|
3573
|
+
requireMembershipScope(seat, input);
|
|
3574
|
+
const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
|
|
3575
|
+
requireMembershipScope(member, input);
|
|
3576
|
+
if (seat.status === "refunded" || seat.status === "refund_pending") return;
|
|
3577
|
+
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");
|
|
3578
|
+
const entitlement = seat.entitlementId ? await membershipLoad(db, "networkEntitlement", String(seat.entitlementId)) : void 0;
|
|
3579
|
+
await db.transact([
|
|
3580
|
+
membershipUpdate("networkSeat", input.seatId, { status: "refund_pending", refundReason: input.reason, revision: Number(seat.revision) + 1, updatedAt: now }),
|
|
3581
|
+
...entitlement ? [membershipUpdate("networkEntitlement", String(entitlement.id), { status: "closed", revision: Number(entitlement.revision) + 1, updatedAt: now })] : [],
|
|
3582
|
+
membershipUpdate("membershipEffect", `${seat.id}:refund`, {
|
|
3583
|
+
id: `${seat.id}:refund`,
|
|
3584
|
+
chapterId: input.chapterId,
|
|
3585
|
+
environment: input.environment,
|
|
3586
|
+
runtime: input.runtime,
|
|
3587
|
+
membershipId: member.id,
|
|
3588
|
+
seatId: seat.id,
|
|
3589
|
+
kind: "refund_seat",
|
|
3590
|
+
status: "pending",
|
|
3591
|
+
attempts: 0,
|
|
3592
|
+
createdAt: now,
|
|
3593
|
+
updatedAt: now
|
|
3594
|
+
}),
|
|
3595
|
+
...seat.recipientUserId ? [membershipUpdate("membershipEffect", `${seat.id}:revoke`, {
|
|
3596
|
+
id: `${seat.id}:revoke`,
|
|
3597
|
+
chapterId: input.chapterId,
|
|
3598
|
+
environment: input.environment,
|
|
3599
|
+
runtime: input.runtime,
|
|
3600
|
+
membershipId: member.id,
|
|
3601
|
+
seatId: seat.id,
|
|
3602
|
+
kind: "revoke_named_recipient_access",
|
|
3603
|
+
status: "pending",
|
|
3604
|
+
attempts: 0,
|
|
3605
|
+
createdAt: now,
|
|
3606
|
+
updatedAt: now
|
|
3607
|
+
})] : []
|
|
3608
|
+
], { mutationId: `named-seat-refund-request:${seat.id}`, guards: [
|
|
3609
|
+
{ ns: "networkSeat", id: input.seatId, exists: true, equals: { revision: seat.revision } },
|
|
3610
|
+
{ ns: "networkMembership", id: String(member.id), exists: true, equals: { revision: member.revision } },
|
|
3611
|
+
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
|
|
3612
|
+
...entitlement ? [{ ns: "networkEntitlement", id: String(entitlement.id), exists: true, equals: { revision: entitlement.revision } }] : []
|
|
3613
|
+
] });
|
|
3614
|
+
}
|
|
3615
|
+
async function completeNamedSeatRefund(db, seatId, secretKey, now) {
|
|
3616
|
+
membershipClock(now);
|
|
3617
|
+
const seat = await membershipLoad(db, "networkSeat", seatId);
|
|
3618
|
+
if (!seat) return membershipFail("named_seat_not_found");
|
|
3619
|
+
await loadMembershipAuthorityState(db, seat);
|
|
3620
|
+
if (seat.status === "refunded") return;
|
|
3621
|
+
if (seat.status !== "refund_pending") membershipFail("named_seat_refund_not_ready");
|
|
3622
|
+
const result = await refundNamedSeatStripePayment(secretKey, seat);
|
|
3623
|
+
await db.transact([
|
|
3624
|
+
membershipUpdate("networkSeat", seatId, {
|
|
3625
|
+
status: "refunded",
|
|
3626
|
+
refundedCents: result.refundedCents,
|
|
3627
|
+
providerRefundIds: result.refundIds,
|
|
3628
|
+
revision: Number(seat.revision) + 1,
|
|
3629
|
+
updatedAt: now
|
|
3630
|
+
}),
|
|
3631
|
+
membershipUpdate("membershipEffect", `${seatId}:refund`, { status: "complete", updatedAt: now })
|
|
3632
|
+
], { mutationId: `named-seat-refunded:${seatId}`, guards: [
|
|
3633
|
+
{ ns: "networkSeat", id: seatId, exists: true, equals: { status: "refund_pending", revision: seat.revision } }
|
|
3634
|
+
] });
|
|
3635
|
+
}
|
|
3636
|
+
|
|
3637
|
+
// src/membership-seat-operator-refund.ts
|
|
3638
|
+
async function refundNamedSeatByOperator(db, input, stripeSecretKey, now) {
|
|
3639
|
+
const seat = await membershipLoad(db, "networkSeat", input.seatId);
|
|
3640
|
+
requireMembershipScope(seat, input);
|
|
3641
|
+
await requestNamedSeatRefund(db, { ...input, reason: "operator" }, now);
|
|
3642
|
+
await completeNamedSeatRefund(db, input.seatId, stripeSecretKey, now);
|
|
3643
|
+
const refunded = await membershipLoad(db, "networkSeat", input.seatId);
|
|
3644
|
+
if (refunded?.status !== "refunded" || !Number.isSafeInteger(refunded.refundedCents) || Number(refunded.refundedCents) <= 0) {
|
|
3645
|
+
throw new Error("named seat refund unconfirmed");
|
|
3646
|
+
}
|
|
3647
|
+
return { seatId: String(seat.id), status: "refunded", refundedCents: Number(refunded.refundedCents), subscriptionCanceled: false };
|
|
3648
|
+
}
|
|
3649
|
+
|
|
3650
|
+
// src/membership-seat-window.ts
|
|
3651
|
+
async function liftSeatPurchaseWindow(db, scope2, now, limit = 100) {
|
|
3652
|
+
membershipClock(now);
|
|
3653
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200) throw new TypeError("invalid lift limit");
|
|
3654
|
+
const state = await loadMembershipAuthorityState(db, scope2);
|
|
3655
|
+
const rows5 = (await db.query({ networkMembership: { $: { where: { ...scope2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkMembership ?? [];
|
|
3656
|
+
let lifted = 0;
|
|
3657
|
+
for (const member of rows5.slice(0, limit)) {
|
|
3658
|
+
if (Object.entries(scope2).some(([key3, value]) => member[key3] !== value)) throw new Error("membership lift scope mismatch");
|
|
3659
|
+
const offering = membershipOfferingVersion(member.offeringSnapshot);
|
|
3660
|
+
if (offering.seatPurchaseWindowCalendarMonths === void 0) continue;
|
|
3661
|
+
const { seatPurchaseWindowCalendarMonths: _window, ...windowless } = offering;
|
|
3662
|
+
await db.transact([membershipUpdate("networkMembership", String(member.id), {
|
|
3663
|
+
offeringSnapshot: windowless,
|
|
3664
|
+
seatPurchaseEndsAt: Number(member.termEndsAt),
|
|
3665
|
+
revision: Number(member.revision) + 1,
|
|
3666
|
+
updatedAt: now
|
|
3667
|
+
})], { mutationId: `seat-window-lift:${member.id}:${member.revision}`, guards: [
|
|
3668
|
+
{ ns: "networkMembership", id: String(member.id), exists: true, equals: { revision: member.revision } },
|
|
3669
|
+
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative" } }
|
|
3670
|
+
] });
|
|
3671
|
+
lifted++;
|
|
3672
|
+
}
|
|
3673
|
+
return { lifted, more: rows5.length > limit };
|
|
3674
|
+
}
|
|
3675
|
+
|
|
3676
|
+
// src/worker-routes-admin-membership-seats.ts
|
|
3677
|
+
var LIST = "/api/admin/membership-seats";
|
|
3678
|
+
var LIFT = "/api/admin/membership-seats/lift-purchase-window";
|
|
3679
|
+
var ONE = /^\/api\/admin\/membership-seats\/([A-Za-z0-9][A-Za-z0-9_.:-]{2,159})\/refund$/;
|
|
3680
|
+
var SCOPE = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
3681
|
+
var PUBLIC = [
|
|
3682
|
+
"id",
|
|
3683
|
+
"appId",
|
|
3684
|
+
"chapterId",
|
|
3685
|
+
"environment",
|
|
3686
|
+
"runtime",
|
|
3687
|
+
"applicationId",
|
|
3688
|
+
"skuId",
|
|
3689
|
+
"status",
|
|
3690
|
+
"amountCents",
|
|
3691
|
+
"currency",
|
|
3692
|
+
"recipientName",
|
|
3693
|
+
"recipientEmailNormalized",
|
|
3694
|
+
"purchaserUserId",
|
|
3695
|
+
"purchasedAt",
|
|
3696
|
+
"acceptedAt",
|
|
3697
|
+
"journeyApprovedAt",
|
|
3698
|
+
"refundedCents",
|
|
3699
|
+
"refundReason",
|
|
3700
|
+
"termEndsAt",
|
|
3701
|
+
"updatedAt"
|
|
3702
|
+
];
|
|
3703
|
+
var project = (row2) => Object.fromEntries(PUBLIC.filter((key3) => row2[key3] !== void 0).map((key3) => [key3, row2[key3]]));
|
|
3704
|
+
var handleAdminMembershipSeats = async (req, url, env, ctx) => {
|
|
3705
|
+
const one = url.pathname.match(ONE), lift = url.pathname === LIFT;
|
|
3706
|
+
if (url.pathname !== LIST && !one && !lift) return null;
|
|
3707
|
+
const operator = ctx.membershipAuthorityOperator;
|
|
3708
|
+
if (!operator) return json({ error: "not found" }, 404);
|
|
3709
|
+
if (req.method !== (one || lift ? "POST" : "GET")) return json({ error: "method not allowed" }, 405);
|
|
3710
|
+
const raw2 = ctx.makeDb(env), user = await ctx.verifyUser(req, env);
|
|
3711
|
+
if (!user) return json({ error: "unauthorized" }, 401);
|
|
3712
|
+
if (!await ctx.isSuperAdminEmail(raw2, user.email, user.userId)) return json({ error: "super-admin required", code: "super_admin_required" }, 403);
|
|
3713
|
+
let db;
|
|
3714
|
+
try {
|
|
3715
|
+
db = operator.makeDb(env);
|
|
3716
|
+
} catch {
|
|
3717
|
+
return json({ error: "the membership authority is not hosted by this deployment" }, 404);
|
|
3718
|
+
}
|
|
3719
|
+
if (lift) {
|
|
3720
|
+
let body;
|
|
3721
|
+
try {
|
|
3722
|
+
body = await req.json();
|
|
3723
|
+
} catch {
|
|
3724
|
+
return json({ error: "chapterId and runtime are required" }, 400);
|
|
3725
|
+
}
|
|
3726
|
+
const appId = body.appId ?? body.chapterId;
|
|
3727
|
+
if (typeof body.chapterId !== "string" || !SCOPE.test(body.chapterId) || typeof appId !== "string" || !SCOPE.test(appId) || typeof body.runtime !== "string" || !/^[a-z0-9]{2,12}$/.test(body.runtime)) {
|
|
3728
|
+
return json({ error: "chapterId and runtime are required" }, 400);
|
|
3729
|
+
}
|
|
3730
|
+
try {
|
|
3731
|
+
return json({ ok: true, ...await liftSeatPurchaseWindow(db, { appId, chapterId: body.chapterId, environment: env.ODLA_ENV, runtime: body.runtime }, Date.now()) });
|
|
3732
|
+
} catch (error) {
|
|
3733
|
+
const code = error.code;
|
|
3734
|
+
return json({ error: "purchase window lift is pending; retry to confirm", ...code ? { code } : {} }, code ? 409 : 503);
|
|
3735
|
+
}
|
|
3736
|
+
}
|
|
3737
|
+
if (!one) {
|
|
3738
|
+
const rows5 = (await db.query({ networkSeat: { $: { where: { environment: env.ODLA_ENV }, order: { updatedAt: "desc" }, limit: 201 } } })).networkSeat ?? [];
|
|
3739
|
+
return json({ seats: rows5.slice(0, 200).map(project), truncated: rows5.length > 200 });
|
|
3740
|
+
}
|
|
3741
|
+
const seat = await membershipLoad(db, "networkSeat", one[1]);
|
|
3742
|
+
if (!seat || seat.environment !== env.ODLA_ENV) return json({ error: "not found" }, 404);
|
|
3743
|
+
if (seat.status === "refunded") {
|
|
3744
|
+
return json({ ok: true, seatId: seat.id, status: "refunded", refundedCents: seat.refundedCents, subscriptionCanceled: false, replayed: true });
|
|
3745
|
+
}
|
|
3746
|
+
if (!seat.purchasedAt) return json({ error: "seat has no completed payment to refund", code: "seat_not_refundable" }, 409);
|
|
3747
|
+
const secret = await getVaultSecret(db, operator.stripeSecretName ?? "stripe_secret_key");
|
|
3748
|
+
if (!secret) return json({ error: "network payment provider unavailable" }, 503);
|
|
3749
|
+
try {
|
|
3750
|
+
const receipt = await refundNamedSeatByOperator(db, {
|
|
3751
|
+
appId: String(seat.appId),
|
|
3752
|
+
chapterId: String(seat.chapterId),
|
|
3753
|
+
environment: String(seat.environment),
|
|
3754
|
+
runtime: String(seat.runtime),
|
|
3755
|
+
applicationId: String(seat.applicationId),
|
|
3756
|
+
skuId: String(seat.skuId),
|
|
3757
|
+
seatId: String(seat.id)
|
|
3758
|
+
}, secret, Date.now());
|
|
3759
|
+
return json({ ok: true, ...receipt });
|
|
3760
|
+
} catch (error) {
|
|
3761
|
+
const code = error.code;
|
|
3762
|
+
return json({ error: "seat refund is pending; retry to confirm", ...code ? { code } : {} }, code ? 409 : 503);
|
|
3763
|
+
}
|
|
3764
|
+
};
|
|
3765
|
+
|
|
3413
3766
|
// src/worker-routes-membership-cancel.ts
|
|
3414
3767
|
var handleMembershipCancellation = async (req, url, env, ctx) => {
|
|
3415
3768
|
const match = url.pathname.match(/^\/api\/admin\/applications\/([0-9a-f-]+)\/cancel-renewal$/);
|
|
@@ -4006,6 +4359,29 @@ async function authorizeCrmDiscussionReference(req, env, fetcher = fetch) {
|
|
|
4006
4359
|
}
|
|
4007
4360
|
}
|
|
4008
4361
|
|
|
4362
|
+
// src/join-founding-preview.ts
|
|
4363
|
+
async function foundingPreviews(tiers, env, ctx) {
|
|
4364
|
+
const result = /* @__PURE__ */ new Map();
|
|
4365
|
+
const preview = ctx.membershipAuthority?.previewOffer;
|
|
4366
|
+
if (!preview) return result;
|
|
4367
|
+
const scope2 = { appId: env.ODLA_APP_ID, chapterId: ctx.chapter.id, environment: env.ODLA_ENV, runtime: stripeRuntimeIdentity(env.ODLA_RUNTIME) };
|
|
4368
|
+
await Promise.all(tiers.filter((tier) => !tierIsFree(tier) && tier.stripePriceId).map(async (tier) => {
|
|
4369
|
+
try {
|
|
4370
|
+
const offer = await preview({ ...scope2, skuId: tier.id }, env);
|
|
4371
|
+
if (!offer.founding || !offer.cohortLabel || offer.basePriceCents !== tier.priceCents || offer.dueTodayCents >= offer.basePriceCents) return;
|
|
4372
|
+
result.set(tier.id, {
|
|
4373
|
+
cohortLabel: offer.cohortLabel,
|
|
4374
|
+
dueTodayCents: offer.dueTodayCents,
|
|
4375
|
+
discountCents: offer.basePriceCents - offer.dueTodayCents,
|
|
4376
|
+
renewalCents: offer.renewalCents,
|
|
4377
|
+
...offer.discountDuration ? { discountDuration: offer.discountDuration } : {}
|
|
4378
|
+
});
|
|
4379
|
+
} catch {
|
|
4380
|
+
}
|
|
4381
|
+
}));
|
|
4382
|
+
return result;
|
|
4383
|
+
}
|
|
4384
|
+
|
|
4009
4385
|
// src/admission-grants-contract.ts
|
|
4010
4386
|
var ADMISSION_GRANTS_SCHEMA_VERSION = 1;
|
|
4011
4387
|
var ADMISSION_GRANTS_PATH = "/api/admission-grants";
|
|
@@ -4532,10 +4908,24 @@ async function materializeSignupControl(db, envelope, previous, receivedAt) {
|
|
|
4532
4908
|
}
|
|
4533
4909
|
|
|
4534
4910
|
// src/payment-quote.ts
|
|
4911
|
+
function tierPresentationFor(document, tierId) {
|
|
4912
|
+
const compareAt = document?.membershipPage?.tiers?.find((tier) => tier.tierId === tierId)?.compareAtPriceCents;
|
|
4913
|
+
return Number.isSafeInteger(compareAt) && Number(compareAt) > 0 ? { compareAtPriceCents: Number(compareAt) } : void 0;
|
|
4914
|
+
}
|
|
4915
|
+
function foundingOf(offer) {
|
|
4916
|
+
return offer?.contract?.founding === true && offer.contract.discountPolicy ? { cohortLabel: offer.contract.discountPolicy.cohortLabel } : void 0;
|
|
4917
|
+
}
|
|
4535
4918
|
async function createPaymentQuote(input) {
|
|
4536
4919
|
const { group, tier, policy } = input;
|
|
4537
4920
|
const terms2 = input.billingTerms ? subscriptionBillingTerms(input.billingTerms) : void 0;
|
|
4921
|
+
const compareAt = input.presentation?.compareAtPriceCents;
|
|
4922
|
+
const presentation = Number.isSafeInteger(compareAt) && Number(compareAt) > tier.priceCents ? { compareAtPriceCents: Number(compareAt) } : void 0;
|
|
4538
4923
|
const dueToday = terms2 ? subscriptionAmountDue({ priceCents: tier.priceCents, billingTerms: terms2 }) : void 0;
|
|
4924
|
+
const lines = dueToday === void 0 ? paymentLineItems(group, tier) : {
|
|
4925
|
+
standardCents: tier.priceCents,
|
|
4926
|
+
discountCents: tier.priceCents - dueToday,
|
|
4927
|
+
dueTodayCents: dueToday
|
|
4928
|
+
};
|
|
4539
4929
|
const quote = {
|
|
4540
4930
|
applicationId: input.applicationId,
|
|
4541
4931
|
chapterId: input.chapterId,
|
|
@@ -4543,11 +4933,12 @@ async function createPaymentQuote(input) {
|
|
|
4543
4933
|
tierName: tier.name,
|
|
4544
4934
|
currency: tier.currency ?? String(group.currency ?? "usd"),
|
|
4545
4935
|
interval: tier.interval ?? (group.interval === "month" ? "month" : "year"),
|
|
4546
|
-
lineItems:
|
|
4547
|
-
standardCents:
|
|
4548
|
-
discountCents:
|
|
4549
|
-
dueTodayCents:
|
|
4550
|
-
},
|
|
4936
|
+
lineItems: presentation ? {
|
|
4937
|
+
standardCents: presentation.compareAtPriceCents,
|
|
4938
|
+
discountCents: presentation.compareAtPriceCents - lines.dueTodayCents,
|
|
4939
|
+
dueTodayCents: lines.dueTodayCents
|
|
4940
|
+
} : lines,
|
|
4941
|
+
...presentation ? { presentation } : {},
|
|
4551
4942
|
refundPolicyText: policy?.refundPolicyText ?? String(group.refundPolicyText ?? ""),
|
|
4552
4943
|
merchantDisclosureText: String(group.merchantDisclosureText ?? ""),
|
|
4553
4944
|
trustCopy: policy ? policy.trustCopy ?? "" : String(group.trustCopy ?? ""),
|
|
@@ -4572,37 +4963,39 @@ async function createPaymentQuote(input) {
|
|
|
4572
4963
|
});
|
|
4573
4964
|
const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(contract));
|
|
4574
4965
|
const digest = [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
4575
|
-
return { ...quote, digest };
|
|
4966
|
+
return { ...quote, digest, ...input.founding ? { founding: input.founding } : {} };
|
|
4576
4967
|
}
|
|
4577
4968
|
|
|
4578
4969
|
// src/membership-quote-projection.ts
|
|
4579
4970
|
var MEMBERSHIP_QUOTE_NAMESPACE = "membershipQuoteProjections";
|
|
4580
4971
|
var keys2 = ["appId", "chapterId", "environment", "runtime", "applicationId", "skuId"];
|
|
4581
|
-
var
|
|
4972
|
+
var fail4 = (code) => {
|
|
4582
4973
|
throw Object.assign(new Error(code), { code });
|
|
4583
4974
|
};
|
|
4584
4975
|
var load3 = async (db, ns, id6) => (await db.query({ [ns]: { $: { where: { id: id6 }, limit: 1 } } }))[ns]?.[0];
|
|
4585
4976
|
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
|
|
4977
|
+
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");
|
|
4978
|
+
if (row2.status !== "ready") return fail4("membership_quote_pending");
|
|
4588
4979
|
const offer = await verifyIssuedMembershipOffer(row2.offer, { ...target, offerId: await membershipOfferIdForRequest({
|
|
4589
4980
|
...target,
|
|
4590
4981
|
requestId: row2.requestId
|
|
4591
4982
|
}), now, lifetimeMs: 6e4 });
|
|
4592
|
-
if (offer.issuedAt > now + 3e4 || !allowExpired && offer.expiresAt <= now) return
|
|
4983
|
+
if (offer.issuedAt > now + 3e4 || !allowExpired && offer.expiresAt <= now) return fail4("membership_quote_expired");
|
|
4593
4984
|
const tier = row2.tier, quote = row2.quote;
|
|
4594
4985
|
const offering = offer.contract.offering;
|
|
4595
4986
|
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
|
|
4987
|
+
return fail4("membership_catalog_mismatch");
|
|
4597
4988
|
}
|
|
4598
4989
|
const expected = await createPaymentQuote({
|
|
4599
4990
|
...target,
|
|
4600
4991
|
tier,
|
|
4601
4992
|
group: { merchantDisclosureText: quote.merchantDisclosureText },
|
|
4602
4993
|
policy: { refundPolicyText: quote.refundPolicyText, trustCopy: quote.trustCopy },
|
|
4603
|
-
billingTerms: offer.contract.billingTerms
|
|
4994
|
+
billingTerms: offer.contract.billingTerms,
|
|
4995
|
+
presentation: quote.presentation,
|
|
4996
|
+
founding: foundingOf(offer)
|
|
4604
4997
|
});
|
|
4605
|
-
if (quote.digest !== expected.digest) return
|
|
4998
|
+
if (quote.digest !== expected.digest) return fail4("membership_quote_conflict");
|
|
4606
4999
|
return { tier, quote: expected, billingTerms: offer.contract.billingTerms, membershipOffer: offer, projectionRevision: Number(row2.revision) };
|
|
4607
5000
|
}
|
|
4608
5001
|
async function readMembershipQuote(db, target, now) {
|
|
@@ -4610,9 +5003,9 @@ async function readMembershipQuote(db, target, now) {
|
|
|
4610
5003
|
}
|
|
4611
5004
|
async function prepareMembershipQuote(db, input, issue) {
|
|
4612
5005
|
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
|
|
5006
|
+
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
5007
|
let row2 = await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, id6);
|
|
4615
|
-
if (row2 && keys2.some((key3) => row2[key3] !== input[key3])) return
|
|
5008
|
+
if (row2 && keys2.some((key3) => row2[key3] !== input[key3])) return fail4("membership_quote_conflict");
|
|
4616
5009
|
if (row2?.status === "ready") {
|
|
4617
5010
|
try {
|
|
4618
5011
|
return await restore2(row2, input, now);
|
|
@@ -4657,7 +5050,7 @@ async function prepareMembershipQuote(db, input, issue) {
|
|
|
4657
5050
|
}
|
|
4658
5051
|
if (row2?.status === "ready") return restore2(row2, input, now);
|
|
4659
5052
|
if (!row2 || row2.status !== "pending" || keys2.some((key3) => row2[key3] !== input[key3]) || typeof row2.requestId !== "string") {
|
|
4660
|
-
return
|
|
5053
|
+
return fail4("membership_quote_pending");
|
|
4661
5054
|
}
|
|
4662
5055
|
const target = Object.fromEntries(keys2.map((key3) => [key3, input[key3]]));
|
|
4663
5056
|
const offer = await verifyIssuedMembershipOffer(await issue({ ...target, requestId: row2.requestId }), {
|
|
@@ -4671,7 +5064,15 @@ async function prepareMembershipQuote(db, input, issue) {
|
|
|
4671
5064
|
currency: input.tier.currency ?? String(input.group.currency ?? "usd"),
|
|
4672
5065
|
interval: input.tier.interval ?? (input.group.interval === "month" ? "month" : "year")
|
|
4673
5066
|
};
|
|
4674
|
-
const quote = await createPaymentQuote({
|
|
5067
|
+
const quote = await createPaymentQuote({
|
|
5068
|
+
...target,
|
|
5069
|
+
tier,
|
|
5070
|
+
group: input.group,
|
|
5071
|
+
policy: input.policy,
|
|
5072
|
+
billingTerms: offer.contract.billingTerms,
|
|
5073
|
+
presentation: input.presentation,
|
|
5074
|
+
founding: foundingOf(offer)
|
|
5075
|
+
});
|
|
4675
5076
|
const ready = { ...row2, offer, tier, quote, status: "ready", updatedAt: now };
|
|
4676
5077
|
await restore2(ready, target, now, true);
|
|
4677
5078
|
try {
|
|
@@ -4726,7 +5127,9 @@ async function checkoutQuoteForApplication(db, app, group, env, ctx, prepare = f
|
|
|
4726
5127
|
group: { merchantDisclosureText: quote2.merchantDisclosureText },
|
|
4727
5128
|
tier: tier2,
|
|
4728
5129
|
policy: { refundPolicyText: quote2.refundPolicyText, trustCopy: quote2.trustCopy },
|
|
4729
|
-
billingTerms
|
|
5130
|
+
billingTerms,
|
|
5131
|
+
presentation: quote2.presentation,
|
|
5132
|
+
founding: foundingOf(intent.membershipOffer)
|
|
4730
5133
|
});
|
|
4731
5134
|
if (expected.digest !== quote2.digest) throw new Error("invalid commercial snapshot");
|
|
4732
5135
|
return { tier: tier2, quote: expected, billingTerms, membershipOffer: intent.membershipOffer };
|
|
@@ -4753,9 +5156,10 @@ async function checkoutQuoteForApplication(db, app, group, env, ctx, prepare = f
|
|
|
4753
5156
|
if (!tier || tierIsFree(tier) || !tier.stripePriceId) throw Object.assign(new Error("tier unavailable"), { code: "tier_unresolved" });
|
|
4754
5157
|
const control3 = await loadActiveSignupControl(db, ctx.chapter.id, env.ODLA_ENV);
|
|
4755
5158
|
const policy = control3?.document.policy?.tierPolicies?.[tier.id];
|
|
5159
|
+
const presentation = tierPresentationFor(control3?.document, tier.id);
|
|
4756
5160
|
if (ctx.membershipAuthority) return prepareMembershipQuote(
|
|
4757
5161
|
db,
|
|
4758
|
-
{ ...target, app, group, tier, policy, now: Date.now() },
|
|
5162
|
+
{ ...target, app, group, tier, policy, presentation, now: Date.now() },
|
|
4759
5163
|
(input) => ctx.membershipAuthority.issueOffer(input, env)
|
|
4760
5164
|
);
|
|
4761
5165
|
const quote = await createPaymentQuote({
|
|
@@ -4766,7 +5170,8 @@ async function checkoutQuoteForApplication(db, app, group, env, ctx, prepare = f
|
|
|
4766
5170
|
runtime: stripeRuntimeIdentity(env.ODLA_RUNTIME),
|
|
4767
5171
|
group,
|
|
4768
5172
|
tier,
|
|
4769
|
-
policy
|
|
5173
|
+
policy,
|
|
5174
|
+
presentation
|
|
4770
5175
|
});
|
|
4771
5176
|
return { tier, quote, billingTerms: void 0 };
|
|
4772
5177
|
}
|
|
@@ -5420,10 +5825,11 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
5420
5825
|
group,
|
|
5421
5826
|
Boolean(stripeKey)
|
|
5422
5827
|
);
|
|
5828
|
+
const founding = await foundingPreviews(offered, env, ctx);
|
|
5423
5829
|
const activeControl = chapter.signupControl ? await loadActiveSignupControl(db, chapter.id, env.ODLA_ENV) : null;
|
|
5424
5830
|
const application = controlledApplication(chapter.application, activeControl?.document);
|
|
5425
5831
|
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),
|
|
5832
|
+
...joinConfig(group, paymentsReady, joinConfigTiers(offered, { currency: typeof group.currency === "string" ? group.currency : void 0, interval: group.interval === "month" ? "month" : "year" }, founding), application.conditions),
|
|
5427
5833
|
fieldContract: chapterFlowFieldContract(chapter, "join", application),
|
|
5428
5834
|
copy: { ...chapter.copy.join, ...activeControl?.document.application?.copy ?? {} },
|
|
5429
5835
|
...activeControl ? { signupControl: {
|
|
@@ -7116,7 +7522,7 @@ async function deliverAdmissionGrants(options) {
|
|
|
7116
7522
|
}
|
|
7117
7523
|
|
|
7118
7524
|
// src/worker-routes-network-admission.ts
|
|
7119
|
-
var
|
|
7525
|
+
var PATH2 = "/api/admin/network/admissions";
|
|
7120
7526
|
var RETRY_PATH = "/api/admin/network/admissions/retry";
|
|
7121
7527
|
var REVOKE_PATH = "/api/admin/network/admissions/revoke";
|
|
7122
7528
|
var MUTATION_ID2 = /^[A-Za-z0-9_-]{16,128}$/;
|
|
@@ -7196,11 +7602,11 @@ async function parseBody(req) {
|
|
|
7196
7602
|
}
|
|
7197
7603
|
}
|
|
7198
7604
|
var handleAdminNetworkAdmissions = async (req, url, env, ctx) => {
|
|
7199
|
-
if (![
|
|
7605
|
+
if (![PATH2, RETRY_PATH, REVOKE_PATH].includes(url.pathname)) return null;
|
|
7200
7606
|
const auth = await gate2(req, env, ctx);
|
|
7201
7607
|
if ("response" in auth) return auth.response;
|
|
7202
7608
|
const db = auth.db;
|
|
7203
|
-
if (url.pathname ===
|
|
7609
|
+
if (url.pathname === PATH2 && req.method === "GET") {
|
|
7204
7610
|
const targetId2 = url.searchParams.get("targetId") ?? "";
|
|
7205
7611
|
const target2 = targetFor(ctx, targetId2);
|
|
7206
7612
|
if (!target2) return reject6(404, "unknown_target", "admission target is not configured");
|
|
@@ -7444,28 +7850,28 @@ var handleSchedule = async (req, url, env, ctx) => {
|
|
|
7444
7850
|
};
|
|
7445
7851
|
|
|
7446
7852
|
// src/payments-stripe.ts
|
|
7447
|
-
import { stripeRequest as
|
|
7853
|
+
import { stripeRequest as stripeRequest8, verifyStripeSignature as verifyStripeSignature2 } from "@odla-ai/stripe";
|
|
7448
7854
|
|
|
7449
7855
|
// src/payments-refund.ts
|
|
7450
|
-
import { stripeRequest as
|
|
7856
|
+
import { stripeRequest as stripeRequest5 } from "@odla-ai/stripe";
|
|
7451
7857
|
var row = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
7452
7858
|
var id2 = (value) => typeof value === "string" ? value : String(row(value).id ?? "");
|
|
7453
7859
|
var providerId2 = (value, prefix) => new RegExp(`^${prefix}_[A-Za-z0-9]+$`).test(value);
|
|
7454
|
-
function
|
|
7860
|
+
function fail5(code = "refund_target_mismatch") {
|
|
7455
7861
|
throw Object.assign(new Error(code), { code });
|
|
7456
7862
|
}
|
|
7457
7863
|
async function refundSubscriptionPayment(secretKey, input) {
|
|
7458
|
-
if (!providerId2(input.customerId, "cus") || !providerId2(input.subscriptionId, "sub"))
|
|
7864
|
+
if (!providerId2(input.customerId, "cus") || !providerId2(input.subscriptionId, "sub")) fail5();
|
|
7459
7865
|
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))
|
|
7866
|
+
const result = await stripeRequest5({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
|
|
7867
|
+
if (!result.ok) fail5("stripe_error");
|
|
7868
|
+
if (!result.body || typeof result.body !== "object" || Array.isArray(result.body)) fail5("stripe_invalid_response");
|
|
7463
7869
|
return result.body;
|
|
7464
7870
|
};
|
|
7465
7871
|
const sub = await call("GET", `/v1/subscriptions/${input.subscriptionId}`);
|
|
7466
|
-
if (id2(sub) !== input.subscriptionId || id2(sub.customer) !== input.customerId)
|
|
7872
|
+
if (id2(sub) !== input.subscriptionId || id2(sub.customer) !== input.customerId) fail5();
|
|
7467
7873
|
for (const [key3, value] of Object.entries(input.metadata ?? {})) {
|
|
7468
|
-
if (row(sub.metadata)[key3] !== value)
|
|
7874
|
+
if (row(sub.metadata)[key3] !== value) fail5();
|
|
7469
7875
|
}
|
|
7470
7876
|
let cursor;
|
|
7471
7877
|
let earliest;
|
|
@@ -7478,69 +7884,69 @@ async function refundSubscriptionPayment(secretKey, input) {
|
|
|
7478
7884
|
limit: 100,
|
|
7479
7885
|
...cursor ? { starting_after: cursor } : {}
|
|
7480
7886
|
});
|
|
7481
|
-
if (!Array.isArray(list.data) || typeof list.has_more !== "boolean")
|
|
7887
|
+
if (!Array.isArray(list.data) || typeof list.has_more !== "boolean") fail5("stripe_invalid_response");
|
|
7482
7888
|
for (const value of list.data) {
|
|
7483
7889
|
const invoice2 = row(value);
|
|
7484
7890
|
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)
|
|
7891
|
+
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
7892
|
seen.add(id2(invoice2));
|
|
7487
7893
|
if (Number(invoice2.amount_paid) > 0 && (!earliest || Number(invoice2.created) < Number(earliest.created))) earliest = invoice2;
|
|
7488
7894
|
}
|
|
7489
7895
|
if (!list.has_more) break;
|
|
7490
|
-
if (!list.data.length || page === 9)
|
|
7896
|
+
if (!list.data.length || page === 9) fail5("refund_history_incomplete");
|
|
7491
7897
|
cursor = id2(list.data[list.data.length - 1]);
|
|
7492
7898
|
}
|
|
7493
|
-
if (!earliest)
|
|
7899
|
+
if (!earliest) fail5("no_charge");
|
|
7494
7900
|
const invoice = earliest;
|
|
7495
|
-
if (input.expectedInvoiceId !== void 0 && id2(invoice) !== input.expectedInvoiceId || input.expectedAmountCents !== void 0 && invoice.amount_paid !== input.expectedAmountCents)
|
|
7901
|
+
if (input.expectedInvoiceId !== void 0 && id2(invoice) !== input.expectedInvoiceId || input.expectedAmountCents !== void 0 && invoice.amount_paid !== input.expectedAmountCents) fail5();
|
|
7496
7902
|
let chargeId = id2(invoice.charge);
|
|
7497
7903
|
let paymentIntentId = id2(invoice.payment_intent);
|
|
7498
7904
|
if (!chargeId && !paymentIntentId) {
|
|
7499
7905
|
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)
|
|
7906
|
+
if (payments.has_more !== false || !Array.isArray(payments.data) || payments.data.length !== 1) fail5("refund_payment_ambiguous");
|
|
7501
7907
|
const payment = row(payments.data[0]);
|
|
7502
|
-
if (id2(payment.invoice) !== id2(invoice) || payment.status !== "paid" || row(payment.payment).type !== "payment_intent")
|
|
7908
|
+
if (id2(payment.invoice) !== id2(invoice) || payment.status !== "paid" || row(payment.payment).type !== "payment_intent") fail5();
|
|
7503
7909
|
paymentIntentId = id2(row(payment.payment).payment_intent);
|
|
7504
7910
|
}
|
|
7505
7911
|
if (!chargeId) {
|
|
7506
|
-
if (!providerId2(paymentIntentId, "pi"))
|
|
7912
|
+
if (!providerId2(paymentIntentId, "pi")) fail5();
|
|
7507
7913
|
const payment = await call("GET", `/v1/payment_intents/${paymentIntentId}`);
|
|
7508
|
-
if (id2(payment) !== paymentIntentId || id2(payment.customer) !== input.customerId || payment.status !== "succeeded")
|
|
7914
|
+
if (id2(payment) !== paymentIntentId || id2(payment.customer) !== input.customerId || payment.status !== "succeeded") fail5();
|
|
7509
7915
|
chargeId = id2(payment.latest_charge);
|
|
7510
7916
|
}
|
|
7511
|
-
if (!providerId2(chargeId, "ch"))
|
|
7917
|
+
if (!providerId2(chargeId, "ch")) fail5();
|
|
7512
7918
|
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))
|
|
7919
|
+
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();
|
|
7920
|
+
if (input.alreadyRefundedChargeId !== void 0 && (chargeId !== input.alreadyRefundedChargeId || charge.amount_refunded !== charge.amount || charge.refunded !== true)) fail5();
|
|
7515
7921
|
if (charge.amount_refunded !== charge.amount) {
|
|
7516
7922
|
const refund = await call("POST", "/v1/refunds", {
|
|
7517
7923
|
charge: chargeId,
|
|
7518
7924
|
...input.metadata ? { metadata: input.metadata } : {}
|
|
7519
7925
|
}, `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))
|
|
7926
|
+
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
7927
|
}
|
|
7522
7928
|
let subscriptionCanceled = sub.status === "canceled";
|
|
7523
7929
|
if (input.cancelSubscription !== false && !subscriptionCanceled) {
|
|
7524
7930
|
const canceled = await call("DELETE", `/v1/subscriptions/${input.subscriptionId}`);
|
|
7525
|
-
if (id2(canceled) !== input.subscriptionId || canceled.status !== "canceled")
|
|
7931
|
+
if (id2(canceled) !== input.subscriptionId || canceled.status !== "canceled") fail5("refund_cancel_incomplete");
|
|
7526
7932
|
subscriptionCanceled = true;
|
|
7527
7933
|
}
|
|
7528
7934
|
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)
|
|
7935
|
+
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
7936
|
return { refundedCents: Number(charge.amount), subscriptionCanceled };
|
|
7531
7937
|
}
|
|
7532
7938
|
|
|
7533
7939
|
// src/subscription-payment-identity.ts
|
|
7534
|
-
import { stripeRequest as
|
|
7940
|
+
import { stripeRequest as stripeRequest7 } from "@odla-ai/stripe";
|
|
7535
7941
|
|
|
7536
7942
|
// src/subscription-provider-contract.ts
|
|
7537
|
-
import { stripeRequest as
|
|
7943
|
+
import { stripeRequest as stripeRequest6 } from "@odla-ai/stripe";
|
|
7538
7944
|
function subscriptionContractMismatch() {
|
|
7539
7945
|
throw Object.assign(new Error("Stripe subscription does not match the checkout contract"), { code: "stripe_contract_mismatch" });
|
|
7540
7946
|
}
|
|
7541
7947
|
async function verifySubscriptionPrice(secretKey, input, resume = false) {
|
|
7542
7948
|
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
|
|
7949
|
+
const result = await stripeRequest6({ secretKey }, "GET", `/v1/prices/${encodeURIComponent(input.priceId)}`);
|
|
7544
7950
|
const price = result.body;
|
|
7545
7951
|
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
7952
|
return typeof price.product === "string" ? price.product : void 0;
|
|
@@ -7559,7 +7965,7 @@ async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata,
|
|
|
7559
7965
|
let paymentId = id3(invoice.payment_intent);
|
|
7560
7966
|
if (!paymentId) {
|
|
7561
7967
|
if (!/^in_[A-Za-z0-9]+$/.test(id3(invoice))) subscriptionContractMismatch();
|
|
7562
|
-
const result2 = await
|
|
7968
|
+
const result2 = await stripeRequest7({ secretKey }, "GET", "/v1/invoice_payments", { invoice: id3(invoice), limit: 100 });
|
|
7563
7969
|
const list = result2.body;
|
|
7564
7970
|
if (!result2.ok || list?.has_more !== false || !Array.isArray(list.data) || list.data.length !== 1) subscriptionContractMismatch();
|
|
7565
7971
|
const link = list.data[0];
|
|
@@ -7568,11 +7974,11 @@ async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata,
|
|
|
7568
7974
|
}
|
|
7569
7975
|
if (!/^pi_[A-Za-z0-9]+$/.test(paymentId)) subscriptionContractMismatch();
|
|
7570
7976
|
const path = `/v1/payment_intents/${paymentId}`;
|
|
7571
|
-
const result = await
|
|
7977
|
+
const result = await stripeRequest7({ secretKey }, "GET", path);
|
|
7572
7978
|
const payment = result.body;
|
|
7573
7979
|
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
7980
|
if (Object.entries(metadata).every(([key3, value]) => payment.metadata?.[key3] === value)) return;
|
|
7575
|
-
const tagged = await
|
|
7981
|
+
const tagged = await stripeRequest7(
|
|
7576
7982
|
{ secretKey },
|
|
7577
7983
|
"POST",
|
|
7578
7984
|
path,
|
|
@@ -7584,7 +7990,7 @@ async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata,
|
|
|
7584
7990
|
|
|
7585
7991
|
// src/payments-stripe.ts
|
|
7586
7992
|
async function stripeCall(sk, method, path, params, idempotencyKey) {
|
|
7587
|
-
return
|
|
7993
|
+
return stripeRequest8(
|
|
7588
7994
|
{ secretKey: sk },
|
|
7589
7995
|
method,
|
|
7590
7996
|
path,
|
|
@@ -7592,7 +7998,7 @@ async function stripeCall(sk, method, path, params, idempotencyKey) {
|
|
|
7592
7998
|
idempotencyKey ? { idempotencyKey } : {}
|
|
7593
7999
|
);
|
|
7594
8000
|
}
|
|
7595
|
-
function
|
|
8001
|
+
function fail6(op, r) {
|
|
7596
8002
|
const err = new Error(`stripe ${op} failed: ${r.status}`);
|
|
7597
8003
|
err.code = "stripe_error";
|
|
7598
8004
|
throw err;
|
|
@@ -7642,7 +8048,7 @@ function createStripeProvider(config) {
|
|
|
7642
8048
|
{ email: input.email, name: input.name, metadata: meta },
|
|
7643
8049
|
`cus:${input.applicationId}`
|
|
7644
8050
|
);
|
|
7645
|
-
if (!cust.ok)
|
|
8051
|
+
if (!cust.ok) fail6("customer create", cust);
|
|
7646
8052
|
if (typeof cust.body.id !== "string" || !/^cus_[A-Za-z0-9]+$/.test(cust.body.id)) subscriptionContractMismatch();
|
|
7647
8053
|
customerId = cust.body.id;
|
|
7648
8054
|
}
|
|
@@ -7674,7 +8080,7 @@ function createStripeProvider(config) {
|
|
|
7674
8080
|
// retry between create and the db write can't orphan a second one.
|
|
7675
8081
|
`sub:${input.applicationId}${input.purchaseAttemptId ? `:${input.purchaseAttemptId}` : ""}`
|
|
7676
8082
|
);
|
|
7677
|
-
if (!sub.ok)
|
|
8083
|
+
if (!sub.ok) fail6("subscription create", sub);
|
|
7678
8084
|
if (input.billingTerms?.discount?.duration === "once" && Array.isArray(sub.body.discounts) && sub.body.discounts.length === 0) {
|
|
7679
8085
|
const subscriptionId = sub.body.id;
|
|
7680
8086
|
if (typeof subscriptionId !== "string" || !/^sub_[A-Za-z0-9]+$/.test(subscriptionId)) subscriptionContractMismatch();
|
|
@@ -7684,14 +8090,14 @@ function createStripeProvider(config) {
|
|
|
7684
8090
|
"expand[2]": "discounts",
|
|
7685
8091
|
"expand[3]": "latest_invoice.discounts"
|
|
7686
8092
|
});
|
|
7687
|
-
if (!expanded.ok)
|
|
8093
|
+
if (!expanded.ok) fail6("subscription invoice evidence", expanded);
|
|
7688
8094
|
if (expanded.body.id !== subscriptionId) subscriptionContractMismatch();
|
|
7689
8095
|
sub = expanded;
|
|
7690
8096
|
}
|
|
7691
8097
|
verifySubscriptionResult(sub.body, input, customerId, meta);
|
|
7692
8098
|
const invoice = sub.body.latest_invoice;
|
|
7693
8099
|
const clientSecret2 = clientSecretOf(sub.body);
|
|
7694
|
-
if (!clientSecret2)
|
|
8100
|
+
if (!clientSecret2) fail6("subscription confirmation-secret missing", sub);
|
|
7695
8101
|
await tagSubscriptionPayment(
|
|
7696
8102
|
sk,
|
|
7697
8103
|
invoice,
|
|
@@ -8363,7 +8769,7 @@ async function namedSeatOfferDigest(offer) {
|
|
|
8363
8769
|
"named-only",
|
|
8364
8770
|
"full-upfront",
|
|
8365
8771
|
"auto-renew-coterm",
|
|
8366
|
-
"
|
|
8772
|
+
"no-automatic-refund"
|
|
8367
8773
|
])));
|
|
8368
8774
|
return [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
8369
8775
|
}
|
|
@@ -8375,15 +8781,16 @@ async function readNamedSeatOffer(db, input, now) {
|
|
|
8375
8781
|
const offering = membershipOfferingVersion(member.offeringSnapshot);
|
|
8376
8782
|
const seat = member.namedSeatId ? await membershipLoad(db, "networkSeat", String(member.namedSeatId)) : void 0;
|
|
8377
8783
|
if (seat) requireMembershipScope(seat, input);
|
|
8784
|
+
const purchaseEndsAt = seatPurchaseDeadline(offering, member);
|
|
8378
8785
|
const offer = {
|
|
8379
8786
|
membershipId: id6,
|
|
8380
8787
|
amountCents: offering.seatPriceCents,
|
|
8381
8788
|
currency: offering.currency,
|
|
8382
8789
|
interval: offering.interval,
|
|
8383
8790
|
termEndsAt: Number(member.termEndsAt),
|
|
8384
|
-
purchaseEndsAt
|
|
8791
|
+
purchaseEndsAt,
|
|
8385
8792
|
autoRenew: member.autoRenew === true,
|
|
8386
|
-
eligible: ["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) && member.refundedCents === 0 && now >= Number(member.initialTermStartedAt) && now < Number(member.termEndsAt) && now <=
|
|
8793
|
+
eligible: ["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) && member.refundedCents === 0 && now >= Number(member.initialTermStartedAt) && now < Number(member.termEndsAt) && now <= purchaseEndsAt && (!seat || ["payment_pending", "payment_processing"].includes(String(seat.status))),
|
|
8387
8794
|
...seat ? { seat: {
|
|
8388
8795
|
seatId: String(seat.id),
|
|
8389
8796
|
status: String(seat.status),
|
|
@@ -8391,7 +8798,7 @@ async function readNamedSeatOffer(db, input, now) {
|
|
|
8391
8798
|
recipientEmail: String(seat.recipientEmailNormalized)
|
|
8392
8799
|
} } : {}
|
|
8393
8800
|
};
|
|
8394
|
-
return { ...offer, digest: await namedSeatOfferDigest(offer) };
|
|
8801
|
+
return { ...offer, digest: await namedSeatOfferDigest(offer), purchaseWindowed: offering.seatPurchaseWindowCalendarMonths !== void 0 };
|
|
8395
8802
|
}
|
|
8396
8803
|
|
|
8397
8804
|
// src/named-seat-request.ts
|
|
@@ -8768,6 +9175,23 @@ async function updateChapterPerson(opts) {
|
|
|
8768
9175
|
});
|
|
8769
9176
|
}
|
|
8770
9177
|
|
|
9178
|
+
// src/application-actions.ts
|
|
9179
|
+
function isApplicationStage(ctx, stage) {
|
|
9180
|
+
return ctx.chapter.pipeline.stages.includes(stage);
|
|
9181
|
+
}
|
|
9182
|
+
function applicationActionStages(app, env, ctx) {
|
|
9183
|
+
if (!chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME)) return [];
|
|
9184
|
+
const from = String(app.status ?? "");
|
|
9185
|
+
if (app.canceled === true || ["refunded", "canceled"].includes(from)) return [];
|
|
9186
|
+
return ctx.chapter.pipeline.stages.filter((to) => {
|
|
9187
|
+
if (to === from) return false;
|
|
9188
|
+
if (to === "approved") return canApprove(from, ctx.chapter.pipeline) && from !== "declined";
|
|
9189
|
+
if (to === "refunded") return Boolean(app.namedSeatId || app.stripeCustomerId && app.stripeSubscriptionId) && (!ctx.chapter.operations.refund.allowedFrom || ctx.chapter.operations.refund.allowedFrom.includes(from));
|
|
9190
|
+
if (ctx.membershipAuthority && ["submitted", "paid_pending_vetting", "declined", "canceled"].includes(to)) return false;
|
|
9191
|
+
return canTransition(from, to, ctx.chapter.pipeline);
|
|
9192
|
+
});
|
|
9193
|
+
}
|
|
9194
|
+
|
|
8771
9195
|
// src/worker-routes-people-profile.ts
|
|
8772
9196
|
var handlePeopleProfile = async (req, url, env, ctx) => {
|
|
8773
9197
|
if (ctx.chapter.mode !== "chapter") return null;
|
|
@@ -8789,7 +9213,16 @@ var handlePeopleProfile = async (req, url, env, ctx) => {
|
|
|
8789
9213
|
const record4 = await getRecord3({ crm: ctx.chapter.crm, db }, id6);
|
|
8790
9214
|
if (!record4) return json({ error: "not found" }, 404);
|
|
8791
9215
|
if (record4.type !== "person") return dedicated ? json({ error: "person required" }, 422) : null;
|
|
8792
|
-
if (match?.[2])
|
|
9216
|
+
if (match?.[2]) {
|
|
9217
|
+
const linked = typeof record4.fields.applicationId === "string" && record4.fields.applicationId !== "";
|
|
9218
|
+
let to = "";
|
|
9219
|
+
try {
|
|
9220
|
+
to = String((await req.clone().json()).to ?? "");
|
|
9221
|
+
} catch {
|
|
9222
|
+
}
|
|
9223
|
+
if (!linked && to && !isApplicationStage(ctx, to)) return null;
|
|
9224
|
+
return json({ error: "use the application lifecycle workflow", code: "application_workflow_required" }, 409);
|
|
9225
|
+
}
|
|
8793
9226
|
if (req.method === "GET" && dedicated) {
|
|
8794
9227
|
const excluded = profileReadOnlyFields(record4, ctx);
|
|
8795
9228
|
const fields = Object.entries(ctx.chapter.crm.type(record4.type).fields).filter(([id7]) => !excluded.includes(id7)).map(([id7, field]) => ({
|
|
@@ -9480,20 +9913,6 @@ var handleAdminBilling = async (req, url, env, ctx) => {
|
|
|
9480
9913
|
// src/worker-routes-admin-lifecycle.ts
|
|
9481
9914
|
import { computeBookableSlots as computeBookableSlots2, initCalendar as initCalendar3 } from "@odla-ai/calendar";
|
|
9482
9915
|
|
|
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
9916
|
// src/refund-application.ts
|
|
9498
9917
|
async function refundApplication(db, env, ctx, app) {
|
|
9499
9918
|
if (!chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME)) return json({ error: "not found" }, 404);
|
|
@@ -9684,6 +10103,12 @@ var handleAdminRefund = async (req, url, env, ctx) => {
|
|
|
9684
10103
|
const app = await loadApp(db, m[1]);
|
|
9685
10104
|
if (!app) return json({ error: "not found" }, 404);
|
|
9686
10105
|
if (!chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME)) return json({ error: "not found" }, 404);
|
|
10106
|
+
if (app.namedSeatId) {
|
|
10107
|
+
const actor = await ctx.verifyUser(req, env);
|
|
10108
|
+
if (!actor || !await ctx.isSuperAdminEmail(db, actor.email, actor.userId)) {
|
|
10109
|
+
return json({ error: "super-admin required", code: "super_admin_required" }, 403);
|
|
10110
|
+
}
|
|
10111
|
+
}
|
|
9687
10112
|
return refundApplication(db, env, ctx, app);
|
|
9688
10113
|
};
|
|
9689
10114
|
var handleAdminApplicationPatch = async (req, url, env, ctx) => {
|
|
@@ -10298,7 +10723,7 @@ async function operationalReaderRecord(record4, reader, db, env, ctx) {
|
|
|
10298
10723
|
const appId = record4.fields.applicationId;
|
|
10299
10724
|
const app = typeof appId === "string" ? (await db.query({ applications: { $: { where: { id: appId }, limit: 1 } } })).applications?.[0] : void 0;
|
|
10300
10725
|
if (reader.stageTransitions.includes(record4.type)) {
|
|
10301
|
-
result.edit.allowedStages = app ? applicationActionStages(app, env, ctx).map((id6) => ({ id: id6, label: id6.replaceAll("_", " ") })) :
|
|
10726
|
+
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
10727
|
const lock = (await db.query({ crm_activity: { $: { where: { id: `network-action-lock:${record4.id}` }, limit: 1 } } })).crm_activity?.[0];
|
|
10303
10728
|
const meta = lock?.meta;
|
|
10304
10729
|
if (meta?.state === "pending" && typeof meta.activityId === "string") {
|
|
@@ -10516,7 +10941,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
|
10516
10941
|
}
|
|
10517
10942
|
};
|
|
10518
10943
|
try {
|
|
10519
|
-
if (hasStage && ctx.chapter.mode === "chapter" && record4.type === "person") {
|
|
10944
|
+
if (hasStage && ctx.chapter.mode === "chapter" && record4.type === "person" && (record4.fields.applicationId || isApplicationStage(ctx, input.stage))) {
|
|
10520
10945
|
const updated2 = await networkApplicationAction({
|
|
10521
10946
|
db,
|
|
10522
10947
|
env,
|
|
@@ -11876,7 +12301,7 @@ async function revealNetworkPersonRegistryRecord(db, input, mutationId, actor, n
|
|
|
11876
12301
|
}
|
|
11877
12302
|
|
|
11878
12303
|
// src/worker-routes-network-person-registry.ts
|
|
11879
|
-
var
|
|
12304
|
+
var PATH3 = "/api/admin/network-person-registry";
|
|
11880
12305
|
var MAX_BODY2 = 8 * 1024 * 1024;
|
|
11881
12306
|
var MUTATION_ID4 = /^[A-Za-z0-9_-]{16,128}$/;
|
|
11882
12307
|
var positiveRevision = (value) => {
|
|
@@ -11901,7 +12326,7 @@ function registryError(error) {
|
|
|
11901
12326
|
return null;
|
|
11902
12327
|
}
|
|
11903
12328
|
var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
11904
|
-
if (url.pathname !==
|
|
12329
|
+
if (url.pathname !== PATH3) return null;
|
|
11905
12330
|
if (!ctx.chapter.networkPersonRegistry) return json({ error: "not found" }, 404);
|
|
11906
12331
|
if (req.method !== "GET" && req.method !== "POST") return json({ error: "method not allowed" }, 405);
|
|
11907
12332
|
if (req.method === "POST" && Number(req.headers.get("content-length") ?? 0) > MAX_BODY2) {
|
|
@@ -13097,7 +13522,7 @@ var handleAdminBuilderLibrary = async (req, url, env, ctx) => {
|
|
|
13097
13522
|
};
|
|
13098
13523
|
|
|
13099
13524
|
// src/membership-cancel-authority.ts
|
|
13100
|
-
import { stripeRequest as
|
|
13525
|
+
import { stripeRequest as stripeRequest9 } from "@odla-ai/stripe";
|
|
13101
13526
|
async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
|
|
13102
13527
|
membershipClock(now);
|
|
13103
13528
|
const id6 = await networkMembershipId(target);
|
|
@@ -13108,14 +13533,14 @@ async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
|
|
|
13108
13533
|
membershipFail("membership_cancellation_unavailable");
|
|
13109
13534
|
}
|
|
13110
13535
|
const path = `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`;
|
|
13111
|
-
const read2 = await
|
|
13536
|
+
const read2 = await stripeRequest9({ secretKey: stripeSecret }, "GET", path);
|
|
13112
13537
|
const sub = read2.body;
|
|
13113
13538
|
if (!read2.ok || sub.id !== member.providerSubscriptionId || sub.customer !== member.providerCustomerId || sub.livemode !== (target.environment === "prod") || !Object.entries({
|
|
13114
13539
|
...stripePartitionMetadata(target),
|
|
13115
13540
|
applicationId: target.applicationId
|
|
13116
13541
|
}).every(([key3, value]) => sub.metadata?.[key3] === value)) membershipFail("membership_cancellation_provider_mismatch");
|
|
13117
13542
|
if (sub.status !== "canceled" && sub.cancel_at_period_end !== true) {
|
|
13118
|
-
const write = await
|
|
13543
|
+
const write = await stripeRequest9(
|
|
13119
13544
|
{ secretKey: stripeSecret },
|
|
13120
13545
|
"POST",
|
|
13121
13546
|
path,
|
|
@@ -13337,7 +13762,7 @@ async function refundNetworkMembership(db, input, secretKey, now) {
|
|
|
13337
13762
|
}
|
|
13338
13763
|
|
|
13339
13764
|
// src/membership-provider-lifecycle.ts
|
|
13340
|
-
import { stripeRequest as
|
|
13765
|
+
import { stripeRequest as stripeRequest10 } from "@odla-ai/stripe";
|
|
13341
13766
|
|
|
13342
13767
|
// src/membership-renewal-authority.ts
|
|
13343
13768
|
async function reconcileMembershipRenewal(db, input, now) {
|
|
@@ -13390,7 +13815,11 @@ async function reconcileMembershipRenewal(db, input, now) {
|
|
|
13390
13815
|
membershipUpdate("networkMembership", id6, {
|
|
13391
13816
|
status: status2,
|
|
13392
13817
|
graceEndsAt,
|
|
13393
|
-
...paid ? {
|
|
13818
|
+
...paid ? {
|
|
13819
|
+
termStartedAt: start,
|
|
13820
|
+
termEndsAt: end,
|
|
13821
|
+
...membershipOfferingVersion(member.offeringSnapshot).seatPurchaseWindowCalendarMonths === void 0 ? { seatPurchaseEndsAt: end } : {}
|
|
13822
|
+
} : {},
|
|
13394
13823
|
revision: Number(member.revision) + 1,
|
|
13395
13824
|
updatedAt: now
|
|
13396
13825
|
}),
|
|
@@ -13495,7 +13924,7 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
13495
13924
|
}
|
|
13496
13925
|
if (fact.subscriptionId !== member.providerSubscriptionId || fact.objectKind !== "subscription") membershipFail("membership_billing_scope_mismatch");
|
|
13497
13926
|
if (["refund_pending", "refunded", "declined", "canceled", "restarting"].includes(String(member.status))) return receipt();
|
|
13498
|
-
const result = await
|
|
13927
|
+
const result = await stripeRequest10({ secretKey: sk }, "GET", `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`);
|
|
13499
13928
|
const sub = result.body;
|
|
13500
13929
|
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
13930
|
membershipFail("membership_billing_provider_mismatch");
|
|
@@ -13550,10 +13979,11 @@ async function reserveNamedMembershipSeat(db, input, now) {
|
|
|
13550
13979
|
const id6 = (await membershipOfferIdForRequest({ ...input, requestId: "named_seat" })).replace(/^offer_/, "seat_");
|
|
13551
13980
|
let seat = await membershipLoad(db, "networkSeat", id6);
|
|
13552
13981
|
if (!seat) {
|
|
13553
|
-
|
|
13982
|
+
const offering = membershipOfferingVersion(member.offeringSnapshot), claimId = await recipientKey(input, email);
|
|
13983
|
+
const purchaseEndsAt = seatPurchaseDeadline(offering, member);
|
|
13984
|
+
if (!["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) || Number(member.refundedCents) !== 0 || now < Number(member.initialTermStartedAt) || now >= Number(member.termEndsAt) || now > purchaseEndsAt || !Number.isSafeInteger(purchaseEndsAt) || member.namedSeatId !== void 0) {
|
|
13554
13985
|
membershipFail("named_seat_purchase_unavailable");
|
|
13555
13986
|
}
|
|
13556
|
-
const offering = membershipOfferingVersion(member.offeringSnapshot), claimId = await recipientKey(input, email);
|
|
13557
13987
|
const attrs = {
|
|
13558
13988
|
id: id6,
|
|
13559
13989
|
...membershipScope(input),
|
|
@@ -13572,7 +14002,7 @@ async function reserveNamedMembershipSeat(db, input, now) {
|
|
|
13572
14002
|
status: "payment_pending",
|
|
13573
14003
|
preparedAt: now,
|
|
13574
14004
|
termEndsAt: member.termEndsAt,
|
|
13575
|
-
seatPurchaseEndsAt:
|
|
14005
|
+
seatPurchaseEndsAt: purchaseEndsAt,
|
|
13576
14006
|
offeringSnapshot: offering,
|
|
13577
14007
|
revision: 1,
|
|
13578
14008
|
createdAt: now,
|
|
@@ -13610,7 +14040,7 @@ async function reserveNamedMembershipSeat(db, input, now) {
|
|
|
13610
14040
|
}
|
|
13611
14041
|
|
|
13612
14042
|
// src/membership-seat-stripe.ts
|
|
13613
|
-
import { stripeRequest as
|
|
14043
|
+
import { stripeRequest as stripeRequest11 } from "@odla-ai/stripe";
|
|
13614
14044
|
var reject7 = () => {
|
|
13615
14045
|
throw Object.assign(new Error("named seat provider contract mismatch"), { code: "named_seat_provider_mismatch" });
|
|
13616
14046
|
};
|
|
@@ -13622,7 +14052,7 @@ var metadataFor = (seat) => ({
|
|
|
13622
14052
|
});
|
|
13623
14053
|
var matches = (value, expected) => value && Object.entries(expected).every(([key3, item]) => value[key3] === item);
|
|
13624
14054
|
async function read(sk, path, params) {
|
|
13625
|
-
const response = await
|
|
14055
|
+
const response = await stripeRequest11({ secretKey: sk }, "GET", path, params);
|
|
13626
14056
|
if (!response.ok) return reject7();
|
|
13627
14057
|
return response.body;
|
|
13628
14058
|
}
|
|
@@ -13659,11 +14089,11 @@ async function createNamedSeatStripePayment(sk, seat, now) {
|
|
|
13659
14089
|
}
|
|
13660
14090
|
await subscriptionForSeat(sk, seat);
|
|
13661
14091
|
const metadata = metadataFor(seat);
|
|
13662
|
-
const result = seat.providerPaymentIntentId ? await
|
|
14092
|
+
const result = seat.providerPaymentIntentId ? await stripeRequest11(
|
|
13663
14093
|
{ secretKey: sk },
|
|
13664
14094
|
"GET",
|
|
13665
14095
|
`/v1/payment_intents/${encodeURIComponent(seat.providerPaymentIntentId)}`
|
|
13666
|
-
) : await
|
|
14096
|
+
) : await stripeRequest11({ secretKey: sk }, "POST", "/v1/payment_intents", {
|
|
13667
14097
|
amount: seat.amountCents,
|
|
13668
14098
|
currency: seat.currency,
|
|
13669
14099
|
customer: seat.providerCustomerId,
|
|
@@ -13681,7 +14111,7 @@ async function attachNamedSeatStripeRenewal(sk, seat) {
|
|
|
13681
14111
|
const sub = await subscriptionForSeat(sk, seat);
|
|
13682
14112
|
const existing = sub.items.data.find((item2) => item2.metadata?.seatId === seat.id);
|
|
13683
14113
|
if (existing) return String(existing.id);
|
|
13684
|
-
const result = await
|
|
14114
|
+
const result = await stripeRequest11({ secretKey: sk }, "POST", "/v1/subscription_items", {
|
|
13685
14115
|
subscription: seat.providerSubscriptionId,
|
|
13686
14116
|
price: seat.priceId,
|
|
13687
14117
|
quantity: 1,
|
|
@@ -13774,14 +14204,14 @@ async function confirmNamedMembershipSeatPayment(db, input, now) {
|
|
|
13774
14204
|
}
|
|
13775
14205
|
if (seat.purchasedAt === void 0) {
|
|
13776
14206
|
if (!["payment_processing", "payment_canceling"].includes(String(seat.status))) membershipFail("named_seat_payment_not_expected");
|
|
13777
|
-
const eligible = seat.status !== "payment_canceling" && ["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) && Number(member.refundedCents) === 0 && paidAt <=
|
|
14207
|
+
const eligible = seat.status !== "payment_canceling" && ["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) && Number(member.refundedCents) === 0 && paidAt <= seatPurchaseDeadline(membershipOfferingVersion(member.offeringSnapshot), member) && now < Number(member.termEndsAt);
|
|
13778
14208
|
const status2 = eligible ? "renewal_pending" : "refund_pending", effectId = `${seat.id}:${eligible ? "attach-renewal" : "refund"}`;
|
|
13779
14209
|
try {
|
|
13780
14210
|
await db.transact([
|
|
13781
14211
|
membershipUpdate("networkSeat", input.seatId, {
|
|
13782
14212
|
status: status2,
|
|
13783
14213
|
purchasedAt: paidAt,
|
|
13784
|
-
acceptanceEndsAt:
|
|
14214
|
+
acceptanceEndsAt: Number(member.termEndsAt),
|
|
13785
14215
|
termStartedAt: paidAt,
|
|
13786
14216
|
termEndsAt: member.termEndsAt,
|
|
13787
14217
|
paymentEventId: input.eventId,
|
|
@@ -13829,7 +14259,7 @@ async function completeNamedSeatRenewal(db, seatId, secretKey, now) {
|
|
|
13829
14259
|
if (seat.status !== "renewal_pending" || !seat.purchasedAt) membershipFail("named_seat_renewal_not_ready");
|
|
13830
14260
|
const member = await membershipLoad(db, "networkMembership", target.primaryMembershipId);
|
|
13831
14261
|
requireMembershipScope(member, target);
|
|
13832
|
-
if (now >= Number(
|
|
14262
|
+
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
14263
|
const itemId = await attachNamedSeatStripeRenewal(secretKey, target);
|
|
13834
14264
|
const entitlementId = `${seatId}:entitlement`, effectId = `${seatId}:attach-renewal`;
|
|
13835
14265
|
try {
|
|
@@ -13904,7 +14334,7 @@ async function recordRecipient(db, input, now, approve) {
|
|
|
13904
14334
|
requireMembershipScope(entitlement, input);
|
|
13905
14335
|
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
14336
|
if (seat.acceptedAt === void 0) {
|
|
13907
|
-
if (approve ||
|
|
14337
|
+
if (approve || normalizeChapterEmail(input.verifiedRecipientEmail) !== seat.recipientEmailNormalized) {
|
|
13908
14338
|
membershipFail("named_seat_recipient_verification_required");
|
|
13909
14339
|
}
|
|
13910
14340
|
} else if (seat.recipientUserId !== input.recipientUserId || seat.recipientApplicationId !== input.recipientApplicationId) {
|
|
@@ -13954,129 +14384,6 @@ async function recordRecipient(db, input, now, approve) {
|
|
|
13954
14384
|
return membershipFail("named_seat_recipient_retry_required");
|
|
13955
14385
|
}
|
|
13956
14386
|
|
|
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
14387
|
// src/membership-seat-provider-refund.ts
|
|
14081
14388
|
async function confirmNamedSeatProviderRefund(db, input, now) {
|
|
14082
14389
|
const seat = await membershipLoad(db, "networkSeat", input.seatId);
|
|
@@ -14098,7 +14405,7 @@ async function readNamedSeatInvitation(db, input, now) {
|
|
|
14098
14405
|
await loadMembershipAuthorityState(db, target);
|
|
14099
14406
|
const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
|
|
14100
14407
|
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)
|
|
14408
|
+
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
14409
|
return {
|
|
14103
14410
|
seatId: input.seatId,
|
|
14104
14411
|
primaryApplicationId: target.applicationId,
|
|
@@ -14128,7 +14435,7 @@ async function readMembershipChapterEffect(db, input, now) {
|
|
|
14128
14435
|
requireMembershipScope(member, input);
|
|
14129
14436
|
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
14437
|
let applicable;
|
|
14131
|
-
if (input.kind === "invite_named_recipient") applicable = seat.status === "pending_acceptance" && seat.acceptedAt === void 0 && now < Number(
|
|
14438
|
+
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
14439
|
else if (input.kind === "revoke_named_recipient_access") applicable = ["refund_pending", "refunded"].includes(String(seat.status));
|
|
14133
14440
|
else applicable = (await readNamedSeatAccess(db, {
|
|
14134
14441
|
...input,
|
|
@@ -14157,6 +14464,7 @@ var MEMBERSHIP_SEAT_ACTION_FIELDS = {
|
|
|
14157
14464
|
"seat-reservations": ["recipientName", "recipientEmail", "purchaserEmail", "purchaserUserId"],
|
|
14158
14465
|
"seat-payments": ["seatId", "eventId"],
|
|
14159
14466
|
"seat-refunds": ["seatId", "eventId"],
|
|
14467
|
+
"seat-operator-refund": ["seatId"],
|
|
14160
14468
|
"seat-accept": recipient,
|
|
14161
14469
|
"seat-approve": recipient,
|
|
14162
14470
|
"seat-decline": recipient,
|
|
@@ -14225,6 +14533,7 @@ async function handleMembershipSeatAction(action, db, peer, input, now, stripeSe
|
|
|
14225
14533
|
if (refunded?.status !== "refunded" || refunded.refundedCents !== seat.amountCents) throw new Error("named seat refund unconfirmed");
|
|
14226
14534
|
return { seatId: seat.id, status: "refunded", refundedCents: refunded.refundedCents, subscriptionCanceled: false };
|
|
14227
14535
|
}
|
|
14536
|
+
if (action === "seat-operator-refund") return refundNamedSeatByOperator(db, { ...target, seatId: String(input.seatId) }, secret, now);
|
|
14228
14537
|
if (action === "seat-checkouts") return prepareNamedMembershipSeatPayment(db, {
|
|
14229
14538
|
...target,
|
|
14230
14539
|
priceId: peer.seatPriceId,
|
|
@@ -14313,7 +14622,7 @@ function createMembershipAuthorityHandler(options) {
|
|
|
14313
14622
|
if (req.method !== "POST") return respond({ error: "method_not_allowed" }, 405);
|
|
14314
14623
|
const sender = req.headers.get(FEDERATION_HEADERS2.sender);
|
|
14315
14624
|
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);
|
|
14625
|
+
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
14626
|
const peer = peers.find((candidate) => candidate.appId === sender && url.pathname === `${MEMBERSHIP_AUTHORITY_PATH}/${candidate.runtime}/${action}` && !url.search);
|
|
14318
14627
|
if (!peer) return respond({ error: "unauthorized" }, 401);
|
|
14319
14628
|
let body;
|
|
@@ -14335,13 +14644,23 @@ function createMembershipAuthorityHandler(options) {
|
|
|
14335
14644
|
let input;
|
|
14336
14645
|
try {
|
|
14337
14646
|
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] ?? []];
|
|
14647
|
+
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
14648
|
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
14649
|
throw new TypeError("invalid request");
|
|
14341
14650
|
}
|
|
14342
14651
|
} catch {
|
|
14343
14652
|
return respond({ error: "invalid_offer_request" }, 400);
|
|
14344
14653
|
}
|
|
14654
|
+
if (action === "offer-preview") {
|
|
14655
|
+
return respond({ ok: true, preview: await previewMembershipCommercialOffer(db, {
|
|
14656
|
+
appId: peer.appId,
|
|
14657
|
+
chapterId: peer.chapterId,
|
|
14658
|
+
environment: peer.environment,
|
|
14659
|
+
runtime: peer.runtime,
|
|
14660
|
+
skuId: String(input.skuId),
|
|
14661
|
+
now: options.now?.() ?? Date.now()
|
|
14662
|
+
}) });
|
|
14663
|
+
}
|
|
14345
14664
|
if (Object.hasOwn(MEMBERSHIP_SEAT_ACTION_FIELDS, action)) {
|
|
14346
14665
|
return respond({ ok: true, seat: await handleMembershipSeatAction(
|
|
14347
14666
|
action,
|
|
@@ -14735,7 +15054,7 @@ async function drainMembershipEffects(db, options, now) {
|
|
|
14735
15054
|
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100 || !options.stripeSecretKey) throw new TypeError("invalid membership recovery configuration");
|
|
14736
15055
|
await loadMembershipAuthorityState(db, scope2);
|
|
14737
15056
|
const matches2 = (row2) => Object.entries(scope2).every(([key3, value]) => row2[key3] === value);
|
|
14738
|
-
let
|
|
15057
|
+
let pending = 0, complete = 0, more = false;
|
|
14739
15058
|
for (const status2 of ["payment_pending", "payment_processing", "payment_canceling"]) {
|
|
14740
15059
|
const seats = (await db.query({ networkSeat: { $: { where: { ...scope2, status: status2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkSeat ?? [];
|
|
14741
15060
|
more ||= seats.length > limit;
|
|
@@ -14753,26 +15072,6 @@ async function drainMembershipEffects(db, options, now) {
|
|
|
14753
15072
|
}
|
|
14754
15073
|
}
|
|
14755
15074
|
}
|
|
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
15075
|
const effects = (await db.query({ membershipEffect: { $: { where: {
|
|
14777
15076
|
chapterId: scope2.chapterId,
|
|
14778
15077
|
environment: scope2.environment,
|
|
@@ -14834,7 +15133,7 @@ async function drainMembershipEffects(db, options, now) {
|
|
|
14834
15133
|
pending++;
|
|
14835
15134
|
}
|
|
14836
15135
|
}
|
|
14837
|
-
return { complete, pending,
|
|
15136
|
+
return { complete, pending, more };
|
|
14838
15137
|
}
|
|
14839
15138
|
|
|
14840
15139
|
// src/approval-effects-retry.ts
|
|
@@ -14984,7 +15283,7 @@ function createMembershipChapterEffectRoute(options) {
|
|
|
14984
15283
|
config.emailTemplates = { ...config.emailTemplates, namedSeatInvitation: config.emailTemplates?.namedSeatInvitation ?? {
|
|
14985
15284
|
enabled: true,
|
|
14986
15285
|
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
|
|
15286
|
+
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
15287
|
} };
|
|
14989
15288
|
const template = config.emailTemplates.namedSeatInvitation;
|
|
14990
15289
|
if (!template.text.includes("{{purchaserName}}")) config.emailTemplates.namedSeatInvitation = {
|
|
@@ -15057,6 +15356,12 @@ async function requestMembershipCommercialOffer(options) {
|
|
|
15057
15356
|
lifetimeMs: 6e4
|
|
15058
15357
|
});
|
|
15059
15358
|
}
|
|
15359
|
+
async function requestMembershipOfferPreview(options) {
|
|
15360
|
+
const result = await requestAuthority(options, "offer-preview", { skuId: options.skuId });
|
|
15361
|
+
const preview = result.preview;
|
|
15362
|
+
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");
|
|
15363
|
+
return preview;
|
|
15364
|
+
}
|
|
15060
15365
|
async function requestMembershipCheckoutBinding(options) {
|
|
15061
15366
|
const result = await requestAuthority(options, "checkouts", {
|
|
15062
15367
|
applicationId: options.applicationId,
|
|
@@ -15095,6 +15400,12 @@ async function requestNamedSeatDecline(options) {
|
|
|
15095
15400
|
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
15401
|
return seat;
|
|
15097
15402
|
}
|
|
15403
|
+
async function requestNamedSeatOperatorRefund(options) {
|
|
15404
|
+
const result = await requestAuthority(options, "seat-operator-refund", { applicationId: options.applicationId, skuId: options.skuId, seatId: options.seatId });
|
|
15405
|
+
const seat = result.seat;
|
|
15406
|
+
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");
|
|
15407
|
+
return seat;
|
|
15408
|
+
}
|
|
15098
15409
|
async function requestNamedSeatReservation(options) {
|
|
15099
15410
|
const result = await requestAuthority(options, "seat-reservations", {
|
|
15100
15411
|
applicationId: options.applicationId,
|
|
@@ -15112,7 +15423,7 @@ async function requestNamedSeatReservation(options) {
|
|
|
15112
15423
|
async function requestNamedSeatOffer(options) {
|
|
15113
15424
|
const result = await requestAuthority(options, "seat-offer", { applicationId: options.applicationId, skuId: options.skuId });
|
|
15114
15425
|
const offer = result.seat;
|
|
15115
|
-
if (!offer || offer.membershipId !== await networkMembershipId(options) || !Number.isSafeInteger(offer.amountCents) || offer.amountCents <= 0 || !/^[a-z]{3}$/.test(offer.currency) || !["month", "year"].includes(offer.interval) || typeof offer.eligible !== "boolean" || typeof offer.autoRenew !== "boolean" || !Number.isSafeInteger(offer.termEndsAt) || !Number.isSafeInteger(offer.purchaseEndsAt) || offer.digest !== await namedSeatOfferDigest(offer)) throw new Error("invalid named seat offer");
|
|
15426
|
+
if (!offer || offer.membershipId !== await networkMembershipId(options) || !Number.isSafeInteger(offer.amountCents) || offer.amountCents <= 0 || !/^[a-z]{3}$/.test(offer.currency) || !["month", "year"].includes(offer.interval) || typeof offer.eligible !== "boolean" || typeof offer.autoRenew !== "boolean" || !Number.isSafeInteger(offer.termEndsAt) || !Number.isSafeInteger(offer.purchaseEndsAt) || offer.purchaseWindowed !== void 0 && typeof offer.purchaseWindowed !== "boolean" || offer.digest !== await namedSeatOfferDigest(offer)) throw new Error("invalid named seat offer");
|
|
15116
15427
|
return offer;
|
|
15117
15428
|
}
|
|
15118
15429
|
async function requestNamedSeatInvitation(options) {
|
|
@@ -15280,6 +15591,7 @@ function createMembershipWorkerAdapter(options) {
|
|
|
15280
15591
|
return {
|
|
15281
15592
|
authority: {
|
|
15282
15593
|
issueOffer: async (input, env) => requestMembershipCommercialOffer({ ...input, ...await transport(input, env) }),
|
|
15594
|
+
previewOffer: async (input, env) => requestMembershipOfferPreview({ ...input, ...await transport(input, env) }),
|
|
15283
15595
|
bindCheckout: async (input, env) => requestMembershipCheckoutBinding({ ...input, ...await transport(input, env) }),
|
|
15284
15596
|
confirmPayment: async (input, env) => requestMembershipPayment({ ...input, ...await transport(input, env) }),
|
|
15285
15597
|
approveMembership: async (input, env) => requestMembershipApproval({ ...input, ...await transport(input, env) }),
|
|
@@ -15298,7 +15610,8 @@ function createMembershipWorkerAdapter(options) {
|
|
|
15298
15610
|
confirmRefund: async (input, env) => requestNamedSeatRefund2({ ...input, ...await transport(input, env) }),
|
|
15299
15611
|
accept: async (input, env) => requestNamedSeatAcceptance({ ...input, ...await transport(input, env) }),
|
|
15300
15612
|
approve: async (input, env) => requestNamedSeatApproval({ ...input, ...await transport(input, env) }),
|
|
15301
|
-
decline: async (input, env) => requestNamedSeatDecline({ ...input, ...await transport(input, env) })
|
|
15613
|
+
decline: async (input, env) => requestNamedSeatDecline({ ...input, ...await transport(input, env) }),
|
|
15614
|
+
operatorRefund: async (input, env) => requestNamedSeatOperatorRefund({ ...input, ...await transport(input, env) })
|
|
15302
15615
|
} } : {}
|
|
15303
15616
|
},
|
|
15304
15617
|
readEffect: async (input, env) => requestMembershipChapterEffect({ ...input, ...await transport(input, env) })
|
|
@@ -15553,6 +15866,8 @@ var BUILTIN_ROUTES = [
|
|
|
15553
15866
|
handleMembershipRestart,
|
|
15554
15867
|
handleMembershipCancellation,
|
|
15555
15868
|
handlePaymentRecovery,
|
|
15869
|
+
handleAdminNamedSeat,
|
|
15870
|
+
handleAdminMembershipSeats,
|
|
15556
15871
|
handleAdminMeetings,
|
|
15557
15872
|
handleAdminScheduling,
|
|
15558
15873
|
// Roster + identity
|
|
@@ -15633,6 +15948,7 @@ export {
|
|
|
15633
15948
|
deliverMembershipChapterEffect,
|
|
15634
15949
|
deliverSignupControl,
|
|
15635
15950
|
drainMembershipEffects,
|
|
15951
|
+
refundNamedSeatByOperator,
|
|
15636
15952
|
renderActiveSignupControlHtml,
|
|
15637
15953
|
resolveNetworkTargetTransport,
|
|
15638
15954
|
retryChapterApprovalEffects,
|