@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.cjs
CHANGED
|
@@ -36,6 +36,7 @@ __export(worker_exports, {
|
|
|
36
36
|
deliverMembershipChapterEffect: () => deliverMembershipChapterEffect,
|
|
37
37
|
deliverSignupControl: () => deliverSignupControl,
|
|
38
38
|
drainMembershipEffects: () => drainMembershipEffects,
|
|
39
|
+
refundNamedSeatByOperator: () => refundNamedSeatByOperator,
|
|
39
40
|
renderActiveSignupControlHtml: () => renderActiveSignupControlHtml,
|
|
40
41
|
resolveNetworkTargetTransport: () => resolveNetworkTargetTransport,
|
|
41
42
|
retryChapterApprovalEffects: () => retryChapterApprovalEffects,
|
|
@@ -375,6 +376,7 @@ function createWorkerContext(options) {
|
|
|
375
376
|
crmBase,
|
|
376
377
|
requirePaymentQuote: options.requirePaymentQuote === true,
|
|
377
378
|
membershipAuthority: options.membershipAuthority,
|
|
379
|
+
membershipAuthorityOperator: options.membershipAuthorityOperator,
|
|
378
380
|
getPublicConfig,
|
|
379
381
|
verifyUser,
|
|
380
382
|
makeDb,
|
|
@@ -1700,6 +1702,11 @@ function canApprove(status2, p) {
|
|
|
1700
1702
|
}
|
|
1701
1703
|
|
|
1702
1704
|
// src/membership-policy.ts
|
|
1705
|
+
function seatPurchaseDeadline(offering, member) {
|
|
1706
|
+
if (offering.seatPurchaseWindowCalendarMonths === void 0) return Number(member.termEndsAt);
|
|
1707
|
+
if (Number.isSafeInteger(member.seatPurchaseEndsAt)) return Number(member.seatPurchaseEndsAt);
|
|
1708
|
+
return addMembershipCalendarMonths(Number(member.initialTermStartedAt ?? member.termStartedAt), offering.seatPurchaseWindowCalendarMonths);
|
|
1709
|
+
}
|
|
1703
1710
|
var fail2 = (field) => {
|
|
1704
1711
|
throw new TypeError(`invalid membership policy: ${field}`);
|
|
1705
1712
|
};
|
|
@@ -1756,7 +1763,7 @@ function membershipOfferingVersion(value) {
|
|
|
1756
1763
|
autoRenew: true,
|
|
1757
1764
|
paymentGraceDays: integer2(input.paymentGraceDays, 0, 60),
|
|
1758
1765
|
seatPriceCents: integer2(input.seatPriceCents, 1, Number.MAX_SAFE_INTEGER),
|
|
1759
|
-
seatPurchaseWindowCalendarMonths: integer2(input.seatPurchaseWindowCalendarMonths, 1, 120),
|
|
1766
|
+
...input.seatPurchaseWindowCalendarMonths === void 0 || input.seatPurchaseWindowCalendarMonths === null ? {} : { seatPurchaseWindowCalendarMonths: integer2(input.seatPurchaseWindowCalendarMonths, 1, 120) },
|
|
1760
1767
|
requiredDisclosures: ids(input.requiredDisclosures)
|
|
1761
1768
|
};
|
|
1762
1769
|
}
|
|
@@ -1883,24 +1890,63 @@ async function verifyIssuedMembershipOffer(value, expected) {
|
|
|
1883
1890
|
if (!value || typeof value !== "object" || Array.isArray(value)) return reject("invalid_offer_snapshot");
|
|
1884
1891
|
return restore({ ...value, approvedFoundingCount: 0 }, expected);
|
|
1885
1892
|
}
|
|
1886
|
-
async function
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1893
|
+
async function previewMembershipCommercialOffer(db, input) {
|
|
1894
|
+
for (const key3 of ["appId", "chapterId", "environment", "runtime", "skuId"]) {
|
|
1895
|
+
if (typeof input[key3] !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_.:-]{1,159}$/.test(input[key3])) reject("invalid_offer_identity");
|
|
1896
|
+
}
|
|
1897
|
+
if (!Number.isSafeInteger(input.now) || input.now < 0) reject("invalid_offer_window");
|
|
1898
|
+
const state = await authoritativeState(db, input);
|
|
1899
|
+
const { offering, policy } = await currentPolicy(db, input, false, state);
|
|
1900
|
+
const approvedFoundingCount = policy ? state.cohortCounts[policy.cohortLabel] ?? 0 : 0;
|
|
1901
|
+
if (!Number.isSafeInteger(approvedFoundingCount) || Number(approvedFoundingCount) < 0) reject("invalid_cohort_count");
|
|
1902
|
+
const contract = createMembershipCommercialOffer({
|
|
1903
|
+
offering,
|
|
1904
|
+
discountPolicy: policy,
|
|
1905
|
+
approvedFoundingCount: Number(approvedFoundingCount),
|
|
1906
|
+
now: input.now,
|
|
1907
|
+
couponId: await couponId(input, policy?.id)
|
|
1908
|
+
});
|
|
1909
|
+
const discount = contract.billingTerms.discount;
|
|
1910
|
+
return {
|
|
1911
|
+
skuId: input.skuId,
|
|
1912
|
+
offeringVersionId: offering.id,
|
|
1913
|
+
basePriceCents: offering.basePriceCents,
|
|
1914
|
+
currency: offering.currency,
|
|
1915
|
+
interval: offering.interval,
|
|
1916
|
+
founding: contract.founding,
|
|
1917
|
+
...contract.founding && contract.discountPolicy ? { cohortLabel: contract.discountPolicy.cohortLabel } : {},
|
|
1918
|
+
dueTodayCents: contract.dueTodayCents,
|
|
1919
|
+
renewalCents: contract.renewalCents,
|
|
1920
|
+
...discount ? { discountDuration: discount.duration === "once" ? "first_term" : "while_active" } : {},
|
|
1921
|
+
...contract.initialTermEndsAt !== void 0 ? { initialTermEndsAt: contract.initialTermEndsAt } : {}
|
|
1922
|
+
};
|
|
1923
|
+
}
|
|
1924
|
+
async function authoritativeState(db, input) {
|
|
1925
|
+
const state = await load(db, "membershipAuthorityState", `${input.chapterId}:${input.environment}`);
|
|
1890
1926
|
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");
|
|
1891
|
-
|
|
1892
|
-
|
|
1927
|
+
return state;
|
|
1928
|
+
}
|
|
1929
|
+
async function currentPolicy(db, input, restartAware, state) {
|
|
1893
1930
|
const rows5 = await db.query({
|
|
1894
1931
|
membershipOfferingVersion: { $: { where: { chapterId: input.chapterId }, limit: 200 } },
|
|
1895
1932
|
discountPolicyVersion: { $: { where: { chapterId: input.chapterId }, limit: 200 } }
|
|
1896
1933
|
});
|
|
1897
1934
|
if (!Array.isArray(rows5.membershipOfferingVersion) || !Array.isArray(rows5.discountPolicyVersion) || rows5.membershipOfferingVersion.length >= 200 || rows5.discountPolicyVersion.length >= 200) return reject("policy_history_incomplete");
|
|
1898
|
-
const offerings = rows5.membershipOfferingVersion.map((
|
|
1899
|
-
const policies = rows5.discountPolicyVersion.map((
|
|
1935
|
+
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));
|
|
1936
|
+
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);
|
|
1900
1937
|
const offering = offerings.sort((a, b) => b.version - a.version)[0];
|
|
1901
|
-
const restart = await load(db, "membershipRestart", (await membershipOfferIdForRequest({ ...input, requestId: "checkout_binding" })).replace(/^offer_/, "membership_"));
|
|
1938
|
+
const restart = restartAware && input.applicationId ? await load(db, "membershipRestart", (await membershipOfferIdForRequest({ ...input, applicationId: input.applicationId, requestId: "checkout_binding" })).replace(/^offer_/, "membership_")) : void 0;
|
|
1902
1939
|
const policy = restart ? void 0 : policies.sort((a, b) => b.version - a.version)[0];
|
|
1903
1940
|
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");
|
|
1941
|
+
return { offering, policy };
|
|
1942
|
+
}
|
|
1943
|
+
async function issueMembershipCommercialOffer(db, input) {
|
|
1944
|
+
validate(input);
|
|
1945
|
+
const stateId = `${input.chapterId}:${input.environment}`;
|
|
1946
|
+
const state = await authoritativeState(db, input);
|
|
1947
|
+
const existing = await load(db, "membershipCheckoutOffer", input.offerId);
|
|
1948
|
+
if (existing) return restore(existing, input);
|
|
1949
|
+
const { offering, policy } = await currentPolicy(db, input, true, state);
|
|
1904
1950
|
const approvedFoundingCount = policy ? state.cohortCounts[policy.cohortLabel] ?? 0 : 0;
|
|
1905
1951
|
if (!Number.isSafeInteger(approvedFoundingCount) || Number(approvedFoundingCount) < 0) reject("invalid_cohort_count");
|
|
1906
1952
|
const contract = createMembershipCommercialOffer({
|
|
@@ -2108,7 +2154,7 @@ async function confirmMembershipPayment(db, input, now) {
|
|
|
2108
2154
|
initialTermEndsAt: endsAt,
|
|
2109
2155
|
termStartedAt: paidAt,
|
|
2110
2156
|
termEndsAt: endsAt,
|
|
2111
|
-
seatPurchaseEndsAt:
|
|
2157
|
+
seatPurchaseEndsAt: seatPurchaseDeadline(offering, { initialTermStartedAt: paidAt, termEndsAt: endsAt }),
|
|
2112
2158
|
autoRenew: true,
|
|
2113
2159
|
refundedCents: 0,
|
|
2114
2160
|
revision: 1,
|
|
@@ -2208,7 +2254,7 @@ function findTier(tiers, tierId) {
|
|
|
2208
2254
|
if (!tierId) return tiers.length === 1 ? tiers[0] : null;
|
|
2209
2255
|
return tiers.find((tier) => tier.id === tierId) ?? null;
|
|
2210
2256
|
}
|
|
2211
|
-
function joinConfigTiers(tiers, defaults = {}) {
|
|
2257
|
+
function joinConfigTiers(tiers, defaults = {}, founding = /* @__PURE__ */ new Map()) {
|
|
2212
2258
|
return tiers.map((tier) => ({
|
|
2213
2259
|
id: tier.id,
|
|
2214
2260
|
name: tier.name,
|
|
@@ -2216,7 +2262,8 @@ function joinConfigTiers(tiers, defaults = {}) {
|
|
|
2216
2262
|
currency: tier.currency ?? defaults.currency ?? "usd",
|
|
2217
2263
|
interval: tier.interval ?? defaults.interval ?? "year",
|
|
2218
2264
|
blurb: tier.blurb ?? "",
|
|
2219
|
-
free: tierIsFree(tier)
|
|
2265
|
+
free: tierIsFree(tier),
|
|
2266
|
+
...founding.has(tier.id) ? { founding: founding.get(tier.id) } : {}
|
|
2220
2267
|
}));
|
|
2221
2268
|
}
|
|
2222
2269
|
|
|
@@ -3451,6 +3498,313 @@ var handlePaymentRecovery = async (req, url, env, ctx) => {
|
|
|
3451
3498
|
}
|
|
3452
3499
|
};
|
|
3453
3500
|
|
|
3501
|
+
// src/worker-routes-admin-named-seat.ts
|
|
3502
|
+
var PATH = /^\/api\/admin\/applications\/([0-9a-f-]+)\/named-seat(\/refund)?$/;
|
|
3503
|
+
var REFUNDABLE = /* @__PURE__ */ new Set(["renewal_pending", "pending_acceptance", "pending_approval", "pending_primary", "active"]);
|
|
3504
|
+
var handleAdminNamedSeat = async (req, url, env, ctx) => {
|
|
3505
|
+
const match = url.pathname.match(PATH);
|
|
3506
|
+
if (!match) return null;
|
|
3507
|
+
const seats = ctx.membershipAuthority?.namedSeats;
|
|
3508
|
+
if (ctx.chapter.mode !== "chapter" || !seats) return json({ error: "named seats are not configured" }, 404);
|
|
3509
|
+
const refund = Boolean(match[2]);
|
|
3510
|
+
if (req.method !== (refund ? "POST" : "GET")) return json({ error: "method not allowed" }, 405);
|
|
3511
|
+
const raw2 = ctx.makeDb(env), user = await ctx.verifyUser(req, env);
|
|
3512
|
+
if (!user) return json({ error: "unauthorized" }, 401);
|
|
3513
|
+
if (!await ctx.isSuperAdminEmail(raw2, user.email, user.userId)) return json({ error: "super-admin required", code: "super_admin_required" }, 403);
|
|
3514
|
+
const db = raw2, app = await membershipLoad(db, "applications", match[1]);
|
|
3515
|
+
if (!app || !chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME) || app.namedSeatId) return json({ error: "not found" }, 404);
|
|
3516
|
+
const intent = await membershipLoad(db, "subscriptionCheckoutIntents", String(app.id));
|
|
3517
|
+
if (!intent) return json({ error: "paid membership required" }, 409);
|
|
3518
|
+
try {
|
|
3519
|
+
const target = await membershipTargetForIntent(intent, app, env, ctx);
|
|
3520
|
+
const offer = await seats.offer(target, env);
|
|
3521
|
+
const seat = offer.seat ?? null, refundable = Boolean(seat && REFUNDABLE.has(seat.status));
|
|
3522
|
+
if (!refund) {
|
|
3523
|
+
return json({
|
|
3524
|
+
seat,
|
|
3525
|
+
amountCents: offer.amountCents,
|
|
3526
|
+
currency: offer.currency,
|
|
3527
|
+
termEndsAt: offer.termEndsAt,
|
|
3528
|
+
purchaseEndsAt: offer.purchaseEndsAt,
|
|
3529
|
+
refundable
|
|
3530
|
+
});
|
|
3531
|
+
}
|
|
3532
|
+
if (!seat) return json({ error: "no purchased seat", code: "seat_not_found" }, 409);
|
|
3533
|
+
if (seat.status === "refunded") {
|
|
3534
|
+
await closeRecipientApplication(db, env, ctx, seat.seatId);
|
|
3535
|
+
return json({ ok: true, seatId: seat.seatId, status: "refunded", replayed: true });
|
|
3536
|
+
}
|
|
3537
|
+
if (!refundable) return json({ error: `seat is ${seat.status.replaceAll("_", " ")}`, code: "seat_not_refundable" }, 409);
|
|
3538
|
+
const receipt = await seats.operatorRefund({ ...target, seatId: seat.seatId }, env);
|
|
3539
|
+
await closeRecipientApplication(db, env, ctx, seat.seatId);
|
|
3540
|
+
return json({ ok: true, ...receipt });
|
|
3541
|
+
} catch {
|
|
3542
|
+
return json({ error: "named seat service unavailable; retry to confirm" }, 503);
|
|
3543
|
+
}
|
|
3544
|
+
};
|
|
3545
|
+
async function closeRecipientApplication(db, env, ctx, seatId) {
|
|
3546
|
+
const recipient2 = (await db.query({ applications: { $: { where: { namedSeatId: seatId }, limit: 1 } } })).applications?.[0];
|
|
3547
|
+
if (!recipient2 || !chapterBillingApplication(recipient2, ctx.chapter.id, env.ODLA_RUNTIME) || recipient2.status === "refunded") return;
|
|
3548
|
+
const attrs = { status: "refunded", renewalAt: 0, canceled: true };
|
|
3549
|
+
await db.transact(
|
|
3550
|
+
[{ t: "update", ns: "applications", id: String(recipient2.id), attrs }],
|
|
3551
|
+
{ mutationId: `verified-refund:${String(recipient2.id)}:${seatId}` }
|
|
3552
|
+
);
|
|
3553
|
+
await syncApplicationToCrm(crmDeps(db, ctx), { app: { ...recipient2, ...attrs }, stage: "refunded" }).catch(() => void 0);
|
|
3554
|
+
}
|
|
3555
|
+
|
|
3556
|
+
// src/membership-seat-refund-stripe.ts
|
|
3557
|
+
var import_stripe6 = require("@odla-ai/stripe");
|
|
3558
|
+
var fail3 = (code = "named_seat_refund_mismatch") => {
|
|
3559
|
+
throw Object.assign(new Error(code), { code });
|
|
3560
|
+
};
|
|
3561
|
+
async function refundNamedSeatStripePayment(secretKey, seat) {
|
|
3562
|
+
if (!seat.providerPaymentIntentId || seat.status !== "refund_pending") fail3();
|
|
3563
|
+
const call = async (method, path, params, idempotencyKey) => {
|
|
3564
|
+
const result = await (0, import_stripe6.stripeRequest)({ secretKey }, method, path, params, idempotencyKey ? { idempotencyKey } : {});
|
|
3565
|
+
if (!result.ok || !result.body) return fail3("named_seat_refund_provider_unavailable");
|
|
3566
|
+
return result.body;
|
|
3567
|
+
};
|
|
3568
|
+
const metadata = { ...stripePartitionMetadata(seat), applicationId: seat.applicationId, membershipId: seat.primaryMembershipId, seatId: seat.id };
|
|
3569
|
+
const payment = await call("GET", `/v1/payment_intents/${encodeURIComponent(seat.providerPaymentIntentId)}`);
|
|
3570
|
+
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();
|
|
3571
|
+
const sub = await call("GET", `/v1/subscriptions/${encodeURIComponent(seat.providerSubscriptionId)}`);
|
|
3572
|
+
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();
|
|
3573
|
+
const items = sub.items.data.filter((item) => item.metadata?.seatId === seat.id);
|
|
3574
|
+
if (items.length > 1) fail3();
|
|
3575
|
+
if (items.length && sub.status !== "canceled") {
|
|
3576
|
+
const item = items[0];
|
|
3577
|
+
if (!/^si_[A-Za-z0-9_]+$/.test(item.id) || item.price?.id !== seat.priceId || item.quantity !== 1) fail3();
|
|
3578
|
+
const removed = await call("DELETE", `/v1/subscription_items/${encodeURIComponent(item.id)}`, { proration_behavior: "none" });
|
|
3579
|
+
if (removed.id !== item.id || removed.deleted !== true) fail3("named_seat_renewal_removal_unconfirmed");
|
|
3580
|
+
}
|
|
3581
|
+
const list = await call("GET", "/v1/refunds", { payment_intent: seat.providerPaymentIntentId, limit: 100 });
|
|
3582
|
+
if (!Array.isArray(list.data) || list.has_more !== false || list.data.length >= 100) fail3("named_seat_refund_history_incomplete");
|
|
3583
|
+
let refundedCents = 0, pendingCents = 0;
|
|
3584
|
+
const refundIds = [];
|
|
3585
|
+
for (const refund of list.data) {
|
|
3586
|
+
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();
|
|
3587
|
+
if (refund.status === "succeeded") {
|
|
3588
|
+
refundedCents += refund.amount;
|
|
3589
|
+
refundIds.push(refund.id);
|
|
3590
|
+
} else if (refund.status === "pending" || refund.status === "requires_action") pendingCents += refund.amount;
|
|
3591
|
+
}
|
|
3592
|
+
if (refundedCents + pendingCents > seat.amountCents) fail3();
|
|
3593
|
+
if (pendingCents) fail3("named_seat_refund_pending");
|
|
3594
|
+
if (refundedCents < seat.amountCents) {
|
|
3595
|
+
const remaining = seat.amountCents - refundedCents;
|
|
3596
|
+
const refund = await call("POST", "/v1/refunds", {
|
|
3597
|
+
payment_intent: seat.providerPaymentIntentId,
|
|
3598
|
+
amount: remaining,
|
|
3599
|
+
reason: "requested_by_customer",
|
|
3600
|
+
metadata
|
|
3601
|
+
}, `named-seat-refund:${seat.id}:${remaining}`);
|
|
3602
|
+
if (refund.payment_intent !== seat.providerPaymentIntentId || refund.amount !== remaining || refund.currency !== seat.currency || !/^re_[A-Za-z0-9_]+$/.test(refund.id)) fail3();
|
|
3603
|
+
if (refund.status !== "succeeded") fail3("named_seat_refund_pending");
|
|
3604
|
+
refundedCents += remaining;
|
|
3605
|
+
refundIds.push(refund.id);
|
|
3606
|
+
}
|
|
3607
|
+
return { refundedCents, refundIds };
|
|
3608
|
+
}
|
|
3609
|
+
|
|
3610
|
+
// src/membership-seat-refund.ts
|
|
3611
|
+
async function requestNamedSeatRefund(db, input, now) {
|
|
3612
|
+
membershipClock(now);
|
|
3613
|
+
const state = await loadMembershipAuthorityState(db, input);
|
|
3614
|
+
const seat = await membershipLoad(db, "networkSeat", input.seatId);
|
|
3615
|
+
requireMembershipScope(seat, input);
|
|
3616
|
+
const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
|
|
3617
|
+
requireMembershipScope(member, input);
|
|
3618
|
+
if (seat.status === "refunded" || seat.status === "refund_pending") return;
|
|
3619
|
+
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");
|
|
3620
|
+
const entitlement = seat.entitlementId ? await membershipLoad(db, "networkEntitlement", String(seat.entitlementId)) : void 0;
|
|
3621
|
+
await db.transact([
|
|
3622
|
+
membershipUpdate("networkSeat", input.seatId, { status: "refund_pending", refundReason: input.reason, revision: Number(seat.revision) + 1, updatedAt: now }),
|
|
3623
|
+
...entitlement ? [membershipUpdate("networkEntitlement", String(entitlement.id), { status: "closed", revision: Number(entitlement.revision) + 1, updatedAt: now })] : [],
|
|
3624
|
+
membershipUpdate("membershipEffect", `${seat.id}:refund`, {
|
|
3625
|
+
id: `${seat.id}:refund`,
|
|
3626
|
+
chapterId: input.chapterId,
|
|
3627
|
+
environment: input.environment,
|
|
3628
|
+
runtime: input.runtime,
|
|
3629
|
+
membershipId: member.id,
|
|
3630
|
+
seatId: seat.id,
|
|
3631
|
+
kind: "refund_seat",
|
|
3632
|
+
status: "pending",
|
|
3633
|
+
attempts: 0,
|
|
3634
|
+
createdAt: now,
|
|
3635
|
+
updatedAt: now
|
|
3636
|
+
}),
|
|
3637
|
+
...seat.recipientUserId ? [membershipUpdate("membershipEffect", `${seat.id}:revoke`, {
|
|
3638
|
+
id: `${seat.id}:revoke`,
|
|
3639
|
+
chapterId: input.chapterId,
|
|
3640
|
+
environment: input.environment,
|
|
3641
|
+
runtime: input.runtime,
|
|
3642
|
+
membershipId: member.id,
|
|
3643
|
+
seatId: seat.id,
|
|
3644
|
+
kind: "revoke_named_recipient_access",
|
|
3645
|
+
status: "pending",
|
|
3646
|
+
attempts: 0,
|
|
3647
|
+
createdAt: now,
|
|
3648
|
+
updatedAt: now
|
|
3649
|
+
})] : []
|
|
3650
|
+
], { mutationId: `named-seat-refund-request:${seat.id}`, guards: [
|
|
3651
|
+
{ ns: "networkSeat", id: input.seatId, exists: true, equals: { revision: seat.revision } },
|
|
3652
|
+
{ ns: "networkMembership", id: String(member.id), exists: true, equals: { revision: member.revision } },
|
|
3653
|
+
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
|
|
3654
|
+
...entitlement ? [{ ns: "networkEntitlement", id: String(entitlement.id), exists: true, equals: { revision: entitlement.revision } }] : []
|
|
3655
|
+
] });
|
|
3656
|
+
}
|
|
3657
|
+
async function completeNamedSeatRefund(db, seatId, secretKey, now) {
|
|
3658
|
+
membershipClock(now);
|
|
3659
|
+
const seat = await membershipLoad(db, "networkSeat", seatId);
|
|
3660
|
+
if (!seat) return membershipFail("named_seat_not_found");
|
|
3661
|
+
await loadMembershipAuthorityState(db, seat);
|
|
3662
|
+
if (seat.status === "refunded") return;
|
|
3663
|
+
if (seat.status !== "refund_pending") membershipFail("named_seat_refund_not_ready");
|
|
3664
|
+
const result = await refundNamedSeatStripePayment(secretKey, seat);
|
|
3665
|
+
await db.transact([
|
|
3666
|
+
membershipUpdate("networkSeat", seatId, {
|
|
3667
|
+
status: "refunded",
|
|
3668
|
+
refundedCents: result.refundedCents,
|
|
3669
|
+
providerRefundIds: result.refundIds,
|
|
3670
|
+
revision: Number(seat.revision) + 1,
|
|
3671
|
+
updatedAt: now
|
|
3672
|
+
}),
|
|
3673
|
+
membershipUpdate("membershipEffect", `${seatId}:refund`, { status: "complete", updatedAt: now })
|
|
3674
|
+
], { mutationId: `named-seat-refunded:${seatId}`, guards: [
|
|
3675
|
+
{ ns: "networkSeat", id: seatId, exists: true, equals: { status: "refund_pending", revision: seat.revision } }
|
|
3676
|
+
] });
|
|
3677
|
+
}
|
|
3678
|
+
|
|
3679
|
+
// src/membership-seat-operator-refund.ts
|
|
3680
|
+
async function refundNamedSeatByOperator(db, input, stripeSecretKey, now) {
|
|
3681
|
+
const seat = await membershipLoad(db, "networkSeat", input.seatId);
|
|
3682
|
+
requireMembershipScope(seat, input);
|
|
3683
|
+
await requestNamedSeatRefund(db, { ...input, reason: "operator" }, now);
|
|
3684
|
+
await completeNamedSeatRefund(db, input.seatId, stripeSecretKey, now);
|
|
3685
|
+
const refunded = await membershipLoad(db, "networkSeat", input.seatId);
|
|
3686
|
+
if (refunded?.status !== "refunded" || !Number.isSafeInteger(refunded.refundedCents) || Number(refunded.refundedCents) <= 0) {
|
|
3687
|
+
throw new Error("named seat refund unconfirmed");
|
|
3688
|
+
}
|
|
3689
|
+
return { seatId: String(seat.id), status: "refunded", refundedCents: Number(refunded.refundedCents), subscriptionCanceled: false };
|
|
3690
|
+
}
|
|
3691
|
+
|
|
3692
|
+
// src/membership-seat-window.ts
|
|
3693
|
+
async function liftSeatPurchaseWindow(db, scope2, now, limit = 100) {
|
|
3694
|
+
membershipClock(now);
|
|
3695
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200) throw new TypeError("invalid lift limit");
|
|
3696
|
+
const state = await loadMembershipAuthorityState(db, scope2);
|
|
3697
|
+
const rows5 = (await db.query({ networkMembership: { $: { where: { ...scope2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkMembership ?? [];
|
|
3698
|
+
let lifted = 0;
|
|
3699
|
+
for (const member of rows5.slice(0, limit)) {
|
|
3700
|
+
if (Object.entries(scope2).some(([key3, value]) => member[key3] !== value)) throw new Error("membership lift scope mismatch");
|
|
3701
|
+
const offering = membershipOfferingVersion(member.offeringSnapshot);
|
|
3702
|
+
if (offering.seatPurchaseWindowCalendarMonths === void 0) continue;
|
|
3703
|
+
const { seatPurchaseWindowCalendarMonths: _window, ...windowless } = offering;
|
|
3704
|
+
await db.transact([membershipUpdate("networkMembership", String(member.id), {
|
|
3705
|
+
offeringSnapshot: windowless,
|
|
3706
|
+
seatPurchaseEndsAt: Number(member.termEndsAt),
|
|
3707
|
+
revision: Number(member.revision) + 1,
|
|
3708
|
+
updatedAt: now
|
|
3709
|
+
})], { mutationId: `seat-window-lift:${member.id}:${member.revision}`, guards: [
|
|
3710
|
+
{ ns: "networkMembership", id: String(member.id), exists: true, equals: { revision: member.revision } },
|
|
3711
|
+
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative" } }
|
|
3712
|
+
] });
|
|
3713
|
+
lifted++;
|
|
3714
|
+
}
|
|
3715
|
+
return { lifted, more: rows5.length > limit };
|
|
3716
|
+
}
|
|
3717
|
+
|
|
3718
|
+
// src/worker-routes-admin-membership-seats.ts
|
|
3719
|
+
var LIST = "/api/admin/membership-seats";
|
|
3720
|
+
var LIFT = "/api/admin/membership-seats/lift-purchase-window";
|
|
3721
|
+
var ONE = /^\/api\/admin\/membership-seats\/([A-Za-z0-9][A-Za-z0-9_.:-]{2,159})\/refund$/;
|
|
3722
|
+
var SCOPE = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
3723
|
+
var PUBLIC = [
|
|
3724
|
+
"id",
|
|
3725
|
+
"appId",
|
|
3726
|
+
"chapterId",
|
|
3727
|
+
"environment",
|
|
3728
|
+
"runtime",
|
|
3729
|
+
"applicationId",
|
|
3730
|
+
"skuId",
|
|
3731
|
+
"status",
|
|
3732
|
+
"amountCents",
|
|
3733
|
+
"currency",
|
|
3734
|
+
"recipientName",
|
|
3735
|
+
"recipientEmailNormalized",
|
|
3736
|
+
"purchaserUserId",
|
|
3737
|
+
"purchasedAt",
|
|
3738
|
+
"acceptedAt",
|
|
3739
|
+
"journeyApprovedAt",
|
|
3740
|
+
"refundedCents",
|
|
3741
|
+
"refundReason",
|
|
3742
|
+
"termEndsAt",
|
|
3743
|
+
"updatedAt"
|
|
3744
|
+
];
|
|
3745
|
+
var project = (row2) => Object.fromEntries(PUBLIC.filter((key3) => row2[key3] !== void 0).map((key3) => [key3, row2[key3]]));
|
|
3746
|
+
var handleAdminMembershipSeats = async (req, url, env, ctx) => {
|
|
3747
|
+
const one = url.pathname.match(ONE), lift = url.pathname === LIFT;
|
|
3748
|
+
if (url.pathname !== LIST && !one && !lift) return null;
|
|
3749
|
+
const operator = ctx.membershipAuthorityOperator;
|
|
3750
|
+
if (!operator) return json({ error: "not found" }, 404);
|
|
3751
|
+
if (req.method !== (one || lift ? "POST" : "GET")) return json({ error: "method not allowed" }, 405);
|
|
3752
|
+
const raw2 = ctx.makeDb(env), user = await ctx.verifyUser(req, env);
|
|
3753
|
+
if (!user) return json({ error: "unauthorized" }, 401);
|
|
3754
|
+
if (!await ctx.isSuperAdminEmail(raw2, user.email, user.userId)) return json({ error: "super-admin required", code: "super_admin_required" }, 403);
|
|
3755
|
+
let db;
|
|
3756
|
+
try {
|
|
3757
|
+
db = operator.makeDb(env);
|
|
3758
|
+
} catch {
|
|
3759
|
+
return json({ error: "the membership authority is not hosted by this deployment" }, 404);
|
|
3760
|
+
}
|
|
3761
|
+
if (lift) {
|
|
3762
|
+
let body;
|
|
3763
|
+
try {
|
|
3764
|
+
body = await req.json();
|
|
3765
|
+
} catch {
|
|
3766
|
+
return json({ error: "chapterId and runtime are required" }, 400);
|
|
3767
|
+
}
|
|
3768
|
+
const appId = body.appId ?? body.chapterId;
|
|
3769
|
+
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)) {
|
|
3770
|
+
return json({ error: "chapterId and runtime are required" }, 400);
|
|
3771
|
+
}
|
|
3772
|
+
try {
|
|
3773
|
+
return json({ ok: true, ...await liftSeatPurchaseWindow(db, { appId, chapterId: body.chapterId, environment: env.ODLA_ENV, runtime: body.runtime }, Date.now()) });
|
|
3774
|
+
} catch (error) {
|
|
3775
|
+
const code = error.code;
|
|
3776
|
+
return json({ error: "purchase window lift is pending; retry to confirm", ...code ? { code } : {} }, code ? 409 : 503);
|
|
3777
|
+
}
|
|
3778
|
+
}
|
|
3779
|
+
if (!one) {
|
|
3780
|
+
const rows5 = (await db.query({ networkSeat: { $: { where: { environment: env.ODLA_ENV }, order: { updatedAt: "desc" }, limit: 201 } } })).networkSeat ?? [];
|
|
3781
|
+
return json({ seats: rows5.slice(0, 200).map(project), truncated: rows5.length > 200 });
|
|
3782
|
+
}
|
|
3783
|
+
const seat = await membershipLoad(db, "networkSeat", one[1]);
|
|
3784
|
+
if (!seat || seat.environment !== env.ODLA_ENV) return json({ error: "not found" }, 404);
|
|
3785
|
+
if (seat.status === "refunded") {
|
|
3786
|
+
return json({ ok: true, seatId: seat.id, status: "refunded", refundedCents: seat.refundedCents, subscriptionCanceled: false, replayed: true });
|
|
3787
|
+
}
|
|
3788
|
+
if (!seat.purchasedAt) return json({ error: "seat has no completed payment to refund", code: "seat_not_refundable" }, 409);
|
|
3789
|
+
const secret = await getVaultSecret(db, operator.stripeSecretName ?? "stripe_secret_key");
|
|
3790
|
+
if (!secret) return json({ error: "network payment provider unavailable" }, 503);
|
|
3791
|
+
try {
|
|
3792
|
+
const receipt = await refundNamedSeatByOperator(db, {
|
|
3793
|
+
appId: String(seat.appId),
|
|
3794
|
+
chapterId: String(seat.chapterId),
|
|
3795
|
+
environment: String(seat.environment),
|
|
3796
|
+
runtime: String(seat.runtime),
|
|
3797
|
+
applicationId: String(seat.applicationId),
|
|
3798
|
+
skuId: String(seat.skuId),
|
|
3799
|
+
seatId: String(seat.id)
|
|
3800
|
+
}, secret, Date.now());
|
|
3801
|
+
return json({ ok: true, ...receipt });
|
|
3802
|
+
} catch (error) {
|
|
3803
|
+
const code = error.code;
|
|
3804
|
+
return json({ error: "seat refund is pending; retry to confirm", ...code ? { code } : {} }, code ? 409 : 503);
|
|
3805
|
+
}
|
|
3806
|
+
};
|
|
3807
|
+
|
|
3454
3808
|
// src/worker-routes-membership-cancel.ts
|
|
3455
3809
|
var handleMembershipCancellation = async (req, url, env, ctx) => {
|
|
3456
3810
|
const match = url.pathname.match(/^\/api\/admin\/applications\/([0-9a-f-]+)\/cancel-renewal$/);
|
|
@@ -4047,6 +4401,29 @@ async function authorizeCrmDiscussionReference(req, env, fetcher = fetch) {
|
|
|
4047
4401
|
}
|
|
4048
4402
|
}
|
|
4049
4403
|
|
|
4404
|
+
// src/join-founding-preview.ts
|
|
4405
|
+
async function foundingPreviews(tiers, env, ctx) {
|
|
4406
|
+
const result = /* @__PURE__ */ new Map();
|
|
4407
|
+
const preview = ctx.membershipAuthority?.previewOffer;
|
|
4408
|
+
if (!preview) return result;
|
|
4409
|
+
const scope2 = { appId: env.ODLA_APP_ID, chapterId: ctx.chapter.id, environment: env.ODLA_ENV, runtime: stripeRuntimeIdentity(env.ODLA_RUNTIME) };
|
|
4410
|
+
await Promise.all(tiers.filter((tier) => !tierIsFree(tier) && tier.stripePriceId).map(async (tier) => {
|
|
4411
|
+
try {
|
|
4412
|
+
const offer = await preview({ ...scope2, skuId: tier.id }, env);
|
|
4413
|
+
if (!offer.founding || !offer.cohortLabel || offer.basePriceCents !== tier.priceCents || offer.dueTodayCents >= offer.basePriceCents) return;
|
|
4414
|
+
result.set(tier.id, {
|
|
4415
|
+
cohortLabel: offer.cohortLabel,
|
|
4416
|
+
dueTodayCents: offer.dueTodayCents,
|
|
4417
|
+
discountCents: offer.basePriceCents - offer.dueTodayCents,
|
|
4418
|
+
renewalCents: offer.renewalCents,
|
|
4419
|
+
...offer.discountDuration ? { discountDuration: offer.discountDuration } : {}
|
|
4420
|
+
});
|
|
4421
|
+
} catch {
|
|
4422
|
+
}
|
|
4423
|
+
}));
|
|
4424
|
+
return result;
|
|
4425
|
+
}
|
|
4426
|
+
|
|
4050
4427
|
// src/admission-grants-contract.ts
|
|
4051
4428
|
var ADMISSION_GRANTS_SCHEMA_VERSION = 1;
|
|
4052
4429
|
var ADMISSION_GRANTS_PATH = "/api/admission-grants";
|
|
@@ -4573,10 +4950,24 @@ async function materializeSignupControl(db, envelope, previous, receivedAt) {
|
|
|
4573
4950
|
}
|
|
4574
4951
|
|
|
4575
4952
|
// src/payment-quote.ts
|
|
4953
|
+
function tierPresentationFor(document, tierId) {
|
|
4954
|
+
const compareAt = document?.membershipPage?.tiers?.find((tier) => tier.tierId === tierId)?.compareAtPriceCents;
|
|
4955
|
+
return Number.isSafeInteger(compareAt) && Number(compareAt) > 0 ? { compareAtPriceCents: Number(compareAt) } : void 0;
|
|
4956
|
+
}
|
|
4957
|
+
function foundingOf(offer) {
|
|
4958
|
+
return offer?.contract?.founding === true && offer.contract.discountPolicy ? { cohortLabel: offer.contract.discountPolicy.cohortLabel } : void 0;
|
|
4959
|
+
}
|
|
4576
4960
|
async function createPaymentQuote(input) {
|
|
4577
4961
|
const { group, tier, policy } = input;
|
|
4578
4962
|
const terms2 = input.billingTerms ? subscriptionBillingTerms(input.billingTerms) : void 0;
|
|
4963
|
+
const compareAt = input.presentation?.compareAtPriceCents;
|
|
4964
|
+
const presentation = Number.isSafeInteger(compareAt) && Number(compareAt) > tier.priceCents ? { compareAtPriceCents: Number(compareAt) } : void 0;
|
|
4579
4965
|
const dueToday = terms2 ? subscriptionAmountDue({ priceCents: tier.priceCents, billingTerms: terms2 }) : void 0;
|
|
4966
|
+
const lines = dueToday === void 0 ? paymentLineItems(group, tier) : {
|
|
4967
|
+
standardCents: tier.priceCents,
|
|
4968
|
+
discountCents: tier.priceCents - dueToday,
|
|
4969
|
+
dueTodayCents: dueToday
|
|
4970
|
+
};
|
|
4580
4971
|
const quote = {
|
|
4581
4972
|
applicationId: input.applicationId,
|
|
4582
4973
|
chapterId: input.chapterId,
|
|
@@ -4584,11 +4975,12 @@ async function createPaymentQuote(input) {
|
|
|
4584
4975
|
tierName: tier.name,
|
|
4585
4976
|
currency: tier.currency ?? String(group.currency ?? "usd"),
|
|
4586
4977
|
interval: tier.interval ?? (group.interval === "month" ? "month" : "year"),
|
|
4587
|
-
lineItems:
|
|
4588
|
-
standardCents:
|
|
4589
|
-
discountCents:
|
|
4590
|
-
dueTodayCents:
|
|
4591
|
-
},
|
|
4978
|
+
lineItems: presentation ? {
|
|
4979
|
+
standardCents: presentation.compareAtPriceCents,
|
|
4980
|
+
discountCents: presentation.compareAtPriceCents - lines.dueTodayCents,
|
|
4981
|
+
dueTodayCents: lines.dueTodayCents
|
|
4982
|
+
} : lines,
|
|
4983
|
+
...presentation ? { presentation } : {},
|
|
4592
4984
|
refundPolicyText: policy?.refundPolicyText ?? String(group.refundPolicyText ?? ""),
|
|
4593
4985
|
merchantDisclosureText: String(group.merchantDisclosureText ?? ""),
|
|
4594
4986
|
trustCopy: policy ? policy.trustCopy ?? "" : String(group.trustCopy ?? ""),
|
|
@@ -4613,37 +5005,39 @@ async function createPaymentQuote(input) {
|
|
|
4613
5005
|
});
|
|
4614
5006
|
const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(contract));
|
|
4615
5007
|
const digest = [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
4616
|
-
return { ...quote, digest };
|
|
5008
|
+
return { ...quote, digest, ...input.founding ? { founding: input.founding } : {} };
|
|
4617
5009
|
}
|
|
4618
5010
|
|
|
4619
5011
|
// src/membership-quote-projection.ts
|
|
4620
5012
|
var MEMBERSHIP_QUOTE_NAMESPACE = "membershipQuoteProjections";
|
|
4621
5013
|
var keys2 = ["appId", "chapterId", "environment", "runtime", "applicationId", "skuId"];
|
|
4622
|
-
var
|
|
5014
|
+
var fail4 = (code) => {
|
|
4623
5015
|
throw Object.assign(new Error(code), { code });
|
|
4624
5016
|
};
|
|
4625
5017
|
var load3 = async (db, ns, id6) => (await db.query({ [ns]: { $: { where: { id: id6 }, limit: 1 } } }))[ns]?.[0];
|
|
4626
5018
|
async function restore2(row2, target, now, allowExpired = false) {
|
|
4627
|
-
if (!row2 || keys2.some((key3) => row2[key3] !== target[key3]) || !Number.isSafeInteger(row2.revision) || Number(row2.revision) < 1 || typeof row2.requestId !== "string") return
|
|
4628
|
-
if (row2.status !== "ready") return
|
|
5019
|
+
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");
|
|
5020
|
+
if (row2.status !== "ready") return fail4("membership_quote_pending");
|
|
4629
5021
|
const offer = await verifyIssuedMembershipOffer(row2.offer, { ...target, offerId: await membershipOfferIdForRequest({
|
|
4630
5022
|
...target,
|
|
4631
5023
|
requestId: row2.requestId
|
|
4632
5024
|
}), now, lifetimeMs: 6e4 });
|
|
4633
|
-
if (offer.issuedAt > now + 3e4 || !allowExpired && offer.expiresAt <= now) return
|
|
5025
|
+
if (offer.issuedAt > now + 3e4 || !allowExpired && offer.expiresAt <= now) return fail4("membership_quote_expired");
|
|
4634
5026
|
const tier = row2.tier, quote = row2.quote;
|
|
4635
5027
|
const offering = offer.contract.offering;
|
|
4636
5028
|
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) {
|
|
4637
|
-
return
|
|
5029
|
+
return fail4("membership_catalog_mismatch");
|
|
4638
5030
|
}
|
|
4639
5031
|
const expected = await createPaymentQuote({
|
|
4640
5032
|
...target,
|
|
4641
5033
|
tier,
|
|
4642
5034
|
group: { merchantDisclosureText: quote.merchantDisclosureText },
|
|
4643
5035
|
policy: { refundPolicyText: quote.refundPolicyText, trustCopy: quote.trustCopy },
|
|
4644
|
-
billingTerms: offer.contract.billingTerms
|
|
5036
|
+
billingTerms: offer.contract.billingTerms,
|
|
5037
|
+
presentation: quote.presentation,
|
|
5038
|
+
founding: foundingOf(offer)
|
|
4645
5039
|
});
|
|
4646
|
-
if (quote.digest !== expected.digest) return
|
|
5040
|
+
if (quote.digest !== expected.digest) return fail4("membership_quote_conflict");
|
|
4647
5041
|
return { tier, quote: expected, billingTerms: offer.contract.billingTerms, membershipOffer: offer, projectionRevision: Number(row2.revision) };
|
|
4648
5042
|
}
|
|
4649
5043
|
async function readMembershipQuote(db, target, now) {
|
|
@@ -4651,9 +5045,9 @@ async function readMembershipQuote(db, target, now) {
|
|
|
4651
5045
|
}
|
|
4652
5046
|
async function prepareMembershipQuote(db, input, issue) {
|
|
4653
5047
|
const { applicationId: id6, now, app } = input;
|
|
4654
|
-
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
|
|
5048
|
+
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");
|
|
4655
5049
|
let row2 = await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, id6);
|
|
4656
|
-
if (row2 && keys2.some((key3) => row2[key3] !== input[key3])) return
|
|
5050
|
+
if (row2 && keys2.some((key3) => row2[key3] !== input[key3])) return fail4("membership_quote_conflict");
|
|
4657
5051
|
if (row2?.status === "ready") {
|
|
4658
5052
|
try {
|
|
4659
5053
|
return await restore2(row2, input, now);
|
|
@@ -4698,7 +5092,7 @@ async function prepareMembershipQuote(db, input, issue) {
|
|
|
4698
5092
|
}
|
|
4699
5093
|
if (row2?.status === "ready") return restore2(row2, input, now);
|
|
4700
5094
|
if (!row2 || row2.status !== "pending" || keys2.some((key3) => row2[key3] !== input[key3]) || typeof row2.requestId !== "string") {
|
|
4701
|
-
return
|
|
5095
|
+
return fail4("membership_quote_pending");
|
|
4702
5096
|
}
|
|
4703
5097
|
const target = Object.fromEntries(keys2.map((key3) => [key3, input[key3]]));
|
|
4704
5098
|
const offer = await verifyIssuedMembershipOffer(await issue({ ...target, requestId: row2.requestId }), {
|
|
@@ -4712,7 +5106,15 @@ async function prepareMembershipQuote(db, input, issue) {
|
|
|
4712
5106
|
currency: input.tier.currency ?? String(input.group.currency ?? "usd"),
|
|
4713
5107
|
interval: input.tier.interval ?? (input.group.interval === "month" ? "month" : "year")
|
|
4714
5108
|
};
|
|
4715
|
-
const quote = await createPaymentQuote({
|
|
5109
|
+
const quote = await createPaymentQuote({
|
|
5110
|
+
...target,
|
|
5111
|
+
tier,
|
|
5112
|
+
group: input.group,
|
|
5113
|
+
policy: input.policy,
|
|
5114
|
+
billingTerms: offer.contract.billingTerms,
|
|
5115
|
+
presentation: input.presentation,
|
|
5116
|
+
founding: foundingOf(offer)
|
|
5117
|
+
});
|
|
4716
5118
|
const ready = { ...row2, offer, tier, quote, status: "ready", updatedAt: now };
|
|
4717
5119
|
await restore2(ready, target, now, true);
|
|
4718
5120
|
try {
|
|
@@ -4767,7 +5169,9 @@ async function checkoutQuoteForApplication(db, app, group, env, ctx, prepare = f
|
|
|
4767
5169
|
group: { merchantDisclosureText: quote2.merchantDisclosureText },
|
|
4768
5170
|
tier: tier2,
|
|
4769
5171
|
policy: { refundPolicyText: quote2.refundPolicyText, trustCopy: quote2.trustCopy },
|
|
4770
|
-
billingTerms
|
|
5172
|
+
billingTerms,
|
|
5173
|
+
presentation: quote2.presentation,
|
|
5174
|
+
founding: foundingOf(intent.membershipOffer)
|
|
4771
5175
|
});
|
|
4772
5176
|
if (expected.digest !== quote2.digest) throw new Error("invalid commercial snapshot");
|
|
4773
5177
|
return { tier: tier2, quote: expected, billingTerms, membershipOffer: intent.membershipOffer };
|
|
@@ -4794,9 +5198,10 @@ async function checkoutQuoteForApplication(db, app, group, env, ctx, prepare = f
|
|
|
4794
5198
|
if (!tier || tierIsFree(tier) || !tier.stripePriceId) throw Object.assign(new Error("tier unavailable"), { code: "tier_unresolved" });
|
|
4795
5199
|
const control3 = await loadActiveSignupControl(db, ctx.chapter.id, env.ODLA_ENV);
|
|
4796
5200
|
const policy = control3?.document.policy?.tierPolicies?.[tier.id];
|
|
5201
|
+
const presentation = tierPresentationFor(control3?.document, tier.id);
|
|
4797
5202
|
if (ctx.membershipAuthority) return prepareMembershipQuote(
|
|
4798
5203
|
db,
|
|
4799
|
-
{ ...target, app, group, tier, policy, now: Date.now() },
|
|
5204
|
+
{ ...target, app, group, tier, policy, presentation, now: Date.now() },
|
|
4800
5205
|
(input) => ctx.membershipAuthority.issueOffer(input, env)
|
|
4801
5206
|
);
|
|
4802
5207
|
const quote = await createPaymentQuote({
|
|
@@ -4807,7 +5212,8 @@ async function checkoutQuoteForApplication(db, app, group, env, ctx, prepare = f
|
|
|
4807
5212
|
runtime: stripeRuntimeIdentity(env.ODLA_RUNTIME),
|
|
4808
5213
|
group,
|
|
4809
5214
|
tier,
|
|
4810
|
-
policy
|
|
5215
|
+
policy,
|
|
5216
|
+
presentation
|
|
4811
5217
|
});
|
|
4812
5218
|
return { tier, quote, billingTerms: void 0 };
|
|
4813
5219
|
}
|
|
@@ -5461,10 +5867,11 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
5461
5867
|
group,
|
|
5462
5868
|
Boolean(stripeKey)
|
|
5463
5869
|
);
|
|
5870
|
+
const founding = await foundingPreviews(offered, env, ctx);
|
|
5464
5871
|
const activeControl = chapter.signupControl ? await loadActiveSignupControl(db, chapter.id, env.ODLA_ENV) : null;
|
|
5465
5872
|
const application = controlledApplication(chapter.application, activeControl?.document);
|
|
5466
5873
|
return json({
|
|
5467
|
-
...joinConfig(group, paymentsReady, joinConfigTiers(offered, { currency: typeof group.currency === "string" ? group.currency : void 0, interval: group.interval === "month" ? "month" : "year" }), application.conditions),
|
|
5874
|
+
...joinConfig(group, paymentsReady, joinConfigTiers(offered, { currency: typeof group.currency === "string" ? group.currency : void 0, interval: group.interval === "month" ? "month" : "year" }, founding), application.conditions),
|
|
5468
5875
|
fieldContract: chapterFlowFieldContract(chapter, "join", application),
|
|
5469
5876
|
copy: { ...chapter.copy.join, ...activeControl?.document.application?.copy ?? {} },
|
|
5470
5877
|
...activeControl ? { signupControl: {
|
|
@@ -7157,7 +7564,7 @@ async function deliverAdmissionGrants(options) {
|
|
|
7157
7564
|
}
|
|
7158
7565
|
|
|
7159
7566
|
// src/worker-routes-network-admission.ts
|
|
7160
|
-
var
|
|
7567
|
+
var PATH2 = "/api/admin/network/admissions";
|
|
7161
7568
|
var RETRY_PATH = "/api/admin/network/admissions/retry";
|
|
7162
7569
|
var REVOKE_PATH = "/api/admin/network/admissions/revoke";
|
|
7163
7570
|
var MUTATION_ID2 = /^[A-Za-z0-9_-]{16,128}$/;
|
|
@@ -7237,11 +7644,11 @@ async function parseBody(req) {
|
|
|
7237
7644
|
}
|
|
7238
7645
|
}
|
|
7239
7646
|
var handleAdminNetworkAdmissions = async (req, url, env, ctx) => {
|
|
7240
|
-
if (![
|
|
7647
|
+
if (![PATH2, RETRY_PATH, REVOKE_PATH].includes(url.pathname)) return null;
|
|
7241
7648
|
const auth = await gate2(req, env, ctx);
|
|
7242
7649
|
if ("response" in auth) return auth.response;
|
|
7243
7650
|
const db = auth.db;
|
|
7244
|
-
if (url.pathname ===
|
|
7651
|
+
if (url.pathname === PATH2 && req.method === "GET") {
|
|
7245
7652
|
const targetId2 = url.searchParams.get("targetId") ?? "";
|
|
7246
7653
|
const target2 = targetFor(ctx, targetId2);
|
|
7247
7654
|
if (!target2) return reject6(404, "unknown_target", "admission target is not configured");
|
|
@@ -7485,28 +7892,28 @@ var handleSchedule = async (req, url, env, ctx) => {
|
|
|
7485
7892
|
};
|
|
7486
7893
|
|
|
7487
7894
|
// src/payments-stripe.ts
|
|
7488
|
-
var
|
|
7895
|
+
var import_stripe10 = require("@odla-ai/stripe");
|
|
7489
7896
|
|
|
7490
7897
|
// src/payments-refund.ts
|
|
7491
|
-
var
|
|
7898
|
+
var import_stripe7 = require("@odla-ai/stripe");
|
|
7492
7899
|
var row = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
7493
7900
|
var id2 = (value) => typeof value === "string" ? value : String(row(value).id ?? "");
|
|
7494
7901
|
var providerId2 = (value, prefix) => new RegExp(`^${prefix}_[A-Za-z0-9]+$`).test(value);
|
|
7495
|
-
function
|
|
7902
|
+
function fail5(code = "refund_target_mismatch") {
|
|
7496
7903
|
throw Object.assign(new Error(code), { code });
|
|
7497
7904
|
}
|
|
7498
7905
|
async function refundSubscriptionPayment(secretKey, input) {
|
|
7499
|
-
if (!providerId2(input.customerId, "cus") || !providerId2(input.subscriptionId, "sub"))
|
|
7906
|
+
if (!providerId2(input.customerId, "cus") || !providerId2(input.subscriptionId, "sub")) fail5();
|
|
7500
7907
|
const call = async (method, path, params, key3) => {
|
|
7501
|
-
const result = await (0,
|
|
7502
|
-
if (!result.ok)
|
|
7503
|
-
if (!result.body || typeof result.body !== "object" || Array.isArray(result.body))
|
|
7908
|
+
const result = await (0, import_stripe7.stripeRequest)({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
|
|
7909
|
+
if (!result.ok) fail5("stripe_error");
|
|
7910
|
+
if (!result.body || typeof result.body !== "object" || Array.isArray(result.body)) fail5("stripe_invalid_response");
|
|
7504
7911
|
return result.body;
|
|
7505
7912
|
};
|
|
7506
7913
|
const sub = await call("GET", `/v1/subscriptions/${input.subscriptionId}`);
|
|
7507
|
-
if (id2(sub) !== input.subscriptionId || id2(sub.customer) !== input.customerId)
|
|
7914
|
+
if (id2(sub) !== input.subscriptionId || id2(sub.customer) !== input.customerId) fail5();
|
|
7508
7915
|
for (const [key3, value] of Object.entries(input.metadata ?? {})) {
|
|
7509
|
-
if (row(sub.metadata)[key3] !== value)
|
|
7916
|
+
if (row(sub.metadata)[key3] !== value) fail5();
|
|
7510
7917
|
}
|
|
7511
7918
|
let cursor;
|
|
7512
7919
|
let earliest;
|
|
@@ -7519,69 +7926,69 @@ async function refundSubscriptionPayment(secretKey, input) {
|
|
|
7519
7926
|
limit: 100,
|
|
7520
7927
|
...cursor ? { starting_after: cursor } : {}
|
|
7521
7928
|
});
|
|
7522
|
-
if (!Array.isArray(list.data) || typeof list.has_more !== "boolean")
|
|
7929
|
+
if (!Array.isArray(list.data) || typeof list.has_more !== "boolean") fail5("stripe_invalid_response");
|
|
7523
7930
|
for (const value of list.data) {
|
|
7524
7931
|
const invoice2 = row(value);
|
|
7525
7932
|
const subscription = invoice2.subscription ?? row(row(invoice2.parent).subscription_details).subscription;
|
|
7526
|
-
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)
|
|
7933
|
+
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();
|
|
7527
7934
|
seen.add(id2(invoice2));
|
|
7528
7935
|
if (Number(invoice2.amount_paid) > 0 && (!earliest || Number(invoice2.created) < Number(earliest.created))) earliest = invoice2;
|
|
7529
7936
|
}
|
|
7530
7937
|
if (!list.has_more) break;
|
|
7531
|
-
if (!list.data.length || page === 9)
|
|
7938
|
+
if (!list.data.length || page === 9) fail5("refund_history_incomplete");
|
|
7532
7939
|
cursor = id2(list.data[list.data.length - 1]);
|
|
7533
7940
|
}
|
|
7534
|
-
if (!earliest)
|
|
7941
|
+
if (!earliest) fail5("no_charge");
|
|
7535
7942
|
const invoice = earliest;
|
|
7536
|
-
if (input.expectedInvoiceId !== void 0 && id2(invoice) !== input.expectedInvoiceId || input.expectedAmountCents !== void 0 && invoice.amount_paid !== input.expectedAmountCents)
|
|
7943
|
+
if (input.expectedInvoiceId !== void 0 && id2(invoice) !== input.expectedInvoiceId || input.expectedAmountCents !== void 0 && invoice.amount_paid !== input.expectedAmountCents) fail5();
|
|
7537
7944
|
let chargeId = id2(invoice.charge);
|
|
7538
7945
|
let paymentIntentId = id2(invoice.payment_intent);
|
|
7539
7946
|
if (!chargeId && !paymentIntentId) {
|
|
7540
7947
|
const payments = await call("GET", "/v1/invoice_payments", { invoice: id2(invoice), status: "paid", limit: 100 });
|
|
7541
|
-
if (payments.has_more !== false || !Array.isArray(payments.data) || payments.data.length !== 1)
|
|
7948
|
+
if (payments.has_more !== false || !Array.isArray(payments.data) || payments.data.length !== 1) fail5("refund_payment_ambiguous");
|
|
7542
7949
|
const payment = row(payments.data[0]);
|
|
7543
|
-
if (id2(payment.invoice) !== id2(invoice) || payment.status !== "paid" || row(payment.payment).type !== "payment_intent")
|
|
7950
|
+
if (id2(payment.invoice) !== id2(invoice) || payment.status !== "paid" || row(payment.payment).type !== "payment_intent") fail5();
|
|
7544
7951
|
paymentIntentId = id2(row(payment.payment).payment_intent);
|
|
7545
7952
|
}
|
|
7546
7953
|
if (!chargeId) {
|
|
7547
|
-
if (!providerId2(paymentIntentId, "pi"))
|
|
7954
|
+
if (!providerId2(paymentIntentId, "pi")) fail5();
|
|
7548
7955
|
const payment = await call("GET", `/v1/payment_intents/${paymentIntentId}`);
|
|
7549
|
-
if (id2(payment) !== paymentIntentId || id2(payment.customer) !== input.customerId || payment.status !== "succeeded")
|
|
7956
|
+
if (id2(payment) !== paymentIntentId || id2(payment.customer) !== input.customerId || payment.status !== "succeeded") fail5();
|
|
7550
7957
|
chargeId = id2(payment.latest_charge);
|
|
7551
7958
|
}
|
|
7552
|
-
if (!providerId2(chargeId, "ch"))
|
|
7959
|
+
if (!providerId2(chargeId, "ch")) fail5();
|
|
7553
7960
|
const charge = await call("GET", `/v1/charges/${chargeId}`);
|
|
7554
|
-
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))
|
|
7555
|
-
if (input.alreadyRefundedChargeId !== void 0 && (chargeId !== input.alreadyRefundedChargeId || charge.amount_refunded !== charge.amount || charge.refunded !== true))
|
|
7961
|
+
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();
|
|
7962
|
+
if (input.alreadyRefundedChargeId !== void 0 && (chargeId !== input.alreadyRefundedChargeId || charge.amount_refunded !== charge.amount || charge.refunded !== true)) fail5();
|
|
7556
7963
|
if (charge.amount_refunded !== charge.amount) {
|
|
7557
7964
|
const refund = await call("POST", "/v1/refunds", {
|
|
7558
7965
|
charge: chargeId,
|
|
7559
7966
|
...input.metadata ? { metadata: input.metadata } : {}
|
|
7560
7967
|
}, `chapter-initial-refund:${input.subscriptionId}:${chargeId}`);
|
|
7561
|
-
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))
|
|
7968
|
+
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");
|
|
7562
7969
|
}
|
|
7563
7970
|
let subscriptionCanceled = sub.status === "canceled";
|
|
7564
7971
|
if (input.cancelSubscription !== false && !subscriptionCanceled) {
|
|
7565
7972
|
const canceled = await call("DELETE", `/v1/subscriptions/${input.subscriptionId}`);
|
|
7566
|
-
if (id2(canceled) !== input.subscriptionId || canceled.status !== "canceled")
|
|
7973
|
+
if (id2(canceled) !== input.subscriptionId || canceled.status !== "canceled") fail5("refund_cancel_incomplete");
|
|
7567
7974
|
subscriptionCanceled = true;
|
|
7568
7975
|
}
|
|
7569
7976
|
const confirmed = await call("GET", `/v1/charges/${chargeId}`);
|
|
7570
|
-
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)
|
|
7977
|
+
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");
|
|
7571
7978
|
return { refundedCents: Number(charge.amount), subscriptionCanceled };
|
|
7572
7979
|
}
|
|
7573
7980
|
|
|
7574
7981
|
// src/subscription-payment-identity.ts
|
|
7575
|
-
var
|
|
7982
|
+
var import_stripe9 = require("@odla-ai/stripe");
|
|
7576
7983
|
|
|
7577
7984
|
// src/subscription-provider-contract.ts
|
|
7578
|
-
var
|
|
7985
|
+
var import_stripe8 = require("@odla-ai/stripe");
|
|
7579
7986
|
function subscriptionContractMismatch() {
|
|
7580
7987
|
throw Object.assign(new Error("Stripe subscription does not match the checkout contract"), { code: "stripe_contract_mismatch" });
|
|
7581
7988
|
}
|
|
7582
7989
|
async function verifySubscriptionPrice(secretKey, input, resume = false) {
|
|
7583
7990
|
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();
|
|
7584
|
-
const result = await (0,
|
|
7991
|
+
const result = await (0, import_stripe8.stripeRequest)({ secretKey }, "GET", `/v1/prices/${encodeURIComponent(input.priceId)}`);
|
|
7585
7992
|
const price = result.body;
|
|
7586
7993
|
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();
|
|
7587
7994
|
return typeof price.product === "string" ? price.product : void 0;
|
|
@@ -7600,7 +8007,7 @@ async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata,
|
|
|
7600
8007
|
let paymentId = id3(invoice.payment_intent);
|
|
7601
8008
|
if (!paymentId) {
|
|
7602
8009
|
if (!/^in_[A-Za-z0-9]+$/.test(id3(invoice))) subscriptionContractMismatch();
|
|
7603
|
-
const result2 = await (0,
|
|
8010
|
+
const result2 = await (0, import_stripe9.stripeRequest)({ secretKey }, "GET", "/v1/invoice_payments", { invoice: id3(invoice), limit: 100 });
|
|
7604
8011
|
const list = result2.body;
|
|
7605
8012
|
if (!result2.ok || list?.has_more !== false || !Array.isArray(list.data) || list.data.length !== 1) subscriptionContractMismatch();
|
|
7606
8013
|
const link = list.data[0];
|
|
@@ -7609,11 +8016,11 @@ async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata,
|
|
|
7609
8016
|
}
|
|
7610
8017
|
if (!/^pi_[A-Za-z0-9]+$/.test(paymentId)) subscriptionContractMismatch();
|
|
7611
8018
|
const path = `/v1/payment_intents/${paymentId}`;
|
|
7612
|
-
const result = await (0,
|
|
8019
|
+
const result = await (0, import_stripe9.stripeRequest)({ secretKey }, "GET", path);
|
|
7613
8020
|
const payment = result.body;
|
|
7614
8021
|
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();
|
|
7615
8022
|
if (Object.entries(metadata).every(([key3, value]) => payment.metadata?.[key3] === value)) return;
|
|
7616
|
-
const tagged = await (0,
|
|
8023
|
+
const tagged = await (0, import_stripe9.stripeRequest)(
|
|
7617
8024
|
{ secretKey },
|
|
7618
8025
|
"POST",
|
|
7619
8026
|
path,
|
|
@@ -7625,7 +8032,7 @@ async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata,
|
|
|
7625
8032
|
|
|
7626
8033
|
// src/payments-stripe.ts
|
|
7627
8034
|
async function stripeCall(sk, method, path, params, idempotencyKey) {
|
|
7628
|
-
return (0,
|
|
8035
|
+
return (0, import_stripe10.stripeRequest)(
|
|
7629
8036
|
{ secretKey: sk },
|
|
7630
8037
|
method,
|
|
7631
8038
|
path,
|
|
@@ -7633,7 +8040,7 @@ async function stripeCall(sk, method, path, params, idempotencyKey) {
|
|
|
7633
8040
|
idempotencyKey ? { idempotencyKey } : {}
|
|
7634
8041
|
);
|
|
7635
8042
|
}
|
|
7636
|
-
function
|
|
8043
|
+
function fail6(op, r) {
|
|
7637
8044
|
const err = new Error(`stripe ${op} failed: ${r.status}`);
|
|
7638
8045
|
err.code = "stripe_error";
|
|
7639
8046
|
throw err;
|
|
@@ -7683,7 +8090,7 @@ function createStripeProvider(config) {
|
|
|
7683
8090
|
{ email: input.email, name: input.name, metadata: meta },
|
|
7684
8091
|
`cus:${input.applicationId}`
|
|
7685
8092
|
);
|
|
7686
|
-
if (!cust.ok)
|
|
8093
|
+
if (!cust.ok) fail6("customer create", cust);
|
|
7687
8094
|
if (typeof cust.body.id !== "string" || !/^cus_[A-Za-z0-9]+$/.test(cust.body.id)) subscriptionContractMismatch();
|
|
7688
8095
|
customerId = cust.body.id;
|
|
7689
8096
|
}
|
|
@@ -7715,7 +8122,7 @@ function createStripeProvider(config) {
|
|
|
7715
8122
|
// retry between create and the db write can't orphan a second one.
|
|
7716
8123
|
`sub:${input.applicationId}${input.purchaseAttemptId ? `:${input.purchaseAttemptId}` : ""}`
|
|
7717
8124
|
);
|
|
7718
|
-
if (!sub.ok)
|
|
8125
|
+
if (!sub.ok) fail6("subscription create", sub);
|
|
7719
8126
|
if (input.billingTerms?.discount?.duration === "once" && Array.isArray(sub.body.discounts) && sub.body.discounts.length === 0) {
|
|
7720
8127
|
const subscriptionId = sub.body.id;
|
|
7721
8128
|
if (typeof subscriptionId !== "string" || !/^sub_[A-Za-z0-9]+$/.test(subscriptionId)) subscriptionContractMismatch();
|
|
@@ -7725,14 +8132,14 @@ function createStripeProvider(config) {
|
|
|
7725
8132
|
"expand[2]": "discounts",
|
|
7726
8133
|
"expand[3]": "latest_invoice.discounts"
|
|
7727
8134
|
});
|
|
7728
|
-
if (!expanded.ok)
|
|
8135
|
+
if (!expanded.ok) fail6("subscription invoice evidence", expanded);
|
|
7729
8136
|
if (expanded.body.id !== subscriptionId) subscriptionContractMismatch();
|
|
7730
8137
|
sub = expanded;
|
|
7731
8138
|
}
|
|
7732
8139
|
verifySubscriptionResult(sub.body, input, customerId, meta);
|
|
7733
8140
|
const invoice = sub.body.latest_invoice;
|
|
7734
8141
|
const clientSecret2 = clientSecretOf(sub.body);
|
|
7735
|
-
if (!clientSecret2)
|
|
8142
|
+
if (!clientSecret2) fail6("subscription confirmation-secret missing", sub);
|
|
7736
8143
|
await tagSubscriptionPayment(
|
|
7737
8144
|
sk,
|
|
7738
8145
|
invoice,
|
|
@@ -7744,7 +8151,7 @@ function createStripeProvider(config) {
|
|
|
7744
8151
|
return { customerId, subscriptionId: String(sub.body.id), clientSecret: clientSecret2 };
|
|
7745
8152
|
},
|
|
7746
8153
|
async ingestWebhook(rawBody, sigHeader) {
|
|
7747
|
-
if (!webhookSecret || !await (0,
|
|
8154
|
+
if (!webhookSecret || !await (0, import_stripe10.verifyStripeSignature)(rawBody, sigHeader, webhookSecret)) {
|
|
7748
8155
|
return { ok: false, reason: "bad_signature" };
|
|
7749
8156
|
}
|
|
7750
8157
|
const event = JSON.parse(rawBody);
|
|
@@ -8404,7 +8811,7 @@ async function namedSeatOfferDigest(offer) {
|
|
|
8404
8811
|
"named-only",
|
|
8405
8812
|
"full-upfront",
|
|
8406
8813
|
"auto-renew-coterm",
|
|
8407
|
-
"
|
|
8814
|
+
"no-automatic-refund"
|
|
8408
8815
|
])));
|
|
8409
8816
|
return [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
8410
8817
|
}
|
|
@@ -8416,15 +8823,16 @@ async function readNamedSeatOffer(db, input, now) {
|
|
|
8416
8823
|
const offering = membershipOfferingVersion(member.offeringSnapshot);
|
|
8417
8824
|
const seat = member.namedSeatId ? await membershipLoad(db, "networkSeat", String(member.namedSeatId)) : void 0;
|
|
8418
8825
|
if (seat) requireMembershipScope(seat, input);
|
|
8826
|
+
const purchaseEndsAt = seatPurchaseDeadline(offering, member);
|
|
8419
8827
|
const offer = {
|
|
8420
8828
|
membershipId: id6,
|
|
8421
8829
|
amountCents: offering.seatPriceCents,
|
|
8422
8830
|
currency: offering.currency,
|
|
8423
8831
|
interval: offering.interval,
|
|
8424
8832
|
termEndsAt: Number(member.termEndsAt),
|
|
8425
|
-
purchaseEndsAt
|
|
8833
|
+
purchaseEndsAt,
|
|
8426
8834
|
autoRenew: member.autoRenew === true,
|
|
8427
|
-
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 <=
|
|
8835
|
+
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))),
|
|
8428
8836
|
...seat ? { seat: {
|
|
8429
8837
|
seatId: String(seat.id),
|
|
8430
8838
|
status: String(seat.status),
|
|
@@ -8432,7 +8840,7 @@ async function readNamedSeatOffer(db, input, now) {
|
|
|
8432
8840
|
recipientEmail: String(seat.recipientEmailNormalized)
|
|
8433
8841
|
} } : {}
|
|
8434
8842
|
};
|
|
8435
|
-
return { ...offer, digest: await namedSeatOfferDigest(offer) };
|
|
8843
|
+
return { ...offer, digest: await namedSeatOfferDigest(offer), purchaseWindowed: offering.seatPurchaseWindowCalendarMonths !== void 0 };
|
|
8436
8844
|
}
|
|
8437
8845
|
|
|
8438
8846
|
// src/named-seat-request.ts
|
|
@@ -8809,6 +9217,23 @@ async function updateChapterPerson(opts) {
|
|
|
8809
9217
|
});
|
|
8810
9218
|
}
|
|
8811
9219
|
|
|
9220
|
+
// src/application-actions.ts
|
|
9221
|
+
function isApplicationStage(ctx, stage) {
|
|
9222
|
+
return ctx.chapter.pipeline.stages.includes(stage);
|
|
9223
|
+
}
|
|
9224
|
+
function applicationActionStages(app, env, ctx) {
|
|
9225
|
+
if (!chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME)) return [];
|
|
9226
|
+
const from = String(app.status ?? "");
|
|
9227
|
+
if (app.canceled === true || ["refunded", "canceled"].includes(from)) return [];
|
|
9228
|
+
return ctx.chapter.pipeline.stages.filter((to) => {
|
|
9229
|
+
if (to === from) return false;
|
|
9230
|
+
if (to === "approved") return canApprove(from, ctx.chapter.pipeline) && from !== "declined";
|
|
9231
|
+
if (to === "refunded") return Boolean(app.namedSeatId || app.stripeCustomerId && app.stripeSubscriptionId) && (!ctx.chapter.operations.refund.allowedFrom || ctx.chapter.operations.refund.allowedFrom.includes(from));
|
|
9232
|
+
if (ctx.membershipAuthority && ["submitted", "paid_pending_vetting", "declined", "canceled"].includes(to)) return false;
|
|
9233
|
+
return canTransition(from, to, ctx.chapter.pipeline);
|
|
9234
|
+
});
|
|
9235
|
+
}
|
|
9236
|
+
|
|
8812
9237
|
// src/worker-routes-people-profile.ts
|
|
8813
9238
|
var handlePeopleProfile = async (req, url, env, ctx) => {
|
|
8814
9239
|
if (ctx.chapter.mode !== "chapter") return null;
|
|
@@ -8830,7 +9255,16 @@ var handlePeopleProfile = async (req, url, env, ctx) => {
|
|
|
8830
9255
|
const record4 = await (0, import_crm10.getRecord)({ crm: ctx.chapter.crm, db }, id6);
|
|
8831
9256
|
if (!record4) return json({ error: "not found" }, 404);
|
|
8832
9257
|
if (record4.type !== "person") return dedicated ? json({ error: "person required" }, 422) : null;
|
|
8833
|
-
if (match?.[2])
|
|
9258
|
+
if (match?.[2]) {
|
|
9259
|
+
const linked = typeof record4.fields.applicationId === "string" && record4.fields.applicationId !== "";
|
|
9260
|
+
let to = "";
|
|
9261
|
+
try {
|
|
9262
|
+
to = String((await req.clone().json()).to ?? "");
|
|
9263
|
+
} catch {
|
|
9264
|
+
}
|
|
9265
|
+
if (!linked && to && !isApplicationStage(ctx, to)) return null;
|
|
9266
|
+
return json({ error: "use the application lifecycle workflow", code: "application_workflow_required" }, 409);
|
|
9267
|
+
}
|
|
8834
9268
|
if (req.method === "GET" && dedicated) {
|
|
8835
9269
|
const excluded = profileReadOnlyFields(record4, ctx);
|
|
8836
9270
|
const fields = Object.entries(ctx.chapter.crm.type(record4.type).fields).filter(([id7]) => !excluded.includes(id7)).map(([id7, field]) => ({
|
|
@@ -9521,20 +9955,6 @@ var handleAdminBilling = async (req, url, env, ctx) => {
|
|
|
9521
9955
|
// src/worker-routes-admin-lifecycle.ts
|
|
9522
9956
|
var import_calendar3 = require("@odla-ai/calendar");
|
|
9523
9957
|
|
|
9524
|
-
// src/application-actions.ts
|
|
9525
|
-
function applicationActionStages(app, env, ctx) {
|
|
9526
|
-
if (!chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME)) return [];
|
|
9527
|
-
const from = String(app.status ?? "");
|
|
9528
|
-
if (app.canceled === true || ["refunded", "canceled"].includes(from)) return [];
|
|
9529
|
-
return ctx.chapter.pipeline.stages.filter((to) => {
|
|
9530
|
-
if (to === from) return false;
|
|
9531
|
-
if (to === "approved") return canApprove(from, ctx.chapter.pipeline) && from !== "declined";
|
|
9532
|
-
if (to === "refunded") return Boolean(app.namedSeatId || app.stripeCustomerId && app.stripeSubscriptionId) && (!ctx.chapter.operations.refund.allowedFrom || ctx.chapter.operations.refund.allowedFrom.includes(from));
|
|
9533
|
-
if (ctx.membershipAuthority && ["submitted", "paid_pending_vetting", "declined", "canceled"].includes(to)) return false;
|
|
9534
|
-
return canTransition(from, to, ctx.chapter.pipeline);
|
|
9535
|
-
});
|
|
9536
|
-
}
|
|
9537
|
-
|
|
9538
9958
|
// src/refund-application.ts
|
|
9539
9959
|
async function refundApplication(db, env, ctx, app) {
|
|
9540
9960
|
if (!chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME)) return json({ error: "not found" }, 404);
|
|
@@ -9725,6 +10145,12 @@ var handleAdminRefund = async (req, url, env, ctx) => {
|
|
|
9725
10145
|
const app = await loadApp(db, m[1]);
|
|
9726
10146
|
if (!app) return json({ error: "not found" }, 404);
|
|
9727
10147
|
if (!chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME)) return json({ error: "not found" }, 404);
|
|
10148
|
+
if (app.namedSeatId) {
|
|
10149
|
+
const actor = await ctx.verifyUser(req, env);
|
|
10150
|
+
if (!actor || !await ctx.isSuperAdminEmail(db, actor.email, actor.userId)) {
|
|
10151
|
+
return json({ error: "super-admin required", code: "super_admin_required" }, 403);
|
|
10152
|
+
}
|
|
10153
|
+
}
|
|
9728
10154
|
return refundApplication(db, env, ctx, app);
|
|
9729
10155
|
};
|
|
9730
10156
|
var handleAdminApplicationPatch = async (req, url, env, ctx) => {
|
|
@@ -10334,7 +10760,7 @@ async function operationalReaderRecord(record4, reader, db, env, ctx) {
|
|
|
10334
10760
|
const appId = record4.fields.applicationId;
|
|
10335
10761
|
const app = typeof appId === "string" ? (await db.query({ applications: { $: { where: { id: appId }, limit: 1 } } })).applications?.[0] : void 0;
|
|
10336
10762
|
if (reader.stageTransitions.includes(record4.type)) {
|
|
10337
|
-
result.edit.allowedStages = app ? applicationActionStages(app, env, ctx).map((id6) => ({ id: id6, label: id6.replaceAll("_", " ") })) :
|
|
10763
|
+
result.edit.allowedStages = app ? applicationActionStages(app, env, ctx).map((id6) => ({ id: id6, label: id6.replaceAll("_", " ") })) : result.edit.allowedStages.filter((stage) => !isApplicationStage(ctx, stage.id));
|
|
10338
10764
|
const lock = (await db.query({ crm_activity: { $: { where: { id: `network-action-lock:${record4.id}` }, limit: 1 } } })).crm_activity?.[0];
|
|
10339
10765
|
const meta = lock?.meta;
|
|
10340
10766
|
if (meta?.state === "pending" && typeof meta.activityId === "string") {
|
|
@@ -10552,7 +10978,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
|
10552
10978
|
}
|
|
10553
10979
|
};
|
|
10554
10980
|
try {
|
|
10555
|
-
if (hasStage && ctx.chapter.mode === "chapter" && record4.type === "person") {
|
|
10981
|
+
if (hasStage && ctx.chapter.mode === "chapter" && record4.type === "person" && (record4.fields.applicationId || isApplicationStage(ctx, input.stage))) {
|
|
10556
10982
|
const updated2 = await networkApplicationAction({
|
|
10557
10983
|
db,
|
|
10558
10984
|
env,
|
|
@@ -11908,7 +12334,7 @@ async function revealNetworkPersonRegistryRecord(db, input, mutationId, actor, n
|
|
|
11908
12334
|
}
|
|
11909
12335
|
|
|
11910
12336
|
// src/worker-routes-network-person-registry.ts
|
|
11911
|
-
var
|
|
12337
|
+
var PATH3 = "/api/admin/network-person-registry";
|
|
11912
12338
|
var MAX_BODY2 = 8 * 1024 * 1024;
|
|
11913
12339
|
var MUTATION_ID4 = /^[A-Za-z0-9_-]{16,128}$/;
|
|
11914
12340
|
var positiveRevision = (value) => {
|
|
@@ -11933,7 +12359,7 @@ function registryError(error) {
|
|
|
11933
12359
|
return null;
|
|
11934
12360
|
}
|
|
11935
12361
|
var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
11936
|
-
if (url.pathname !==
|
|
12362
|
+
if (url.pathname !== PATH3) return null;
|
|
11937
12363
|
if (!ctx.chapter.networkPersonRegistry) return json({ error: "not found" }, 404);
|
|
11938
12364
|
if (req.method !== "GET" && req.method !== "POST") return json({ error: "method not allowed" }, 405);
|
|
11939
12365
|
if (req.method === "POST" && Number(req.headers.get("content-length") ?? 0) > MAX_BODY2) {
|
|
@@ -12108,7 +12534,7 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
|
12108
12534
|
};
|
|
12109
12535
|
|
|
12110
12536
|
// src/worker-routes-network-commercial.ts
|
|
12111
|
-
var
|
|
12537
|
+
var import_stripe11 = require("@odla-ai/stripe");
|
|
12112
12538
|
|
|
12113
12539
|
// src/worker-routes-network-commercial-parity.ts
|
|
12114
12540
|
var import_db13 = require("@odla-ai/db");
|
|
@@ -12484,7 +12910,7 @@ var handleNetworkCommercialStripeWebhook = async (req, url, env, ctx) => {
|
|
|
12484
12910
|
return json({ error: "commercial webhook is not configured" }, 503);
|
|
12485
12911
|
}
|
|
12486
12912
|
try {
|
|
12487
|
-
const event = await (0,
|
|
12913
|
+
const event = await (0, import_stripe11.constructStripeEvent)(body, signature, secret);
|
|
12488
12914
|
const fact = normalizeNetworkCommercialStripeEvent(event, await stripeEventDigest(body));
|
|
12489
12915
|
if (!fact.eventId || !fact.eventType || !fact.objectId) return json({ error: "Stripe event identity is invalid" }, 400);
|
|
12490
12916
|
const result = await ingestNetworkCommercialFact(db, ctx.chapter.networkCommercialReadModel.sources, fact, Date.now(), await stripePayloadDigest(body));
|
|
@@ -12493,7 +12919,7 @@ var handleNetworkCommercialStripeWebhook = async (req, url, env, ctx) => {
|
|
|
12493
12919
|
}
|
|
12494
12920
|
return json({ ok: true, status: result.status, eventId: fact.eventId });
|
|
12495
12921
|
} catch (error) {
|
|
12496
|
-
if (error instanceof
|
|
12922
|
+
if (error instanceof import_stripe11.StripeError) return json({ error: error.message, code: error.code }, 400);
|
|
12497
12923
|
throw error;
|
|
12498
12924
|
}
|
|
12499
12925
|
};
|
|
@@ -12535,7 +12961,7 @@ var handleAdminNetworkCommercialReadModel = async (req, url, env, ctx) => {
|
|
|
12535
12961
|
const secretKey = await getVaultSecret(db, "stripe_secret_key");
|
|
12536
12962
|
if (!secretKey) return json({ error: "Stripe reconciliation is not configured" }, 503);
|
|
12537
12963
|
try {
|
|
12538
|
-
const stripe = (0,
|
|
12964
|
+
const stripe = (0, import_stripe11.initStripe)({ secretKey });
|
|
12539
12965
|
const [customers, subscriptions, invoices, refunds, payments] = await Promise.all([
|
|
12540
12966
|
listStripeObjects(stripe, "/v1/customers"),
|
|
12541
12967
|
listStripeObjects(stripe, "/v1/subscriptions", { status: "all" }),
|
|
@@ -12549,7 +12975,7 @@ var handleAdminNetworkCommercialReadModel = async (req, url, env, ctx) => {
|
|
|
12549
12975
|
...reconcileStripeSnapshot(projection2, { customers, subscriptions, invoices, refunds, payments }, ctx.chapter.networkCommercialReadModel.sources)
|
|
12550
12976
|
});
|
|
12551
12977
|
} catch (error) {
|
|
12552
|
-
if (error instanceof
|
|
12978
|
+
if (error instanceof import_stripe11.StripeError) return json({ error: "Stripe reconciliation failed", code: error.code }, 502);
|
|
12553
12979
|
return json({ error: "Stripe reconciliation failed" }, 502);
|
|
12554
12980
|
}
|
|
12555
12981
|
}
|
|
@@ -13129,7 +13555,7 @@ var handleAdminBuilderLibrary = async (req, url, env, ctx) => {
|
|
|
13129
13555
|
};
|
|
13130
13556
|
|
|
13131
13557
|
// src/membership-cancel-authority.ts
|
|
13132
|
-
var
|
|
13558
|
+
var import_stripe12 = require("@odla-ai/stripe");
|
|
13133
13559
|
async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
|
|
13134
13560
|
membershipClock(now);
|
|
13135
13561
|
const id6 = await networkMembershipId(target);
|
|
@@ -13140,14 +13566,14 @@ async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
|
|
|
13140
13566
|
membershipFail("membership_cancellation_unavailable");
|
|
13141
13567
|
}
|
|
13142
13568
|
const path = `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`;
|
|
13143
|
-
const read2 = await (0,
|
|
13569
|
+
const read2 = await (0, import_stripe12.stripeRequest)({ secretKey: stripeSecret }, "GET", path);
|
|
13144
13570
|
const sub = read2.body;
|
|
13145
13571
|
if (!read2.ok || sub.id !== member.providerSubscriptionId || sub.customer !== member.providerCustomerId || sub.livemode !== (target.environment === "prod") || !Object.entries({
|
|
13146
13572
|
...stripePartitionMetadata(target),
|
|
13147
13573
|
applicationId: target.applicationId
|
|
13148
13574
|
}).every(([key3, value]) => sub.metadata?.[key3] === value)) membershipFail("membership_cancellation_provider_mismatch");
|
|
13149
13575
|
if (sub.status !== "canceled" && sub.cancel_at_period_end !== true) {
|
|
13150
|
-
const write = await (0,
|
|
13576
|
+
const write = await (0, import_stripe12.stripeRequest)(
|
|
13151
13577
|
{ secretKey: stripeSecret },
|
|
13152
13578
|
"POST",
|
|
13153
13579
|
path,
|
|
@@ -13369,7 +13795,7 @@ async function refundNetworkMembership(db, input, secretKey, now) {
|
|
|
13369
13795
|
}
|
|
13370
13796
|
|
|
13371
13797
|
// src/membership-provider-lifecycle.ts
|
|
13372
|
-
var
|
|
13798
|
+
var import_stripe13 = require("@odla-ai/stripe");
|
|
13373
13799
|
|
|
13374
13800
|
// src/membership-renewal-authority.ts
|
|
13375
13801
|
async function reconcileMembershipRenewal(db, input, now) {
|
|
@@ -13422,7 +13848,11 @@ async function reconcileMembershipRenewal(db, input, now) {
|
|
|
13422
13848
|
membershipUpdate("networkMembership", id6, {
|
|
13423
13849
|
status: status2,
|
|
13424
13850
|
graceEndsAt,
|
|
13425
|
-
...paid ? {
|
|
13851
|
+
...paid ? {
|
|
13852
|
+
termStartedAt: start,
|
|
13853
|
+
termEndsAt: end,
|
|
13854
|
+
...membershipOfferingVersion(member.offeringSnapshot).seatPurchaseWindowCalendarMonths === void 0 ? { seatPurchaseEndsAt: end } : {}
|
|
13855
|
+
} : {},
|
|
13426
13856
|
revision: Number(member.revision) + 1,
|
|
13427
13857
|
updatedAt: now
|
|
13428
13858
|
}),
|
|
@@ -13527,7 +13957,7 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
13527
13957
|
}
|
|
13528
13958
|
if (fact.subscriptionId !== member.providerSubscriptionId || fact.objectKind !== "subscription") membershipFail("membership_billing_scope_mismatch");
|
|
13529
13959
|
if (["refund_pending", "refunded", "declined", "canceled", "restarting"].includes(String(member.status))) return receipt();
|
|
13530
|
-
const result = await (0,
|
|
13960
|
+
const result = await (0, import_stripe13.stripeRequest)({ secretKey: sk }, "GET", `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`);
|
|
13531
13961
|
const sub = result.body;
|
|
13532
13962
|
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)) {
|
|
13533
13963
|
membershipFail("membership_billing_provider_mismatch");
|
|
@@ -13582,10 +14012,11 @@ async function reserveNamedMembershipSeat(db, input, now) {
|
|
|
13582
14012
|
const id6 = (await membershipOfferIdForRequest({ ...input, requestId: "named_seat" })).replace(/^offer_/, "seat_");
|
|
13583
14013
|
let seat = await membershipLoad(db, "networkSeat", id6);
|
|
13584
14014
|
if (!seat) {
|
|
13585
|
-
|
|
14015
|
+
const offering = membershipOfferingVersion(member.offeringSnapshot), claimId = await recipientKey(input, email);
|
|
14016
|
+
const purchaseEndsAt = seatPurchaseDeadline(offering, member);
|
|
14017
|
+
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) {
|
|
13586
14018
|
membershipFail("named_seat_purchase_unavailable");
|
|
13587
14019
|
}
|
|
13588
|
-
const offering = membershipOfferingVersion(member.offeringSnapshot), claimId = await recipientKey(input, email);
|
|
13589
14020
|
const attrs = {
|
|
13590
14021
|
id: id6,
|
|
13591
14022
|
...membershipScope(input),
|
|
@@ -13604,7 +14035,7 @@ async function reserveNamedMembershipSeat(db, input, now) {
|
|
|
13604
14035
|
status: "payment_pending",
|
|
13605
14036
|
preparedAt: now,
|
|
13606
14037
|
termEndsAt: member.termEndsAt,
|
|
13607
|
-
seatPurchaseEndsAt:
|
|
14038
|
+
seatPurchaseEndsAt: purchaseEndsAt,
|
|
13608
14039
|
offeringSnapshot: offering,
|
|
13609
14040
|
revision: 1,
|
|
13610
14041
|
createdAt: now,
|
|
@@ -13642,7 +14073,7 @@ async function reserveNamedMembershipSeat(db, input, now) {
|
|
|
13642
14073
|
}
|
|
13643
14074
|
|
|
13644
14075
|
// src/membership-seat-stripe.ts
|
|
13645
|
-
var
|
|
14076
|
+
var import_stripe14 = require("@odla-ai/stripe");
|
|
13646
14077
|
var reject7 = () => {
|
|
13647
14078
|
throw Object.assign(new Error("named seat provider contract mismatch"), { code: "named_seat_provider_mismatch" });
|
|
13648
14079
|
};
|
|
@@ -13654,7 +14085,7 @@ var metadataFor = (seat) => ({
|
|
|
13654
14085
|
});
|
|
13655
14086
|
var matches = (value, expected) => value && Object.entries(expected).every(([key3, item]) => value[key3] === item);
|
|
13656
14087
|
async function read(sk, path, params) {
|
|
13657
|
-
const response = await (0,
|
|
14088
|
+
const response = await (0, import_stripe14.stripeRequest)({ secretKey: sk }, "GET", path, params);
|
|
13658
14089
|
if (!response.ok) return reject7();
|
|
13659
14090
|
return response.body;
|
|
13660
14091
|
}
|
|
@@ -13691,11 +14122,11 @@ async function createNamedSeatStripePayment(sk, seat, now) {
|
|
|
13691
14122
|
}
|
|
13692
14123
|
await subscriptionForSeat(sk, seat);
|
|
13693
14124
|
const metadata = metadataFor(seat);
|
|
13694
|
-
const result = seat.providerPaymentIntentId ? await (0,
|
|
14125
|
+
const result = seat.providerPaymentIntentId ? await (0, import_stripe14.stripeRequest)(
|
|
13695
14126
|
{ secretKey: sk },
|
|
13696
14127
|
"GET",
|
|
13697
14128
|
`/v1/payment_intents/${encodeURIComponent(seat.providerPaymentIntentId)}`
|
|
13698
|
-
) : await (0,
|
|
14129
|
+
) : await (0, import_stripe14.stripeRequest)({ secretKey: sk }, "POST", "/v1/payment_intents", {
|
|
13699
14130
|
amount: seat.amountCents,
|
|
13700
14131
|
currency: seat.currency,
|
|
13701
14132
|
customer: seat.providerCustomerId,
|
|
@@ -13713,7 +14144,7 @@ async function attachNamedSeatStripeRenewal(sk, seat) {
|
|
|
13713
14144
|
const sub = await subscriptionForSeat(sk, seat);
|
|
13714
14145
|
const existing = sub.items.data.find((item2) => item2.metadata?.seatId === seat.id);
|
|
13715
14146
|
if (existing) return String(existing.id);
|
|
13716
|
-
const result = await (0,
|
|
14147
|
+
const result = await (0, import_stripe14.stripeRequest)({ secretKey: sk }, "POST", "/v1/subscription_items", {
|
|
13717
14148
|
subscription: seat.providerSubscriptionId,
|
|
13718
14149
|
price: seat.priceId,
|
|
13719
14150
|
quantity: 1,
|
|
@@ -13806,14 +14237,14 @@ async function confirmNamedMembershipSeatPayment(db, input, now) {
|
|
|
13806
14237
|
}
|
|
13807
14238
|
if (seat.purchasedAt === void 0) {
|
|
13808
14239
|
if (!["payment_processing", "payment_canceling"].includes(String(seat.status))) membershipFail("named_seat_payment_not_expected");
|
|
13809
|
-
const eligible = seat.status !== "payment_canceling" && ["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) && Number(member.refundedCents) === 0 && paidAt <=
|
|
14240
|
+
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);
|
|
13810
14241
|
const status2 = eligible ? "renewal_pending" : "refund_pending", effectId = `${seat.id}:${eligible ? "attach-renewal" : "refund"}`;
|
|
13811
14242
|
try {
|
|
13812
14243
|
await db.transact([
|
|
13813
14244
|
membershipUpdate("networkSeat", input.seatId, {
|
|
13814
14245
|
status: status2,
|
|
13815
14246
|
purchasedAt: paidAt,
|
|
13816
|
-
acceptanceEndsAt:
|
|
14247
|
+
acceptanceEndsAt: Number(member.termEndsAt),
|
|
13817
14248
|
termStartedAt: paidAt,
|
|
13818
14249
|
termEndsAt: member.termEndsAt,
|
|
13819
14250
|
paymentEventId: input.eventId,
|
|
@@ -13861,7 +14292,7 @@ async function completeNamedSeatRenewal(db, seatId, secretKey, now) {
|
|
|
13861
14292
|
if (seat.status !== "renewal_pending" || !seat.purchasedAt) membershipFail("named_seat_renewal_not_ready");
|
|
13862
14293
|
const member = await membershipLoad(db, "networkMembership", target.primaryMembershipId);
|
|
13863
14294
|
requireMembershipScope(member, target);
|
|
13864
|
-
if (now >= Number(
|
|
14295
|
+
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");
|
|
13865
14296
|
const itemId = await attachNamedSeatStripeRenewal(secretKey, target);
|
|
13866
14297
|
const entitlementId = `${seatId}:entitlement`, effectId = `${seatId}:attach-renewal`;
|
|
13867
14298
|
try {
|
|
@@ -13936,7 +14367,7 @@ async function recordRecipient(db, input, now, approve) {
|
|
|
13936
14367
|
requireMembershipScope(entitlement, input);
|
|
13937
14368
|
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");
|
|
13938
14369
|
if (seat.acceptedAt === void 0) {
|
|
13939
|
-
if (approve ||
|
|
14370
|
+
if (approve || normalizeChapterEmail(input.verifiedRecipientEmail) !== seat.recipientEmailNormalized) {
|
|
13940
14371
|
membershipFail("named_seat_recipient_verification_required");
|
|
13941
14372
|
}
|
|
13942
14373
|
} else if (seat.recipientUserId !== input.recipientUserId || seat.recipientApplicationId !== input.recipientApplicationId) {
|
|
@@ -13986,129 +14417,6 @@ async function recordRecipient(db, input, now, approve) {
|
|
|
13986
14417
|
return membershipFail("named_seat_recipient_retry_required");
|
|
13987
14418
|
}
|
|
13988
14419
|
|
|
13989
|
-
// src/membership-seat-refund-stripe.ts
|
|
13990
|
-
var import_stripe14 = require("@odla-ai/stripe");
|
|
13991
|
-
var fail6 = (code = "named_seat_refund_mismatch") => {
|
|
13992
|
-
throw Object.assign(new Error(code), { code });
|
|
13993
|
-
};
|
|
13994
|
-
async function refundNamedSeatStripePayment(secretKey, seat) {
|
|
13995
|
-
if (!seat.providerPaymentIntentId || seat.status !== "refund_pending") fail6();
|
|
13996
|
-
const call = async (method, path, params, idempotencyKey) => {
|
|
13997
|
-
const result = await (0, import_stripe14.stripeRequest)({ secretKey }, method, path, params, idempotencyKey ? { idempotencyKey } : {});
|
|
13998
|
-
if (!result.ok || !result.body) return fail6("named_seat_refund_provider_unavailable");
|
|
13999
|
-
return result.body;
|
|
14000
|
-
};
|
|
14001
|
-
const metadata = { ...stripePartitionMetadata(seat), applicationId: seat.applicationId, membershipId: seat.primaryMembershipId, seatId: seat.id };
|
|
14002
|
-
const payment = await call("GET", `/v1/payment_intents/${encodeURIComponent(seat.providerPaymentIntentId)}`);
|
|
14003
|
-
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();
|
|
14004
|
-
const sub = await call("GET", `/v1/subscriptions/${encodeURIComponent(seat.providerSubscriptionId)}`);
|
|
14005
|
-
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();
|
|
14006
|
-
const items = sub.items.data.filter((item) => item.metadata?.seatId === seat.id);
|
|
14007
|
-
if (items.length > 1) fail6();
|
|
14008
|
-
if (items.length && sub.status !== "canceled") {
|
|
14009
|
-
const item = items[0];
|
|
14010
|
-
if (!/^si_[A-Za-z0-9_]+$/.test(item.id) || item.price?.id !== seat.priceId || item.quantity !== 1) fail6();
|
|
14011
|
-
const removed = await call("DELETE", `/v1/subscription_items/${encodeURIComponent(item.id)}`, { proration_behavior: "none" });
|
|
14012
|
-
if (removed.id !== item.id || removed.deleted !== true) fail6("named_seat_renewal_removal_unconfirmed");
|
|
14013
|
-
}
|
|
14014
|
-
const list = await call("GET", "/v1/refunds", { payment_intent: seat.providerPaymentIntentId, limit: 100 });
|
|
14015
|
-
if (!Array.isArray(list.data) || list.has_more !== false || list.data.length >= 100) fail6("named_seat_refund_history_incomplete");
|
|
14016
|
-
let refundedCents = 0, pendingCents = 0;
|
|
14017
|
-
const refundIds = [];
|
|
14018
|
-
for (const refund of list.data) {
|
|
14019
|
-
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();
|
|
14020
|
-
if (refund.status === "succeeded") {
|
|
14021
|
-
refundedCents += refund.amount;
|
|
14022
|
-
refundIds.push(refund.id);
|
|
14023
|
-
} else if (refund.status === "pending" || refund.status === "requires_action") pendingCents += refund.amount;
|
|
14024
|
-
}
|
|
14025
|
-
if (refundedCents + pendingCents > seat.amountCents) fail6();
|
|
14026
|
-
if (pendingCents) fail6("named_seat_refund_pending");
|
|
14027
|
-
if (refundedCents < seat.amountCents) {
|
|
14028
|
-
const remaining = seat.amountCents - refundedCents;
|
|
14029
|
-
const refund = await call("POST", "/v1/refunds", {
|
|
14030
|
-
payment_intent: seat.providerPaymentIntentId,
|
|
14031
|
-
amount: remaining,
|
|
14032
|
-
reason: "requested_by_customer",
|
|
14033
|
-
metadata
|
|
14034
|
-
}, `named-seat-refund:${seat.id}:${remaining}`);
|
|
14035
|
-
if (refund.payment_intent !== seat.providerPaymentIntentId || refund.amount !== remaining || refund.currency !== seat.currency || !/^re_[A-Za-z0-9_]+$/.test(refund.id)) fail6();
|
|
14036
|
-
if (refund.status !== "succeeded") fail6("named_seat_refund_pending");
|
|
14037
|
-
refundedCents += remaining;
|
|
14038
|
-
refundIds.push(refund.id);
|
|
14039
|
-
}
|
|
14040
|
-
return { refundedCents, refundIds };
|
|
14041
|
-
}
|
|
14042
|
-
|
|
14043
|
-
// src/membership-seat-refund.ts
|
|
14044
|
-
async function requestNamedSeatRefund(db, input, now) {
|
|
14045
|
-
membershipClock(now);
|
|
14046
|
-
const state = await loadMembershipAuthorityState(db, input);
|
|
14047
|
-
const seat = await membershipLoad(db, "networkSeat", input.seatId);
|
|
14048
|
-
requireMembershipScope(seat, input);
|
|
14049
|
-
const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
|
|
14050
|
-
requireMembershipScope(member, input);
|
|
14051
|
-
if (seat.status === "refunded" || seat.status === "refund_pending") return;
|
|
14052
|
-
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");
|
|
14053
|
-
const entitlement = seat.entitlementId ? await membershipLoad(db, "networkEntitlement", String(seat.entitlementId)) : void 0;
|
|
14054
|
-
await db.transact([
|
|
14055
|
-
membershipUpdate("networkSeat", input.seatId, { status: "refund_pending", refundReason: input.reason, revision: Number(seat.revision) + 1, updatedAt: now }),
|
|
14056
|
-
...entitlement ? [membershipUpdate("networkEntitlement", String(entitlement.id), { status: "closed", revision: Number(entitlement.revision) + 1, updatedAt: now })] : [],
|
|
14057
|
-
membershipUpdate("membershipEffect", `${seat.id}:refund`, {
|
|
14058
|
-
id: `${seat.id}:refund`,
|
|
14059
|
-
chapterId: input.chapterId,
|
|
14060
|
-
environment: input.environment,
|
|
14061
|
-
runtime: input.runtime,
|
|
14062
|
-
membershipId: member.id,
|
|
14063
|
-
seatId: seat.id,
|
|
14064
|
-
kind: "refund_seat",
|
|
14065
|
-
status: "pending",
|
|
14066
|
-
attempts: 0,
|
|
14067
|
-
createdAt: now,
|
|
14068
|
-
updatedAt: now
|
|
14069
|
-
}),
|
|
14070
|
-
...seat.recipientUserId ? [membershipUpdate("membershipEffect", `${seat.id}:revoke`, {
|
|
14071
|
-
id: `${seat.id}:revoke`,
|
|
14072
|
-
chapterId: input.chapterId,
|
|
14073
|
-
environment: input.environment,
|
|
14074
|
-
runtime: input.runtime,
|
|
14075
|
-
membershipId: member.id,
|
|
14076
|
-
seatId: seat.id,
|
|
14077
|
-
kind: "revoke_named_recipient_access",
|
|
14078
|
-
status: "pending",
|
|
14079
|
-
attempts: 0,
|
|
14080
|
-
createdAt: now,
|
|
14081
|
-
updatedAt: now
|
|
14082
|
-
})] : []
|
|
14083
|
-
], { mutationId: `named-seat-refund-request:${seat.id}`, guards: [
|
|
14084
|
-
{ ns: "networkSeat", id: input.seatId, exists: true, equals: { revision: seat.revision } },
|
|
14085
|
-
{ ns: "networkMembership", id: String(member.id), exists: true, equals: { revision: member.revision } },
|
|
14086
|
-
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
|
|
14087
|
-
...entitlement ? [{ ns: "networkEntitlement", id: String(entitlement.id), exists: true, equals: { revision: entitlement.revision } }] : []
|
|
14088
|
-
] });
|
|
14089
|
-
}
|
|
14090
|
-
async function completeNamedSeatRefund(db, seatId, secretKey, now) {
|
|
14091
|
-
membershipClock(now);
|
|
14092
|
-
const seat = await membershipLoad(db, "networkSeat", seatId);
|
|
14093
|
-
if (!seat) return membershipFail("named_seat_not_found");
|
|
14094
|
-
await loadMembershipAuthorityState(db, seat);
|
|
14095
|
-
if (seat.status === "refunded") return;
|
|
14096
|
-
if (seat.status !== "refund_pending") membershipFail("named_seat_refund_not_ready");
|
|
14097
|
-
const result = await refundNamedSeatStripePayment(secretKey, seat);
|
|
14098
|
-
await db.transact([
|
|
14099
|
-
membershipUpdate("networkSeat", seatId, {
|
|
14100
|
-
status: "refunded",
|
|
14101
|
-
refundedCents: result.refundedCents,
|
|
14102
|
-
providerRefundIds: result.refundIds,
|
|
14103
|
-
revision: Number(seat.revision) + 1,
|
|
14104
|
-
updatedAt: now
|
|
14105
|
-
}),
|
|
14106
|
-
membershipUpdate("membershipEffect", `${seatId}:refund`, { status: "complete", updatedAt: now })
|
|
14107
|
-
], { mutationId: `named-seat-refunded:${seatId}`, guards: [
|
|
14108
|
-
{ ns: "networkSeat", id: seatId, exists: true, equals: { status: "refund_pending", revision: seat.revision } }
|
|
14109
|
-
] });
|
|
14110
|
-
}
|
|
14111
|
-
|
|
14112
14420
|
// src/membership-seat-provider-refund.ts
|
|
14113
14421
|
async function confirmNamedSeatProviderRefund(db, input, now) {
|
|
14114
14422
|
const seat = await membershipLoad(db, "networkSeat", input.seatId);
|
|
@@ -14130,7 +14438,7 @@ async function readNamedSeatInvitation(db, input, now) {
|
|
|
14130
14438
|
await loadMembershipAuthorityState(db, target);
|
|
14131
14439
|
const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
|
|
14132
14440
|
requireMembershipScope(member, target);
|
|
14133
|
-
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)
|
|
14441
|
+
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");
|
|
14134
14442
|
return {
|
|
14135
14443
|
seatId: input.seatId,
|
|
14136
14444
|
primaryApplicationId: target.applicationId,
|
|
@@ -14160,7 +14468,7 @@ async function readMembershipChapterEffect(db, input, now) {
|
|
|
14160
14468
|
requireMembershipScope(member, input);
|
|
14161
14469
|
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");
|
|
14162
14470
|
let applicable;
|
|
14163
|
-
if (input.kind === "invite_named_recipient") applicable = seat.status === "pending_acceptance" && seat.acceptedAt === void 0 && now < Number(
|
|
14471
|
+
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));
|
|
14164
14472
|
else if (input.kind === "revoke_named_recipient_access") applicable = ["refund_pending", "refunded"].includes(String(seat.status));
|
|
14165
14473
|
else applicable = (await readNamedSeatAccess(db, {
|
|
14166
14474
|
...input,
|
|
@@ -14189,6 +14497,7 @@ var MEMBERSHIP_SEAT_ACTION_FIELDS = {
|
|
|
14189
14497
|
"seat-reservations": ["recipientName", "recipientEmail", "purchaserEmail", "purchaserUserId"],
|
|
14190
14498
|
"seat-payments": ["seatId", "eventId"],
|
|
14191
14499
|
"seat-refunds": ["seatId", "eventId"],
|
|
14500
|
+
"seat-operator-refund": ["seatId"],
|
|
14192
14501
|
"seat-accept": recipient,
|
|
14193
14502
|
"seat-approve": recipient,
|
|
14194
14503
|
"seat-decline": recipient,
|
|
@@ -14257,6 +14566,7 @@ async function handleMembershipSeatAction(action, db, peer, input, now, stripeSe
|
|
|
14257
14566
|
if (refunded?.status !== "refunded" || refunded.refundedCents !== seat.amountCents) throw new Error("named seat refund unconfirmed");
|
|
14258
14567
|
return { seatId: seat.id, status: "refunded", refundedCents: refunded.refundedCents, subscriptionCanceled: false };
|
|
14259
14568
|
}
|
|
14569
|
+
if (action === "seat-operator-refund") return refundNamedSeatByOperator(db, { ...target, seatId: String(input.seatId) }, secret, now);
|
|
14260
14570
|
if (action === "seat-checkouts") return prepareNamedMembershipSeatPayment(db, {
|
|
14261
14571
|
...target,
|
|
14262
14572
|
priceId: peer.seatPriceId,
|
|
@@ -14345,7 +14655,7 @@ function createMembershipAuthorityHandler(options) {
|
|
|
14345
14655
|
if (req.method !== "POST") return respond({ error: "method_not_allowed" }, 405);
|
|
14346
14656
|
const sender = req.headers.get(import_db15.FEDERATION_HEADERS.sender);
|
|
14347
14657
|
const action = url.pathname.split("/").at(-1);
|
|
14348
|
-
if (!["offers", "checkouts", "payments", "approvals", "refunds", "renewals", "access", "restarts", "cancel-renewal", ...Object.keys(MEMBERSHIP_SEAT_ACTION_FIELDS)].includes(action ?? "")) return respond({ error: "unauthorized" }, 401);
|
|
14658
|
+
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);
|
|
14349
14659
|
const peer = peers.find((candidate) => candidate.appId === sender && url.pathname === `${MEMBERSHIP_AUTHORITY_PATH}/${candidate.runtime}/${action}` && !url.search);
|
|
14350
14660
|
if (!peer) return respond({ error: "unauthorized" }, 401);
|
|
14351
14661
|
let body;
|
|
@@ -14367,13 +14677,23 @@ function createMembershipAuthorityHandler(options) {
|
|
|
14367
14677
|
let input;
|
|
14368
14678
|
try {
|
|
14369
14679
|
input = JSON.parse(verified.body);
|
|
14370
|
-
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] ?? []];
|
|
14680
|
+
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] ?? []];
|
|
14371
14681
|
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])))) {
|
|
14372
14682
|
throw new TypeError("invalid request");
|
|
14373
14683
|
}
|
|
14374
14684
|
} catch {
|
|
14375
14685
|
return respond({ error: "invalid_offer_request" }, 400);
|
|
14376
14686
|
}
|
|
14687
|
+
if (action === "offer-preview") {
|
|
14688
|
+
return respond({ ok: true, preview: await previewMembershipCommercialOffer(db, {
|
|
14689
|
+
appId: peer.appId,
|
|
14690
|
+
chapterId: peer.chapterId,
|
|
14691
|
+
environment: peer.environment,
|
|
14692
|
+
runtime: peer.runtime,
|
|
14693
|
+
skuId: String(input.skuId),
|
|
14694
|
+
now: options.now?.() ?? Date.now()
|
|
14695
|
+
}) });
|
|
14696
|
+
}
|
|
14377
14697
|
if (Object.hasOwn(MEMBERSHIP_SEAT_ACTION_FIELDS, action)) {
|
|
14378
14698
|
return respond({ ok: true, seat: await handleMembershipSeatAction(
|
|
14379
14699
|
action,
|
|
@@ -14767,7 +15087,7 @@ async function drainMembershipEffects(db, options, now) {
|
|
|
14767
15087
|
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100 || !options.stripeSecretKey) throw new TypeError("invalid membership recovery configuration");
|
|
14768
15088
|
await loadMembershipAuthorityState(db, scope2);
|
|
14769
15089
|
const matches2 = (row2) => Object.entries(scope2).every(([key3, value]) => row2[key3] === value);
|
|
14770
|
-
let
|
|
15090
|
+
let pending = 0, complete = 0, more = false;
|
|
14771
15091
|
for (const status2 of ["payment_pending", "payment_processing", "payment_canceling"]) {
|
|
14772
15092
|
const seats = (await db.query({ networkSeat: { $: { where: { ...scope2, status: status2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkSeat ?? [];
|
|
14773
15093
|
more ||= seats.length > limit;
|
|
@@ -14785,26 +15105,6 @@ async function drainMembershipEffects(db, options, now) {
|
|
|
14785
15105
|
}
|
|
14786
15106
|
}
|
|
14787
15107
|
}
|
|
14788
|
-
for (const status2 of ["renewal_pending", "pending_acceptance"]) {
|
|
14789
|
-
const seats = (await db.query({ networkSeat: { $: { where: { ...scope2, status: status2 }, order: { acceptanceEndsAt: "asc" }, limit: limit + 1 } } })).networkSeat ?? [];
|
|
14790
|
-
more ||= seats.length > limit;
|
|
14791
|
-
for (const seat of seats.slice(0, limit)) {
|
|
14792
|
-
if (!matches2(seat)) throw new Error("membership recovery scope mismatch");
|
|
14793
|
-
if (seat.acceptedAt !== void 0 || now < Number(seat.acceptanceEndsAt)) continue;
|
|
14794
|
-
try {
|
|
14795
|
-
await requestNamedSeatRefund(db, {
|
|
14796
|
-
...scope2,
|
|
14797
|
-
applicationId: String(seat.applicationId),
|
|
14798
|
-
skuId: String(seat.skuId),
|
|
14799
|
-
seatId: String(seat.id),
|
|
14800
|
-
reason: "expired"
|
|
14801
|
-
}, now);
|
|
14802
|
-
expired++;
|
|
14803
|
-
} catch {
|
|
14804
|
-
pending++;
|
|
14805
|
-
}
|
|
14806
|
-
}
|
|
14807
|
-
}
|
|
14808
15108
|
const effects = (await db.query({ membershipEffect: { $: { where: {
|
|
14809
15109
|
chapterId: scope2.chapterId,
|
|
14810
15110
|
environment: scope2.environment,
|
|
@@ -14866,7 +15166,7 @@ async function drainMembershipEffects(db, options, now) {
|
|
|
14866
15166
|
pending++;
|
|
14867
15167
|
}
|
|
14868
15168
|
}
|
|
14869
|
-
return { complete, pending,
|
|
15169
|
+
return { complete, pending, more };
|
|
14870
15170
|
}
|
|
14871
15171
|
|
|
14872
15172
|
// src/approval-effects-retry.ts
|
|
@@ -15016,7 +15316,7 @@ function createMembershipChapterEffectRoute(options) {
|
|
|
15016
15316
|
config.emailTemplates = { ...config.emailTemplates, namedSeatInvitation: config.emailTemplates?.namedSeatInvitation ?? {
|
|
15017
15317
|
enabled: true,
|
|
15018
15318
|
subject: "{{purchaserName}} invited you to {{chapterName}}",
|
|
15019
|
-
text: "Hello {{firstName}},\n\n{{purchaserName}} has purchased a membership for you at {{chapterName}}. Your seat is already paid for.\n\nAccept your invitation
|
|
15319
|
+
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."
|
|
15020
15320
|
} };
|
|
15021
15321
|
const template = config.emailTemplates.namedSeatInvitation;
|
|
15022
15322
|
if (!template.text.includes("{{purchaserName}}")) config.emailTemplates.namedSeatInvitation = {
|
|
@@ -15089,6 +15389,12 @@ async function requestMembershipCommercialOffer(options) {
|
|
|
15089
15389
|
lifetimeMs: 6e4
|
|
15090
15390
|
});
|
|
15091
15391
|
}
|
|
15392
|
+
async function requestMembershipOfferPreview(options) {
|
|
15393
|
+
const result = await requestAuthority(options, "offer-preview", { skuId: options.skuId });
|
|
15394
|
+
const preview = result.preview;
|
|
15395
|
+
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");
|
|
15396
|
+
return preview;
|
|
15397
|
+
}
|
|
15092
15398
|
async function requestMembershipCheckoutBinding(options) {
|
|
15093
15399
|
const result = await requestAuthority(options, "checkouts", {
|
|
15094
15400
|
applicationId: options.applicationId,
|
|
@@ -15127,6 +15433,12 @@ async function requestNamedSeatDecline(options) {
|
|
|
15127
15433
|
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");
|
|
15128
15434
|
return seat;
|
|
15129
15435
|
}
|
|
15436
|
+
async function requestNamedSeatOperatorRefund(options) {
|
|
15437
|
+
const result = await requestAuthority(options, "seat-operator-refund", { applicationId: options.applicationId, skuId: options.skuId, seatId: options.seatId });
|
|
15438
|
+
const seat = result.seat;
|
|
15439
|
+
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");
|
|
15440
|
+
return seat;
|
|
15441
|
+
}
|
|
15130
15442
|
async function requestNamedSeatReservation(options) {
|
|
15131
15443
|
const result = await requestAuthority(options, "seat-reservations", {
|
|
15132
15444
|
applicationId: options.applicationId,
|
|
@@ -15144,7 +15456,7 @@ async function requestNamedSeatReservation(options) {
|
|
|
15144
15456
|
async function requestNamedSeatOffer(options) {
|
|
15145
15457
|
const result = await requestAuthority(options, "seat-offer", { applicationId: options.applicationId, skuId: options.skuId });
|
|
15146
15458
|
const offer = result.seat;
|
|
15147
|
-
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");
|
|
15459
|
+
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");
|
|
15148
15460
|
return offer;
|
|
15149
15461
|
}
|
|
15150
15462
|
async function requestNamedSeatInvitation(options) {
|
|
@@ -15312,6 +15624,7 @@ function createMembershipWorkerAdapter(options) {
|
|
|
15312
15624
|
return {
|
|
15313
15625
|
authority: {
|
|
15314
15626
|
issueOffer: async (input, env) => requestMembershipCommercialOffer({ ...input, ...await transport(input, env) }),
|
|
15627
|
+
previewOffer: async (input, env) => requestMembershipOfferPreview({ ...input, ...await transport(input, env) }),
|
|
15315
15628
|
bindCheckout: async (input, env) => requestMembershipCheckoutBinding({ ...input, ...await transport(input, env) }),
|
|
15316
15629
|
confirmPayment: async (input, env) => requestMembershipPayment({ ...input, ...await transport(input, env) }),
|
|
15317
15630
|
approveMembership: async (input, env) => requestMembershipApproval({ ...input, ...await transport(input, env) }),
|
|
@@ -15330,7 +15643,8 @@ function createMembershipWorkerAdapter(options) {
|
|
|
15330
15643
|
confirmRefund: async (input, env) => requestNamedSeatRefund2({ ...input, ...await transport(input, env) }),
|
|
15331
15644
|
accept: async (input, env) => requestNamedSeatAcceptance({ ...input, ...await transport(input, env) }),
|
|
15332
15645
|
approve: async (input, env) => requestNamedSeatApproval({ ...input, ...await transport(input, env) }),
|
|
15333
|
-
decline: async (input, env) => requestNamedSeatDecline({ ...input, ...await transport(input, env) })
|
|
15646
|
+
decline: async (input, env) => requestNamedSeatDecline({ ...input, ...await transport(input, env) }),
|
|
15647
|
+
operatorRefund: async (input, env) => requestNamedSeatOperatorRefund({ ...input, ...await transport(input, env) })
|
|
15334
15648
|
} } : {}
|
|
15335
15649
|
},
|
|
15336
15650
|
readEffect: async (input, env) => requestMembershipChapterEffect({ ...input, ...await transport(input, env) })
|
|
@@ -15585,6 +15899,8 @@ var BUILTIN_ROUTES = [
|
|
|
15585
15899
|
handleMembershipRestart,
|
|
15586
15900
|
handleMembershipCancellation,
|
|
15587
15901
|
handlePaymentRecovery,
|
|
15902
|
+
handleAdminNamedSeat,
|
|
15903
|
+
handleAdminMembershipSeats,
|
|
15588
15904
|
handleAdminMeetings,
|
|
15589
15905
|
handleAdminScheduling,
|
|
15590
15906
|
// Roster + identity
|