@odla-ai/chapter 0.52.1 → 0.54.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 +45 -1
- package/dist/{chunk-WWF2TO3H.js → chunk-NDZCR3OW.js} +43 -9
- package/dist/chunk-NDZCR3OW.js.map +1 -0
- package/dist/{chunk-TEHCORNC.js → chunk-SPEL4OGE.js} +57 -23
- package/dist/chunk-SPEL4OGE.js.map +1 -0
- package/dist/index.cjs +227 -113
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +33 -10
- package/dist/index.d.ts +33 -10
- package/dist/index.js +227 -113
- package/dist/index.js.map +1 -1
- package/dist/ui/admin/index.d.ts +4 -3
- package/dist/ui/admin/index.js +1 -1
- package/dist/ui/index.d.ts +1 -1
- package/dist/ui/index.js +8 -2
- package/dist/ui/member/index.d.ts +32 -1
- package/dist/ui/member/index.js +7 -1
- package/dist/worker/index.cjs +595 -212
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +116 -9
- package/dist/worker/index.d.ts +116 -9
- package/dist/worker/index.js +588 -205
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-TEHCORNC.js.map +0 -1
- package/dist/chunk-WWF2TO3H.js.map +0 -1
package/dist/worker/index.js
CHANGED
|
@@ -497,30 +497,30 @@ async function checkBillingTerms(secretKey, input, resume, now, createMissing) {
|
|
|
497
497
|
const terms2 = subscriptionBillingTerms(input.billingTerms);
|
|
498
498
|
if (!resume && terms2.initialTermEndsAt !== void 0 && (terms2.initialTermEndsAt <= now || terms2.initialTermEndsAt - now > 730 * 864e5)) fail();
|
|
499
499
|
if (!terms2.discount) return;
|
|
500
|
-
const { couponId
|
|
500
|
+
const { couponId, percentOffBps, duration, policyVersionId } = terms2.discount;
|
|
501
501
|
const productId = input.discountProductId;
|
|
502
502
|
if (productId !== void 0 && !/^prod_[A-Za-z0-9_]+$/.test(productId)) fail();
|
|
503
503
|
const metadata = { ...stripePartitionMetadata(input), discountPolicyVersionId: policyVersionId };
|
|
504
504
|
const expansion = { "expand[0]": "applies_to" };
|
|
505
|
-
let result = await stripeRequest({ secretKey }, "GET", `/v1/coupons/${encodeURIComponent(
|
|
505
|
+
let result = await stripeRequest({ secretKey }, "GET", `/v1/coupons/${encodeURIComponent(couponId)}`, expansion);
|
|
506
506
|
if (createMissing && result.status === 404 && result.body?.error?.code === "resource_missing") {
|
|
507
507
|
result = await stripeRequest(
|
|
508
508
|
{ secretKey },
|
|
509
509
|
"POST",
|
|
510
510
|
"/v1/coupons",
|
|
511
511
|
{
|
|
512
|
-
id:
|
|
512
|
+
id: couponId,
|
|
513
513
|
percent_off: percentOffBps / 100,
|
|
514
514
|
duration,
|
|
515
515
|
metadata,
|
|
516
516
|
...expansion,
|
|
517
517
|
...productId ? { "applies_to[products][0]": productId } : {}
|
|
518
518
|
},
|
|
519
|
-
{ idempotencyKey: `chapter-policy-coupon:${
|
|
519
|
+
{ idempotencyKey: `chapter-policy-coupon:${couponId}` }
|
|
520
520
|
);
|
|
521
521
|
}
|
|
522
522
|
const coupon = result.body;
|
|
523
|
-
if (!result.ok || !coupon || coupon.id !==
|
|
523
|
+
if (!result.ok || !coupon || coupon.id !== couponId || !resume && coupon.valid !== true || coupon.livemode !== (input.environment === "prod") || coupon.percent_off !== percentOffBps / 100 || coupon.duration !== duration || coupon.amount_off != null || (productId ? !Array.isArray(coupon.applies_to?.products) || coupon.applies_to.products.length !== 1 || coupon.applies_to.products[0] !== productId : coupon.applies_to != null) || !Object.entries(metadata).every(([key3, value]) => coupon.metadata?.[key3] === value)) fail();
|
|
524
524
|
}
|
|
525
525
|
function subscriptionBillingParams(input, productId) {
|
|
526
526
|
if (!input.billingTerms) return {};
|
|
@@ -1760,8 +1760,8 @@ var object = (value) => {
|
|
|
1760
1760
|
var id = (value) => typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9_.:-]{2,159}$/.test(value) ? value : fail2("id");
|
|
1761
1761
|
var integer2 = (value, min, max) => typeof value === "number" && Number.isSafeInteger(value) && value >= min && value <= max ? value : fail2("integer");
|
|
1762
1762
|
var time = (value) => {
|
|
1763
|
-
const
|
|
1764
|
-
return integer2(
|
|
1763
|
+
const at2 = typeof value === "number" ? value : typeof value === "string" ? Date.parse(value) : NaN;
|
|
1764
|
+
return integer2(at2, 0, 864e13);
|
|
1765
1765
|
};
|
|
1766
1766
|
var ids = (value, allowEmpty = false) => {
|
|
1767
1767
|
if (!Array.isArray(value) || value.length > 100 || !allowEmpty && value.length === 0) return fail2("ids");
|
|
@@ -1805,7 +1805,7 @@ function membershipOfferingVersion(value) {
|
|
|
1805
1805
|
term: normalizedTerm,
|
|
1806
1806
|
autoRenew: true,
|
|
1807
1807
|
paymentGraceDays: integer2(input.paymentGraceDays, 0, 60),
|
|
1808
|
-
seatPriceCents: integer2(input.seatPriceCents,
|
|
1808
|
+
seatPriceCents: integer2(input.seatPriceCents, 0, Number.MAX_SAFE_INTEGER),
|
|
1809
1809
|
...input.seatPurchaseWindowCalendarMonths === void 0 || input.seatPurchaseWindowCalendarMonths === null ? {} : { seatPurchaseWindowCalendarMonths: integer2(input.seatPurchaseWindowCalendarMonths, 1, 120) },
|
|
1810
1810
|
requiredDisclosures: ids(input.requiredDisclosures)
|
|
1811
1811
|
};
|
|
@@ -1890,7 +1890,7 @@ async function hash(value) {
|
|
|
1890
1890
|
const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify(value)));
|
|
1891
1891
|
return [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
1892
1892
|
}
|
|
1893
|
-
async function
|
|
1893
|
+
async function membershipDiscountCouponId(input, policyId) {
|
|
1894
1894
|
return policyId ? `odla_m_${await hash([input.appId, input.chapterId, input.environment, input.runtime, input.skuId, policyId])}` : void 0;
|
|
1895
1895
|
}
|
|
1896
1896
|
async function load(db, ns, id6) {
|
|
@@ -1916,7 +1916,7 @@ async function restore(row2, input) {
|
|
|
1916
1916
|
discountPolicy: contract.discountPolicy,
|
|
1917
1917
|
approvedFoundingCount: Number(row2.approvedFoundingCount),
|
|
1918
1918
|
now: Number(row2.issuedAt),
|
|
1919
|
-
couponId: await
|
|
1919
|
+
couponId: await membershipDiscountCouponId(input, contract.discountPolicy?.id)
|
|
1920
1920
|
});
|
|
1921
1921
|
if (expected.offering.chapterId !== input.chapterId || expected.offering.baseSkuId !== input.skuId) reject("invalid_offer_snapshot");
|
|
1922
1922
|
const result = {
|
|
@@ -1947,7 +1947,7 @@ async function previewMembershipCommercialOffer(db, input) {
|
|
|
1947
1947
|
discountPolicy: policy,
|
|
1948
1948
|
approvedFoundingCount: Number(approvedFoundingCount),
|
|
1949
1949
|
now: input.now,
|
|
1950
|
-
couponId: await
|
|
1950
|
+
couponId: await membershipDiscountCouponId(input, policy?.id)
|
|
1951
1951
|
});
|
|
1952
1952
|
const discount = contract.billingTerms.discount;
|
|
1953
1953
|
return {
|
|
@@ -1997,7 +1997,7 @@ async function issueMembershipCommercialOffer(db, input) {
|
|
|
1997
1997
|
discountPolicy: policy,
|
|
1998
1998
|
approvedFoundingCount: Number(approvedFoundingCount),
|
|
1999
1999
|
now: input.now,
|
|
2000
|
-
couponId: await
|
|
2000
|
+
couponId: await membershipDiscountCouponId(input, policy?.id)
|
|
2001
2001
|
});
|
|
2002
2002
|
const value = {
|
|
2003
2003
|
id: input.offerId,
|
|
@@ -2893,7 +2893,7 @@ async function checkoutVariables(db, group, app) {
|
|
|
2893
2893
|
const frozen = await frozenCheckoutVariables(db, app);
|
|
2894
2894
|
return frozen.tierName ? frozen : { ...frozen, tierName: await catalogTierName(db, group, app) };
|
|
2895
2895
|
}
|
|
2896
|
-
function formatInstant(
|
|
2896
|
+
function formatInstant(at2, timeZone) {
|
|
2897
2897
|
return new Intl.DateTimeFormat("en-US", {
|
|
2898
2898
|
weekday: "long",
|
|
2899
2899
|
month: "long",
|
|
@@ -2903,7 +2903,7 @@ function formatInstant(at, timeZone) {
|
|
|
2903
2903
|
minute: "2-digit",
|
|
2904
2904
|
timeZoneName: "short",
|
|
2905
2905
|
timeZone
|
|
2906
|
-
}).format(new Date(
|
|
2906
|
+
}).format(new Date(at2));
|
|
2907
2907
|
}
|
|
2908
2908
|
var adminUrlFor = (origin) => `${origin}/admin/`;
|
|
2909
2909
|
var membersUrlFor = (origin) => `${origin}/members/`;
|
|
@@ -3851,6 +3851,181 @@ async function refundNamedSeatStripePayment(secretKey, seat) {
|
|
|
3851
3851
|
return { refundedCents, refundIds };
|
|
3852
3852
|
}
|
|
3853
3853
|
|
|
3854
|
+
// src/membership-seat-reservation.ts
|
|
3855
|
+
async function recipientKey(input, email) {
|
|
3856
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify([input.appId, input.chapterId, input.environment, input.runtime, email])));
|
|
3857
|
+
return `seat-recipient:${[...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
3858
|
+
}
|
|
3859
|
+
async function reserveNamedMembershipSeat(db, input, now) {
|
|
3860
|
+
membershipClock(now);
|
|
3861
|
+
const name = typeof input.recipientName === "string" ? input.recipientName.trim().replace(/\s+/g, " ") : "";
|
|
3862
|
+
const email = normalizeChapterEmail(typeof input.recipientEmail === "string" ? input.recipientEmail : "");
|
|
3863
|
+
const purchaser = normalizeChapterEmail(typeof input.purchaserEmail === "string" ? input.purchaserEmail : "");
|
|
3864
|
+
if (name.length < 2 || name.length > 160 || /[\u0000-\u001f\u007f]/.test(input.recipientName) || email.length > 254 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) || !purchaser || email === purchaser || !/^price_[A-Za-z0-9_]+$/.test(input.priceId) || !/^user_[A-Za-z0-9_]+$/.test(input.purchaserUserId)) membershipFail("invalid_named_seat_recipient");
|
|
3865
|
+
const state = await loadMembershipAuthorityState(db, input);
|
|
3866
|
+
const memberId = await networkMembershipId(input), member = await membershipLoad(db, "networkMembership", memberId);
|
|
3867
|
+
requireMembershipScope(member, input);
|
|
3868
|
+
if (!Number.isSafeInteger(member.revision) || !/^price_[A-Za-z0-9_]+$/.test(String(member.providerPriceId)) || member.providerPriceId === input.priceId) membershipFail("named_seat_purchase_unavailable");
|
|
3869
|
+
const id6 = (await membershipOfferIdForRequest({ ...input, requestId: "named_seat" })).replace(/^offer_/, "seat_");
|
|
3870
|
+
let seat = await membershipLoad(db, "networkSeat", id6);
|
|
3871
|
+
if (!seat) {
|
|
3872
|
+
const offering = membershipOfferingVersion(member.offeringSnapshot), claimId = await recipientKey(input, email);
|
|
3873
|
+
const purchaseEndsAt = seatPurchaseDeadline(offering, member);
|
|
3874
|
+
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) {
|
|
3875
|
+
membershipFail("named_seat_purchase_unavailable");
|
|
3876
|
+
}
|
|
3877
|
+
const attrs = {
|
|
3878
|
+
id: id6,
|
|
3879
|
+
...membershipScope(input),
|
|
3880
|
+
primaryMembershipId: memberId,
|
|
3881
|
+
recipientName: name,
|
|
3882
|
+
recipientEmailNormalized: email,
|
|
3883
|
+
purchaserUserId: input.purchaserUserId,
|
|
3884
|
+
recipientClaimId: claimId,
|
|
3885
|
+
priceId: input.priceId,
|
|
3886
|
+
primaryPriceId: member.providerPriceId,
|
|
3887
|
+
amountCents: offering.seatPriceCents,
|
|
3888
|
+
currency: offering.currency,
|
|
3889
|
+
interval: offering.interval,
|
|
3890
|
+
providerCustomerId: member.providerCustomerId,
|
|
3891
|
+
providerSubscriptionId: member.providerSubscriptionId,
|
|
3892
|
+
status: "payment_pending",
|
|
3893
|
+
preparedAt: now,
|
|
3894
|
+
termEndsAt: member.termEndsAt,
|
|
3895
|
+
seatPurchaseEndsAt: purchaseEndsAt,
|
|
3896
|
+
offeringSnapshot: offering,
|
|
3897
|
+
revision: 1,
|
|
3898
|
+
createdAt: now,
|
|
3899
|
+
updatedAt: now
|
|
3900
|
+
};
|
|
3901
|
+
try {
|
|
3902
|
+
await db.transact([
|
|
3903
|
+
membershipUpdate("networkSeat", id6, attrs),
|
|
3904
|
+
membershipUpdate("networkSeatRecipient", claimId, {
|
|
3905
|
+
id: claimId,
|
|
3906
|
+
appId: input.appId,
|
|
3907
|
+
chapterId: input.chapterId,
|
|
3908
|
+
environment: input.environment,
|
|
3909
|
+
runtime: input.runtime,
|
|
3910
|
+
seatId: id6,
|
|
3911
|
+
recipientEmailNormalized: email,
|
|
3912
|
+
createdAt: now
|
|
3913
|
+
}),
|
|
3914
|
+
membershipUpdate("networkMembership", memberId, { namedSeatId: id6, revision: Number(member.revision) + 1, updatedAt: now })
|
|
3915
|
+
], { mutationId: `named-seat-reserve:${id6}`, guards: [
|
|
3916
|
+
{ ns: "networkSeat", id: id6, exists: false },
|
|
3917
|
+
{ ns: "networkSeatRecipient", id: claimId, exists: false },
|
|
3918
|
+
{ ns: "networkMembership", id: memberId, exists: true, equals: { revision: member.revision }, absent: ["namedSeatId"] },
|
|
3919
|
+
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
|
|
3920
|
+
] });
|
|
3921
|
+
} catch (error) {
|
|
3922
|
+
if (error.code !== "transact_guard_failed") throw error;
|
|
3923
|
+
}
|
|
3924
|
+
seat = await membershipLoad(db, "networkSeat", id6);
|
|
3925
|
+
if (!seat && await membershipLoad(db, "networkSeatRecipient", claimId)) membershipFail("named_seat_recipient_unavailable");
|
|
3926
|
+
}
|
|
3927
|
+
requireMembershipScope(seat, input);
|
|
3928
|
+
if (seat.primaryMembershipId !== memberId || seat.recipientName !== name || seat.recipientEmailNormalized !== email || seat.priceId !== input.priceId || seat.purchaserUserId !== input.purchaserUserId) membershipFail("named_seat_purchase_conflict");
|
|
3929
|
+
return seat;
|
|
3930
|
+
}
|
|
3931
|
+
|
|
3932
|
+
// src/membership-seat-included.ts
|
|
3933
|
+
var namedSeatIncluded = (seat) => Number(seat.amountCents) === 0;
|
|
3934
|
+
var namedSeatSettled = (seat) => namedSeatIncluded(seat) ? seat.purchasedAt !== void 0 : Boolean(seat.providerPaymentIntentId && seat.providerSubscriptionItemId);
|
|
3935
|
+
function namedSeatRecipientState(seat) {
|
|
3936
|
+
switch (String(seat.status)) {
|
|
3937
|
+
case "renewal_pending":
|
|
3938
|
+
case "pending_acceptance":
|
|
3939
|
+
return "invited";
|
|
3940
|
+
case "pending_approval":
|
|
3941
|
+
case "pending_primary":
|
|
3942
|
+
return "accepted";
|
|
3943
|
+
case "active":
|
|
3944
|
+
return "joined";
|
|
3945
|
+
case "refund_pending":
|
|
3946
|
+
case "refunded":
|
|
3947
|
+
return seat.refundReason === "declined" ? "declined" : "refunded";
|
|
3948
|
+
case "canceled":
|
|
3949
|
+
return "canceled";
|
|
3950
|
+
default:
|
|
3951
|
+
return "payment_pending";
|
|
3952
|
+
}
|
|
3953
|
+
}
|
|
3954
|
+
var OPEN = ["pending_acceptance", "pending_approval", "pending_primary", "active"];
|
|
3955
|
+
async function grantIncludedNamedSeat(db, input, now) {
|
|
3956
|
+
membershipClock(now);
|
|
3957
|
+
const seat = await reserveNamedMembershipSeat(db, input, now);
|
|
3958
|
+
if (!namedSeatIncluded(seat)) membershipFail("named_seat_not_included");
|
|
3959
|
+
const state = await loadMembershipAuthorityState(db, input);
|
|
3960
|
+
const member = await membershipLoad(db, "networkMembership", seat.primaryMembershipId);
|
|
3961
|
+
requireMembershipScope(member, input);
|
|
3962
|
+
if (!["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) || Number(member.refundedCents) !== 0 || member.namedSeatId !== seat.id || now >= Number(member.termEndsAt)) {
|
|
3963
|
+
membershipFail("named_seat_purchase_unavailable");
|
|
3964
|
+
}
|
|
3965
|
+
if (seat.status === "payment_pending") {
|
|
3966
|
+
const entitlementId = `${seat.id}:entitlement`, inviteId = `${seat.id}:invite`, source = `included:${seat.id}`;
|
|
3967
|
+
try {
|
|
3968
|
+
await db.transact([
|
|
3969
|
+
membershipUpdate("networkSeat", seat.id, {
|
|
3970
|
+
status: "pending_acceptance",
|
|
3971
|
+
purchasedAt: now,
|
|
3972
|
+
termStartedAt: now,
|
|
3973
|
+
termEndsAt: member.termEndsAt,
|
|
3974
|
+
acceptanceEndsAt: Number(member.termEndsAt),
|
|
3975
|
+
entitlementId,
|
|
3976
|
+
revision: Number(seat.revision) + 1,
|
|
3977
|
+
updatedAt: now
|
|
3978
|
+
}),
|
|
3979
|
+
membershipUpdate("networkEntitlement", entitlementId, {
|
|
3980
|
+
id: entitlementId,
|
|
3981
|
+
...membershipScope(input),
|
|
3982
|
+
membershipId: member.id,
|
|
3983
|
+
seatId: seat.id,
|
|
3984
|
+
kind: "named_seat",
|
|
3985
|
+
status: "pending_acceptance",
|
|
3986
|
+
termStartedAt: now,
|
|
3987
|
+
termEndsAt: member.termEndsAt,
|
|
3988
|
+
offeringSnapshot: member.offeringSnapshot,
|
|
3989
|
+
sourceEventId: source,
|
|
3990
|
+
revision: 1,
|
|
3991
|
+
createdAt: now,
|
|
3992
|
+
updatedAt: now
|
|
3993
|
+
}),
|
|
3994
|
+
membershipUpdate("membershipEffect", inviteId, {
|
|
3995
|
+
id: inviteId,
|
|
3996
|
+
chapterId: input.chapterId,
|
|
3997
|
+
environment: input.environment,
|
|
3998
|
+
runtime: input.runtime,
|
|
3999
|
+
membershipId: member.id,
|
|
4000
|
+
seatId: seat.id,
|
|
4001
|
+
kind: "invite_named_recipient",
|
|
4002
|
+
status: "pending",
|
|
4003
|
+
attempts: 0,
|
|
4004
|
+
createdAt: now,
|
|
4005
|
+
updatedAt: now
|
|
4006
|
+
})
|
|
4007
|
+
], { mutationId: `named-seat-included:${seat.id}`, guards: [
|
|
4008
|
+
{
|
|
4009
|
+
ns: "networkSeat",
|
|
4010
|
+
id: seat.id,
|
|
4011
|
+
exists: true,
|
|
4012
|
+
equals: { status: "payment_pending", revision: seat.revision, amountCents: 0 },
|
|
4013
|
+
absent: ["purchasedAt", "providerPaymentIntentId"]
|
|
4014
|
+
},
|
|
4015
|
+
{ ns: "networkEntitlement", id: entitlementId, exists: false },
|
|
4016
|
+
{ ns: "networkMembership", id: String(member.id), exists: true, equals: { revision: member.revision, namedSeatId: seat.id } },
|
|
4017
|
+
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
|
|
4018
|
+
] });
|
|
4019
|
+
} catch (error) {
|
|
4020
|
+
if (error.code !== "transact_guard_failed") throw error;
|
|
4021
|
+
}
|
|
4022
|
+
}
|
|
4023
|
+
const current = await membershipLoad(db, "networkSeat", seat.id);
|
|
4024
|
+
requireMembershipScope(current, input);
|
|
4025
|
+
if (!namedSeatIncluded(current) || current.purchasedAt === void 0 || !OPEN.includes(String(current.status))) membershipFail("named_seat_purchase_unavailable");
|
|
4026
|
+
return { seatId: seat.id, amountCents: 0, currency: seat.currency, termEndsAt: Number(current.termEndsAt), status: String(current.status), included: true };
|
|
4027
|
+
}
|
|
4028
|
+
|
|
3854
4029
|
// src/membership-seat-refund.ts
|
|
3855
4030
|
async function requestNamedSeatRefund(db, input, now) {
|
|
3856
4031
|
membershipClock(now);
|
|
@@ -3860,7 +4035,7 @@ async function requestNamedSeatRefund(db, input, now) {
|
|
|
3860
4035
|
const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
|
|
3861
4036
|
requireMembershipScope(member, input);
|
|
3862
4037
|
if (seat.status === "refunded" || seat.status === "refund_pending") return;
|
|
3863
|
-
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");
|
|
4038
|
+
if (!seat.purchasedAt || !seat.providerPaymentIntentId && !namedSeatIncluded(seat) || input.reason === "declined" && seat.journeyApprovedAt !== void 0 || input.reason === "primary_closed" && !["refunded", "declined"].includes(String(member.status))) membershipFail("named_seat_refund_not_eligible");
|
|
3864
4039
|
const entitlement = seat.entitlementId ? await membershipLoad(db, "networkEntitlement", String(seat.entitlementId)) : void 0;
|
|
3865
4040
|
await db.transact([
|
|
3866
4041
|
membershipUpdate("networkSeat", input.seatId, { status: "refund_pending", refundReason: input.reason, revision: Number(seat.revision) + 1, updatedAt: now }),
|
|
@@ -3905,7 +4080,7 @@ async function completeNamedSeatRefund(db, seatId, secretKey, now) {
|
|
|
3905
4080
|
await loadMembershipAuthorityState(db, seat);
|
|
3906
4081
|
if (seat.status === "refunded") return;
|
|
3907
4082
|
if (seat.status !== "refund_pending") membershipFail("named_seat_refund_not_ready");
|
|
3908
|
-
const result = await refundNamedSeatStripePayment(secretKey, seat);
|
|
4083
|
+
const result = namedSeatIncluded(seat) ? { refundedCents: 0, refundIds: [] } : await refundNamedSeatStripePayment(secretKey, seat);
|
|
3909
4084
|
await db.transact([
|
|
3910
4085
|
membershipUpdate("networkSeat", seatId, {
|
|
3911
4086
|
status: "refunded",
|
|
@@ -3927,7 +4102,7 @@ async function refundNamedSeatByOperator(db, input, stripeSecretKey, now) {
|
|
|
3927
4102
|
await requestNamedSeatRefund(db, { ...input, reason: "operator" }, now);
|
|
3928
4103
|
await completeNamedSeatRefund(db, input.seatId, stripeSecretKey, now);
|
|
3929
4104
|
const refunded = await membershipLoad(db, "networkSeat", input.seatId);
|
|
3930
|
-
if (refunded?.status !== "refunded" || !Number.isSafeInteger(refunded.refundedCents) || Number(refunded.refundedCents)
|
|
4105
|
+
if (refunded?.status !== "refunded" || !Number.isSafeInteger(refunded.refundedCents) || Number(refunded.refundedCents) !== Number(seat.amountCents)) {
|
|
3931
4106
|
throw new Error("named seat refund unconfirmed");
|
|
3932
4107
|
}
|
|
3933
4108
|
return { seatId: String(seat.id), status: "refunded", refundedCents: Number(refunded.refundedCents), subscriptionCanceled: false };
|
|
@@ -3959,9 +4134,56 @@ async function liftSeatPurchaseWindow(db, scope2, now, limit = 100) {
|
|
|
3959
4134
|
return { lifted, more: rows5.length > limit };
|
|
3960
4135
|
}
|
|
3961
4136
|
|
|
4137
|
+
// src/membership-seat-reprice.ts
|
|
4138
|
+
async function lowerSeatPrice(db, scope2, now, limit = 100) {
|
|
4139
|
+
membershipClock(now);
|
|
4140
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200) throw new TypeError("invalid reprice limit");
|
|
4141
|
+
const state = await loadMembershipAuthorityState(db, scope2);
|
|
4142
|
+
const versions = (await db.query({ membershipOfferingVersion: { $: { where: { chapterId: scope2.chapterId }, limit: 200 } } })).membershipOfferingVersion ?? [];
|
|
4143
|
+
if (versions.length >= 200) throw Object.assign(new Error("policy_history_incomplete"), { code: "policy_history_incomplete" });
|
|
4144
|
+
const current = versions.map((row2) => membershipOfferingVersion(row2.snapshot)).filter((offer) => offer.baseSkuId === scope2.skuId && offer.status === "published" && now >= offer.effectiveFrom && (offer.effectiveUntil === void 0 || now < offer.effectiveUntil)).sort((a, b) => b.version - a.version)[0];
|
|
4145
|
+
if (!current) throw Object.assign(new Error("ambiguous_membership_policy"), { code: "ambiguous_membership_policy" });
|
|
4146
|
+
const target = current.seatPriceCents;
|
|
4147
|
+
const rows5 = (await db.query({ networkMembership: { $: { where: { ...scope2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkMembership ?? [];
|
|
4148
|
+
const receipt = { seatPriceCents: target, repriced: 0, skipped: [], more: rows5.length > limit };
|
|
4149
|
+
for (const member of rows5.slice(0, limit)) {
|
|
4150
|
+
if (Object.entries(scope2).some(([key3, value]) => member[key3] !== value)) throw new Error("membership reprice scope mismatch");
|
|
4151
|
+
const id6 = String(member.id), skip = (reason) => {
|
|
4152
|
+
receipt.skipped.push({ membershipId: id6, reason });
|
|
4153
|
+
};
|
|
4154
|
+
const offering = membershipOfferingVersion(member.offeringSnapshot);
|
|
4155
|
+
if (offering.seatPriceCents <= target) {
|
|
4156
|
+
skip("seat price already at or below the current offering");
|
|
4157
|
+
continue;
|
|
4158
|
+
}
|
|
4159
|
+
if (!["paid_pending_approval", "active", "cancel_at_term_end", "payment_grace"].includes(String(member.status))) {
|
|
4160
|
+
skip(`membership is ${String(member.status)}`);
|
|
4161
|
+
continue;
|
|
4162
|
+
}
|
|
4163
|
+
const seat = member.namedSeatId ? await membershipLoad(db, "networkSeat", String(member.namedSeatId)) : void 0;
|
|
4164
|
+
if (seat && (seat.status !== "payment_pending" || seat.providerPaymentIntentId !== void 0)) {
|
|
4165
|
+
skip(`seat is ${String(seat.status)}`);
|
|
4166
|
+
continue;
|
|
4167
|
+
}
|
|
4168
|
+
const ops = [membershipUpdate("networkMembership", id6, { offeringSnapshot: { ...offering, seatPriceCents: target }, revision: Number(member.revision) + 1, updatedAt: now })];
|
|
4169
|
+
const guards = [
|
|
4170
|
+
{ ns: "networkMembership", id: id6, exists: true, equals: { revision: member.revision } },
|
|
4171
|
+
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative" } }
|
|
4172
|
+
];
|
|
4173
|
+
if (seat) {
|
|
4174
|
+
ops.push(membershipUpdate("networkSeat", String(seat.id), { amountCents: target, offeringSnapshot: { ...offering, seatPriceCents: target }, revision: Number(seat.revision) + 1, updatedAt: now }));
|
|
4175
|
+
guards.push({ ns: "networkSeat", id: String(seat.id), exists: true, equals: { revision: seat.revision, status: "payment_pending" } });
|
|
4176
|
+
}
|
|
4177
|
+
await db.transact(ops, { mutationId: `seat-reprice:${id6}:${member.revision}`, guards });
|
|
4178
|
+
receipt.repriced++;
|
|
4179
|
+
}
|
|
4180
|
+
return receipt;
|
|
4181
|
+
}
|
|
4182
|
+
|
|
3962
4183
|
// src/worker-routes-admin-membership-seats.ts
|
|
3963
4184
|
var LIST = "/api/admin/membership-seats";
|
|
3964
4185
|
var LIFT = "/api/admin/membership-seats/lift-purchase-window";
|
|
4186
|
+
var REPRICE = "/api/admin/membership-seats/reprice";
|
|
3965
4187
|
var ONE = /^\/api\/admin\/membership-seats\/([A-Za-z0-9][A-Za-z0-9_.:-]{2,159})\/refund$/;
|
|
3966
4188
|
var SCOPE = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
3967
4189
|
var PUBLIC = [
|
|
@@ -3988,11 +4210,11 @@ var PUBLIC = [
|
|
|
3988
4210
|
];
|
|
3989
4211
|
var project = (row2) => Object.fromEntries(PUBLIC.filter((key3) => row2[key3] !== void 0).map((key3) => [key3, row2[key3]]));
|
|
3990
4212
|
var handleAdminMembershipSeats = async (req, url, env, ctx) => {
|
|
3991
|
-
const one = url.pathname.match(ONE), lift = url.pathname === LIFT;
|
|
3992
|
-
if (url.pathname !== LIST && !one && !lift) return null;
|
|
4213
|
+
const one = url.pathname.match(ONE), lift = url.pathname === LIFT, reprice = url.pathname === REPRICE;
|
|
4214
|
+
if (url.pathname !== LIST && !one && !lift && !reprice) return null;
|
|
3993
4215
|
const operator = ctx.membershipAuthorityOperator;
|
|
3994
4216
|
if (!operator) return json({ error: "not found" }, 404);
|
|
3995
|
-
if (req.method !== (one || lift ? "POST" : "GET")) return json({ error: "method not allowed" }, 405);
|
|
4217
|
+
if (req.method !== (one || lift || reprice ? "POST" : "GET")) return json({ error: "method not allowed" }, 405);
|
|
3996
4218
|
const raw2 = ctx.makeDb(env), user = await ctx.verifyUser(req, env);
|
|
3997
4219
|
if (!user) return json({ error: "unauthorized" }, 401);
|
|
3998
4220
|
if (!await ctx.isSuperAdminEmail(raw2, user.email, user.userId)) return json({ error: "super-admin required", code: "super_admin_required" }, 403);
|
|
@@ -4002,7 +4224,7 @@ var handleAdminMembershipSeats = async (req, url, env, ctx) => {
|
|
|
4002
4224
|
} catch {
|
|
4003
4225
|
return json({ error: "the membership authority is not hosted by this deployment" }, 404);
|
|
4004
4226
|
}
|
|
4005
|
-
if (lift) {
|
|
4227
|
+
if (lift || reprice) {
|
|
4006
4228
|
let body;
|
|
4007
4229
|
try {
|
|
4008
4230
|
body = await req.json();
|
|
@@ -4010,14 +4232,16 @@ var handleAdminMembershipSeats = async (req, url, env, ctx) => {
|
|
|
4010
4232
|
return json({ error: "chapterId and runtime are required" }, 400);
|
|
4011
4233
|
}
|
|
4012
4234
|
const appId = body.appId ?? body.chapterId;
|
|
4013
|
-
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)) {
|
|
4014
|
-
return json({ error: "chapterId and runtime are required" }, 400);
|
|
4235
|
+
if (typeof body.chapterId !== "string" || !SCOPE.test(body.chapterId) || typeof appId !== "string" || !SCOPE.test(appId) || typeof body.runtime !== "string" || !/^[a-z0-9]{2,12}$/.test(body.runtime) || reprice && (typeof body.skuId !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_.:-]{2,159}$/.test(body.skuId))) {
|
|
4236
|
+
return json({ error: reprice ? "chapterId, runtime and skuId are required" : "chapterId and runtime are required" }, 400);
|
|
4015
4237
|
}
|
|
4238
|
+
const scope2 = { appId, chapterId: body.chapterId, environment: env.ODLA_ENV, runtime: body.runtime };
|
|
4016
4239
|
try {
|
|
4017
|
-
return json({ ok: true, ...await
|
|
4240
|
+
if (reprice) return json({ ok: true, ...await lowerSeatPrice(db, { ...scope2, skuId: String(body.skuId) }, Date.now()) });
|
|
4241
|
+
return json({ ok: true, ...await liftSeatPurchaseWindow(db, scope2, Date.now()) });
|
|
4018
4242
|
} catch (error) {
|
|
4019
4243
|
const code = error.code;
|
|
4020
|
-
return json({ error: "purchase window lift is pending; retry to confirm
|
|
4244
|
+
return json({ error: `${reprice ? "seat reprice" : "purchase window lift"} is pending; retry to confirm`, ...code ? { code } : {} }, code ? 409 : 503);
|
|
4021
4245
|
}
|
|
4022
4246
|
}
|
|
4023
4247
|
if (!one) {
|
|
@@ -4053,8 +4277,8 @@ var handleAdminMembershipSeats = async (req, url, env, ctx) => {
|
|
|
4053
4277
|
var LIMIT = 200;
|
|
4054
4278
|
var text2 = (value) => typeof value === "string" && value.length > 0 && value.length <= 200 ? value : void 0;
|
|
4055
4279
|
var instant = (value) => {
|
|
4056
|
-
const
|
|
4057
|
-
return Number.isSafeInteger(
|
|
4280
|
+
const at2 = typeof value === "number" ? value : typeof value === "string" ? Date.parse(value) : NaN;
|
|
4281
|
+
return Number.isSafeInteger(at2) && at2 >= 0 ? at2 : void 0;
|
|
4058
4282
|
};
|
|
4059
4283
|
function provenance(row2) {
|
|
4060
4284
|
const source = text2(row2.source), actor = text2(row2.actor ?? row2.$createdBy), recordedAt = instant(row2.recordedAt ?? row2.createdAt ?? row2.$createdAt);
|
|
@@ -4150,6 +4374,239 @@ var handleAdminMembershipPolicyVersions = async (req, url, env, ctx) => {
|
|
|
4150
4374
|
return json(await listMembershipPolicyVersions(db, { chapterId, now: Date.now() }));
|
|
4151
4375
|
};
|
|
4152
4376
|
|
|
4377
|
+
// src/membership-discount-reterm.ts
|
|
4378
|
+
import { stripeRequest as stripeRequest6 } from "@odla-ai/stripe";
|
|
4379
|
+
|
|
4380
|
+
// src/subscription-provider-contract.ts
|
|
4381
|
+
import { stripeRequest as stripeRequest5 } from "@odla-ai/stripe";
|
|
4382
|
+
function subscriptionContractMismatch() {
|
|
4383
|
+
throw Object.assign(new Error("Stripe subscription does not match the checkout contract"), { code: "stripe_contract_mismatch" });
|
|
4384
|
+
}
|
|
4385
|
+
async function verifySubscriptionPrice(secretKey, input, resume = false) {
|
|
4386
|
+
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();
|
|
4387
|
+
const result = await stripeRequest5({ secretKey }, "GET", `/v1/prices/${encodeURIComponent(input.priceId)}`);
|
|
4388
|
+
const price = result.body;
|
|
4389
|
+
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();
|
|
4390
|
+
return typeof price.product === "string" ? price.product : void 0;
|
|
4391
|
+
}
|
|
4392
|
+
function verifySubscriptionResult(sub, input, customerId, metadata) {
|
|
4393
|
+
const items = sub?.items;
|
|
4394
|
+
if (!sub || !/^sub_[A-Za-z0-9]+$/.test(sub.id) || sub.customer !== customerId || input.existingSubscriptionId && sub.id !== input.existingSubscriptionId || !(input.billingTerms?.initialTermEndsAt ? ["incomplete", "active", "trialing"] : ["incomplete", "active"]).includes(sub.status) || sub.livemode !== (input.environment === "prod") || !Object.entries(metadata).every(([key3, value]) => sub.metadata?.[key3] === value) || !Array.isArray(items?.data) || items.has_more !== false || items.data.length !== 1 || items.data[0]?.price?.id !== input.priceId || items.data[0]?.quantity !== 1) subscriptionContractMismatch();
|
|
4395
|
+
const invoice = sub.latest_invoice;
|
|
4396
|
+
if (!invoice || typeof invoice !== "object" || invoice.customer !== customerId || invoice.currency !== input.currency || invoice.total !== subscriptionAmountDue(input) || invoice.billing_reason !== "subscription_create") subscriptionContractMismatch();
|
|
4397
|
+
verifySubscriptionBillingResult(sub, input);
|
|
4398
|
+
}
|
|
4399
|
+
|
|
4400
|
+
// src/membership-discount-reterm.ts
|
|
4401
|
+
var SAFE = /^[A-Za-z0-9][A-Za-z0-9_.:-]{2,159}$/;
|
|
4402
|
+
var RETERMABLE = /* @__PURE__ */ new Set(["active", "paid_pending_approval", "cancel_at_term_end", "payment_grace"]);
|
|
4403
|
+
var PROVIDER_LIVE = /* @__PURE__ */ new Set(["active", "trialing", "past_due"]);
|
|
4404
|
+
var couponOf = (discount) => {
|
|
4405
|
+
const ref = discount?.source?.coupon ?? discount?.coupon;
|
|
4406
|
+
return typeof ref === "string" ? ref : typeof ref?.id === "string" ? ref.id : void 0;
|
|
4407
|
+
};
|
|
4408
|
+
async function retermPolicy(db, scope2, id6) {
|
|
4409
|
+
const row2 = await membershipLoad(db, "discountPolicyVersion", id6);
|
|
4410
|
+
if (!row2) membershipFail("membership_reterm_policy_unknown");
|
|
4411
|
+
let policy;
|
|
4412
|
+
try {
|
|
4413
|
+
policy = membershipDiscountPolicyVersion(row2.snapshot);
|
|
4414
|
+
} catch {
|
|
4415
|
+
return membershipFail("membership_reterm_policy_ineligible");
|
|
4416
|
+
}
|
|
4417
|
+
if (policy.id !== id6 || policy.chapterId !== scope2.chapterId || policy.status !== "published" || policy.benefitDuration.kind !== "while_active" || policy.percentOffBps <= 0 || policy.discountEligibleSkuIds.length === 0) membershipFail("membership_reterm_policy_ineligible");
|
|
4418
|
+
return policy;
|
|
4419
|
+
}
|
|
4420
|
+
async function attachProviderDiscount(sk, scope2, member, policy, couponId, productId) {
|
|
4421
|
+
const subId = String(member.providerSubscriptionId), path = `/v1/subscriptions/${encodeURIComponent(subId)}`;
|
|
4422
|
+
const read2 = await stripeRequest6({ secretKey: sk }, "GET", path, { "expand[0]": "discounts" });
|
|
4423
|
+
const sub = read2.body, items = sub?.items?.data;
|
|
4424
|
+
const metadata = { ...stripePartitionMetadata(scope2), applicationId: String(member.applicationId) };
|
|
4425
|
+
if (!read2.ok || sub.id !== subId || sub.customer !== member.providerCustomerId || sub.livemode !== (scope2.environment === "prod") || !PROVIDER_LIVE.has(sub.status) || !Object.entries(metadata).every(([key3, value]) => sub.metadata?.[key3] === value) || sub.items?.has_more !== false || !Array.isArray(items) || items.length < 1 || items.length > 2 || items.filter((item) => item.price?.id === member.providerPriceId && item.quantity === 1 && item.price?.product === productId).length !== 1) {
|
|
4426
|
+
throw Object.assign(new Error("provider subscription does not match the frozen membership"), { code: "provider_mismatch" });
|
|
4427
|
+
}
|
|
4428
|
+
const discounts = sub.discounts ?? (sub.discount ? [sub.discount] : []);
|
|
4429
|
+
if (!Array.isArray(discounts)) throw Object.assign(new Error("provider discounts unreadable"), { code: "provider_mismatch" });
|
|
4430
|
+
const present = discounts.map(couponOf);
|
|
4431
|
+
if (present.length === 1 && present[0] === couponId) return false;
|
|
4432
|
+
if (present.length > 0) throw Object.assign(new Error("provider subscription carries an unrelated discount"), { code: "provider_mismatch" });
|
|
4433
|
+
const written = await stripeRequest6({ secretKey: sk }, "POST", path, {
|
|
4434
|
+
"discounts[0][coupon]": couponId,
|
|
4435
|
+
"expand[0]": "discounts",
|
|
4436
|
+
"metadata[discountPolicyVersionId]": policy.id
|
|
4437
|
+
}, { idempotencyKey: `membership-reterm:${String(member.id)}:${policy.id}` });
|
|
4438
|
+
const after = written.body?.discounts;
|
|
4439
|
+
if (!written.ok || written.body?.id !== subId || !Array.isArray(after) || after.length !== 1 || couponOf(after[0]) !== couponId) {
|
|
4440
|
+
membershipFail("membership_reterm_provider_unconfirmed");
|
|
4441
|
+
}
|
|
4442
|
+
return true;
|
|
4443
|
+
}
|
|
4444
|
+
async function retermMembershipDiscount(db, stripeSecretKey, scope2, input, now) {
|
|
4445
|
+
membershipClock(now);
|
|
4446
|
+
const limit = input.limit ?? 100;
|
|
4447
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200) throw new TypeError("invalid reterm limit");
|
|
4448
|
+
if (typeof input.policyVersionId !== "string" || !SAFE.test(input.policyVersionId)) throw new TypeError("invalid policy version id");
|
|
4449
|
+
if (input.membershipIds !== void 0 && (!Array.isArray(input.membershipIds) || input.membershipIds.length === 0 || input.membershipIds.length > limit || input.membershipIds.some((id6) => typeof id6 !== "string" || !SAFE.test(id6)))) throw new TypeError("invalid membership ids");
|
|
4450
|
+
const state = await loadMembershipAuthorityState(db, scope2);
|
|
4451
|
+
const policy = await retermPolicy(db, scope2, input.policyVersionId);
|
|
4452
|
+
const receipt = { policyVersionId: policy.id, retermed: [], unchanged: [], skipped: [], more: false };
|
|
4453
|
+
const skip = (membershipId, reason) => {
|
|
4454
|
+
receipt.skipped.push({ membershipId, reason });
|
|
4455
|
+
};
|
|
4456
|
+
let rows5;
|
|
4457
|
+
if (input.membershipIds) {
|
|
4458
|
+
rows5 = [];
|
|
4459
|
+
for (const id6 of input.membershipIds) {
|
|
4460
|
+
const member = await membershipLoad(db, "networkMembership", id6);
|
|
4461
|
+
if (!member) skip(id6, "not_found");
|
|
4462
|
+
else if (Object.entries(scope2).some(([key3, value]) => member[key3] !== value)) skip(id6, "scope_mismatch");
|
|
4463
|
+
else rows5.push(member);
|
|
4464
|
+
}
|
|
4465
|
+
} else {
|
|
4466
|
+
const found = (await db.query({ networkMembership: { $: { where: { ...scope2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkMembership ?? [];
|
|
4467
|
+
if (found.some((member) => Object.entries(scope2).some(([key3, value]) => member[key3] !== value))) throw new Error("membership reterm scope mismatch");
|
|
4468
|
+
receipt.more = found.length > limit;
|
|
4469
|
+
rows5 = found.slice(0, limit);
|
|
4470
|
+
}
|
|
4471
|
+
for (const member of rows5) {
|
|
4472
|
+
const id6 = String(member.id);
|
|
4473
|
+
if (member.discountPolicyVersionId === policy.id) {
|
|
4474
|
+
receipt.unchanged.push(id6);
|
|
4475
|
+
continue;
|
|
4476
|
+
}
|
|
4477
|
+
if (!RETERMABLE.has(String(member.status))) {
|
|
4478
|
+
skip(id6, "status");
|
|
4479
|
+
continue;
|
|
4480
|
+
}
|
|
4481
|
+
if (member.founding !== true) {
|
|
4482
|
+
skip(id6, "not_founding");
|
|
4483
|
+
continue;
|
|
4484
|
+
}
|
|
4485
|
+
if (member.cohort !== policy.cohortLabel) {
|
|
4486
|
+
skip(id6, "cohort_mismatch");
|
|
4487
|
+
continue;
|
|
4488
|
+
}
|
|
4489
|
+
const skuId = String(member.baseSkuId);
|
|
4490
|
+
if (!policy.discountEligibleSkuIds.includes(skuId)) {
|
|
4491
|
+
skip(id6, "sku_not_discounted");
|
|
4492
|
+
continue;
|
|
4493
|
+
}
|
|
4494
|
+
const base = Number(member.baseAmountCents), current = Number(member.renewalAmountCents);
|
|
4495
|
+
const renewal = Math.round(base * (1e4 - policy.percentOffBps) / 1e4);
|
|
4496
|
+
if (!Number.isSafeInteger(base) || base <= 0 || !Number.isSafeInteger(current) || renewal > current) {
|
|
4497
|
+
skip(id6, "would_not_widen");
|
|
4498
|
+
continue;
|
|
4499
|
+
}
|
|
4500
|
+
const couponId = await membershipDiscountCouponId({ ...scope2, skuId }, policy.id);
|
|
4501
|
+
let providerUpdated;
|
|
4502
|
+
try {
|
|
4503
|
+
const productId = await verifySubscriptionPrice(stripeSecretKey, {
|
|
4504
|
+
priceId: String(member.providerPriceId),
|
|
4505
|
+
priceCents: base,
|
|
4506
|
+
currency: String(member.currency),
|
|
4507
|
+
interval: member.interval === "month" ? "month" : "year",
|
|
4508
|
+
environment: scope2.environment
|
|
4509
|
+
}, true);
|
|
4510
|
+
if (!productId) throw Object.assign(new Error("price has no product"), { code: "provider_mismatch" });
|
|
4511
|
+
await prepareSubscriptionBillingTerms(stripeSecretKey, {
|
|
4512
|
+
...scope2,
|
|
4513
|
+
priceCents: base,
|
|
4514
|
+
currency: String(member.currency),
|
|
4515
|
+
discountProductId: productId,
|
|
4516
|
+
billingTerms: {
|
|
4517
|
+
offeringVersionId: String(member.offeringVersionId),
|
|
4518
|
+
discount: { policyVersionId: policy.id, couponId, percentOffBps: policy.percentOffBps, duration: "forever" }
|
|
4519
|
+
}
|
|
4520
|
+
}, false);
|
|
4521
|
+
providerUpdated = await attachProviderDiscount(stripeSecretKey, scope2, member, policy, couponId, productId);
|
|
4522
|
+
} catch (error) {
|
|
4523
|
+
const code = error.code;
|
|
4524
|
+
if (code === "provider_mismatch" || code === "stripe_contract_mismatch") {
|
|
4525
|
+
skip(id6, "provider_mismatch");
|
|
4526
|
+
continue;
|
|
4527
|
+
}
|
|
4528
|
+
throw error;
|
|
4529
|
+
}
|
|
4530
|
+
try {
|
|
4531
|
+
await db.transact([membershipUpdate("networkMembership", id6, {
|
|
4532
|
+
discountPolicyVersionId: policy.id,
|
|
4533
|
+
discountSnapshot: policy,
|
|
4534
|
+
renewalAmountCents: renewal,
|
|
4535
|
+
revision: Number(member.revision) + 1,
|
|
4536
|
+
updatedAt: now
|
|
4537
|
+
})], {
|
|
4538
|
+
mutationId: `membership-reterm:${id6}:${policy.id}:${member.revision}`,
|
|
4539
|
+
guards: [
|
|
4540
|
+
{ ns: "networkMembership", id: id6, exists: true, equals: { revision: member.revision, status: member.status } },
|
|
4541
|
+
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative" } },
|
|
4542
|
+
{ ns: "discountPolicyVersion", id: policy.id, exists: true }
|
|
4543
|
+
]
|
|
4544
|
+
});
|
|
4545
|
+
} catch (error) {
|
|
4546
|
+
if (error.code !== "transact_guard_failed") throw error;
|
|
4547
|
+
return membershipFail("membership_reterm_retry_required");
|
|
4548
|
+
}
|
|
4549
|
+
receipt.retermed.push({
|
|
4550
|
+
membershipId: id6,
|
|
4551
|
+
...typeof member.discountPolicyVersionId === "string" ? { previousPolicyVersionId: member.discountPolicyVersionId } : {},
|
|
4552
|
+
previousRenewalAmountCents: current,
|
|
4553
|
+
renewalAmountCents: renewal,
|
|
4554
|
+
providerUpdated
|
|
4555
|
+
});
|
|
4556
|
+
}
|
|
4557
|
+
return receipt;
|
|
4558
|
+
}
|
|
4559
|
+
|
|
4560
|
+
// src/worker-routes-admin-membership-reterm.ts
|
|
4561
|
+
var PATH4 = "/api/admin/membership-discount/reterm";
|
|
4562
|
+
var SCOPE3 = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
4563
|
+
var ID = /^[A-Za-z0-9][A-Za-z0-9_.:-]{2,159}$/;
|
|
4564
|
+
var BAD = "chapterId and policyVersionId are required; runtime, appId, membershipIds and limit are optional";
|
|
4565
|
+
var handleAdminMembershipReterm = async (req, url, env, ctx) => {
|
|
4566
|
+
if (url.pathname !== PATH4) return null;
|
|
4567
|
+
const operator = ctx.membershipAuthorityOperator;
|
|
4568
|
+
if (!operator) return json({ error: "not found" }, 404);
|
|
4569
|
+
if (req.method !== "POST") return json({ error: "method not allowed" }, 405);
|
|
4570
|
+
const raw2 = ctx.makeDb(env), user = await ctx.verifyUser(req, env);
|
|
4571
|
+
if (!user) return json({ error: "unauthorized" }, 401);
|
|
4572
|
+
if (!await ctx.isSuperAdminEmail(raw2, user.email, user.userId)) return json({ error: "super-admin required", code: "super_admin_required" }, 403);
|
|
4573
|
+
let db;
|
|
4574
|
+
try {
|
|
4575
|
+
db = operator.makeDb(env);
|
|
4576
|
+
} catch {
|
|
4577
|
+
return json({ error: "the membership authority is not hosted by this deployment" }, 404);
|
|
4578
|
+
}
|
|
4579
|
+
let body;
|
|
4580
|
+
try {
|
|
4581
|
+
body = await req.json();
|
|
4582
|
+
} catch {
|
|
4583
|
+
return json({ error: BAD }, 400);
|
|
4584
|
+
}
|
|
4585
|
+
const appId = body.appId ?? body.chapterId, runtime = body.runtime ?? env.ODLA_RUNTIME;
|
|
4586
|
+
if (typeof body.chapterId !== "string" || !SCOPE3.test(body.chapterId) || typeof appId !== "string" || !SCOPE3.test(appId) || typeof runtime !== "string" || !/^[a-z0-9]{2,12}$/.test(runtime) || typeof body.policyVersionId !== "string" || !ID.test(body.policyVersionId) || body.membershipIds !== void 0 && (!Array.isArray(body.membershipIds) || body.membershipIds.length === 0 || body.membershipIds.length > 200 || body.membershipIds.some((id6) => typeof id6 !== "string" || !ID.test(id6))) || body.limit !== void 0 && (!Number.isSafeInteger(body.limit) || Number(body.limit) < 1 || Number(body.limit) > 200)) {
|
|
4587
|
+
return json({ error: BAD }, 400);
|
|
4588
|
+
}
|
|
4589
|
+
const secret = await getVaultSecret(db, operator.stripeSecretName ?? "stripe_secret_key");
|
|
4590
|
+
if (!secret) return json({ error: "network payment provider unavailable" }, 503);
|
|
4591
|
+
try {
|
|
4592
|
+
const receipt = await retermMembershipDiscount(
|
|
4593
|
+
db,
|
|
4594
|
+
secret,
|
|
4595
|
+
{ appId, chapterId: body.chapterId, environment: env.ODLA_ENV, runtime },
|
|
4596
|
+
{
|
|
4597
|
+
policyVersionId: body.policyVersionId,
|
|
4598
|
+
...body.membershipIds ? { membershipIds: body.membershipIds } : {},
|
|
4599
|
+
...body.limit !== void 0 ? { limit: Number(body.limit) } : {}
|
|
4600
|
+
},
|
|
4601
|
+
Date.now()
|
|
4602
|
+
);
|
|
4603
|
+
return json({ ok: true, ...receipt });
|
|
4604
|
+
} catch (error) {
|
|
4605
|
+
const code = error.code;
|
|
4606
|
+
return json({ error: "re-term is pending; retry to confirm", ...code ? { code } : {} }, code ? 409 : 503);
|
|
4607
|
+
}
|
|
4608
|
+
};
|
|
4609
|
+
|
|
4153
4610
|
// src/worker-routes-membership-cancel.ts
|
|
4154
4611
|
var handleMembershipCancellation = async (req, url, env, ctx) => {
|
|
4155
4612
|
const match = url.pathname.match(/^\/api\/admin\/applications\/([0-9a-f-]+)\/cancel-renewal$/);
|
|
@@ -4810,7 +5267,7 @@ async function readAdmissionRows(db, namespace, where, maximum) {
|
|
|
4810
5267
|
}
|
|
4811
5268
|
|
|
4812
5269
|
// src/admission-grants.ts
|
|
4813
|
-
var
|
|
5270
|
+
var ID2 = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/;
|
|
4814
5271
|
var MAX_GRANTS = 2e3;
|
|
4815
5272
|
var MAX_NOTE = 2e3;
|
|
4816
5273
|
var EMAIL_RE2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
@@ -4832,10 +5289,10 @@ function validateAdmissionGrantDocument(input) {
|
|
|
4832
5289
|
const sourceActor = typeof source.actor === "string" ? source.actor.trim() : "";
|
|
4833
5290
|
const chapterId = typeof target.chapterId === "string" ? target.chapterId : "";
|
|
4834
5291
|
const environment = typeof target.environment === "string" ? target.environment : "";
|
|
4835
|
-
if (!
|
|
5292
|
+
if (!ID2.test(sourceId)) errors.push("document.source.appId must be an identifier");
|
|
4836
5293
|
if (!sourceActor || sourceActor.length > 256) errors.push("document.source.actor must be 1-256 characters");
|
|
4837
|
-
if (!
|
|
4838
|
-
if (!
|
|
5294
|
+
if (!ID2.test(chapterId)) errors.push("document.target.chapterId must be an identifier");
|
|
5295
|
+
if (!ID2.test(environment)) errors.push("document.target.environment must be an identifier");
|
|
4839
5296
|
if (!Array.isArray(root.grants)) errors.push("document.grants must be an array");
|
|
4840
5297
|
const rawGrants = Array.isArray(root.grants) ? root.grants : [];
|
|
4841
5298
|
if (rawGrants.length > MAX_GRANTS) errors.push(`document.grants supports at most ${MAX_GRANTS} entries`);
|
|
@@ -4850,7 +5307,7 @@ function validateAdmissionGrantDocument(input) {
|
|
|
4850
5307
|
const decision = grant.decision;
|
|
4851
5308
|
const note = typeof grant.note === "string" ? grant.note.trim() : void 0;
|
|
4852
5309
|
const expiresAt = grant.expiresAt === void 0 ? void 0 : Number(grant.expiresAt);
|
|
4853
|
-
if (!
|
|
5310
|
+
if (!ID2.test(id6)) errors.push(`document.grants[${index2}].id must be an identifier`);
|
|
4854
5311
|
if (seenIds.has(id6)) errors.push(`document.grants repeats id "${id6}"`);
|
|
4855
5312
|
if (!isAdmissionEmail(normalized)) errors.push(`document.grants[${index2}].email must be valid`);
|
|
4856
5313
|
if (seenEmails.has(normalized)) errors.push(`document.grants repeats email "${normalized}"`);
|
|
@@ -5066,7 +5523,7 @@ function chapterFlowFieldContract(chapter, flow, application = chapter.applicati
|
|
|
5066
5523
|
// src/gift-offers.ts
|
|
5067
5524
|
var GIFT_OFFER_CATALOG_VERSION = 1;
|
|
5068
5525
|
var MAX_GIFT_OFFERS = 20;
|
|
5069
|
-
var
|
|
5526
|
+
var ID3 = /^[a-z][a-z0-9-]{1,79}$/;
|
|
5070
5527
|
var PRICE = /^price_[A-Za-z0-9]{12,}$/;
|
|
5071
5528
|
var CURRENCY = /^[a-z]{3}$/;
|
|
5072
5529
|
var record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
@@ -5099,7 +5556,7 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
5099
5556
|
const allowed2 = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
|
|
5100
5557
|
for (const key3 of Object.keys(item)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
|
|
5101
5558
|
const id6 = text3(item.id, `${path}.id`, errors, 80);
|
|
5102
|
-
if (id6 && !
|
|
5559
|
+
if (id6 && !ID3.test(id6)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
5103
5560
|
if (ids2.has(id6)) errors.push(`${path}.id duplicates "${id6}"`);
|
|
5104
5561
|
ids2.add(id6);
|
|
5105
5562
|
const targetRaw = record(item.target);
|
|
@@ -5110,7 +5567,7 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
5110
5567
|
target = { type: "tier", tierId };
|
|
5111
5568
|
} else if (targetRaw?.type === "entitlement") {
|
|
5112
5569
|
const entitlementKey = text3(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
|
|
5113
|
-
if (entitlementKey && !
|
|
5570
|
+
if (entitlementKey && !ID3.test(entitlementKey)) errors.push(`${path}.target.entitlementKey must be a lowercase kebab-case id`);
|
|
5114
5571
|
target = { type: "entitlement", entitlementKey };
|
|
5115
5572
|
} else errors.push(`${path}.target.type must be tier or entitlement`);
|
|
5116
5573
|
if (item.billingMode !== "one_time" && item.billingMode !== "subscription") errors.push(`${path}.billingMode must be one_time or subscription`);
|
|
@@ -6194,7 +6651,7 @@ var handleMe = async (req, url, env, ctx) => {
|
|
|
6194
6651
|
} catch {
|
|
6195
6652
|
}
|
|
6196
6653
|
}
|
|
6197
|
-
return json({ ...base, memberAccess, membershipRestart, application: member?.application ?? null, namedSeatsEnabled: Boolean(
|
|
6654
|
+
return json({ ...base, memberAccess, membershipRestart, application: member?.application ?? null, namedSeatsEnabled: Boolean(ctx.membershipAuthority?.namedSeats && member?.application.paid && member.source.status !== "submitted") });
|
|
6198
6655
|
};
|
|
6199
6656
|
var handleCrm = async (req, url, env, ctx) => {
|
|
6200
6657
|
const crmBase = ctx.crmBase;
|
|
@@ -6896,7 +7353,7 @@ async function memberContentDigest(document) {
|
|
|
6896
7353
|
}
|
|
6897
7354
|
|
|
6898
7355
|
// src/member-content-validation-helpers.ts
|
|
6899
|
-
var
|
|
7356
|
+
var ID4 = /^[a-z][a-z0-9-]{0,79}$/;
|
|
6900
7357
|
var SLUG2 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
6901
7358
|
var ROOT_KEYS3 = /* @__PURE__ */ new Set(["schemaVersion", "revision", "generatedAt", "source", "target", "pages", "upsellOverrides"]);
|
|
6902
7359
|
var SOURCE_KEYS = /* @__PURE__ */ new Set(["type", "appId"]);
|
|
@@ -6936,7 +7393,7 @@ function offerValue(value, path, errors, tiers, audience) {
|
|
|
6936
7393
|
}
|
|
6937
7394
|
unknownKeys2(raw2, OFFER_KEYS, path, errors);
|
|
6938
7395
|
const tierId = textValue(raw2.tierId, `${path}.tierId`, errors, 80, true) ?? "";
|
|
6939
|
-
if (tierId && !
|
|
7396
|
+
if (tierId && !ID4.test(tierId)) errors.push(`${path}.tierId must be a lowercase kebab-case id`);
|
|
6940
7397
|
if (tiers.size && !tiers.has(tierId)) errors.push(`${path}.tierId must reference a declared tier`);
|
|
6941
7398
|
if (audience?.kind === "tiers" && tierId && !audience.tierIds.includes(tierId)) {
|
|
6942
7399
|
errors.push(`${path}.tierId must grant access to this page`);
|
|
@@ -6969,7 +7426,7 @@ function validateMemberContentDocument(input, options = {}) {
|
|
|
6969
7426
|
const type = sourceRaw.type;
|
|
6970
7427
|
const appId = textValue(sourceRaw.appId, "document.source.appId", errors, 63, true) ?? "";
|
|
6971
7428
|
if (type !== "parent" && type !== "chapter") errors.push("document.source.type must be parent or chapter");
|
|
6972
|
-
if (appId && !
|
|
7429
|
+
if (appId && !ID4.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
6973
7430
|
if (options.sourceType && type !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
|
|
6974
7431
|
if (options.sourceAppId && appId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
|
|
6975
7432
|
if (type === "parent" || type === "chapter") source = { type, appId };
|
|
@@ -7006,7 +7463,7 @@ function validateMemberContentDocument(input, options = {}) {
|
|
|
7006
7463
|
const id6 = textValue(raw2.id, `${path}.id`, errors, 80, true) ?? "";
|
|
7007
7464
|
const slug = textValue(raw2.slug, `${path}.slug`, errors, 120, true) ?? "";
|
|
7008
7465
|
const title = textValue(raw2.title, `${path}.title`, errors, MAX_TEXT, true) ?? "";
|
|
7009
|
-
if (id6 && !
|
|
7466
|
+
if (id6 && !ID4.test(id6)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
7010
7467
|
if (slug && !SLUG2.test(slug)) errors.push(`${path}.slug must be a lowercase URL slug`);
|
|
7011
7468
|
if (ids2.has(id6) || knownParentIds.has(id6)) errors.push(`${path}.id duplicates page "${id6}"`);
|
|
7012
7469
|
if (slugs.has(slug) || (options.parentPages ?? []).some((page) => page.slug === slug)) errors.push(`${path}.slug duplicates "${slug}"`);
|
|
@@ -7933,7 +8390,7 @@ async function deliverAdmissionGrants(options) {
|
|
|
7933
8390
|
}
|
|
7934
8391
|
|
|
7935
8392
|
// src/worker-routes-network-admission.ts
|
|
7936
|
-
var
|
|
8393
|
+
var PATH5 = "/api/admin/network/admissions";
|
|
7937
8394
|
var RETRY_PATH = "/api/admin/network/admissions/retry";
|
|
7938
8395
|
var REVOKE_PATH = "/api/admin/network/admissions/revoke";
|
|
7939
8396
|
var MUTATION_ID2 = /^[A-Za-z0-9_-]{16,128}$/;
|
|
@@ -8013,11 +8470,11 @@ async function parseBody(req) {
|
|
|
8013
8470
|
}
|
|
8014
8471
|
}
|
|
8015
8472
|
var handleAdminNetworkAdmissions = async (req, url, env, ctx) => {
|
|
8016
|
-
if (![
|
|
8473
|
+
if (![PATH5, RETRY_PATH, REVOKE_PATH].includes(url.pathname)) return null;
|
|
8017
8474
|
const auth = await gate2(req, env, ctx);
|
|
8018
8475
|
if ("response" in auth) return auth.response;
|
|
8019
8476
|
const db = auth.db;
|
|
8020
|
-
if (url.pathname ===
|
|
8477
|
+
if (url.pathname === PATH5 && req.method === "GET") {
|
|
8021
8478
|
const targetId2 = url.searchParams.get("targetId") ?? "";
|
|
8022
8479
|
const target2 = targetFor(ctx, targetId2);
|
|
8023
8480
|
if (!target2) return reject6(404, "unknown_target", "admission target is not configured");
|
|
@@ -8291,10 +8748,10 @@ var handleSchedule = async (req, url, env, ctx) => {
|
|
|
8291
8748
|
};
|
|
8292
8749
|
|
|
8293
8750
|
// src/payments-stripe.ts
|
|
8294
|
-
import { stripeRequest as
|
|
8751
|
+
import { stripeRequest as stripeRequest9, verifyStripeSignature as verifyStripeSignature2 } from "@odla-ai/stripe";
|
|
8295
8752
|
|
|
8296
8753
|
// src/payments-refund.ts
|
|
8297
|
-
import { stripeRequest as
|
|
8754
|
+
import { stripeRequest as stripeRequest7 } from "@odla-ai/stripe";
|
|
8298
8755
|
var row = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
8299
8756
|
var id2 = (value) => typeof value === "string" ? value : String(row(value).id ?? "");
|
|
8300
8757
|
var providerId2 = (value, prefix) => new RegExp(`^${prefix}_[A-Za-z0-9]+$`).test(value);
|
|
@@ -8304,7 +8761,7 @@ function fail6(code = "refund_target_mismatch") {
|
|
|
8304
8761
|
async function refundSubscriptionPayment(secretKey, input) {
|
|
8305
8762
|
if (!providerId2(input.customerId, "cus") || !providerId2(input.subscriptionId, "sub")) fail6();
|
|
8306
8763
|
const call = async (method, path, params, key3) => {
|
|
8307
|
-
const result = await
|
|
8764
|
+
const result = await stripeRequest7({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
|
|
8308
8765
|
if (!result.ok) fail6("stripe_error");
|
|
8309
8766
|
if (!result.body || typeof result.body !== "object" || Array.isArray(result.body)) fail6("stripe_invalid_response");
|
|
8310
8767
|
return result.body;
|
|
@@ -8378,35 +8835,13 @@ async function refundSubscriptionPayment(secretKey, input) {
|
|
|
8378
8835
|
}
|
|
8379
8836
|
|
|
8380
8837
|
// src/subscription-payment-identity.ts
|
|
8381
|
-
import { stripeRequest as
|
|
8382
|
-
|
|
8383
|
-
// src/subscription-provider-contract.ts
|
|
8384
|
-
import { stripeRequest as stripeRequest6 } from "@odla-ai/stripe";
|
|
8385
|
-
function subscriptionContractMismatch() {
|
|
8386
|
-
throw Object.assign(new Error("Stripe subscription does not match the checkout contract"), { code: "stripe_contract_mismatch" });
|
|
8387
|
-
}
|
|
8388
|
-
async function verifySubscriptionPrice(secretKey, input, resume = false) {
|
|
8389
|
-
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();
|
|
8390
|
-
const result = await stripeRequest6({ secretKey }, "GET", `/v1/prices/${encodeURIComponent(input.priceId)}`);
|
|
8391
|
-
const price = result.body;
|
|
8392
|
-
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();
|
|
8393
|
-
return typeof price.product === "string" ? price.product : void 0;
|
|
8394
|
-
}
|
|
8395
|
-
function verifySubscriptionResult(sub, input, customerId, metadata) {
|
|
8396
|
-
const items = sub?.items;
|
|
8397
|
-
if (!sub || !/^sub_[A-Za-z0-9]+$/.test(sub.id) || sub.customer !== customerId || input.existingSubscriptionId && sub.id !== input.existingSubscriptionId || !(input.billingTerms?.initialTermEndsAt ? ["incomplete", "active", "trialing"] : ["incomplete", "active"]).includes(sub.status) || sub.livemode !== (input.environment === "prod") || !Object.entries(metadata).every(([key3, value]) => sub.metadata?.[key3] === value) || !Array.isArray(items?.data) || items.has_more !== false || items.data.length !== 1 || items.data[0]?.price?.id !== input.priceId || items.data[0]?.quantity !== 1) subscriptionContractMismatch();
|
|
8398
|
-
const invoice = sub.latest_invoice;
|
|
8399
|
-
if (!invoice || typeof invoice !== "object" || invoice.customer !== customerId || invoice.currency !== input.currency || invoice.total !== subscriptionAmountDue(input) || invoice.billing_reason !== "subscription_create") subscriptionContractMismatch();
|
|
8400
|
-
verifySubscriptionBillingResult(sub, input);
|
|
8401
|
-
}
|
|
8402
|
-
|
|
8403
|
-
// src/subscription-payment-identity.ts
|
|
8838
|
+
import { stripeRequest as stripeRequest8 } from "@odla-ai/stripe";
|
|
8404
8839
|
var id3 = (value) => typeof value === "string" ? value : String(value?.id ?? "");
|
|
8405
8840
|
async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata, clientSecret2, applicationId) {
|
|
8406
8841
|
let paymentId = id3(invoice.payment_intent);
|
|
8407
8842
|
if (!paymentId) {
|
|
8408
8843
|
if (!/^in_[A-Za-z0-9]+$/.test(id3(invoice))) subscriptionContractMismatch();
|
|
8409
|
-
const result2 = await
|
|
8844
|
+
const result2 = await stripeRequest8({ secretKey }, "GET", "/v1/invoice_payments", { invoice: id3(invoice), limit: 100 });
|
|
8410
8845
|
const list = result2.body;
|
|
8411
8846
|
if (!result2.ok || list?.has_more !== false || !Array.isArray(list.data) || list.data.length !== 1) subscriptionContractMismatch();
|
|
8412
8847
|
const link = list.data[0];
|
|
@@ -8415,11 +8850,11 @@ async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata,
|
|
|
8415
8850
|
}
|
|
8416
8851
|
if (!/^pi_[A-Za-z0-9]+$/.test(paymentId)) subscriptionContractMismatch();
|
|
8417
8852
|
const path = `/v1/payment_intents/${paymentId}`;
|
|
8418
|
-
const result = await
|
|
8853
|
+
const result = await stripeRequest8({ secretKey }, "GET", path);
|
|
8419
8854
|
const payment = result.body;
|
|
8420
8855
|
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();
|
|
8421
8856
|
if (Object.entries(metadata).every(([key3, value]) => payment.metadata?.[key3] === value)) return;
|
|
8422
|
-
const tagged = await
|
|
8857
|
+
const tagged = await stripeRequest8(
|
|
8423
8858
|
{ secretKey },
|
|
8424
8859
|
"POST",
|
|
8425
8860
|
path,
|
|
@@ -8431,7 +8866,7 @@ async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata,
|
|
|
8431
8866
|
|
|
8432
8867
|
// src/payments-stripe.ts
|
|
8433
8868
|
async function stripeCall(sk, method, path, params, idempotencyKey) {
|
|
8434
|
-
return
|
|
8869
|
+
return stripeRequest9(
|
|
8435
8870
|
{ secretKey: sk },
|
|
8436
8871
|
method,
|
|
8437
8872
|
path,
|
|
@@ -9214,6 +9649,7 @@ async function namedSeatOfferDigest(offer) {
|
|
|
9214
9649
|
])));
|
|
9215
9650
|
return [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
9216
9651
|
}
|
|
9652
|
+
var at = (value) => Number.isSafeInteger(value) ? Number(value) : void 0;
|
|
9217
9653
|
async function readNamedSeatOffer(db, input, now) {
|
|
9218
9654
|
membershipClock(now);
|
|
9219
9655
|
await loadMembershipAuthorityState(db, input);
|
|
@@ -9236,10 +9672,19 @@ async function readNamedSeatOffer(db, input, now) {
|
|
|
9236
9672
|
seatId: String(seat.id),
|
|
9237
9673
|
status: String(seat.status),
|
|
9238
9674
|
recipientName: String(seat.recipientName),
|
|
9239
|
-
recipientEmail: String(seat.recipientEmailNormalized)
|
|
9675
|
+
recipientEmail: String(seat.recipientEmailNormalized),
|
|
9676
|
+
recipientState: namedSeatRecipientState(seat),
|
|
9677
|
+
...at(seat.purchasedAt) !== void 0 ? { purchasedAt: at(seat.purchasedAt) } : {},
|
|
9678
|
+
...at(seat.acceptedAt) !== void 0 ? { acceptedAt: at(seat.acceptedAt) } : {},
|
|
9679
|
+
...seat.status === "active" && at(seat.journeyApprovedAt) !== void 0 ? { joinedAt: at(seat.journeyApprovedAt) } : {}
|
|
9240
9680
|
} } : {}
|
|
9241
9681
|
};
|
|
9242
|
-
return {
|
|
9682
|
+
return {
|
|
9683
|
+
...offer,
|
|
9684
|
+
digest: await namedSeatOfferDigest(offer),
|
|
9685
|
+
included: offering.seatPriceCents === 0,
|
|
9686
|
+
purchaseWindowed: offering.seatPurchaseWindowCalendarMonths !== void 0
|
|
9687
|
+
};
|
|
9243
9688
|
}
|
|
9244
9689
|
|
|
9245
9690
|
// src/named-seat-request.ts
|
|
@@ -9315,8 +9760,9 @@ var handleNamedSeat = async (req, url, env, ctx) => {
|
|
|
9315
9760
|
if (recipientName.length < 2 || /[\u0000-\u001f\u007f]/.test(body.recipientName) || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(recipientEmail) || recipientEmail === normalizeChapterEmail(user.email)) {
|
|
9316
9761
|
return json({ error: "a valid name and email for someone other than the purchaser are required" }, 400);
|
|
9317
9762
|
}
|
|
9318
|
-
|
|
9319
|
-
if (!
|
|
9763
|
+
const granted = offer.included && offer.seat !== void 0 && offer.seat.recipientEmail === recipientEmail && offer.seat.recipientName === recipientName && ["pending_acceptance", "pending_approval", "pending_primary", "active"].includes(offer.seat.status);
|
|
9764
|
+
if (!offer.eligible && !granted || body.quoteDigest !== offer.digest) return json({ error: "seat offer changed or is unavailable; refresh before paying" }, 409);
|
|
9765
|
+
if (!group?.refundPolicyText || !group.merchantDisclosureText || !offer.included && !group.stripePublishableKey) return json({ error: "seat checkout disclosures unavailable" }, 503);
|
|
9320
9766
|
const consentId = `${offer.membershipId}:named-seat-consent`;
|
|
9321
9767
|
const prior = await membershipLoad(db, "namedSeatConsents", consentId);
|
|
9322
9768
|
const attrs = {
|
|
@@ -9355,7 +9801,7 @@ var handleNamedSeat = async (req, url, env, ctx) => {
|
|
|
9355
9801
|
}
|
|
9356
9802
|
return json({
|
|
9357
9803
|
...receipt,
|
|
9358
|
-
publishableKey: String(group.stripePublishableKey),
|
|
9804
|
+
...group.stripePublishableKey ? { publishableKey: String(group.stripePublishableKey) } : {},
|
|
9359
9805
|
lineItems: { standardCents: offer.amountCents, discountCents: 0, dueTodayCents: offer.amountCents }
|
|
9360
9806
|
});
|
|
9361
9807
|
} catch {
|
|
@@ -10218,6 +10664,7 @@ function dashboardMetricData(points, now) {
|
|
|
10218
10664
|
}
|
|
10219
10665
|
|
|
10220
10666
|
// src/chapter-billing-amount.ts
|
|
10667
|
+
var hasProviderDiscount = (sub, items) => Boolean(sub?.discount || Array.isArray(sub?.discounts) && sub.discounts.length || items.some((it) => it.discounts?.length));
|
|
10221
10668
|
function chapterBillingAmount(sub, intent, env) {
|
|
10222
10669
|
const unknown = { amountCents: null, listAmountCents: null, interval: "year", annualCents: null };
|
|
10223
10670
|
const items = sub?.items?.data;
|
|
@@ -10234,10 +10681,11 @@ function chapterBillingAmount(sub, intent, env) {
|
|
|
10234
10681
|
const primary = items.filter((it) => it.price?.id === intent.priceId);
|
|
10235
10682
|
if (primary.length !== 1 || primary[0].quantity !== 1 || primary[0].price.unit_amount !== intent.priceCents || intent.interval !== interval || intent.currency !== "usd") return { ...unknown, listAmountCents, interval };
|
|
10236
10683
|
if (terms2?.discount?.duration === "forever") amountCents = listAmountCents - intent.priceCents + Math.round(intent.priceCents * (1e4 - terms2.discount.percentOffBps) / 1e4);
|
|
10684
|
+
else if (hasProviderDiscount(sub, items)) amountCents = null;
|
|
10237
10685
|
} catch {
|
|
10238
10686
|
amountCents = null;
|
|
10239
10687
|
}
|
|
10240
|
-
} else if (
|
|
10688
|
+
} else if (hasProviderDiscount(sub, items)) amountCents = null;
|
|
10241
10689
|
return { amountCents, listAmountCents, interval, annualCents: amountCents === null ? null : amountCents * (interval === "month" ? 12 : 1) };
|
|
10242
10690
|
}
|
|
10243
10691
|
function chapterBillingActive(sub, app) {
|
|
@@ -11155,7 +11603,7 @@ async function networkApplicationAction(opts) {
|
|
|
11155
11603
|
if (app.status !== input.expectedStage || !applicationActionStages(app, env, ctx).includes(input.stage)) {
|
|
11156
11604
|
return json({ error: "application action is no longer available; reload", code: "invalid_application_action" }, 409);
|
|
11157
11605
|
}
|
|
11158
|
-
const
|
|
11606
|
+
const at2 = Date.now();
|
|
11159
11607
|
const meta = { [ACTION_META]: {
|
|
11160
11608
|
version: 1,
|
|
11161
11609
|
state: "pending",
|
|
@@ -11175,7 +11623,7 @@ async function networkApplicationAction(opts) {
|
|
|
11175
11623
|
recordId: record4.id,
|
|
11176
11624
|
kind: "note",
|
|
11177
11625
|
authorId: `network:${sourceId}`,
|
|
11178
|
-
createdAt:
|
|
11626
|
+
createdAt: at2,
|
|
11179
11627
|
body: `Requested ${input.expectedStage} \u2192 ${input.stage} from ${sourceId}`,
|
|
11180
11628
|
meta
|
|
11181
11629
|
} },
|
|
@@ -11184,7 +11632,7 @@ async function networkApplicationAction(opts) {
|
|
|
11184
11632
|
recordId: record4.id,
|
|
11185
11633
|
kind: "note",
|
|
11186
11634
|
authorId: `network:${sourceId}`,
|
|
11187
|
-
createdAt:
|
|
11635
|
+
createdAt: at2,
|
|
11188
11636
|
body: "Application operation reconciliation",
|
|
11189
11637
|
meta: { state: "pending", activityId }
|
|
11190
11638
|
} }
|
|
@@ -12035,9 +12483,9 @@ function normalizeNetworkIdentity(kind, value) {
|
|
|
12035
12483
|
return trimmed;
|
|
12036
12484
|
}
|
|
12037
12485
|
function emailLooksReal(value) {
|
|
12038
|
-
const
|
|
12039
|
-
if (
|
|
12040
|
-
const domain = value.slice(
|
|
12486
|
+
const at2 = value.lastIndexOf("@");
|
|
12487
|
+
if (at2 <= 0 || at2 === value.length - 1) return false;
|
|
12488
|
+
const domain = value.slice(at2 + 1).toLowerCase();
|
|
12041
12489
|
return !domain.endsWith(".invalid") && domain !== "example.com" && domain !== "example.test";
|
|
12042
12490
|
}
|
|
12043
12491
|
function validateNetworkPersonImport(input, allowedChapterIds) {
|
|
@@ -12825,7 +13273,7 @@ async function revealNetworkPersonRegistryRecord(db, input, mutationId, actor, n
|
|
|
12825
13273
|
}
|
|
12826
13274
|
|
|
12827
13275
|
// src/worker-routes-network-person-registry.ts
|
|
12828
|
-
var
|
|
13276
|
+
var PATH6 = "/api/admin/network-person-registry";
|
|
12829
13277
|
var MAX_BODY2 = 8 * 1024 * 1024;
|
|
12830
13278
|
var MUTATION_ID4 = /^[A-Za-z0-9_-]{16,128}$/;
|
|
12831
13279
|
var positiveRevision = (value) => {
|
|
@@ -12850,7 +13298,7 @@ function registryError(error) {
|
|
|
12850
13298
|
return null;
|
|
12851
13299
|
}
|
|
12852
13300
|
var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
12853
|
-
if (url.pathname !==
|
|
13301
|
+
if (url.pathname !== PATH6) return null;
|
|
12854
13302
|
if (!ctx.chapter.networkPersonRegistry) return json({ error: "not found" }, 404);
|
|
12855
13303
|
if (req.method !== "GET" && req.method !== "POST") return json({ error: "method not allowed" }, 405);
|
|
12856
13304
|
if (req.method === "POST" && Number(req.headers.get("content-length") ?? 0) > MAX_BODY2) {
|
|
@@ -13596,7 +14044,7 @@ var handleFormation = async (req, url, env, ctx) => {
|
|
|
13596
14044
|
import { verifyFederatedRequest as verifyFederatedRequest9 } from "@odla-ai/db";
|
|
13597
14045
|
|
|
13598
14046
|
// src/builder-library-validation.ts
|
|
13599
|
-
var
|
|
14047
|
+
var ID5 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
13600
14048
|
var APP_ID = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
13601
14049
|
var MAX_RESOURCES = 200;
|
|
13602
14050
|
var MAX_MARKDOWN3 = 65536;
|
|
@@ -13657,7 +14105,7 @@ function resourceValue(value, path, errors) {
|
|
|
13657
14105
|
}
|
|
13658
14106
|
unknownKeys3(raw2, RESOURCE_KEYS, path, errors);
|
|
13659
14107
|
const id6 = text7(raw2.id, `${path}.id`, errors, 80) ?? "";
|
|
13660
|
-
if (id6 && !
|
|
14108
|
+
if (id6 && !ID5.test(id6)) errors.push(`${path}.id must be lowercase kebab-case`);
|
|
13661
14109
|
const kind = text7(raw2.kind, `${path}.kind`, errors) ?? "";
|
|
13662
14110
|
if (!KINDS.has(kind)) errors.push(`${path}.kind is unsupported`);
|
|
13663
14111
|
const title = text7(raw2.title, `${path}.title`, errors, 240) ?? "";
|
|
@@ -14079,7 +14527,7 @@ var handleAdminBuilderLibrary = async (req, url, env, ctx) => {
|
|
|
14079
14527
|
};
|
|
14080
14528
|
|
|
14081
14529
|
// src/membership-cancel-authority.ts
|
|
14082
|
-
import { stripeRequest as
|
|
14530
|
+
import { stripeRequest as stripeRequest10 } from "@odla-ai/stripe";
|
|
14083
14531
|
async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
|
|
14084
14532
|
membershipClock(now);
|
|
14085
14533
|
const id6 = await networkMembershipId(target);
|
|
@@ -14090,14 +14538,14 @@ async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
|
|
|
14090
14538
|
membershipFail("membership_cancellation_unavailable");
|
|
14091
14539
|
}
|
|
14092
14540
|
const path = `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`;
|
|
14093
|
-
const read2 = await
|
|
14541
|
+
const read2 = await stripeRequest10({ secretKey: stripeSecret }, "GET", path);
|
|
14094
14542
|
const sub = read2.body;
|
|
14095
14543
|
if (!read2.ok || sub.id !== member.providerSubscriptionId || sub.customer !== member.providerCustomerId || sub.livemode !== (target.environment === "prod") || !Object.entries({
|
|
14096
14544
|
...stripePartitionMetadata(target),
|
|
14097
14545
|
applicationId: target.applicationId
|
|
14098
14546
|
}).every(([key3, value]) => sub.metadata?.[key3] === value)) membershipFail("membership_cancellation_provider_mismatch");
|
|
14099
14547
|
if (sub.status !== "canceled" && sub.cancel_at_period_end !== true) {
|
|
14100
|
-
const write = await
|
|
14548
|
+
const write = await stripeRequest10(
|
|
14101
14549
|
{ secretKey: stripeSecret },
|
|
14102
14550
|
"POST",
|
|
14103
14551
|
path,
|
|
@@ -14319,7 +14767,7 @@ async function refundNetworkMembership(db, input, secretKey, now) {
|
|
|
14319
14767
|
}
|
|
14320
14768
|
|
|
14321
14769
|
// src/membership-provider-lifecycle.ts
|
|
14322
|
-
import { stripeRequest as
|
|
14770
|
+
import { stripeRequest as stripeRequest11 } from "@odla-ai/stripe";
|
|
14323
14771
|
|
|
14324
14772
|
// src/membership-renewal-authority.ts
|
|
14325
14773
|
async function reconcileMembershipRenewal(db, input, now) {
|
|
@@ -14356,7 +14804,9 @@ async function reconcileMembershipRenewal(db, input, now) {
|
|
|
14356
14804
|
if (start !== member.termEndsAt || start > now + 3e4) membershipFail("membership_renewal_period_gap");
|
|
14357
14805
|
const seat = member.namedSeatId ? await membershipLoad(db, "networkSeat", String(member.namedSeatId)) : void 0;
|
|
14358
14806
|
if (seat) requireMembershipScope(seat, input);
|
|
14359
|
-
const
|
|
14807
|
+
const openSeat = seat && !["refunded", "refund_pending", "canceled"].includes(String(seat.status)) ? seat : void 0;
|
|
14808
|
+
const billedSeat = openSeat && openSeat.providerSubscriptionId === member.providerSubscriptionId && openSeat.providerSubscriptionItemId ? openSeat : void 0;
|
|
14809
|
+
const carriedSeat = billedSeat ?? (openSeat && namedSeatIncluded(openSeat) && openSeat.purchasedAt !== void 0 ? openSeat : void 0);
|
|
14360
14810
|
const prices = [String(member.providerPriceId), ...billedSeat ? [String(billedSeat.priceId)] : []].sort();
|
|
14361
14811
|
const amount = Number(member.renewalAmountCents) + Number(billedSeat?.amountCents ?? 0);
|
|
14362
14812
|
if (!Number.isSafeInteger(amount) || amount <= 0 || fact.invoiceTotalCents !== amount || JSON.stringify(lines.map((line) => line.priceId).sort()) !== JSON.stringify(prices)) membershipFail("membership_renewal_amount_mismatch");
|
|
@@ -14396,19 +14846,19 @@ async function reconcileMembershipRenewal(db, input, now) {
|
|
|
14396
14846
|
evidenceGuard
|
|
14397
14847
|
];
|
|
14398
14848
|
if (seat) guards.push({ ns: "networkSeat", id: String(seat.id), exists: true, equals: { revision: seat.revision } });
|
|
14399
|
-
if (
|
|
14400
|
-
const seatEntitlement = await membershipLoad(db, "networkEntitlement", String(
|
|
14849
|
+
if (carriedSeat?.entitlementId) {
|
|
14850
|
+
const seatEntitlement = await membershipLoad(db, "networkEntitlement", String(carriedSeat.entitlementId));
|
|
14401
14851
|
requireMembershipScope(seatEntitlement, input);
|
|
14402
14852
|
guards.push({ ns: "networkEntitlement", id: String(seatEntitlement.id), exists: true, equals: { revision: seatEntitlement.revision } });
|
|
14403
14853
|
ops.push(
|
|
14404
|
-
membershipUpdate("networkSeat", String(
|
|
14854
|
+
membershipUpdate("networkSeat", String(carriedSeat.id), {
|
|
14405
14855
|
...paid ? { termStartedAt: start, termEndsAt: end } : {},
|
|
14406
|
-
revision: Number(
|
|
14856
|
+
revision: Number(carriedSeat.revision) + 1,
|
|
14407
14857
|
updatedAt: now
|
|
14408
14858
|
}),
|
|
14409
14859
|
membershipUpdate("networkEntitlement", String(seatEntitlement.id), {
|
|
14410
14860
|
...paid ? { termStartedAt: start, termEndsAt: end } : {},
|
|
14411
|
-
status:
|
|
14861
|
+
status: carriedSeat.journeyApprovedAt ? entitlementStatus : seatEntitlement.status,
|
|
14412
14862
|
revision: Number(seatEntitlement.revision) + 1,
|
|
14413
14863
|
updatedAt: now
|
|
14414
14864
|
})
|
|
@@ -14481,7 +14931,7 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
14481
14931
|
}
|
|
14482
14932
|
if (fact.subscriptionId !== member.providerSubscriptionId || fact.objectKind !== "subscription") membershipFail("membership_billing_scope_mismatch");
|
|
14483
14933
|
if (["refund_pending", "refunded", "declined", "canceled", "restarting"].includes(String(member.status))) return receipt();
|
|
14484
|
-
const result = await
|
|
14934
|
+
const result = await stripeRequest11({ secretKey: sk }, "GET", `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`);
|
|
14485
14935
|
const sub = result.body;
|
|
14486
14936
|
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)) {
|
|
14487
14937
|
membershipFail("membership_billing_provider_mismatch");
|
|
@@ -14518,86 +14968,8 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
14518
14968
|
return membershipFail("membership_billing_retry_required");
|
|
14519
14969
|
}
|
|
14520
14970
|
|
|
14521
|
-
// src/membership-seat-reservation.ts
|
|
14522
|
-
async function recipientKey(input, email) {
|
|
14523
|
-
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify([input.appId, input.chapterId, input.environment, input.runtime, email])));
|
|
14524
|
-
return `seat-recipient:${[...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
14525
|
-
}
|
|
14526
|
-
async function reserveNamedMembershipSeat(db, input, now) {
|
|
14527
|
-
membershipClock(now);
|
|
14528
|
-
const name = typeof input.recipientName === "string" ? input.recipientName.trim().replace(/\s+/g, " ") : "";
|
|
14529
|
-
const email = normalizeChapterEmail(typeof input.recipientEmail === "string" ? input.recipientEmail : "");
|
|
14530
|
-
const purchaser = normalizeChapterEmail(typeof input.purchaserEmail === "string" ? input.purchaserEmail : "");
|
|
14531
|
-
if (name.length < 2 || name.length > 160 || /[\u0000-\u001f\u007f]/.test(input.recipientName) || email.length > 254 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) || !purchaser || email === purchaser || !/^price_[A-Za-z0-9_]+$/.test(input.priceId) || !/^user_[A-Za-z0-9_]+$/.test(input.purchaserUserId)) membershipFail("invalid_named_seat_recipient");
|
|
14532
|
-
const state = await loadMembershipAuthorityState(db, input);
|
|
14533
|
-
const memberId = await networkMembershipId(input), member = await membershipLoad(db, "networkMembership", memberId);
|
|
14534
|
-
requireMembershipScope(member, input);
|
|
14535
|
-
if (!Number.isSafeInteger(member.revision) || !/^price_[A-Za-z0-9_]+$/.test(String(member.providerPriceId)) || member.providerPriceId === input.priceId) membershipFail("named_seat_purchase_unavailable");
|
|
14536
|
-
const id6 = (await membershipOfferIdForRequest({ ...input, requestId: "named_seat" })).replace(/^offer_/, "seat_");
|
|
14537
|
-
let seat = await membershipLoad(db, "networkSeat", id6);
|
|
14538
|
-
if (!seat) {
|
|
14539
|
-
const offering = membershipOfferingVersion(member.offeringSnapshot), claimId = await recipientKey(input, email);
|
|
14540
|
-
const purchaseEndsAt = seatPurchaseDeadline(offering, member);
|
|
14541
|
-
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) {
|
|
14542
|
-
membershipFail("named_seat_purchase_unavailable");
|
|
14543
|
-
}
|
|
14544
|
-
const attrs = {
|
|
14545
|
-
id: id6,
|
|
14546
|
-
...membershipScope(input),
|
|
14547
|
-
primaryMembershipId: memberId,
|
|
14548
|
-
recipientName: name,
|
|
14549
|
-
recipientEmailNormalized: email,
|
|
14550
|
-
purchaserUserId: input.purchaserUserId,
|
|
14551
|
-
recipientClaimId: claimId,
|
|
14552
|
-
priceId: input.priceId,
|
|
14553
|
-
primaryPriceId: member.providerPriceId,
|
|
14554
|
-
amountCents: offering.seatPriceCents,
|
|
14555
|
-
currency: offering.currency,
|
|
14556
|
-
interval: offering.interval,
|
|
14557
|
-
providerCustomerId: member.providerCustomerId,
|
|
14558
|
-
providerSubscriptionId: member.providerSubscriptionId,
|
|
14559
|
-
status: "payment_pending",
|
|
14560
|
-
preparedAt: now,
|
|
14561
|
-
termEndsAt: member.termEndsAt,
|
|
14562
|
-
seatPurchaseEndsAt: purchaseEndsAt,
|
|
14563
|
-
offeringSnapshot: offering,
|
|
14564
|
-
revision: 1,
|
|
14565
|
-
createdAt: now,
|
|
14566
|
-
updatedAt: now
|
|
14567
|
-
};
|
|
14568
|
-
try {
|
|
14569
|
-
await db.transact([
|
|
14570
|
-
membershipUpdate("networkSeat", id6, attrs),
|
|
14571
|
-
membershipUpdate("networkSeatRecipient", claimId, {
|
|
14572
|
-
id: claimId,
|
|
14573
|
-
appId: input.appId,
|
|
14574
|
-
chapterId: input.chapterId,
|
|
14575
|
-
environment: input.environment,
|
|
14576
|
-
runtime: input.runtime,
|
|
14577
|
-
seatId: id6,
|
|
14578
|
-
recipientEmailNormalized: email,
|
|
14579
|
-
createdAt: now
|
|
14580
|
-
}),
|
|
14581
|
-
membershipUpdate("networkMembership", memberId, { namedSeatId: id6, revision: Number(member.revision) + 1, updatedAt: now })
|
|
14582
|
-
], { mutationId: `named-seat-reserve:${id6}`, guards: [
|
|
14583
|
-
{ ns: "networkSeat", id: id6, exists: false },
|
|
14584
|
-
{ ns: "networkSeatRecipient", id: claimId, exists: false },
|
|
14585
|
-
{ ns: "networkMembership", id: memberId, exists: true, equals: { revision: member.revision }, absent: ["namedSeatId"] },
|
|
14586
|
-
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
|
|
14587
|
-
] });
|
|
14588
|
-
} catch (error) {
|
|
14589
|
-
if (error.code !== "transact_guard_failed") throw error;
|
|
14590
|
-
}
|
|
14591
|
-
seat = await membershipLoad(db, "networkSeat", id6);
|
|
14592
|
-
if (!seat && await membershipLoad(db, "networkSeatRecipient", claimId)) membershipFail("named_seat_recipient_unavailable");
|
|
14593
|
-
}
|
|
14594
|
-
requireMembershipScope(seat, input);
|
|
14595
|
-
if (seat.primaryMembershipId !== memberId || seat.recipientName !== name || seat.recipientEmailNormalized !== email || seat.priceId !== input.priceId || seat.purchaserUserId !== input.purchaserUserId) membershipFail("named_seat_purchase_conflict");
|
|
14596
|
-
return seat;
|
|
14597
|
-
}
|
|
14598
|
-
|
|
14599
14971
|
// src/membership-seat-stripe.ts
|
|
14600
|
-
import { stripeRequest as
|
|
14972
|
+
import { stripeRequest as stripeRequest12 } from "@odla-ai/stripe";
|
|
14601
14973
|
var reject7 = () => {
|
|
14602
14974
|
throw Object.assign(new Error("named seat provider contract mismatch"), { code: "named_seat_provider_mismatch" });
|
|
14603
14975
|
};
|
|
@@ -14609,7 +14981,7 @@ var metadataFor = (seat) => ({
|
|
|
14609
14981
|
});
|
|
14610
14982
|
var matches = (value, expected) => value && Object.entries(expected).every(([key3, item]) => value[key3] === item);
|
|
14611
14983
|
async function read(sk, path, params) {
|
|
14612
|
-
const response = await
|
|
14984
|
+
const response = await stripeRequest12({ secretKey: sk }, "GET", path, params);
|
|
14613
14985
|
if (!response.ok) return reject7();
|
|
14614
14986
|
return response.body;
|
|
14615
14987
|
}
|
|
@@ -14632,9 +15004,9 @@ async function subscriptionForSeat(sk, seat) {
|
|
|
14632
15004
|
if (!Array.isArray(discounts) || discounts.length > 5) reject7();
|
|
14633
15005
|
for (const discount of discounts) {
|
|
14634
15006
|
const couponRef = discount.source?.coupon ?? discount.coupon;
|
|
14635
|
-
const
|
|
14636
|
-
if (!
|
|
14637
|
-
const coupon = await read(sk, `/v1/coupons/${encodeURIComponent(
|
|
15007
|
+
const couponId = typeof couponRef === "string" ? couponRef : couponRef?.id;
|
|
15008
|
+
if (!couponId || typeof couponId !== "string") reject7();
|
|
15009
|
+
const coupon = await read(sk, `/v1/coupons/${encodeURIComponent(couponId)}`, { "expand[0]": "applies_to" });
|
|
14638
15010
|
if (coupon.livemode !== (seat.environment === "prod") || !Array.isArray(coupon.applies_to?.products) || coupon.applies_to.products.length !== 1 || coupon.applies_to.products[0] !== primary.price.product) reject7();
|
|
14639
15011
|
}
|
|
14640
15012
|
return sub;
|
|
@@ -14646,11 +15018,11 @@ async function createNamedSeatStripePayment(sk, seat, now) {
|
|
|
14646
15018
|
}
|
|
14647
15019
|
await subscriptionForSeat(sk, seat);
|
|
14648
15020
|
const metadata = metadataFor(seat);
|
|
14649
|
-
const result = seat.providerPaymentIntentId ? await
|
|
15021
|
+
const result = seat.providerPaymentIntentId ? await stripeRequest12(
|
|
14650
15022
|
{ secretKey: sk },
|
|
14651
15023
|
"GET",
|
|
14652
15024
|
`/v1/payment_intents/${encodeURIComponent(seat.providerPaymentIntentId)}`
|
|
14653
|
-
) : await
|
|
15025
|
+
) : await stripeRequest12({ secretKey: sk }, "POST", "/v1/payment_intents", {
|
|
14654
15026
|
amount: seat.amountCents,
|
|
14655
15027
|
currency: seat.currency,
|
|
14656
15028
|
customer: seat.providerCustomerId,
|
|
@@ -14668,7 +15040,7 @@ async function attachNamedSeatStripeRenewal(sk, seat) {
|
|
|
14668
15040
|
const sub = await subscriptionForSeat(sk, seat);
|
|
14669
15041
|
const existing = sub.items.data.find((item2) => item2.metadata?.seatId === seat.id);
|
|
14670
15042
|
if (existing) return String(existing.id);
|
|
14671
|
-
const result = await
|
|
15043
|
+
const result = await stripeRequest12({ secretKey: sk }, "POST", "/v1/subscription_items", {
|
|
14672
15044
|
subscription: seat.providerSubscriptionId,
|
|
14673
15045
|
price: seat.priceId,
|
|
14674
15046
|
quantity: 1,
|
|
@@ -14683,6 +15055,7 @@ async function attachNamedSeatStripeRenewal(sk, seat) {
|
|
|
14683
15055
|
// src/membership-seat-checkout.ts
|
|
14684
15056
|
async function prepareNamedMembershipSeatPayment(db, input, secretKey, now) {
|
|
14685
15057
|
let seat = await reserveNamedMembershipSeat(db, input, now);
|
|
15058
|
+
if (namedSeatIncluded(seat)) membershipFail("named_seat_included");
|
|
14686
15059
|
const state = await loadMembershipAuthorityState(db, input);
|
|
14687
15060
|
const member = await membershipLoad(db, "networkMembership", seat.primaryMembershipId);
|
|
14688
15061
|
requireMembershipScope(member, input);
|
|
@@ -14889,7 +15262,7 @@ async function recordRecipient(db, input, now, approve) {
|
|
|
14889
15262
|
requireMembershipScope(member, input);
|
|
14890
15263
|
const entitlement = await membershipLoad(db, "networkEntitlement", String(seat.entitlementId));
|
|
14891
15264
|
requireMembershipScope(entitlement, input);
|
|
14892
|
-
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
|
|
15265
|
+
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) || !namedSeatSettled(seat)) membershipFail("named_seat_acceptance_unavailable");
|
|
14893
15266
|
if (seat.acceptedAt === void 0) {
|
|
14894
15267
|
if (approve || normalizeChapterEmail(input.verifiedRecipientEmail) !== seat.recipientEmailNormalized) {
|
|
14895
15268
|
membershipFail("named_seat_recipient_verification_required");
|
|
@@ -15076,6 +15449,15 @@ async function handleMembershipSeatAction(action, db, peer, input, now, stripeSe
|
|
|
15076
15449
|
};
|
|
15077
15450
|
if (action === "seat-accept") return acceptNamedSeatRecipient(db, recipientInput, now);
|
|
15078
15451
|
if (action === "seat-approve") return approveNamedSeatRecipient(db, recipientInput, now);
|
|
15452
|
+
const checkout = {
|
|
15453
|
+
...target,
|
|
15454
|
+
priceId: peer.seatPriceId,
|
|
15455
|
+
recipientName: String(input.recipientName),
|
|
15456
|
+
recipientEmail: String(input.recipientEmail),
|
|
15457
|
+
purchaserEmail: String(input.purchaserEmail),
|
|
15458
|
+
purchaserUserId: String(input.purchaserUserId)
|
|
15459
|
+
};
|
|
15460
|
+
if (action === "seat-checkouts" && namedSeatIncluded(await reserveNamedMembershipSeat(db, checkout, now))) return grantIncludedNamedSeat(db, checkout, now);
|
|
15079
15461
|
const secret = await getVaultSecret(db, stripeSecretName);
|
|
15080
15462
|
if (!secret) throw new Error("network Stripe provider is unavailable");
|
|
15081
15463
|
if (action === "seat-decline") {
|
|
@@ -15091,14 +15473,7 @@ async function handleMembershipSeatAction(action, db, peer, input, now, stripeSe
|
|
|
15091
15473
|
return { seatId: seat.id, status: "refunded", refundedCents: refunded.refundedCents, subscriptionCanceled: false };
|
|
15092
15474
|
}
|
|
15093
15475
|
if (action === "seat-operator-refund") return refundNamedSeatByOperator(db, { ...target, seatId: String(input.seatId) }, secret, now);
|
|
15094
|
-
if (action === "seat-checkouts") return prepareNamedMembershipSeatPayment(db,
|
|
15095
|
-
...target,
|
|
15096
|
-
priceId: peer.seatPriceId,
|
|
15097
|
-
recipientName: String(input.recipientName),
|
|
15098
|
-
recipientEmail: String(input.recipientEmail),
|
|
15099
|
-
purchaserEmail: String(input.purchaserEmail),
|
|
15100
|
-
purchaserUserId: String(input.purchaserUserId)
|
|
15101
|
-
}, secret, now);
|
|
15476
|
+
if (action === "seat-checkouts") return prepareNamedMembershipSeatPayment(db, checkout, secret, now);
|
|
15102
15477
|
const evidence = { ...target, seatId: String(input.seatId), eventId: String(input.eventId) };
|
|
15103
15478
|
if (action === "seat-payments") {
|
|
15104
15479
|
const receipt = await confirmNamedMembershipSeatPayment(db, evidence, now);
|
|
@@ -15120,7 +15495,7 @@ var respond = (body, status2 = 200) => new Response(JSON.stringify(body), {
|
|
|
15120
15495
|
status: status2,
|
|
15121
15496
|
headers: { "content-type": "application/json", "cache-control": "no-store" }
|
|
15122
15497
|
});
|
|
15123
|
-
var
|
|
15498
|
+
var SAFE2 = /^[A-Za-z0-9][A-Za-z0-9_.:-]{2,159}$/;
|
|
15124
15499
|
var SAFE_ERROR = /* @__PURE__ */ new Set([
|
|
15125
15500
|
"offer_identity_conflict",
|
|
15126
15501
|
"membership_authority_unavailable",
|
|
@@ -15168,7 +15543,7 @@ function createMembershipAuthorityHandler(options) {
|
|
|
15168
15543
|
if (options.stripeSecretName !== void 0 && !/^[A-Za-z][A-Za-z0-9_-]{1,127}$/.test(options.stripeSecretName)) throw new TypeError("invalid Stripe secret slot");
|
|
15169
15544
|
const identities = /* @__PURE__ */ new Set();
|
|
15170
15545
|
for (const peer of peers) {
|
|
15171
|
-
if (!/^[a-z0-9][a-z0-9-]{1,62}$/.test(peer.appId) || !
|
|
15546
|
+
if (!/^[a-z0-9][a-z0-9-]{1,62}$/.test(peer.appId) || !SAFE2.test(peer.chapterId) || !/^[a-z0-9][a-z0-9-]{1,31}$/.test(peer.environment) || !/^[a-z0-9]{2,12}$/.test(peer.runtime) || !/^[A-Za-z][A-Za-z0-9_-]{1,127}$/.test(peer.secretName)) throw new TypeError("invalid authority peer");
|
|
15172
15547
|
const identity = `${peer.appId}:${peer.runtime}`;
|
|
15173
15548
|
if (identities.has(identity)) throw new TypeError("ambiguous authority peer");
|
|
15174
15549
|
identities.add(identity);
|
|
@@ -15202,7 +15577,7 @@ function createMembershipAuthorityHandler(options) {
|
|
|
15202
15577
|
try {
|
|
15203
15578
|
input = JSON.parse(verified.body);
|
|
15204
15579
|
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] ?? []];
|
|
15205
|
-
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]) :
|
|
15580
|
+
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]) : SAFE2.test(input[key3])))) {
|
|
15206
15581
|
throw new TypeError("invalid request");
|
|
15207
15582
|
}
|
|
15208
15583
|
} catch {
|
|
@@ -15539,7 +15914,7 @@ function createMembershipAuthorityStorage(db, secretNames) {
|
|
|
15539
15914
|
}
|
|
15540
15915
|
|
|
15541
15916
|
// src/membership-seat-cancel.ts
|
|
15542
|
-
import { initStripe as initStripe3, stripeRequest as
|
|
15917
|
+
import { initStripe as initStripe3, stripeRequest as stripeRequest13 } from "@odla-ai/stripe";
|
|
15543
15918
|
async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
|
|
15544
15919
|
membershipClock(now);
|
|
15545
15920
|
const state = await loadMembershipAuthorityState(db, target);
|
|
@@ -15567,7 +15942,7 @@ async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
|
|
|
15567
15942
|
}
|
|
15568
15943
|
const metadata = { ...stripePartitionMetadata(target), applicationId: target.applicationId, membershipId: seat.primaryMembershipId, seatId: seat.id };
|
|
15569
15944
|
const request = async (method, path, params) => {
|
|
15570
|
-
const result = await
|
|
15945
|
+
const result = await stripeRequest13(
|
|
15571
15946
|
{ secretKey },
|
|
15572
15947
|
method,
|
|
15573
15948
|
path,
|
|
@@ -15954,13 +16329,13 @@ async function requestNamedSeatDecline(options) {
|
|
|
15954
16329
|
verifiedRecipientEmail: options.verifiedRecipientEmail
|
|
15955
16330
|
});
|
|
15956
16331
|
const seat = result.seat;
|
|
15957
|
-
if (!seat || seat.seatId !== options.seatId || seat.status !== "refunded" || seat.subscriptionCanceled !== false || !Number.isSafeInteger(seat.refundedCents) || seat.refundedCents
|
|
16332
|
+
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");
|
|
15958
16333
|
return seat;
|
|
15959
16334
|
}
|
|
15960
16335
|
async function requestNamedSeatOperatorRefund(options) {
|
|
15961
16336
|
const result = await requestAuthority(options, "seat-operator-refund", { applicationId: options.applicationId, skuId: options.skuId, seatId: options.seatId });
|
|
15962
16337
|
const seat = result.seat;
|
|
15963
|
-
if (!seat || seat.seatId !== options.seatId || seat.status !== "refunded" || seat.subscriptionCanceled !== false || !Number.isSafeInteger(seat.refundedCents) || seat.refundedCents
|
|
16338
|
+
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");
|
|
15964
16339
|
return seat;
|
|
15965
16340
|
}
|
|
15966
16341
|
async function requestNamedSeatReservation(options) {
|
|
@@ -15974,13 +16349,13 @@ async function requestNamedSeatReservation(options) {
|
|
|
15974
16349
|
});
|
|
15975
16350
|
const seat = result.seat;
|
|
15976
16351
|
const expected = (await membershipOfferIdForRequest({ ...options, requestId: "named_seat" })).replace(/^offer_/, "seat_");
|
|
15977
|
-
if (!seat || seat.seatId !== expected || !Number.isSafeInteger(seat.amountCents) || seat.amountCents
|
|
16352
|
+
if (!seat || seat.seatId !== expected || !Number.isSafeInteger(seat.amountCents) || seat.amountCents < 0 || !/^[a-z]{3}$/.test(seat.currency) || !Number.isSafeInteger(seat.termEndsAt)) throw new Error("invalid seat reservation receipt");
|
|
15978
16353
|
return { seatId: seat.seatId, amountCents: seat.amountCents, currency: seat.currency, termEndsAt: seat.termEndsAt };
|
|
15979
16354
|
}
|
|
15980
16355
|
async function requestNamedSeatOffer(options) {
|
|
15981
16356
|
const result = await requestAuthority(options, "seat-offer", { applicationId: options.applicationId, skuId: options.skuId });
|
|
15982
16357
|
const offer = result.seat;
|
|
15983
|
-
if (!offer || offer.membershipId !== await networkMembershipId(options) || !Number.isSafeInteger(offer.amountCents) || offer.amountCents
|
|
16358
|
+
if (!offer || offer.membershipId !== await networkMembershipId(options) || !Number.isSafeInteger(offer.amountCents) || offer.amountCents < 0 || offer.included !== (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");
|
|
15984
16359
|
return offer;
|
|
15985
16360
|
}
|
|
15986
16361
|
async function requestNamedSeatInvitation(options) {
|
|
@@ -16016,7 +16391,8 @@ async function requestNamedSeatCheckout(options) {
|
|
|
16016
16391
|
});
|
|
16017
16392
|
const seat = result.seat;
|
|
16018
16393
|
const expected = (await membershipOfferIdForRequest({ ...options, requestId: "named_seat" })).replace(/^offer_/, "seat_");
|
|
16019
|
-
|
|
16394
|
+
const included = seat?.amountCents === 0;
|
|
16395
|
+
if (!seat || seat.seatId !== expected || !Number.isSafeInteger(seat.amountCents) || seat.amountCents < 0 || !/^[a-z]{3}$/.test(seat.currency) || !Number.isSafeInteger(seat.termEndsAt) || (included ? seat.included !== true || seat.paymentIntentId !== void 0 || seat.clientSecret !== void 0 || !["pending_acceptance", "pending_approval", "pending_primary", "active"].includes(seat.status) : seat.included !== void 0 || !/^pi_[A-Za-z0-9_]+$/.test(String(seat.paymentIntentId)) || typeof seat.clientSecret !== "string" || !["payment_processing", "renewal_pending", "pending_acceptance", "pending_approval", "active"].includes(seat.status))) {
|
|
16020
16396
|
throw new Error("invalid named seat checkout receipt");
|
|
16021
16397
|
}
|
|
16022
16398
|
return seat;
|
|
@@ -16426,6 +16802,7 @@ var BUILTIN_ROUTES = [
|
|
|
16426
16802
|
handleAdminNamedSeat,
|
|
16427
16803
|
handleAdminMembershipSeats,
|
|
16428
16804
|
handleAdminMembershipPolicyVersions,
|
|
16805
|
+
handleAdminMembershipReterm,
|
|
16429
16806
|
handleAdminMeetings,
|
|
16430
16807
|
handleAdminScheduling,
|
|
16431
16808
|
// Roster + identity
|
|
@@ -16506,9 +16883,15 @@ export {
|
|
|
16506
16883
|
deliverMembershipChapterEffect,
|
|
16507
16884
|
deliverSignupControl,
|
|
16508
16885
|
drainMembershipEffects,
|
|
16886
|
+
grantIncludedNamedSeat,
|
|
16887
|
+
lowerSeatPrice,
|
|
16888
|
+
namedSeatIncluded,
|
|
16889
|
+
namedSeatRecipientState,
|
|
16890
|
+
readNamedSeatOffer,
|
|
16509
16891
|
refundNamedSeatByOperator,
|
|
16510
16892
|
renderActiveSignupControlHtml,
|
|
16511
16893
|
resolveNetworkTargetTransport,
|
|
16894
|
+
retermMembershipDiscount,
|
|
16512
16895
|
retryChapterApprovalEffects,
|
|
16513
16896
|
retryLeaderAdmissionDeliveries,
|
|
16514
16897
|
withActiveSignupControlTheme
|