@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.cjs
CHANGED
|
@@ -36,9 +36,15 @@ __export(worker_exports, {
|
|
|
36
36
|
deliverMembershipChapterEffect: () => deliverMembershipChapterEffect,
|
|
37
37
|
deliverSignupControl: () => deliverSignupControl,
|
|
38
38
|
drainMembershipEffects: () => drainMembershipEffects,
|
|
39
|
+
grantIncludedNamedSeat: () => grantIncludedNamedSeat,
|
|
40
|
+
lowerSeatPrice: () => lowerSeatPrice,
|
|
41
|
+
namedSeatIncluded: () => namedSeatIncluded,
|
|
42
|
+
namedSeatRecipientState: () => namedSeatRecipientState,
|
|
43
|
+
readNamedSeatOffer: () => readNamedSeatOffer,
|
|
39
44
|
refundNamedSeatByOperator: () => refundNamedSeatByOperator,
|
|
40
45
|
renderActiveSignupControlHtml: () => renderActiveSignupControlHtml,
|
|
41
46
|
resolveNetworkTargetTransport: () => resolveNetworkTargetTransport,
|
|
47
|
+
retermMembershipDiscount: () => retermMembershipDiscount,
|
|
42
48
|
retryChapterApprovalEffects: () => retryChapterApprovalEffects,
|
|
43
49
|
retryLeaderAdmissionDeliveries: () => retryLeaderAdmissionDeliveries,
|
|
44
50
|
withActiveSignupControlTheme: () => withActiveSignupControlTheme
|
|
@@ -544,30 +550,30 @@ async function checkBillingTerms(secretKey, input, resume, now, createMissing) {
|
|
|
544
550
|
const terms2 = subscriptionBillingTerms(input.billingTerms);
|
|
545
551
|
if (!resume && terms2.initialTermEndsAt !== void 0 && (terms2.initialTermEndsAt <= now || terms2.initialTermEndsAt - now > 730 * 864e5)) fail();
|
|
546
552
|
if (!terms2.discount) return;
|
|
547
|
-
const { couponId
|
|
553
|
+
const { couponId, percentOffBps, duration, policyVersionId } = terms2.discount;
|
|
548
554
|
const productId = input.discountProductId;
|
|
549
555
|
if (productId !== void 0 && !/^prod_[A-Za-z0-9_]+$/.test(productId)) fail();
|
|
550
556
|
const metadata = { ...stripePartitionMetadata(input), discountPolicyVersionId: policyVersionId };
|
|
551
557
|
const expansion = { "expand[0]": "applies_to" };
|
|
552
|
-
let result = await (0, import_stripe.stripeRequest)({ secretKey }, "GET", `/v1/coupons/${encodeURIComponent(
|
|
558
|
+
let result = await (0, import_stripe.stripeRequest)({ secretKey }, "GET", `/v1/coupons/${encodeURIComponent(couponId)}`, expansion);
|
|
553
559
|
if (createMissing && result.status === 404 && result.body?.error?.code === "resource_missing") {
|
|
554
560
|
result = await (0, import_stripe.stripeRequest)(
|
|
555
561
|
{ secretKey },
|
|
556
562
|
"POST",
|
|
557
563
|
"/v1/coupons",
|
|
558
564
|
{
|
|
559
|
-
id:
|
|
565
|
+
id: couponId,
|
|
560
566
|
percent_off: percentOffBps / 100,
|
|
561
567
|
duration,
|
|
562
568
|
metadata,
|
|
563
569
|
...expansion,
|
|
564
570
|
...productId ? { "applies_to[products][0]": productId } : {}
|
|
565
571
|
},
|
|
566
|
-
{ idempotencyKey: `chapter-policy-coupon:${
|
|
572
|
+
{ idempotencyKey: `chapter-policy-coupon:${couponId}` }
|
|
567
573
|
);
|
|
568
574
|
}
|
|
569
575
|
const coupon = result.body;
|
|
570
|
-
if (!result.ok || !coupon || coupon.id !==
|
|
576
|
+
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();
|
|
571
577
|
}
|
|
572
578
|
function subscriptionBillingParams(input, productId) {
|
|
573
579
|
if (!input.billingTerms) return {};
|
|
@@ -1802,8 +1808,8 @@ var object = (value) => {
|
|
|
1802
1808
|
var id = (value) => typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9_.:-]{2,159}$/.test(value) ? value : fail2("id");
|
|
1803
1809
|
var integer2 = (value, min, max) => typeof value === "number" && Number.isSafeInteger(value) && value >= min && value <= max ? value : fail2("integer");
|
|
1804
1810
|
var time = (value) => {
|
|
1805
|
-
const
|
|
1806
|
-
return integer2(
|
|
1811
|
+
const at2 = typeof value === "number" ? value : typeof value === "string" ? Date.parse(value) : NaN;
|
|
1812
|
+
return integer2(at2, 0, 864e13);
|
|
1807
1813
|
};
|
|
1808
1814
|
var ids = (value, allowEmpty = false) => {
|
|
1809
1815
|
if (!Array.isArray(value) || value.length > 100 || !allowEmpty && value.length === 0) return fail2("ids");
|
|
@@ -1847,7 +1853,7 @@ function membershipOfferingVersion(value) {
|
|
|
1847
1853
|
term: normalizedTerm,
|
|
1848
1854
|
autoRenew: true,
|
|
1849
1855
|
paymentGraceDays: integer2(input.paymentGraceDays, 0, 60),
|
|
1850
|
-
seatPriceCents: integer2(input.seatPriceCents,
|
|
1856
|
+
seatPriceCents: integer2(input.seatPriceCents, 0, Number.MAX_SAFE_INTEGER),
|
|
1851
1857
|
...input.seatPurchaseWindowCalendarMonths === void 0 || input.seatPurchaseWindowCalendarMonths === null ? {} : { seatPurchaseWindowCalendarMonths: integer2(input.seatPurchaseWindowCalendarMonths, 1, 120) },
|
|
1852
1858
|
requiredDisclosures: ids(input.requiredDisclosures)
|
|
1853
1859
|
};
|
|
@@ -1932,7 +1938,7 @@ async function hash(value) {
|
|
|
1932
1938
|
const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify(value)));
|
|
1933
1939
|
return [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
1934
1940
|
}
|
|
1935
|
-
async function
|
|
1941
|
+
async function membershipDiscountCouponId(input, policyId) {
|
|
1936
1942
|
return policyId ? `odla_m_${await hash([input.appId, input.chapterId, input.environment, input.runtime, input.skuId, policyId])}` : void 0;
|
|
1937
1943
|
}
|
|
1938
1944
|
async function load(db, ns, id6) {
|
|
@@ -1958,7 +1964,7 @@ async function restore(row2, input) {
|
|
|
1958
1964
|
discountPolicy: contract.discountPolicy,
|
|
1959
1965
|
approvedFoundingCount: Number(row2.approvedFoundingCount),
|
|
1960
1966
|
now: Number(row2.issuedAt),
|
|
1961
|
-
couponId: await
|
|
1967
|
+
couponId: await membershipDiscountCouponId(input, contract.discountPolicy?.id)
|
|
1962
1968
|
});
|
|
1963
1969
|
if (expected.offering.chapterId !== input.chapterId || expected.offering.baseSkuId !== input.skuId) reject("invalid_offer_snapshot");
|
|
1964
1970
|
const result = {
|
|
@@ -1989,7 +1995,7 @@ async function previewMembershipCommercialOffer(db, input) {
|
|
|
1989
1995
|
discountPolicy: policy,
|
|
1990
1996
|
approvedFoundingCount: Number(approvedFoundingCount),
|
|
1991
1997
|
now: input.now,
|
|
1992
|
-
couponId: await
|
|
1998
|
+
couponId: await membershipDiscountCouponId(input, policy?.id)
|
|
1993
1999
|
});
|
|
1994
2000
|
const discount = contract.billingTerms.discount;
|
|
1995
2001
|
return {
|
|
@@ -2039,7 +2045,7 @@ async function issueMembershipCommercialOffer(db, input) {
|
|
|
2039
2045
|
discountPolicy: policy,
|
|
2040
2046
|
approvedFoundingCount: Number(approvedFoundingCount),
|
|
2041
2047
|
now: input.now,
|
|
2042
|
-
couponId: await
|
|
2048
|
+
couponId: await membershipDiscountCouponId(input, policy?.id)
|
|
2043
2049
|
});
|
|
2044
2050
|
const value = {
|
|
2045
2051
|
id: input.offerId,
|
|
@@ -2935,7 +2941,7 @@ async function checkoutVariables(db, group, app) {
|
|
|
2935
2941
|
const frozen = await frozenCheckoutVariables(db, app);
|
|
2936
2942
|
return frozen.tierName ? frozen : { ...frozen, tierName: await catalogTierName(db, group, app) };
|
|
2937
2943
|
}
|
|
2938
|
-
function formatInstant(
|
|
2944
|
+
function formatInstant(at2, timeZone) {
|
|
2939
2945
|
return new Intl.DateTimeFormat("en-US", {
|
|
2940
2946
|
weekday: "long",
|
|
2941
2947
|
month: "long",
|
|
@@ -2945,7 +2951,7 @@ function formatInstant(at, timeZone) {
|
|
|
2945
2951
|
minute: "2-digit",
|
|
2946
2952
|
timeZoneName: "short",
|
|
2947
2953
|
timeZone
|
|
2948
|
-
}).format(new Date(
|
|
2954
|
+
}).format(new Date(at2));
|
|
2949
2955
|
}
|
|
2950
2956
|
var adminUrlFor = (origin) => `${origin}/admin/`;
|
|
2951
2957
|
var membersUrlFor = (origin) => `${origin}/members/`;
|
|
@@ -3893,6 +3899,181 @@ async function refundNamedSeatStripePayment(secretKey, seat) {
|
|
|
3893
3899
|
return { refundedCents, refundIds };
|
|
3894
3900
|
}
|
|
3895
3901
|
|
|
3902
|
+
// src/membership-seat-reservation.ts
|
|
3903
|
+
async function recipientKey(input, email) {
|
|
3904
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify([input.appId, input.chapterId, input.environment, input.runtime, email])));
|
|
3905
|
+
return `seat-recipient:${[...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
3906
|
+
}
|
|
3907
|
+
async function reserveNamedMembershipSeat(db, input, now) {
|
|
3908
|
+
membershipClock(now);
|
|
3909
|
+
const name = typeof input.recipientName === "string" ? input.recipientName.trim().replace(/\s+/g, " ") : "";
|
|
3910
|
+
const email = normalizeChapterEmail(typeof input.recipientEmail === "string" ? input.recipientEmail : "");
|
|
3911
|
+
const purchaser = normalizeChapterEmail(typeof input.purchaserEmail === "string" ? input.purchaserEmail : "");
|
|
3912
|
+
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");
|
|
3913
|
+
const state = await loadMembershipAuthorityState(db, input);
|
|
3914
|
+
const memberId = await networkMembershipId(input), member = await membershipLoad(db, "networkMembership", memberId);
|
|
3915
|
+
requireMembershipScope(member, input);
|
|
3916
|
+
if (!Number.isSafeInteger(member.revision) || !/^price_[A-Za-z0-9_]+$/.test(String(member.providerPriceId)) || member.providerPriceId === input.priceId) membershipFail("named_seat_purchase_unavailable");
|
|
3917
|
+
const id6 = (await membershipOfferIdForRequest({ ...input, requestId: "named_seat" })).replace(/^offer_/, "seat_");
|
|
3918
|
+
let seat = await membershipLoad(db, "networkSeat", id6);
|
|
3919
|
+
if (!seat) {
|
|
3920
|
+
const offering = membershipOfferingVersion(member.offeringSnapshot), claimId = await recipientKey(input, email);
|
|
3921
|
+
const purchaseEndsAt = seatPurchaseDeadline(offering, member);
|
|
3922
|
+
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) {
|
|
3923
|
+
membershipFail("named_seat_purchase_unavailable");
|
|
3924
|
+
}
|
|
3925
|
+
const attrs = {
|
|
3926
|
+
id: id6,
|
|
3927
|
+
...membershipScope(input),
|
|
3928
|
+
primaryMembershipId: memberId,
|
|
3929
|
+
recipientName: name,
|
|
3930
|
+
recipientEmailNormalized: email,
|
|
3931
|
+
purchaserUserId: input.purchaserUserId,
|
|
3932
|
+
recipientClaimId: claimId,
|
|
3933
|
+
priceId: input.priceId,
|
|
3934
|
+
primaryPriceId: member.providerPriceId,
|
|
3935
|
+
amountCents: offering.seatPriceCents,
|
|
3936
|
+
currency: offering.currency,
|
|
3937
|
+
interval: offering.interval,
|
|
3938
|
+
providerCustomerId: member.providerCustomerId,
|
|
3939
|
+
providerSubscriptionId: member.providerSubscriptionId,
|
|
3940
|
+
status: "payment_pending",
|
|
3941
|
+
preparedAt: now,
|
|
3942
|
+
termEndsAt: member.termEndsAt,
|
|
3943
|
+
seatPurchaseEndsAt: purchaseEndsAt,
|
|
3944
|
+
offeringSnapshot: offering,
|
|
3945
|
+
revision: 1,
|
|
3946
|
+
createdAt: now,
|
|
3947
|
+
updatedAt: now
|
|
3948
|
+
};
|
|
3949
|
+
try {
|
|
3950
|
+
await db.transact([
|
|
3951
|
+
membershipUpdate("networkSeat", id6, attrs),
|
|
3952
|
+
membershipUpdate("networkSeatRecipient", claimId, {
|
|
3953
|
+
id: claimId,
|
|
3954
|
+
appId: input.appId,
|
|
3955
|
+
chapterId: input.chapterId,
|
|
3956
|
+
environment: input.environment,
|
|
3957
|
+
runtime: input.runtime,
|
|
3958
|
+
seatId: id6,
|
|
3959
|
+
recipientEmailNormalized: email,
|
|
3960
|
+
createdAt: now
|
|
3961
|
+
}),
|
|
3962
|
+
membershipUpdate("networkMembership", memberId, { namedSeatId: id6, revision: Number(member.revision) + 1, updatedAt: now })
|
|
3963
|
+
], { mutationId: `named-seat-reserve:${id6}`, guards: [
|
|
3964
|
+
{ ns: "networkSeat", id: id6, exists: false },
|
|
3965
|
+
{ ns: "networkSeatRecipient", id: claimId, exists: false },
|
|
3966
|
+
{ ns: "networkMembership", id: memberId, exists: true, equals: { revision: member.revision }, absent: ["namedSeatId"] },
|
|
3967
|
+
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
|
|
3968
|
+
] });
|
|
3969
|
+
} catch (error) {
|
|
3970
|
+
if (error.code !== "transact_guard_failed") throw error;
|
|
3971
|
+
}
|
|
3972
|
+
seat = await membershipLoad(db, "networkSeat", id6);
|
|
3973
|
+
if (!seat && await membershipLoad(db, "networkSeatRecipient", claimId)) membershipFail("named_seat_recipient_unavailable");
|
|
3974
|
+
}
|
|
3975
|
+
requireMembershipScope(seat, input);
|
|
3976
|
+
if (seat.primaryMembershipId !== memberId || seat.recipientName !== name || seat.recipientEmailNormalized !== email || seat.priceId !== input.priceId || seat.purchaserUserId !== input.purchaserUserId) membershipFail("named_seat_purchase_conflict");
|
|
3977
|
+
return seat;
|
|
3978
|
+
}
|
|
3979
|
+
|
|
3980
|
+
// src/membership-seat-included.ts
|
|
3981
|
+
var namedSeatIncluded = (seat) => Number(seat.amountCents) === 0;
|
|
3982
|
+
var namedSeatSettled = (seat) => namedSeatIncluded(seat) ? seat.purchasedAt !== void 0 : Boolean(seat.providerPaymentIntentId && seat.providerSubscriptionItemId);
|
|
3983
|
+
function namedSeatRecipientState(seat) {
|
|
3984
|
+
switch (String(seat.status)) {
|
|
3985
|
+
case "renewal_pending":
|
|
3986
|
+
case "pending_acceptance":
|
|
3987
|
+
return "invited";
|
|
3988
|
+
case "pending_approval":
|
|
3989
|
+
case "pending_primary":
|
|
3990
|
+
return "accepted";
|
|
3991
|
+
case "active":
|
|
3992
|
+
return "joined";
|
|
3993
|
+
case "refund_pending":
|
|
3994
|
+
case "refunded":
|
|
3995
|
+
return seat.refundReason === "declined" ? "declined" : "refunded";
|
|
3996
|
+
case "canceled":
|
|
3997
|
+
return "canceled";
|
|
3998
|
+
default:
|
|
3999
|
+
return "payment_pending";
|
|
4000
|
+
}
|
|
4001
|
+
}
|
|
4002
|
+
var OPEN = ["pending_acceptance", "pending_approval", "pending_primary", "active"];
|
|
4003
|
+
async function grantIncludedNamedSeat(db, input, now) {
|
|
4004
|
+
membershipClock(now);
|
|
4005
|
+
const seat = await reserveNamedMembershipSeat(db, input, now);
|
|
4006
|
+
if (!namedSeatIncluded(seat)) membershipFail("named_seat_not_included");
|
|
4007
|
+
const state = await loadMembershipAuthorityState(db, input);
|
|
4008
|
+
const member = await membershipLoad(db, "networkMembership", seat.primaryMembershipId);
|
|
4009
|
+
requireMembershipScope(member, input);
|
|
4010
|
+
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)) {
|
|
4011
|
+
membershipFail("named_seat_purchase_unavailable");
|
|
4012
|
+
}
|
|
4013
|
+
if (seat.status === "payment_pending") {
|
|
4014
|
+
const entitlementId = `${seat.id}:entitlement`, inviteId = `${seat.id}:invite`, source = `included:${seat.id}`;
|
|
4015
|
+
try {
|
|
4016
|
+
await db.transact([
|
|
4017
|
+
membershipUpdate("networkSeat", seat.id, {
|
|
4018
|
+
status: "pending_acceptance",
|
|
4019
|
+
purchasedAt: now,
|
|
4020
|
+
termStartedAt: now,
|
|
4021
|
+
termEndsAt: member.termEndsAt,
|
|
4022
|
+
acceptanceEndsAt: Number(member.termEndsAt),
|
|
4023
|
+
entitlementId,
|
|
4024
|
+
revision: Number(seat.revision) + 1,
|
|
4025
|
+
updatedAt: now
|
|
4026
|
+
}),
|
|
4027
|
+
membershipUpdate("networkEntitlement", entitlementId, {
|
|
4028
|
+
id: entitlementId,
|
|
4029
|
+
...membershipScope(input),
|
|
4030
|
+
membershipId: member.id,
|
|
4031
|
+
seatId: seat.id,
|
|
4032
|
+
kind: "named_seat",
|
|
4033
|
+
status: "pending_acceptance",
|
|
4034
|
+
termStartedAt: now,
|
|
4035
|
+
termEndsAt: member.termEndsAt,
|
|
4036
|
+
offeringSnapshot: member.offeringSnapshot,
|
|
4037
|
+
sourceEventId: source,
|
|
4038
|
+
revision: 1,
|
|
4039
|
+
createdAt: now,
|
|
4040
|
+
updatedAt: now
|
|
4041
|
+
}),
|
|
4042
|
+
membershipUpdate("membershipEffect", inviteId, {
|
|
4043
|
+
id: inviteId,
|
|
4044
|
+
chapterId: input.chapterId,
|
|
4045
|
+
environment: input.environment,
|
|
4046
|
+
runtime: input.runtime,
|
|
4047
|
+
membershipId: member.id,
|
|
4048
|
+
seatId: seat.id,
|
|
4049
|
+
kind: "invite_named_recipient",
|
|
4050
|
+
status: "pending",
|
|
4051
|
+
attempts: 0,
|
|
4052
|
+
createdAt: now,
|
|
4053
|
+
updatedAt: now
|
|
4054
|
+
})
|
|
4055
|
+
], { mutationId: `named-seat-included:${seat.id}`, guards: [
|
|
4056
|
+
{
|
|
4057
|
+
ns: "networkSeat",
|
|
4058
|
+
id: seat.id,
|
|
4059
|
+
exists: true,
|
|
4060
|
+
equals: { status: "payment_pending", revision: seat.revision, amountCents: 0 },
|
|
4061
|
+
absent: ["purchasedAt", "providerPaymentIntentId"]
|
|
4062
|
+
},
|
|
4063
|
+
{ ns: "networkEntitlement", id: entitlementId, exists: false },
|
|
4064
|
+
{ ns: "networkMembership", id: String(member.id), exists: true, equals: { revision: member.revision, namedSeatId: seat.id } },
|
|
4065
|
+
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
|
|
4066
|
+
] });
|
|
4067
|
+
} catch (error) {
|
|
4068
|
+
if (error.code !== "transact_guard_failed") throw error;
|
|
4069
|
+
}
|
|
4070
|
+
}
|
|
4071
|
+
const current = await membershipLoad(db, "networkSeat", seat.id);
|
|
4072
|
+
requireMembershipScope(current, input);
|
|
4073
|
+
if (!namedSeatIncluded(current) || current.purchasedAt === void 0 || !OPEN.includes(String(current.status))) membershipFail("named_seat_purchase_unavailable");
|
|
4074
|
+
return { seatId: seat.id, amountCents: 0, currency: seat.currency, termEndsAt: Number(current.termEndsAt), status: String(current.status), included: true };
|
|
4075
|
+
}
|
|
4076
|
+
|
|
3896
4077
|
// src/membership-seat-refund.ts
|
|
3897
4078
|
async function requestNamedSeatRefund(db, input, now) {
|
|
3898
4079
|
membershipClock(now);
|
|
@@ -3902,7 +4083,7 @@ async function requestNamedSeatRefund(db, input, now) {
|
|
|
3902
4083
|
const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
|
|
3903
4084
|
requireMembershipScope(member, input);
|
|
3904
4085
|
if (seat.status === "refunded" || seat.status === "refund_pending") return;
|
|
3905
|
-
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");
|
|
4086
|
+
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");
|
|
3906
4087
|
const entitlement = seat.entitlementId ? await membershipLoad(db, "networkEntitlement", String(seat.entitlementId)) : void 0;
|
|
3907
4088
|
await db.transact([
|
|
3908
4089
|
membershipUpdate("networkSeat", input.seatId, { status: "refund_pending", refundReason: input.reason, revision: Number(seat.revision) + 1, updatedAt: now }),
|
|
@@ -3947,7 +4128,7 @@ async function completeNamedSeatRefund(db, seatId, secretKey, now) {
|
|
|
3947
4128
|
await loadMembershipAuthorityState(db, seat);
|
|
3948
4129
|
if (seat.status === "refunded") return;
|
|
3949
4130
|
if (seat.status !== "refund_pending") membershipFail("named_seat_refund_not_ready");
|
|
3950
|
-
const result = await refundNamedSeatStripePayment(secretKey, seat);
|
|
4131
|
+
const result = namedSeatIncluded(seat) ? { refundedCents: 0, refundIds: [] } : await refundNamedSeatStripePayment(secretKey, seat);
|
|
3951
4132
|
await db.transact([
|
|
3952
4133
|
membershipUpdate("networkSeat", seatId, {
|
|
3953
4134
|
status: "refunded",
|
|
@@ -3969,7 +4150,7 @@ async function refundNamedSeatByOperator(db, input, stripeSecretKey, now) {
|
|
|
3969
4150
|
await requestNamedSeatRefund(db, { ...input, reason: "operator" }, now);
|
|
3970
4151
|
await completeNamedSeatRefund(db, input.seatId, stripeSecretKey, now);
|
|
3971
4152
|
const refunded = await membershipLoad(db, "networkSeat", input.seatId);
|
|
3972
|
-
if (refunded?.status !== "refunded" || !Number.isSafeInteger(refunded.refundedCents) || Number(refunded.refundedCents)
|
|
4153
|
+
if (refunded?.status !== "refunded" || !Number.isSafeInteger(refunded.refundedCents) || Number(refunded.refundedCents) !== Number(seat.amountCents)) {
|
|
3973
4154
|
throw new Error("named seat refund unconfirmed");
|
|
3974
4155
|
}
|
|
3975
4156
|
return { seatId: String(seat.id), status: "refunded", refundedCents: Number(refunded.refundedCents), subscriptionCanceled: false };
|
|
@@ -4001,9 +4182,56 @@ async function liftSeatPurchaseWindow(db, scope2, now, limit = 100) {
|
|
|
4001
4182
|
return { lifted, more: rows5.length > limit };
|
|
4002
4183
|
}
|
|
4003
4184
|
|
|
4185
|
+
// src/membership-seat-reprice.ts
|
|
4186
|
+
async function lowerSeatPrice(db, scope2, now, limit = 100) {
|
|
4187
|
+
membershipClock(now);
|
|
4188
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200) throw new TypeError("invalid reprice limit");
|
|
4189
|
+
const state = await loadMembershipAuthorityState(db, scope2);
|
|
4190
|
+
const versions = (await db.query({ membershipOfferingVersion: { $: { where: { chapterId: scope2.chapterId }, limit: 200 } } })).membershipOfferingVersion ?? [];
|
|
4191
|
+
if (versions.length >= 200) throw Object.assign(new Error("policy_history_incomplete"), { code: "policy_history_incomplete" });
|
|
4192
|
+
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];
|
|
4193
|
+
if (!current) throw Object.assign(new Error("ambiguous_membership_policy"), { code: "ambiguous_membership_policy" });
|
|
4194
|
+
const target = current.seatPriceCents;
|
|
4195
|
+
const rows5 = (await db.query({ networkMembership: { $: { where: { ...scope2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkMembership ?? [];
|
|
4196
|
+
const receipt = { seatPriceCents: target, repriced: 0, skipped: [], more: rows5.length > limit };
|
|
4197
|
+
for (const member of rows5.slice(0, limit)) {
|
|
4198
|
+
if (Object.entries(scope2).some(([key3, value]) => member[key3] !== value)) throw new Error("membership reprice scope mismatch");
|
|
4199
|
+
const id6 = String(member.id), skip = (reason) => {
|
|
4200
|
+
receipt.skipped.push({ membershipId: id6, reason });
|
|
4201
|
+
};
|
|
4202
|
+
const offering = membershipOfferingVersion(member.offeringSnapshot);
|
|
4203
|
+
if (offering.seatPriceCents <= target) {
|
|
4204
|
+
skip("seat price already at or below the current offering");
|
|
4205
|
+
continue;
|
|
4206
|
+
}
|
|
4207
|
+
if (!["paid_pending_approval", "active", "cancel_at_term_end", "payment_grace"].includes(String(member.status))) {
|
|
4208
|
+
skip(`membership is ${String(member.status)}`);
|
|
4209
|
+
continue;
|
|
4210
|
+
}
|
|
4211
|
+
const seat = member.namedSeatId ? await membershipLoad(db, "networkSeat", String(member.namedSeatId)) : void 0;
|
|
4212
|
+
if (seat && (seat.status !== "payment_pending" || seat.providerPaymentIntentId !== void 0)) {
|
|
4213
|
+
skip(`seat is ${String(seat.status)}`);
|
|
4214
|
+
continue;
|
|
4215
|
+
}
|
|
4216
|
+
const ops = [membershipUpdate("networkMembership", id6, { offeringSnapshot: { ...offering, seatPriceCents: target }, revision: Number(member.revision) + 1, updatedAt: now })];
|
|
4217
|
+
const guards = [
|
|
4218
|
+
{ ns: "networkMembership", id: id6, exists: true, equals: { revision: member.revision } },
|
|
4219
|
+
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative" } }
|
|
4220
|
+
];
|
|
4221
|
+
if (seat) {
|
|
4222
|
+
ops.push(membershipUpdate("networkSeat", String(seat.id), { amountCents: target, offeringSnapshot: { ...offering, seatPriceCents: target }, revision: Number(seat.revision) + 1, updatedAt: now }));
|
|
4223
|
+
guards.push({ ns: "networkSeat", id: String(seat.id), exists: true, equals: { revision: seat.revision, status: "payment_pending" } });
|
|
4224
|
+
}
|
|
4225
|
+
await db.transact(ops, { mutationId: `seat-reprice:${id6}:${member.revision}`, guards });
|
|
4226
|
+
receipt.repriced++;
|
|
4227
|
+
}
|
|
4228
|
+
return receipt;
|
|
4229
|
+
}
|
|
4230
|
+
|
|
4004
4231
|
// src/worker-routes-admin-membership-seats.ts
|
|
4005
4232
|
var LIST = "/api/admin/membership-seats";
|
|
4006
4233
|
var LIFT = "/api/admin/membership-seats/lift-purchase-window";
|
|
4234
|
+
var REPRICE = "/api/admin/membership-seats/reprice";
|
|
4007
4235
|
var ONE = /^\/api\/admin\/membership-seats\/([A-Za-z0-9][A-Za-z0-9_.:-]{2,159})\/refund$/;
|
|
4008
4236
|
var SCOPE = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
4009
4237
|
var PUBLIC = [
|
|
@@ -4030,11 +4258,11 @@ var PUBLIC = [
|
|
|
4030
4258
|
];
|
|
4031
4259
|
var project = (row2) => Object.fromEntries(PUBLIC.filter((key3) => row2[key3] !== void 0).map((key3) => [key3, row2[key3]]));
|
|
4032
4260
|
var handleAdminMembershipSeats = async (req, url, env, ctx) => {
|
|
4033
|
-
const one = url.pathname.match(ONE), lift = url.pathname === LIFT;
|
|
4034
|
-
if (url.pathname !== LIST && !one && !lift) return null;
|
|
4261
|
+
const one = url.pathname.match(ONE), lift = url.pathname === LIFT, reprice = url.pathname === REPRICE;
|
|
4262
|
+
if (url.pathname !== LIST && !one && !lift && !reprice) return null;
|
|
4035
4263
|
const operator = ctx.membershipAuthorityOperator;
|
|
4036
4264
|
if (!operator) return json({ error: "not found" }, 404);
|
|
4037
|
-
if (req.method !== (one || lift ? "POST" : "GET")) return json({ error: "method not allowed" }, 405);
|
|
4265
|
+
if (req.method !== (one || lift || reprice ? "POST" : "GET")) return json({ error: "method not allowed" }, 405);
|
|
4038
4266
|
const raw2 = ctx.makeDb(env), user = await ctx.verifyUser(req, env);
|
|
4039
4267
|
if (!user) return json({ error: "unauthorized" }, 401);
|
|
4040
4268
|
if (!await ctx.isSuperAdminEmail(raw2, user.email, user.userId)) return json({ error: "super-admin required", code: "super_admin_required" }, 403);
|
|
@@ -4044,7 +4272,7 @@ var handleAdminMembershipSeats = async (req, url, env, ctx) => {
|
|
|
4044
4272
|
} catch {
|
|
4045
4273
|
return json({ error: "the membership authority is not hosted by this deployment" }, 404);
|
|
4046
4274
|
}
|
|
4047
|
-
if (lift) {
|
|
4275
|
+
if (lift || reprice) {
|
|
4048
4276
|
let body;
|
|
4049
4277
|
try {
|
|
4050
4278
|
body = await req.json();
|
|
@@ -4052,14 +4280,16 @@ var handleAdminMembershipSeats = async (req, url, env, ctx) => {
|
|
|
4052
4280
|
return json({ error: "chapterId and runtime are required" }, 400);
|
|
4053
4281
|
}
|
|
4054
4282
|
const appId = body.appId ?? body.chapterId;
|
|
4055
|
-
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)) {
|
|
4056
|
-
return json({ error: "chapterId and runtime are required" }, 400);
|
|
4283
|
+
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))) {
|
|
4284
|
+
return json({ error: reprice ? "chapterId, runtime and skuId are required" : "chapterId and runtime are required" }, 400);
|
|
4057
4285
|
}
|
|
4286
|
+
const scope2 = { appId, chapterId: body.chapterId, environment: env.ODLA_ENV, runtime: body.runtime };
|
|
4058
4287
|
try {
|
|
4059
|
-
return json({ ok: true, ...await
|
|
4288
|
+
if (reprice) return json({ ok: true, ...await lowerSeatPrice(db, { ...scope2, skuId: String(body.skuId) }, Date.now()) });
|
|
4289
|
+
return json({ ok: true, ...await liftSeatPurchaseWindow(db, scope2, Date.now()) });
|
|
4060
4290
|
} catch (error) {
|
|
4061
4291
|
const code = error.code;
|
|
4062
|
-
return json({ error: "purchase window lift is pending; retry to confirm
|
|
4292
|
+
return json({ error: `${reprice ? "seat reprice" : "purchase window lift"} is pending; retry to confirm`, ...code ? { code } : {} }, code ? 409 : 503);
|
|
4063
4293
|
}
|
|
4064
4294
|
}
|
|
4065
4295
|
if (!one) {
|
|
@@ -4095,8 +4325,8 @@ var handleAdminMembershipSeats = async (req, url, env, ctx) => {
|
|
|
4095
4325
|
var LIMIT = 200;
|
|
4096
4326
|
var text2 = (value) => typeof value === "string" && value.length > 0 && value.length <= 200 ? value : void 0;
|
|
4097
4327
|
var instant = (value) => {
|
|
4098
|
-
const
|
|
4099
|
-
return Number.isSafeInteger(
|
|
4328
|
+
const at2 = typeof value === "number" ? value : typeof value === "string" ? Date.parse(value) : NaN;
|
|
4329
|
+
return Number.isSafeInteger(at2) && at2 >= 0 ? at2 : void 0;
|
|
4100
4330
|
};
|
|
4101
4331
|
function provenance(row2) {
|
|
4102
4332
|
const source = text2(row2.source), actor = text2(row2.actor ?? row2.$createdBy), recordedAt = instant(row2.recordedAt ?? row2.createdAt ?? row2.$createdAt);
|
|
@@ -4192,6 +4422,239 @@ var handleAdminMembershipPolicyVersions = async (req, url, env, ctx) => {
|
|
|
4192
4422
|
return json(await listMembershipPolicyVersions(db, { chapterId, now: Date.now() }));
|
|
4193
4423
|
};
|
|
4194
4424
|
|
|
4425
|
+
// src/membership-discount-reterm.ts
|
|
4426
|
+
var import_stripe8 = require("@odla-ai/stripe");
|
|
4427
|
+
|
|
4428
|
+
// src/subscription-provider-contract.ts
|
|
4429
|
+
var import_stripe7 = require("@odla-ai/stripe");
|
|
4430
|
+
function subscriptionContractMismatch() {
|
|
4431
|
+
throw Object.assign(new Error("Stripe subscription does not match the checkout contract"), { code: "stripe_contract_mismatch" });
|
|
4432
|
+
}
|
|
4433
|
+
async function verifySubscriptionPrice(secretKey, input, resume = false) {
|
|
4434
|
+
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();
|
|
4435
|
+
const result = await (0, import_stripe7.stripeRequest)({ secretKey }, "GET", `/v1/prices/${encodeURIComponent(input.priceId)}`);
|
|
4436
|
+
const price = result.body;
|
|
4437
|
+
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();
|
|
4438
|
+
return typeof price.product === "string" ? price.product : void 0;
|
|
4439
|
+
}
|
|
4440
|
+
function verifySubscriptionResult(sub, input, customerId, metadata) {
|
|
4441
|
+
const items = sub?.items;
|
|
4442
|
+
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();
|
|
4443
|
+
const invoice = sub.latest_invoice;
|
|
4444
|
+
if (!invoice || typeof invoice !== "object" || invoice.customer !== customerId || invoice.currency !== input.currency || invoice.total !== subscriptionAmountDue(input) || invoice.billing_reason !== "subscription_create") subscriptionContractMismatch();
|
|
4445
|
+
verifySubscriptionBillingResult(sub, input);
|
|
4446
|
+
}
|
|
4447
|
+
|
|
4448
|
+
// src/membership-discount-reterm.ts
|
|
4449
|
+
var SAFE = /^[A-Za-z0-9][A-Za-z0-9_.:-]{2,159}$/;
|
|
4450
|
+
var RETERMABLE = /* @__PURE__ */ new Set(["active", "paid_pending_approval", "cancel_at_term_end", "payment_grace"]);
|
|
4451
|
+
var PROVIDER_LIVE = /* @__PURE__ */ new Set(["active", "trialing", "past_due"]);
|
|
4452
|
+
var couponOf = (discount) => {
|
|
4453
|
+
const ref = discount?.source?.coupon ?? discount?.coupon;
|
|
4454
|
+
return typeof ref === "string" ? ref : typeof ref?.id === "string" ? ref.id : void 0;
|
|
4455
|
+
};
|
|
4456
|
+
async function retermPolicy(db, scope2, id6) {
|
|
4457
|
+
const row2 = await membershipLoad(db, "discountPolicyVersion", id6);
|
|
4458
|
+
if (!row2) membershipFail("membership_reterm_policy_unknown");
|
|
4459
|
+
let policy;
|
|
4460
|
+
try {
|
|
4461
|
+
policy = membershipDiscountPolicyVersion(row2.snapshot);
|
|
4462
|
+
} catch {
|
|
4463
|
+
return membershipFail("membership_reterm_policy_ineligible");
|
|
4464
|
+
}
|
|
4465
|
+
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");
|
|
4466
|
+
return policy;
|
|
4467
|
+
}
|
|
4468
|
+
async function attachProviderDiscount(sk, scope2, member, policy, couponId, productId) {
|
|
4469
|
+
const subId = String(member.providerSubscriptionId), path = `/v1/subscriptions/${encodeURIComponent(subId)}`;
|
|
4470
|
+
const read2 = await (0, import_stripe8.stripeRequest)({ secretKey: sk }, "GET", path, { "expand[0]": "discounts" });
|
|
4471
|
+
const sub = read2.body, items = sub?.items?.data;
|
|
4472
|
+
const metadata = { ...stripePartitionMetadata(scope2), applicationId: String(member.applicationId) };
|
|
4473
|
+
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) {
|
|
4474
|
+
throw Object.assign(new Error("provider subscription does not match the frozen membership"), { code: "provider_mismatch" });
|
|
4475
|
+
}
|
|
4476
|
+
const discounts = sub.discounts ?? (sub.discount ? [sub.discount] : []);
|
|
4477
|
+
if (!Array.isArray(discounts)) throw Object.assign(new Error("provider discounts unreadable"), { code: "provider_mismatch" });
|
|
4478
|
+
const present = discounts.map(couponOf);
|
|
4479
|
+
if (present.length === 1 && present[0] === couponId) return false;
|
|
4480
|
+
if (present.length > 0) throw Object.assign(new Error("provider subscription carries an unrelated discount"), { code: "provider_mismatch" });
|
|
4481
|
+
const written = await (0, import_stripe8.stripeRequest)({ secretKey: sk }, "POST", path, {
|
|
4482
|
+
"discounts[0][coupon]": couponId,
|
|
4483
|
+
"expand[0]": "discounts",
|
|
4484
|
+
"metadata[discountPolicyVersionId]": policy.id
|
|
4485
|
+
}, { idempotencyKey: `membership-reterm:${String(member.id)}:${policy.id}` });
|
|
4486
|
+
const after = written.body?.discounts;
|
|
4487
|
+
if (!written.ok || written.body?.id !== subId || !Array.isArray(after) || after.length !== 1 || couponOf(after[0]) !== couponId) {
|
|
4488
|
+
membershipFail("membership_reterm_provider_unconfirmed");
|
|
4489
|
+
}
|
|
4490
|
+
return true;
|
|
4491
|
+
}
|
|
4492
|
+
async function retermMembershipDiscount(db, stripeSecretKey, scope2, input, now) {
|
|
4493
|
+
membershipClock(now);
|
|
4494
|
+
const limit = input.limit ?? 100;
|
|
4495
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200) throw new TypeError("invalid reterm limit");
|
|
4496
|
+
if (typeof input.policyVersionId !== "string" || !SAFE.test(input.policyVersionId)) throw new TypeError("invalid policy version id");
|
|
4497
|
+
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");
|
|
4498
|
+
const state = await loadMembershipAuthorityState(db, scope2);
|
|
4499
|
+
const policy = await retermPolicy(db, scope2, input.policyVersionId);
|
|
4500
|
+
const receipt = { policyVersionId: policy.id, retermed: [], unchanged: [], skipped: [], more: false };
|
|
4501
|
+
const skip = (membershipId, reason) => {
|
|
4502
|
+
receipt.skipped.push({ membershipId, reason });
|
|
4503
|
+
};
|
|
4504
|
+
let rows5;
|
|
4505
|
+
if (input.membershipIds) {
|
|
4506
|
+
rows5 = [];
|
|
4507
|
+
for (const id6 of input.membershipIds) {
|
|
4508
|
+
const member = await membershipLoad(db, "networkMembership", id6);
|
|
4509
|
+
if (!member) skip(id6, "not_found");
|
|
4510
|
+
else if (Object.entries(scope2).some(([key3, value]) => member[key3] !== value)) skip(id6, "scope_mismatch");
|
|
4511
|
+
else rows5.push(member);
|
|
4512
|
+
}
|
|
4513
|
+
} else {
|
|
4514
|
+
const found = (await db.query({ networkMembership: { $: { where: { ...scope2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkMembership ?? [];
|
|
4515
|
+
if (found.some((member) => Object.entries(scope2).some(([key3, value]) => member[key3] !== value))) throw new Error("membership reterm scope mismatch");
|
|
4516
|
+
receipt.more = found.length > limit;
|
|
4517
|
+
rows5 = found.slice(0, limit);
|
|
4518
|
+
}
|
|
4519
|
+
for (const member of rows5) {
|
|
4520
|
+
const id6 = String(member.id);
|
|
4521
|
+
if (member.discountPolicyVersionId === policy.id) {
|
|
4522
|
+
receipt.unchanged.push(id6);
|
|
4523
|
+
continue;
|
|
4524
|
+
}
|
|
4525
|
+
if (!RETERMABLE.has(String(member.status))) {
|
|
4526
|
+
skip(id6, "status");
|
|
4527
|
+
continue;
|
|
4528
|
+
}
|
|
4529
|
+
if (member.founding !== true) {
|
|
4530
|
+
skip(id6, "not_founding");
|
|
4531
|
+
continue;
|
|
4532
|
+
}
|
|
4533
|
+
if (member.cohort !== policy.cohortLabel) {
|
|
4534
|
+
skip(id6, "cohort_mismatch");
|
|
4535
|
+
continue;
|
|
4536
|
+
}
|
|
4537
|
+
const skuId = String(member.baseSkuId);
|
|
4538
|
+
if (!policy.discountEligibleSkuIds.includes(skuId)) {
|
|
4539
|
+
skip(id6, "sku_not_discounted");
|
|
4540
|
+
continue;
|
|
4541
|
+
}
|
|
4542
|
+
const base = Number(member.baseAmountCents), current = Number(member.renewalAmountCents);
|
|
4543
|
+
const renewal = Math.round(base * (1e4 - policy.percentOffBps) / 1e4);
|
|
4544
|
+
if (!Number.isSafeInteger(base) || base <= 0 || !Number.isSafeInteger(current) || renewal > current) {
|
|
4545
|
+
skip(id6, "would_not_widen");
|
|
4546
|
+
continue;
|
|
4547
|
+
}
|
|
4548
|
+
const couponId = await membershipDiscountCouponId({ ...scope2, skuId }, policy.id);
|
|
4549
|
+
let providerUpdated;
|
|
4550
|
+
try {
|
|
4551
|
+
const productId = await verifySubscriptionPrice(stripeSecretKey, {
|
|
4552
|
+
priceId: String(member.providerPriceId),
|
|
4553
|
+
priceCents: base,
|
|
4554
|
+
currency: String(member.currency),
|
|
4555
|
+
interval: member.interval === "month" ? "month" : "year",
|
|
4556
|
+
environment: scope2.environment
|
|
4557
|
+
}, true);
|
|
4558
|
+
if (!productId) throw Object.assign(new Error("price has no product"), { code: "provider_mismatch" });
|
|
4559
|
+
await prepareSubscriptionBillingTerms(stripeSecretKey, {
|
|
4560
|
+
...scope2,
|
|
4561
|
+
priceCents: base,
|
|
4562
|
+
currency: String(member.currency),
|
|
4563
|
+
discountProductId: productId,
|
|
4564
|
+
billingTerms: {
|
|
4565
|
+
offeringVersionId: String(member.offeringVersionId),
|
|
4566
|
+
discount: { policyVersionId: policy.id, couponId, percentOffBps: policy.percentOffBps, duration: "forever" }
|
|
4567
|
+
}
|
|
4568
|
+
}, false);
|
|
4569
|
+
providerUpdated = await attachProviderDiscount(stripeSecretKey, scope2, member, policy, couponId, productId);
|
|
4570
|
+
} catch (error) {
|
|
4571
|
+
const code = error.code;
|
|
4572
|
+
if (code === "provider_mismatch" || code === "stripe_contract_mismatch") {
|
|
4573
|
+
skip(id6, "provider_mismatch");
|
|
4574
|
+
continue;
|
|
4575
|
+
}
|
|
4576
|
+
throw error;
|
|
4577
|
+
}
|
|
4578
|
+
try {
|
|
4579
|
+
await db.transact([membershipUpdate("networkMembership", id6, {
|
|
4580
|
+
discountPolicyVersionId: policy.id,
|
|
4581
|
+
discountSnapshot: policy,
|
|
4582
|
+
renewalAmountCents: renewal,
|
|
4583
|
+
revision: Number(member.revision) + 1,
|
|
4584
|
+
updatedAt: now
|
|
4585
|
+
})], {
|
|
4586
|
+
mutationId: `membership-reterm:${id6}:${policy.id}:${member.revision}`,
|
|
4587
|
+
guards: [
|
|
4588
|
+
{ ns: "networkMembership", id: id6, exists: true, equals: { revision: member.revision, status: member.status } },
|
|
4589
|
+
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative" } },
|
|
4590
|
+
{ ns: "discountPolicyVersion", id: policy.id, exists: true }
|
|
4591
|
+
]
|
|
4592
|
+
});
|
|
4593
|
+
} catch (error) {
|
|
4594
|
+
if (error.code !== "transact_guard_failed") throw error;
|
|
4595
|
+
return membershipFail("membership_reterm_retry_required");
|
|
4596
|
+
}
|
|
4597
|
+
receipt.retermed.push({
|
|
4598
|
+
membershipId: id6,
|
|
4599
|
+
...typeof member.discountPolicyVersionId === "string" ? { previousPolicyVersionId: member.discountPolicyVersionId } : {},
|
|
4600
|
+
previousRenewalAmountCents: current,
|
|
4601
|
+
renewalAmountCents: renewal,
|
|
4602
|
+
providerUpdated
|
|
4603
|
+
});
|
|
4604
|
+
}
|
|
4605
|
+
return receipt;
|
|
4606
|
+
}
|
|
4607
|
+
|
|
4608
|
+
// src/worker-routes-admin-membership-reterm.ts
|
|
4609
|
+
var PATH4 = "/api/admin/membership-discount/reterm";
|
|
4610
|
+
var SCOPE3 = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
4611
|
+
var ID = /^[A-Za-z0-9][A-Za-z0-9_.:-]{2,159}$/;
|
|
4612
|
+
var BAD = "chapterId and policyVersionId are required; runtime, appId, membershipIds and limit are optional";
|
|
4613
|
+
var handleAdminMembershipReterm = async (req, url, env, ctx) => {
|
|
4614
|
+
if (url.pathname !== PATH4) return null;
|
|
4615
|
+
const operator = ctx.membershipAuthorityOperator;
|
|
4616
|
+
if (!operator) return json({ error: "not found" }, 404);
|
|
4617
|
+
if (req.method !== "POST") return json({ error: "method not allowed" }, 405);
|
|
4618
|
+
const raw2 = ctx.makeDb(env), user = await ctx.verifyUser(req, env);
|
|
4619
|
+
if (!user) return json({ error: "unauthorized" }, 401);
|
|
4620
|
+
if (!await ctx.isSuperAdminEmail(raw2, user.email, user.userId)) return json({ error: "super-admin required", code: "super_admin_required" }, 403);
|
|
4621
|
+
let db;
|
|
4622
|
+
try {
|
|
4623
|
+
db = operator.makeDb(env);
|
|
4624
|
+
} catch {
|
|
4625
|
+
return json({ error: "the membership authority is not hosted by this deployment" }, 404);
|
|
4626
|
+
}
|
|
4627
|
+
let body;
|
|
4628
|
+
try {
|
|
4629
|
+
body = await req.json();
|
|
4630
|
+
} catch {
|
|
4631
|
+
return json({ error: BAD }, 400);
|
|
4632
|
+
}
|
|
4633
|
+
const appId = body.appId ?? body.chapterId, runtime = body.runtime ?? env.ODLA_RUNTIME;
|
|
4634
|
+
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)) {
|
|
4635
|
+
return json({ error: BAD }, 400);
|
|
4636
|
+
}
|
|
4637
|
+
const secret = await getVaultSecret(db, operator.stripeSecretName ?? "stripe_secret_key");
|
|
4638
|
+
if (!secret) return json({ error: "network payment provider unavailable" }, 503);
|
|
4639
|
+
try {
|
|
4640
|
+
const receipt = await retermMembershipDiscount(
|
|
4641
|
+
db,
|
|
4642
|
+
secret,
|
|
4643
|
+
{ appId, chapterId: body.chapterId, environment: env.ODLA_ENV, runtime },
|
|
4644
|
+
{
|
|
4645
|
+
policyVersionId: body.policyVersionId,
|
|
4646
|
+
...body.membershipIds ? { membershipIds: body.membershipIds } : {},
|
|
4647
|
+
...body.limit !== void 0 ? { limit: Number(body.limit) } : {}
|
|
4648
|
+
},
|
|
4649
|
+
Date.now()
|
|
4650
|
+
);
|
|
4651
|
+
return json({ ok: true, ...receipt });
|
|
4652
|
+
} catch (error) {
|
|
4653
|
+
const code = error.code;
|
|
4654
|
+
return json({ error: "re-term is pending; retry to confirm", ...code ? { code } : {} }, code ? 409 : 503);
|
|
4655
|
+
}
|
|
4656
|
+
};
|
|
4657
|
+
|
|
4195
4658
|
// src/worker-routes-membership-cancel.ts
|
|
4196
4659
|
var handleMembershipCancellation = async (req, url, env, ctx) => {
|
|
4197
4660
|
const match = url.pathname.match(/^\/api\/admin\/applications\/([0-9a-f-]+)\/cancel-renewal$/);
|
|
@@ -4852,7 +5315,7 @@ async function readAdmissionRows(db, namespace, where, maximum) {
|
|
|
4852
5315
|
}
|
|
4853
5316
|
|
|
4854
5317
|
// src/admission-grants.ts
|
|
4855
|
-
var
|
|
5318
|
+
var ID2 = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/;
|
|
4856
5319
|
var MAX_GRANTS = 2e3;
|
|
4857
5320
|
var MAX_NOTE = 2e3;
|
|
4858
5321
|
var EMAIL_RE2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
@@ -4874,10 +5337,10 @@ function validateAdmissionGrantDocument(input) {
|
|
|
4874
5337
|
const sourceActor = typeof source.actor === "string" ? source.actor.trim() : "";
|
|
4875
5338
|
const chapterId = typeof target.chapterId === "string" ? target.chapterId : "";
|
|
4876
5339
|
const environment = typeof target.environment === "string" ? target.environment : "";
|
|
4877
|
-
if (!
|
|
5340
|
+
if (!ID2.test(sourceId)) errors.push("document.source.appId must be an identifier");
|
|
4878
5341
|
if (!sourceActor || sourceActor.length > 256) errors.push("document.source.actor must be 1-256 characters");
|
|
4879
|
-
if (!
|
|
4880
|
-
if (!
|
|
5342
|
+
if (!ID2.test(chapterId)) errors.push("document.target.chapterId must be an identifier");
|
|
5343
|
+
if (!ID2.test(environment)) errors.push("document.target.environment must be an identifier");
|
|
4881
5344
|
if (!Array.isArray(root.grants)) errors.push("document.grants must be an array");
|
|
4882
5345
|
const rawGrants = Array.isArray(root.grants) ? root.grants : [];
|
|
4883
5346
|
if (rawGrants.length > MAX_GRANTS) errors.push(`document.grants supports at most ${MAX_GRANTS} entries`);
|
|
@@ -4892,7 +5355,7 @@ function validateAdmissionGrantDocument(input) {
|
|
|
4892
5355
|
const decision = grant.decision;
|
|
4893
5356
|
const note = typeof grant.note === "string" ? grant.note.trim() : void 0;
|
|
4894
5357
|
const expiresAt = grant.expiresAt === void 0 ? void 0 : Number(grant.expiresAt);
|
|
4895
|
-
if (!
|
|
5358
|
+
if (!ID2.test(id6)) errors.push(`document.grants[${index2}].id must be an identifier`);
|
|
4896
5359
|
if (seenIds.has(id6)) errors.push(`document.grants repeats id "${id6}"`);
|
|
4897
5360
|
if (!isAdmissionEmail(normalized)) errors.push(`document.grants[${index2}].email must be valid`);
|
|
4898
5361
|
if (seenEmails.has(normalized)) errors.push(`document.grants repeats email "${normalized}"`);
|
|
@@ -5108,7 +5571,7 @@ function chapterFlowFieldContract(chapter, flow, application = chapter.applicati
|
|
|
5108
5571
|
// src/gift-offers.ts
|
|
5109
5572
|
var GIFT_OFFER_CATALOG_VERSION = 1;
|
|
5110
5573
|
var MAX_GIFT_OFFERS = 20;
|
|
5111
|
-
var
|
|
5574
|
+
var ID3 = /^[a-z][a-z0-9-]{1,79}$/;
|
|
5112
5575
|
var PRICE = /^price_[A-Za-z0-9]{12,}$/;
|
|
5113
5576
|
var CURRENCY = /^[a-z]{3}$/;
|
|
5114
5577
|
var record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
@@ -5141,7 +5604,7 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
5141
5604
|
const allowed2 = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
|
|
5142
5605
|
for (const key3 of Object.keys(item)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
|
|
5143
5606
|
const id6 = text3(item.id, `${path}.id`, errors, 80);
|
|
5144
|
-
if (id6 && !
|
|
5607
|
+
if (id6 && !ID3.test(id6)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
5145
5608
|
if (ids2.has(id6)) errors.push(`${path}.id duplicates "${id6}"`);
|
|
5146
5609
|
ids2.add(id6);
|
|
5147
5610
|
const targetRaw = record(item.target);
|
|
@@ -5152,7 +5615,7 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
5152
5615
|
target = { type: "tier", tierId };
|
|
5153
5616
|
} else if (targetRaw?.type === "entitlement") {
|
|
5154
5617
|
const entitlementKey = text3(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
|
|
5155
|
-
if (entitlementKey && !
|
|
5618
|
+
if (entitlementKey && !ID3.test(entitlementKey)) errors.push(`${path}.target.entitlementKey must be a lowercase kebab-case id`);
|
|
5156
5619
|
target = { type: "entitlement", entitlementKey };
|
|
5157
5620
|
} else errors.push(`${path}.target.type must be tier or entitlement`);
|
|
5158
5621
|
if (item.billingMode !== "one_time" && item.billingMode !== "subscription") errors.push(`${path}.billingMode must be one_time or subscription`);
|
|
@@ -6236,7 +6699,7 @@ var handleMe = async (req, url, env, ctx) => {
|
|
|
6236
6699
|
} catch {
|
|
6237
6700
|
}
|
|
6238
6701
|
}
|
|
6239
|
-
return json({ ...base, memberAccess, membershipRestart, application: member?.application ?? null, namedSeatsEnabled: Boolean(
|
|
6702
|
+
return json({ ...base, memberAccess, membershipRestart, application: member?.application ?? null, namedSeatsEnabled: Boolean(ctx.membershipAuthority?.namedSeats && member?.application.paid && member.source.status !== "submitted") });
|
|
6240
6703
|
};
|
|
6241
6704
|
var handleCrm = async (req, url, env, ctx) => {
|
|
6242
6705
|
const crmBase = ctx.crmBase;
|
|
@@ -6938,7 +7401,7 @@ async function memberContentDigest(document) {
|
|
|
6938
7401
|
}
|
|
6939
7402
|
|
|
6940
7403
|
// src/member-content-validation-helpers.ts
|
|
6941
|
-
var
|
|
7404
|
+
var ID4 = /^[a-z][a-z0-9-]{0,79}$/;
|
|
6942
7405
|
var SLUG2 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
6943
7406
|
var ROOT_KEYS3 = /* @__PURE__ */ new Set(["schemaVersion", "revision", "generatedAt", "source", "target", "pages", "upsellOverrides"]);
|
|
6944
7407
|
var SOURCE_KEYS = /* @__PURE__ */ new Set(["type", "appId"]);
|
|
@@ -6978,7 +7441,7 @@ function offerValue(value, path, errors, tiers, audience) {
|
|
|
6978
7441
|
}
|
|
6979
7442
|
unknownKeys2(raw2, OFFER_KEYS, path, errors);
|
|
6980
7443
|
const tierId = textValue(raw2.tierId, `${path}.tierId`, errors, 80, true) ?? "";
|
|
6981
|
-
if (tierId && !
|
|
7444
|
+
if (tierId && !ID4.test(tierId)) errors.push(`${path}.tierId must be a lowercase kebab-case id`);
|
|
6982
7445
|
if (tiers.size && !tiers.has(tierId)) errors.push(`${path}.tierId must reference a declared tier`);
|
|
6983
7446
|
if (audience?.kind === "tiers" && tierId && !audience.tierIds.includes(tierId)) {
|
|
6984
7447
|
errors.push(`${path}.tierId must grant access to this page`);
|
|
@@ -7011,7 +7474,7 @@ function validateMemberContentDocument(input, options = {}) {
|
|
|
7011
7474
|
const type = sourceRaw.type;
|
|
7012
7475
|
const appId = textValue(sourceRaw.appId, "document.source.appId", errors, 63, true) ?? "";
|
|
7013
7476
|
if (type !== "parent" && type !== "chapter") errors.push("document.source.type must be parent or chapter");
|
|
7014
|
-
if (appId && !
|
|
7477
|
+
if (appId && !ID4.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
7015
7478
|
if (options.sourceType && type !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
|
|
7016
7479
|
if (options.sourceAppId && appId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
|
|
7017
7480
|
if (type === "parent" || type === "chapter") source = { type, appId };
|
|
@@ -7048,7 +7511,7 @@ function validateMemberContentDocument(input, options = {}) {
|
|
|
7048
7511
|
const id6 = textValue(raw2.id, `${path}.id`, errors, 80, true) ?? "";
|
|
7049
7512
|
const slug = textValue(raw2.slug, `${path}.slug`, errors, 120, true) ?? "";
|
|
7050
7513
|
const title = textValue(raw2.title, `${path}.title`, errors, MAX_TEXT, true) ?? "";
|
|
7051
|
-
if (id6 && !
|
|
7514
|
+
if (id6 && !ID4.test(id6)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
7052
7515
|
if (slug && !SLUG2.test(slug)) errors.push(`${path}.slug must be a lowercase URL slug`);
|
|
7053
7516
|
if (ids2.has(id6) || knownParentIds.has(id6)) errors.push(`${path}.id duplicates page "${id6}"`);
|
|
7054
7517
|
if (slugs.has(slug) || (options.parentPages ?? []).some((page) => page.slug === slug)) errors.push(`${path}.slug duplicates "${slug}"`);
|
|
@@ -7975,7 +8438,7 @@ async function deliverAdmissionGrants(options) {
|
|
|
7975
8438
|
}
|
|
7976
8439
|
|
|
7977
8440
|
// src/worker-routes-network-admission.ts
|
|
7978
|
-
var
|
|
8441
|
+
var PATH5 = "/api/admin/network/admissions";
|
|
7979
8442
|
var RETRY_PATH = "/api/admin/network/admissions/retry";
|
|
7980
8443
|
var REVOKE_PATH = "/api/admin/network/admissions/revoke";
|
|
7981
8444
|
var MUTATION_ID2 = /^[A-Za-z0-9_-]{16,128}$/;
|
|
@@ -8055,11 +8518,11 @@ async function parseBody(req) {
|
|
|
8055
8518
|
}
|
|
8056
8519
|
}
|
|
8057
8520
|
var handleAdminNetworkAdmissions = async (req, url, env, ctx) => {
|
|
8058
|
-
if (![
|
|
8521
|
+
if (![PATH5, RETRY_PATH, REVOKE_PATH].includes(url.pathname)) return null;
|
|
8059
8522
|
const auth = await gate2(req, env, ctx);
|
|
8060
8523
|
if ("response" in auth) return auth.response;
|
|
8061
8524
|
const db = auth.db;
|
|
8062
|
-
if (url.pathname ===
|
|
8525
|
+
if (url.pathname === PATH5 && req.method === "GET") {
|
|
8063
8526
|
const targetId2 = url.searchParams.get("targetId") ?? "";
|
|
8064
8527
|
const target2 = targetFor(ctx, targetId2);
|
|
8065
8528
|
if (!target2) return reject6(404, "unknown_target", "admission target is not configured");
|
|
@@ -8333,10 +8796,10 @@ var handleSchedule = async (req, url, env, ctx) => {
|
|
|
8333
8796
|
};
|
|
8334
8797
|
|
|
8335
8798
|
// src/payments-stripe.ts
|
|
8336
|
-
var
|
|
8799
|
+
var import_stripe11 = require("@odla-ai/stripe");
|
|
8337
8800
|
|
|
8338
8801
|
// src/payments-refund.ts
|
|
8339
|
-
var
|
|
8802
|
+
var import_stripe9 = require("@odla-ai/stripe");
|
|
8340
8803
|
var row = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
8341
8804
|
var id2 = (value) => typeof value === "string" ? value : String(row(value).id ?? "");
|
|
8342
8805
|
var providerId2 = (value, prefix) => new RegExp(`^${prefix}_[A-Za-z0-9]+$`).test(value);
|
|
@@ -8346,7 +8809,7 @@ function fail6(code = "refund_target_mismatch") {
|
|
|
8346
8809
|
async function refundSubscriptionPayment(secretKey, input) {
|
|
8347
8810
|
if (!providerId2(input.customerId, "cus") || !providerId2(input.subscriptionId, "sub")) fail6();
|
|
8348
8811
|
const call = async (method, path, params, key3) => {
|
|
8349
|
-
const result = await (0,
|
|
8812
|
+
const result = await (0, import_stripe9.stripeRequest)({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
|
|
8350
8813
|
if (!result.ok) fail6("stripe_error");
|
|
8351
8814
|
if (!result.body || typeof result.body !== "object" || Array.isArray(result.body)) fail6("stripe_invalid_response");
|
|
8352
8815
|
return result.body;
|
|
@@ -8420,35 +8883,13 @@ async function refundSubscriptionPayment(secretKey, input) {
|
|
|
8420
8883
|
}
|
|
8421
8884
|
|
|
8422
8885
|
// src/subscription-payment-identity.ts
|
|
8423
|
-
var
|
|
8424
|
-
|
|
8425
|
-
// src/subscription-provider-contract.ts
|
|
8426
|
-
var import_stripe8 = require("@odla-ai/stripe");
|
|
8427
|
-
function subscriptionContractMismatch() {
|
|
8428
|
-
throw Object.assign(new Error("Stripe subscription does not match the checkout contract"), { code: "stripe_contract_mismatch" });
|
|
8429
|
-
}
|
|
8430
|
-
async function verifySubscriptionPrice(secretKey, input, resume = false) {
|
|
8431
|
-
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();
|
|
8432
|
-
const result = await (0, import_stripe8.stripeRequest)({ secretKey }, "GET", `/v1/prices/${encodeURIComponent(input.priceId)}`);
|
|
8433
|
-
const price = result.body;
|
|
8434
|
-
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();
|
|
8435
|
-
return typeof price.product === "string" ? price.product : void 0;
|
|
8436
|
-
}
|
|
8437
|
-
function verifySubscriptionResult(sub, input, customerId, metadata) {
|
|
8438
|
-
const items = sub?.items;
|
|
8439
|
-
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();
|
|
8440
|
-
const invoice = sub.latest_invoice;
|
|
8441
|
-
if (!invoice || typeof invoice !== "object" || invoice.customer !== customerId || invoice.currency !== input.currency || invoice.total !== subscriptionAmountDue(input) || invoice.billing_reason !== "subscription_create") subscriptionContractMismatch();
|
|
8442
|
-
verifySubscriptionBillingResult(sub, input);
|
|
8443
|
-
}
|
|
8444
|
-
|
|
8445
|
-
// src/subscription-payment-identity.ts
|
|
8886
|
+
var import_stripe10 = require("@odla-ai/stripe");
|
|
8446
8887
|
var id3 = (value) => typeof value === "string" ? value : String(value?.id ?? "");
|
|
8447
8888
|
async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata, clientSecret2, applicationId) {
|
|
8448
8889
|
let paymentId = id3(invoice.payment_intent);
|
|
8449
8890
|
if (!paymentId) {
|
|
8450
8891
|
if (!/^in_[A-Za-z0-9]+$/.test(id3(invoice))) subscriptionContractMismatch();
|
|
8451
|
-
const result2 = await (0,
|
|
8892
|
+
const result2 = await (0, import_stripe10.stripeRequest)({ secretKey }, "GET", "/v1/invoice_payments", { invoice: id3(invoice), limit: 100 });
|
|
8452
8893
|
const list = result2.body;
|
|
8453
8894
|
if (!result2.ok || list?.has_more !== false || !Array.isArray(list.data) || list.data.length !== 1) subscriptionContractMismatch();
|
|
8454
8895
|
const link = list.data[0];
|
|
@@ -8457,11 +8898,11 @@ async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata,
|
|
|
8457
8898
|
}
|
|
8458
8899
|
if (!/^pi_[A-Za-z0-9]+$/.test(paymentId)) subscriptionContractMismatch();
|
|
8459
8900
|
const path = `/v1/payment_intents/${paymentId}`;
|
|
8460
|
-
const result = await (0,
|
|
8901
|
+
const result = await (0, import_stripe10.stripeRequest)({ secretKey }, "GET", path);
|
|
8461
8902
|
const payment = result.body;
|
|
8462
8903
|
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();
|
|
8463
8904
|
if (Object.entries(metadata).every(([key3, value]) => payment.metadata?.[key3] === value)) return;
|
|
8464
|
-
const tagged = await (0,
|
|
8905
|
+
const tagged = await (0, import_stripe10.stripeRequest)(
|
|
8465
8906
|
{ secretKey },
|
|
8466
8907
|
"POST",
|
|
8467
8908
|
path,
|
|
@@ -8473,7 +8914,7 @@ async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata,
|
|
|
8473
8914
|
|
|
8474
8915
|
// src/payments-stripe.ts
|
|
8475
8916
|
async function stripeCall(sk, method, path, params, idempotencyKey) {
|
|
8476
|
-
return (0,
|
|
8917
|
+
return (0, import_stripe11.stripeRequest)(
|
|
8477
8918
|
{ secretKey: sk },
|
|
8478
8919
|
method,
|
|
8479
8920
|
path,
|
|
@@ -8592,7 +9033,7 @@ function createStripeProvider(config) {
|
|
|
8592
9033
|
return { customerId, subscriptionId: String(sub.body.id), clientSecret: clientSecret2 };
|
|
8593
9034
|
},
|
|
8594
9035
|
async ingestWebhook(rawBody, sigHeader) {
|
|
8595
|
-
if (!webhookSecret || !await (0,
|
|
9036
|
+
if (!webhookSecret || !await (0, import_stripe11.verifyStripeSignature)(rawBody, sigHeader, webhookSecret)) {
|
|
8596
9037
|
return { ok: false, reason: "bad_signature" };
|
|
8597
9038
|
}
|
|
8598
9039
|
const event = JSON.parse(rawBody);
|
|
@@ -9256,6 +9697,7 @@ async function namedSeatOfferDigest(offer) {
|
|
|
9256
9697
|
])));
|
|
9257
9698
|
return [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
9258
9699
|
}
|
|
9700
|
+
var at = (value) => Number.isSafeInteger(value) ? Number(value) : void 0;
|
|
9259
9701
|
async function readNamedSeatOffer(db, input, now) {
|
|
9260
9702
|
membershipClock(now);
|
|
9261
9703
|
await loadMembershipAuthorityState(db, input);
|
|
@@ -9278,10 +9720,19 @@ async function readNamedSeatOffer(db, input, now) {
|
|
|
9278
9720
|
seatId: String(seat.id),
|
|
9279
9721
|
status: String(seat.status),
|
|
9280
9722
|
recipientName: String(seat.recipientName),
|
|
9281
|
-
recipientEmail: String(seat.recipientEmailNormalized)
|
|
9723
|
+
recipientEmail: String(seat.recipientEmailNormalized),
|
|
9724
|
+
recipientState: namedSeatRecipientState(seat),
|
|
9725
|
+
...at(seat.purchasedAt) !== void 0 ? { purchasedAt: at(seat.purchasedAt) } : {},
|
|
9726
|
+
...at(seat.acceptedAt) !== void 0 ? { acceptedAt: at(seat.acceptedAt) } : {},
|
|
9727
|
+
...seat.status === "active" && at(seat.journeyApprovedAt) !== void 0 ? { joinedAt: at(seat.journeyApprovedAt) } : {}
|
|
9282
9728
|
} } : {}
|
|
9283
9729
|
};
|
|
9284
|
-
return {
|
|
9730
|
+
return {
|
|
9731
|
+
...offer,
|
|
9732
|
+
digest: await namedSeatOfferDigest(offer),
|
|
9733
|
+
included: offering.seatPriceCents === 0,
|
|
9734
|
+
purchaseWindowed: offering.seatPurchaseWindowCalendarMonths !== void 0
|
|
9735
|
+
};
|
|
9285
9736
|
}
|
|
9286
9737
|
|
|
9287
9738
|
// src/named-seat-request.ts
|
|
@@ -9357,8 +9808,9 @@ var handleNamedSeat = async (req, url, env, ctx) => {
|
|
|
9357
9808
|
if (recipientName.length < 2 || /[\u0000-\u001f\u007f]/.test(body.recipientName) || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(recipientEmail) || recipientEmail === normalizeChapterEmail(user.email)) {
|
|
9358
9809
|
return json({ error: "a valid name and email for someone other than the purchaser are required" }, 400);
|
|
9359
9810
|
}
|
|
9360
|
-
|
|
9361
|
-
if (!
|
|
9811
|
+
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);
|
|
9812
|
+
if (!offer.eligible && !granted || body.quoteDigest !== offer.digest) return json({ error: "seat offer changed or is unavailable; refresh before paying" }, 409);
|
|
9813
|
+
if (!group?.refundPolicyText || !group.merchantDisclosureText || !offer.included && !group.stripePublishableKey) return json({ error: "seat checkout disclosures unavailable" }, 503);
|
|
9362
9814
|
const consentId = `${offer.membershipId}:named-seat-consent`;
|
|
9363
9815
|
const prior = await membershipLoad(db, "namedSeatConsents", consentId);
|
|
9364
9816
|
const attrs = {
|
|
@@ -9397,7 +9849,7 @@ var handleNamedSeat = async (req, url, env, ctx) => {
|
|
|
9397
9849
|
}
|
|
9398
9850
|
return json({
|
|
9399
9851
|
...receipt,
|
|
9400
|
-
publishableKey: String(group.stripePublishableKey),
|
|
9852
|
+
...group.stripePublishableKey ? { publishableKey: String(group.stripePublishableKey) } : {},
|
|
9401
9853
|
lineItems: { standardCents: offer.amountCents, discountCents: 0, dueTodayCents: offer.amountCents }
|
|
9402
9854
|
});
|
|
9403
9855
|
} catch {
|
|
@@ -10260,6 +10712,7 @@ function dashboardMetricData(points, now) {
|
|
|
10260
10712
|
}
|
|
10261
10713
|
|
|
10262
10714
|
// src/chapter-billing-amount.ts
|
|
10715
|
+
var hasProviderDiscount = (sub, items) => Boolean(sub?.discount || Array.isArray(sub?.discounts) && sub.discounts.length || items.some((it) => it.discounts?.length));
|
|
10263
10716
|
function chapterBillingAmount(sub, intent, env) {
|
|
10264
10717
|
const unknown = { amountCents: null, listAmountCents: null, interval: "year", annualCents: null };
|
|
10265
10718
|
const items = sub?.items?.data;
|
|
@@ -10276,10 +10729,11 @@ function chapterBillingAmount(sub, intent, env) {
|
|
|
10276
10729
|
const primary = items.filter((it) => it.price?.id === intent.priceId);
|
|
10277
10730
|
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 };
|
|
10278
10731
|
if (terms2?.discount?.duration === "forever") amountCents = listAmountCents - intent.priceCents + Math.round(intent.priceCents * (1e4 - terms2.discount.percentOffBps) / 1e4);
|
|
10732
|
+
else if (hasProviderDiscount(sub, items)) amountCents = null;
|
|
10279
10733
|
} catch {
|
|
10280
10734
|
amountCents = null;
|
|
10281
10735
|
}
|
|
10282
|
-
} else if (
|
|
10736
|
+
} else if (hasProviderDiscount(sub, items)) amountCents = null;
|
|
10283
10737
|
return { amountCents, listAmountCents, interval, annualCents: amountCents === null ? null : amountCents * (interval === "month" ? 12 : 1) };
|
|
10284
10738
|
}
|
|
10285
10739
|
function chapterBillingActive(sub, app) {
|
|
@@ -11192,7 +11646,7 @@ async function networkApplicationAction(opts) {
|
|
|
11192
11646
|
if (app.status !== input.expectedStage || !applicationActionStages(app, env, ctx).includes(input.stage)) {
|
|
11193
11647
|
return json({ error: "application action is no longer available; reload", code: "invalid_application_action" }, 409);
|
|
11194
11648
|
}
|
|
11195
|
-
const
|
|
11649
|
+
const at2 = Date.now();
|
|
11196
11650
|
const meta = { [ACTION_META]: {
|
|
11197
11651
|
version: 1,
|
|
11198
11652
|
state: "pending",
|
|
@@ -11212,7 +11666,7 @@ async function networkApplicationAction(opts) {
|
|
|
11212
11666
|
recordId: record4.id,
|
|
11213
11667
|
kind: "note",
|
|
11214
11668
|
authorId: `network:${sourceId}`,
|
|
11215
|
-
createdAt:
|
|
11669
|
+
createdAt: at2,
|
|
11216
11670
|
body: `Requested ${input.expectedStage} \u2192 ${input.stage} from ${sourceId}`,
|
|
11217
11671
|
meta
|
|
11218
11672
|
} },
|
|
@@ -11221,7 +11675,7 @@ async function networkApplicationAction(opts) {
|
|
|
11221
11675
|
recordId: record4.id,
|
|
11222
11676
|
kind: "note",
|
|
11223
11677
|
authorId: `network:${sourceId}`,
|
|
11224
|
-
createdAt:
|
|
11678
|
+
createdAt: at2,
|
|
11225
11679
|
body: "Application operation reconciliation",
|
|
11226
11680
|
meta: { state: "pending", activityId }
|
|
11227
11681
|
} }
|
|
@@ -12068,9 +12522,9 @@ function normalizeNetworkIdentity(kind, value) {
|
|
|
12068
12522
|
return trimmed;
|
|
12069
12523
|
}
|
|
12070
12524
|
function emailLooksReal(value) {
|
|
12071
|
-
const
|
|
12072
|
-
if (
|
|
12073
|
-
const domain = value.slice(
|
|
12525
|
+
const at2 = value.lastIndexOf("@");
|
|
12526
|
+
if (at2 <= 0 || at2 === value.length - 1) return false;
|
|
12527
|
+
const domain = value.slice(at2 + 1).toLowerCase();
|
|
12074
12528
|
return !domain.endsWith(".invalid") && domain !== "example.com" && domain !== "example.test";
|
|
12075
12529
|
}
|
|
12076
12530
|
function validateNetworkPersonImport(input, allowedChapterIds) {
|
|
@@ -12858,7 +13312,7 @@ async function revealNetworkPersonRegistryRecord(db, input, mutationId, actor, n
|
|
|
12858
13312
|
}
|
|
12859
13313
|
|
|
12860
13314
|
// src/worker-routes-network-person-registry.ts
|
|
12861
|
-
var
|
|
13315
|
+
var PATH6 = "/api/admin/network-person-registry";
|
|
12862
13316
|
var MAX_BODY2 = 8 * 1024 * 1024;
|
|
12863
13317
|
var MUTATION_ID4 = /^[A-Za-z0-9_-]{16,128}$/;
|
|
12864
13318
|
var positiveRevision = (value) => {
|
|
@@ -12883,7 +13337,7 @@ function registryError(error) {
|
|
|
12883
13337
|
return null;
|
|
12884
13338
|
}
|
|
12885
13339
|
var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
12886
|
-
if (url.pathname !==
|
|
13340
|
+
if (url.pathname !== PATH6) return null;
|
|
12887
13341
|
if (!ctx.chapter.networkPersonRegistry) return json({ error: "not found" }, 404);
|
|
12888
13342
|
if (req.method !== "GET" && req.method !== "POST") return json({ error: "method not allowed" }, 405);
|
|
12889
13343
|
if (req.method === "POST" && Number(req.headers.get("content-length") ?? 0) > MAX_BODY2) {
|
|
@@ -13058,7 +13512,7 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
|
13058
13512
|
};
|
|
13059
13513
|
|
|
13060
13514
|
// src/worker-routes-network-commercial.ts
|
|
13061
|
-
var
|
|
13515
|
+
var import_stripe12 = require("@odla-ai/stripe");
|
|
13062
13516
|
|
|
13063
13517
|
// src/worker-routes-network-commercial-parity.ts
|
|
13064
13518
|
var import_db13 = require("@odla-ai/db");
|
|
@@ -13434,7 +13888,7 @@ var handleNetworkCommercialStripeWebhook = async (req, url, env, ctx) => {
|
|
|
13434
13888
|
return json({ error: "commercial webhook is not configured" }, 503);
|
|
13435
13889
|
}
|
|
13436
13890
|
try {
|
|
13437
|
-
const event = await (0,
|
|
13891
|
+
const event = await (0, import_stripe12.constructStripeEvent)(body, signature, secret);
|
|
13438
13892
|
const fact = normalizeNetworkCommercialStripeEvent(event, await stripeEventDigest(body));
|
|
13439
13893
|
if (!fact.eventId || !fact.eventType || !fact.objectId) return json({ error: "Stripe event identity is invalid" }, 400);
|
|
13440
13894
|
const result = await ingestNetworkCommercialFact(
|
|
@@ -13455,7 +13909,7 @@ var handleNetworkCommercialStripeWebhook = async (req, url, env, ctx) => {
|
|
|
13455
13909
|
...result.status === "replayed" && result.resolvedCollision ? { resolvedCollision: true } : {}
|
|
13456
13910
|
});
|
|
13457
13911
|
} catch (error) {
|
|
13458
|
-
if (error instanceof
|
|
13912
|
+
if (error instanceof import_stripe12.StripeError) return json({ error: error.message, code: error.code }, 400);
|
|
13459
13913
|
throw error;
|
|
13460
13914
|
}
|
|
13461
13915
|
};
|
|
@@ -13518,7 +13972,7 @@ var handleAdminNetworkCommercialReadModel = async (req, url, env, ctx) => {
|
|
|
13518
13972
|
const secretKey = await getVaultSecret(db, "stripe_secret_key");
|
|
13519
13973
|
if (!secretKey) return json({ error: "Stripe reconciliation is not configured" }, 503);
|
|
13520
13974
|
try {
|
|
13521
|
-
const stripe = (0,
|
|
13975
|
+
const stripe = (0, import_stripe12.initStripe)({ secretKey });
|
|
13522
13976
|
const [customers, subscriptions, invoices, refunds, payments] = await Promise.all([
|
|
13523
13977
|
listStripeObjects(stripe, "/v1/customers"),
|
|
13524
13978
|
listStripeObjects(stripe, "/v1/subscriptions", { status: "all" }),
|
|
@@ -13532,7 +13986,7 @@ var handleAdminNetworkCommercialReadModel = async (req, url, env, ctx) => {
|
|
|
13532
13986
|
...reconcileStripeSnapshot(projection2, { customers, subscriptions, invoices, refunds, payments }, ctx.chapter.networkCommercialReadModel.sources)
|
|
13533
13987
|
});
|
|
13534
13988
|
} catch (error) {
|
|
13535
|
-
if (error instanceof
|
|
13989
|
+
if (error instanceof import_stripe12.StripeError) return json({ error: "Stripe reconciliation failed", code: error.code }, 502);
|
|
13536
13990
|
return json({ error: "Stripe reconciliation failed" }, 502);
|
|
13537
13991
|
}
|
|
13538
13992
|
}
|
|
@@ -13629,7 +14083,7 @@ var handleFormation = async (req, url, env, ctx) => {
|
|
|
13629
14083
|
var import_db14 = require("@odla-ai/db");
|
|
13630
14084
|
|
|
13631
14085
|
// src/builder-library-validation.ts
|
|
13632
|
-
var
|
|
14086
|
+
var ID5 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
13633
14087
|
var APP_ID = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
13634
14088
|
var MAX_RESOURCES = 200;
|
|
13635
14089
|
var MAX_MARKDOWN3 = 65536;
|
|
@@ -13690,7 +14144,7 @@ function resourceValue(value, path, errors) {
|
|
|
13690
14144
|
}
|
|
13691
14145
|
unknownKeys3(raw2, RESOURCE_KEYS, path, errors);
|
|
13692
14146
|
const id6 = text7(raw2.id, `${path}.id`, errors, 80) ?? "";
|
|
13693
|
-
if (id6 && !
|
|
14147
|
+
if (id6 && !ID5.test(id6)) errors.push(`${path}.id must be lowercase kebab-case`);
|
|
13694
14148
|
const kind = text7(raw2.kind, `${path}.kind`, errors) ?? "";
|
|
13695
14149
|
if (!KINDS.has(kind)) errors.push(`${path}.kind is unsupported`);
|
|
13696
14150
|
const title = text7(raw2.title, `${path}.title`, errors, 240) ?? "";
|
|
@@ -14112,7 +14566,7 @@ var handleAdminBuilderLibrary = async (req, url, env, ctx) => {
|
|
|
14112
14566
|
};
|
|
14113
14567
|
|
|
14114
14568
|
// src/membership-cancel-authority.ts
|
|
14115
|
-
var
|
|
14569
|
+
var import_stripe13 = require("@odla-ai/stripe");
|
|
14116
14570
|
async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
|
|
14117
14571
|
membershipClock(now);
|
|
14118
14572
|
const id6 = await networkMembershipId(target);
|
|
@@ -14123,14 +14577,14 @@ async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
|
|
|
14123
14577
|
membershipFail("membership_cancellation_unavailable");
|
|
14124
14578
|
}
|
|
14125
14579
|
const path = `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`;
|
|
14126
|
-
const read2 = await (0,
|
|
14580
|
+
const read2 = await (0, import_stripe13.stripeRequest)({ secretKey: stripeSecret }, "GET", path);
|
|
14127
14581
|
const sub = read2.body;
|
|
14128
14582
|
if (!read2.ok || sub.id !== member.providerSubscriptionId || sub.customer !== member.providerCustomerId || sub.livemode !== (target.environment === "prod") || !Object.entries({
|
|
14129
14583
|
...stripePartitionMetadata(target),
|
|
14130
14584
|
applicationId: target.applicationId
|
|
14131
14585
|
}).every(([key3, value]) => sub.metadata?.[key3] === value)) membershipFail("membership_cancellation_provider_mismatch");
|
|
14132
14586
|
if (sub.status !== "canceled" && sub.cancel_at_period_end !== true) {
|
|
14133
|
-
const write = await (0,
|
|
14587
|
+
const write = await (0, import_stripe13.stripeRequest)(
|
|
14134
14588
|
{ secretKey: stripeSecret },
|
|
14135
14589
|
"POST",
|
|
14136
14590
|
path,
|
|
@@ -14352,7 +14806,7 @@ async function refundNetworkMembership(db, input, secretKey, now) {
|
|
|
14352
14806
|
}
|
|
14353
14807
|
|
|
14354
14808
|
// src/membership-provider-lifecycle.ts
|
|
14355
|
-
var
|
|
14809
|
+
var import_stripe14 = require("@odla-ai/stripe");
|
|
14356
14810
|
|
|
14357
14811
|
// src/membership-renewal-authority.ts
|
|
14358
14812
|
async function reconcileMembershipRenewal(db, input, now) {
|
|
@@ -14389,7 +14843,9 @@ async function reconcileMembershipRenewal(db, input, now) {
|
|
|
14389
14843
|
if (start !== member.termEndsAt || start > now + 3e4) membershipFail("membership_renewal_period_gap");
|
|
14390
14844
|
const seat = member.namedSeatId ? await membershipLoad(db, "networkSeat", String(member.namedSeatId)) : void 0;
|
|
14391
14845
|
if (seat) requireMembershipScope(seat, input);
|
|
14392
|
-
const
|
|
14846
|
+
const openSeat = seat && !["refunded", "refund_pending", "canceled"].includes(String(seat.status)) ? seat : void 0;
|
|
14847
|
+
const billedSeat = openSeat && openSeat.providerSubscriptionId === member.providerSubscriptionId && openSeat.providerSubscriptionItemId ? openSeat : void 0;
|
|
14848
|
+
const carriedSeat = billedSeat ?? (openSeat && namedSeatIncluded(openSeat) && openSeat.purchasedAt !== void 0 ? openSeat : void 0);
|
|
14393
14849
|
const prices = [String(member.providerPriceId), ...billedSeat ? [String(billedSeat.priceId)] : []].sort();
|
|
14394
14850
|
const amount = Number(member.renewalAmountCents) + Number(billedSeat?.amountCents ?? 0);
|
|
14395
14851
|
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");
|
|
@@ -14429,19 +14885,19 @@ async function reconcileMembershipRenewal(db, input, now) {
|
|
|
14429
14885
|
evidenceGuard
|
|
14430
14886
|
];
|
|
14431
14887
|
if (seat) guards.push({ ns: "networkSeat", id: String(seat.id), exists: true, equals: { revision: seat.revision } });
|
|
14432
|
-
if (
|
|
14433
|
-
const seatEntitlement = await membershipLoad(db, "networkEntitlement", String(
|
|
14888
|
+
if (carriedSeat?.entitlementId) {
|
|
14889
|
+
const seatEntitlement = await membershipLoad(db, "networkEntitlement", String(carriedSeat.entitlementId));
|
|
14434
14890
|
requireMembershipScope(seatEntitlement, input);
|
|
14435
14891
|
guards.push({ ns: "networkEntitlement", id: String(seatEntitlement.id), exists: true, equals: { revision: seatEntitlement.revision } });
|
|
14436
14892
|
ops.push(
|
|
14437
|
-
membershipUpdate("networkSeat", String(
|
|
14893
|
+
membershipUpdate("networkSeat", String(carriedSeat.id), {
|
|
14438
14894
|
...paid ? { termStartedAt: start, termEndsAt: end } : {},
|
|
14439
|
-
revision: Number(
|
|
14895
|
+
revision: Number(carriedSeat.revision) + 1,
|
|
14440
14896
|
updatedAt: now
|
|
14441
14897
|
}),
|
|
14442
14898
|
membershipUpdate("networkEntitlement", String(seatEntitlement.id), {
|
|
14443
14899
|
...paid ? { termStartedAt: start, termEndsAt: end } : {},
|
|
14444
|
-
status:
|
|
14900
|
+
status: carriedSeat.journeyApprovedAt ? entitlementStatus : seatEntitlement.status,
|
|
14445
14901
|
revision: Number(seatEntitlement.revision) + 1,
|
|
14446
14902
|
updatedAt: now
|
|
14447
14903
|
})
|
|
@@ -14514,7 +14970,7 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
14514
14970
|
}
|
|
14515
14971
|
if (fact.subscriptionId !== member.providerSubscriptionId || fact.objectKind !== "subscription") membershipFail("membership_billing_scope_mismatch");
|
|
14516
14972
|
if (["refund_pending", "refunded", "declined", "canceled", "restarting"].includes(String(member.status))) return receipt();
|
|
14517
|
-
const result = await (0,
|
|
14973
|
+
const result = await (0, import_stripe14.stripeRequest)({ secretKey: sk }, "GET", `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`);
|
|
14518
14974
|
const sub = result.body;
|
|
14519
14975
|
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)) {
|
|
14520
14976
|
membershipFail("membership_billing_provider_mismatch");
|
|
@@ -14551,86 +15007,8 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
14551
15007
|
return membershipFail("membership_billing_retry_required");
|
|
14552
15008
|
}
|
|
14553
15009
|
|
|
14554
|
-
// src/membership-seat-reservation.ts
|
|
14555
|
-
async function recipientKey(input, email) {
|
|
14556
|
-
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify([input.appId, input.chapterId, input.environment, input.runtime, email])));
|
|
14557
|
-
return `seat-recipient:${[...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
14558
|
-
}
|
|
14559
|
-
async function reserveNamedMembershipSeat(db, input, now) {
|
|
14560
|
-
membershipClock(now);
|
|
14561
|
-
const name = typeof input.recipientName === "string" ? input.recipientName.trim().replace(/\s+/g, " ") : "";
|
|
14562
|
-
const email = normalizeChapterEmail(typeof input.recipientEmail === "string" ? input.recipientEmail : "");
|
|
14563
|
-
const purchaser = normalizeChapterEmail(typeof input.purchaserEmail === "string" ? input.purchaserEmail : "");
|
|
14564
|
-
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");
|
|
14565
|
-
const state = await loadMembershipAuthorityState(db, input);
|
|
14566
|
-
const memberId = await networkMembershipId(input), member = await membershipLoad(db, "networkMembership", memberId);
|
|
14567
|
-
requireMembershipScope(member, input);
|
|
14568
|
-
if (!Number.isSafeInteger(member.revision) || !/^price_[A-Za-z0-9_]+$/.test(String(member.providerPriceId)) || member.providerPriceId === input.priceId) membershipFail("named_seat_purchase_unavailable");
|
|
14569
|
-
const id6 = (await membershipOfferIdForRequest({ ...input, requestId: "named_seat" })).replace(/^offer_/, "seat_");
|
|
14570
|
-
let seat = await membershipLoad(db, "networkSeat", id6);
|
|
14571
|
-
if (!seat) {
|
|
14572
|
-
const offering = membershipOfferingVersion(member.offeringSnapshot), claimId = await recipientKey(input, email);
|
|
14573
|
-
const purchaseEndsAt = seatPurchaseDeadline(offering, member);
|
|
14574
|
-
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) {
|
|
14575
|
-
membershipFail("named_seat_purchase_unavailable");
|
|
14576
|
-
}
|
|
14577
|
-
const attrs = {
|
|
14578
|
-
id: id6,
|
|
14579
|
-
...membershipScope(input),
|
|
14580
|
-
primaryMembershipId: memberId,
|
|
14581
|
-
recipientName: name,
|
|
14582
|
-
recipientEmailNormalized: email,
|
|
14583
|
-
purchaserUserId: input.purchaserUserId,
|
|
14584
|
-
recipientClaimId: claimId,
|
|
14585
|
-
priceId: input.priceId,
|
|
14586
|
-
primaryPriceId: member.providerPriceId,
|
|
14587
|
-
amountCents: offering.seatPriceCents,
|
|
14588
|
-
currency: offering.currency,
|
|
14589
|
-
interval: offering.interval,
|
|
14590
|
-
providerCustomerId: member.providerCustomerId,
|
|
14591
|
-
providerSubscriptionId: member.providerSubscriptionId,
|
|
14592
|
-
status: "payment_pending",
|
|
14593
|
-
preparedAt: now,
|
|
14594
|
-
termEndsAt: member.termEndsAt,
|
|
14595
|
-
seatPurchaseEndsAt: purchaseEndsAt,
|
|
14596
|
-
offeringSnapshot: offering,
|
|
14597
|
-
revision: 1,
|
|
14598
|
-
createdAt: now,
|
|
14599
|
-
updatedAt: now
|
|
14600
|
-
};
|
|
14601
|
-
try {
|
|
14602
|
-
await db.transact([
|
|
14603
|
-
membershipUpdate("networkSeat", id6, attrs),
|
|
14604
|
-
membershipUpdate("networkSeatRecipient", claimId, {
|
|
14605
|
-
id: claimId,
|
|
14606
|
-
appId: input.appId,
|
|
14607
|
-
chapterId: input.chapterId,
|
|
14608
|
-
environment: input.environment,
|
|
14609
|
-
runtime: input.runtime,
|
|
14610
|
-
seatId: id6,
|
|
14611
|
-
recipientEmailNormalized: email,
|
|
14612
|
-
createdAt: now
|
|
14613
|
-
}),
|
|
14614
|
-
membershipUpdate("networkMembership", memberId, { namedSeatId: id6, revision: Number(member.revision) + 1, updatedAt: now })
|
|
14615
|
-
], { mutationId: `named-seat-reserve:${id6}`, guards: [
|
|
14616
|
-
{ ns: "networkSeat", id: id6, exists: false },
|
|
14617
|
-
{ ns: "networkSeatRecipient", id: claimId, exists: false },
|
|
14618
|
-
{ ns: "networkMembership", id: memberId, exists: true, equals: { revision: member.revision }, absent: ["namedSeatId"] },
|
|
14619
|
-
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
|
|
14620
|
-
] });
|
|
14621
|
-
} catch (error) {
|
|
14622
|
-
if (error.code !== "transact_guard_failed") throw error;
|
|
14623
|
-
}
|
|
14624
|
-
seat = await membershipLoad(db, "networkSeat", id6);
|
|
14625
|
-
if (!seat && await membershipLoad(db, "networkSeatRecipient", claimId)) membershipFail("named_seat_recipient_unavailable");
|
|
14626
|
-
}
|
|
14627
|
-
requireMembershipScope(seat, input);
|
|
14628
|
-
if (seat.primaryMembershipId !== memberId || seat.recipientName !== name || seat.recipientEmailNormalized !== email || seat.priceId !== input.priceId || seat.purchaserUserId !== input.purchaserUserId) membershipFail("named_seat_purchase_conflict");
|
|
14629
|
-
return seat;
|
|
14630
|
-
}
|
|
14631
|
-
|
|
14632
15010
|
// src/membership-seat-stripe.ts
|
|
14633
|
-
var
|
|
15011
|
+
var import_stripe15 = require("@odla-ai/stripe");
|
|
14634
15012
|
var reject7 = () => {
|
|
14635
15013
|
throw Object.assign(new Error("named seat provider contract mismatch"), { code: "named_seat_provider_mismatch" });
|
|
14636
15014
|
};
|
|
@@ -14642,7 +15020,7 @@ var metadataFor = (seat) => ({
|
|
|
14642
15020
|
});
|
|
14643
15021
|
var matches = (value, expected) => value && Object.entries(expected).every(([key3, item]) => value[key3] === item);
|
|
14644
15022
|
async function read(sk, path, params) {
|
|
14645
|
-
const response = await (0,
|
|
15023
|
+
const response = await (0, import_stripe15.stripeRequest)({ secretKey: sk }, "GET", path, params);
|
|
14646
15024
|
if (!response.ok) return reject7();
|
|
14647
15025
|
return response.body;
|
|
14648
15026
|
}
|
|
@@ -14665,9 +15043,9 @@ async function subscriptionForSeat(sk, seat) {
|
|
|
14665
15043
|
if (!Array.isArray(discounts) || discounts.length > 5) reject7();
|
|
14666
15044
|
for (const discount of discounts) {
|
|
14667
15045
|
const couponRef = discount.source?.coupon ?? discount.coupon;
|
|
14668
|
-
const
|
|
14669
|
-
if (!
|
|
14670
|
-
const coupon = await read(sk, `/v1/coupons/${encodeURIComponent(
|
|
15046
|
+
const couponId = typeof couponRef === "string" ? couponRef : couponRef?.id;
|
|
15047
|
+
if (!couponId || typeof couponId !== "string") reject7();
|
|
15048
|
+
const coupon = await read(sk, `/v1/coupons/${encodeURIComponent(couponId)}`, { "expand[0]": "applies_to" });
|
|
14671
15049
|
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();
|
|
14672
15050
|
}
|
|
14673
15051
|
return sub;
|
|
@@ -14679,11 +15057,11 @@ async function createNamedSeatStripePayment(sk, seat, now) {
|
|
|
14679
15057
|
}
|
|
14680
15058
|
await subscriptionForSeat(sk, seat);
|
|
14681
15059
|
const metadata = metadataFor(seat);
|
|
14682
|
-
const result = seat.providerPaymentIntentId ? await (0,
|
|
15060
|
+
const result = seat.providerPaymentIntentId ? await (0, import_stripe15.stripeRequest)(
|
|
14683
15061
|
{ secretKey: sk },
|
|
14684
15062
|
"GET",
|
|
14685
15063
|
`/v1/payment_intents/${encodeURIComponent(seat.providerPaymentIntentId)}`
|
|
14686
|
-
) : await (0,
|
|
15064
|
+
) : await (0, import_stripe15.stripeRequest)({ secretKey: sk }, "POST", "/v1/payment_intents", {
|
|
14687
15065
|
amount: seat.amountCents,
|
|
14688
15066
|
currency: seat.currency,
|
|
14689
15067
|
customer: seat.providerCustomerId,
|
|
@@ -14701,7 +15079,7 @@ async function attachNamedSeatStripeRenewal(sk, seat) {
|
|
|
14701
15079
|
const sub = await subscriptionForSeat(sk, seat);
|
|
14702
15080
|
const existing = sub.items.data.find((item2) => item2.metadata?.seatId === seat.id);
|
|
14703
15081
|
if (existing) return String(existing.id);
|
|
14704
|
-
const result = await (0,
|
|
15082
|
+
const result = await (0, import_stripe15.stripeRequest)({ secretKey: sk }, "POST", "/v1/subscription_items", {
|
|
14705
15083
|
subscription: seat.providerSubscriptionId,
|
|
14706
15084
|
price: seat.priceId,
|
|
14707
15085
|
quantity: 1,
|
|
@@ -14716,6 +15094,7 @@ async function attachNamedSeatStripeRenewal(sk, seat) {
|
|
|
14716
15094
|
// src/membership-seat-checkout.ts
|
|
14717
15095
|
async function prepareNamedMembershipSeatPayment(db, input, secretKey, now) {
|
|
14718
15096
|
let seat = await reserveNamedMembershipSeat(db, input, now);
|
|
15097
|
+
if (namedSeatIncluded(seat)) membershipFail("named_seat_included");
|
|
14719
15098
|
const state = await loadMembershipAuthorityState(db, input);
|
|
14720
15099
|
const member = await membershipLoad(db, "networkMembership", seat.primaryMembershipId);
|
|
14721
15100
|
requireMembershipScope(member, input);
|
|
@@ -14922,7 +15301,7 @@ async function recordRecipient(db, input, now, approve) {
|
|
|
14922
15301
|
requireMembershipScope(member, input);
|
|
14923
15302
|
const entitlement = await membershipLoad(db, "networkEntitlement", String(seat.entitlementId));
|
|
14924
15303
|
requireMembershipScope(entitlement, input);
|
|
14925
|
-
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
|
|
15304
|
+
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");
|
|
14926
15305
|
if (seat.acceptedAt === void 0) {
|
|
14927
15306
|
if (approve || normalizeChapterEmail(input.verifiedRecipientEmail) !== seat.recipientEmailNormalized) {
|
|
14928
15307
|
membershipFail("named_seat_recipient_verification_required");
|
|
@@ -15109,6 +15488,15 @@ async function handleMembershipSeatAction(action, db, peer, input, now, stripeSe
|
|
|
15109
15488
|
};
|
|
15110
15489
|
if (action === "seat-accept") return acceptNamedSeatRecipient(db, recipientInput, now);
|
|
15111
15490
|
if (action === "seat-approve") return approveNamedSeatRecipient(db, recipientInput, now);
|
|
15491
|
+
const checkout = {
|
|
15492
|
+
...target,
|
|
15493
|
+
priceId: peer.seatPriceId,
|
|
15494
|
+
recipientName: String(input.recipientName),
|
|
15495
|
+
recipientEmail: String(input.recipientEmail),
|
|
15496
|
+
purchaserEmail: String(input.purchaserEmail),
|
|
15497
|
+
purchaserUserId: String(input.purchaserUserId)
|
|
15498
|
+
};
|
|
15499
|
+
if (action === "seat-checkouts" && namedSeatIncluded(await reserveNamedMembershipSeat(db, checkout, now))) return grantIncludedNamedSeat(db, checkout, now);
|
|
15112
15500
|
const secret = await getVaultSecret(db, stripeSecretName);
|
|
15113
15501
|
if (!secret) throw new Error("network Stripe provider is unavailable");
|
|
15114
15502
|
if (action === "seat-decline") {
|
|
@@ -15124,14 +15512,7 @@ async function handleMembershipSeatAction(action, db, peer, input, now, stripeSe
|
|
|
15124
15512
|
return { seatId: seat.id, status: "refunded", refundedCents: refunded.refundedCents, subscriptionCanceled: false };
|
|
15125
15513
|
}
|
|
15126
15514
|
if (action === "seat-operator-refund") return refundNamedSeatByOperator(db, { ...target, seatId: String(input.seatId) }, secret, now);
|
|
15127
|
-
if (action === "seat-checkouts") return prepareNamedMembershipSeatPayment(db,
|
|
15128
|
-
...target,
|
|
15129
|
-
priceId: peer.seatPriceId,
|
|
15130
|
-
recipientName: String(input.recipientName),
|
|
15131
|
-
recipientEmail: String(input.recipientEmail),
|
|
15132
|
-
purchaserEmail: String(input.purchaserEmail),
|
|
15133
|
-
purchaserUserId: String(input.purchaserUserId)
|
|
15134
|
-
}, secret, now);
|
|
15515
|
+
if (action === "seat-checkouts") return prepareNamedMembershipSeatPayment(db, checkout, secret, now);
|
|
15135
15516
|
const evidence = { ...target, seatId: String(input.seatId), eventId: String(input.eventId) };
|
|
15136
15517
|
if (action === "seat-payments") {
|
|
15137
15518
|
const receipt = await confirmNamedMembershipSeatPayment(db, evidence, now);
|
|
@@ -15153,7 +15534,7 @@ var respond = (body, status2 = 200) => new Response(JSON.stringify(body), {
|
|
|
15153
15534
|
status: status2,
|
|
15154
15535
|
headers: { "content-type": "application/json", "cache-control": "no-store" }
|
|
15155
15536
|
});
|
|
15156
|
-
var
|
|
15537
|
+
var SAFE2 = /^[A-Za-z0-9][A-Za-z0-9_.:-]{2,159}$/;
|
|
15157
15538
|
var SAFE_ERROR = /* @__PURE__ */ new Set([
|
|
15158
15539
|
"offer_identity_conflict",
|
|
15159
15540
|
"membership_authority_unavailable",
|
|
@@ -15201,7 +15582,7 @@ function createMembershipAuthorityHandler(options) {
|
|
|
15201
15582
|
if (options.stripeSecretName !== void 0 && !/^[A-Za-z][A-Za-z0-9_-]{1,127}$/.test(options.stripeSecretName)) throw new TypeError("invalid Stripe secret slot");
|
|
15202
15583
|
const identities = /* @__PURE__ */ new Set();
|
|
15203
15584
|
for (const peer of peers) {
|
|
15204
|
-
if (!/^[a-z0-9][a-z0-9-]{1,62}$/.test(peer.appId) || !
|
|
15585
|
+
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");
|
|
15205
15586
|
const identity = `${peer.appId}:${peer.runtime}`;
|
|
15206
15587
|
if (identities.has(identity)) throw new TypeError("ambiguous authority peer");
|
|
15207
15588
|
identities.add(identity);
|
|
@@ -15235,7 +15616,7 @@ function createMembershipAuthorityHandler(options) {
|
|
|
15235
15616
|
try {
|
|
15236
15617
|
input = JSON.parse(verified.body);
|
|
15237
15618
|
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] ?? []];
|
|
15238
|
-
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]) :
|
|
15619
|
+
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])))) {
|
|
15239
15620
|
throw new TypeError("invalid request");
|
|
15240
15621
|
}
|
|
15241
15622
|
} catch {
|
|
@@ -15572,7 +15953,7 @@ function createMembershipAuthorityStorage(db, secretNames) {
|
|
|
15572
15953
|
}
|
|
15573
15954
|
|
|
15574
15955
|
// src/membership-seat-cancel.ts
|
|
15575
|
-
var
|
|
15956
|
+
var import_stripe16 = require("@odla-ai/stripe");
|
|
15576
15957
|
async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
|
|
15577
15958
|
membershipClock(now);
|
|
15578
15959
|
const state = await loadMembershipAuthorityState(db, target);
|
|
@@ -15600,7 +15981,7 @@ async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
|
|
|
15600
15981
|
}
|
|
15601
15982
|
const metadata = { ...stripePartitionMetadata(target), applicationId: target.applicationId, membershipId: seat.primaryMembershipId, seatId: seat.id };
|
|
15602
15983
|
const request = async (method, path, params) => {
|
|
15603
|
-
const result = await (0,
|
|
15984
|
+
const result = await (0, import_stripe16.stripeRequest)(
|
|
15604
15985
|
{ secretKey },
|
|
15605
15986
|
method,
|
|
15606
15987
|
path,
|
|
@@ -15613,7 +15994,7 @@ async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
|
|
|
15613
15994
|
let payment;
|
|
15614
15995
|
if (seat.providerPaymentIntentId) payment = await request("GET", `/v1/payment_intents/${encodeURIComponent(String(seat.providerPaymentIntentId))}`);
|
|
15615
15996
|
else {
|
|
15616
|
-
const snapshot = await listStripeObjects((0,
|
|
15997
|
+
const snapshot = await listStripeObjects((0, import_stripe16.initStripe)({ secretKey }), "/v1/payment_intents", { customer: String(seat.providerCustomerId) });
|
|
15617
15998
|
const matches2 = snapshot.objects.filter((row2) => row2.metadata?.seatId === target.seatId);
|
|
15618
15999
|
if (!snapshot.complete || matches2.length !== 1) throw new Error("seat payment creation outcome unresolved");
|
|
15619
16000
|
payment = matches2[0];
|
|
@@ -15987,13 +16368,13 @@ async function requestNamedSeatDecline(options) {
|
|
|
15987
16368
|
verifiedRecipientEmail: options.verifiedRecipientEmail
|
|
15988
16369
|
});
|
|
15989
16370
|
const seat = result.seat;
|
|
15990
|
-
if (!seat || seat.seatId !== options.seatId || seat.status !== "refunded" || seat.subscriptionCanceled !== false || !Number.isSafeInteger(seat.refundedCents) || seat.refundedCents
|
|
16371
|
+
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");
|
|
15991
16372
|
return seat;
|
|
15992
16373
|
}
|
|
15993
16374
|
async function requestNamedSeatOperatorRefund(options) {
|
|
15994
16375
|
const result = await requestAuthority(options, "seat-operator-refund", { applicationId: options.applicationId, skuId: options.skuId, seatId: options.seatId });
|
|
15995
16376
|
const seat = result.seat;
|
|
15996
|
-
if (!seat || seat.seatId !== options.seatId || seat.status !== "refunded" || seat.subscriptionCanceled !== false || !Number.isSafeInteger(seat.refundedCents) || seat.refundedCents
|
|
16377
|
+
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");
|
|
15997
16378
|
return seat;
|
|
15998
16379
|
}
|
|
15999
16380
|
async function requestNamedSeatReservation(options) {
|
|
@@ -16007,13 +16388,13 @@ async function requestNamedSeatReservation(options) {
|
|
|
16007
16388
|
});
|
|
16008
16389
|
const seat = result.seat;
|
|
16009
16390
|
const expected = (await membershipOfferIdForRequest({ ...options, requestId: "named_seat" })).replace(/^offer_/, "seat_");
|
|
16010
|
-
if (!seat || seat.seatId !== expected || !Number.isSafeInteger(seat.amountCents) || seat.amountCents
|
|
16391
|
+
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");
|
|
16011
16392
|
return { seatId: seat.seatId, amountCents: seat.amountCents, currency: seat.currency, termEndsAt: seat.termEndsAt };
|
|
16012
16393
|
}
|
|
16013
16394
|
async function requestNamedSeatOffer(options) {
|
|
16014
16395
|
const result = await requestAuthority(options, "seat-offer", { applicationId: options.applicationId, skuId: options.skuId });
|
|
16015
16396
|
const offer = result.seat;
|
|
16016
|
-
if (!offer || offer.membershipId !== await networkMembershipId(options) || !Number.isSafeInteger(offer.amountCents) || offer.amountCents
|
|
16397
|
+
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");
|
|
16017
16398
|
return offer;
|
|
16018
16399
|
}
|
|
16019
16400
|
async function requestNamedSeatInvitation(options) {
|
|
@@ -16049,7 +16430,8 @@ async function requestNamedSeatCheckout(options) {
|
|
|
16049
16430
|
});
|
|
16050
16431
|
const seat = result.seat;
|
|
16051
16432
|
const expected = (await membershipOfferIdForRequest({ ...options, requestId: "named_seat" })).replace(/^offer_/, "seat_");
|
|
16052
|
-
|
|
16433
|
+
const included = seat?.amountCents === 0;
|
|
16434
|
+
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))) {
|
|
16053
16435
|
throw new Error("invalid named seat checkout receipt");
|
|
16054
16436
|
}
|
|
16055
16437
|
return seat;
|
|
@@ -16459,6 +16841,7 @@ var BUILTIN_ROUTES = [
|
|
|
16459
16841
|
handleAdminNamedSeat,
|
|
16460
16842
|
handleAdminMembershipSeats,
|
|
16461
16843
|
handleAdminMembershipPolicyVersions,
|
|
16844
|
+
handleAdminMembershipReterm,
|
|
16462
16845
|
handleAdminMeetings,
|
|
16463
16846
|
handleAdminScheduling,
|
|
16464
16847
|
// Roster + identity
|