@odla-ai/chapter 0.53.0 → 0.54.1
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 +30 -2
- package/dist/{chunk-PTC43J7F.js → chunk-NDZCR3OW.js} +5 -5
- 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 +219 -106
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +30 -10
- package/dist/index.d.ts +30 -10
- package/dist/index.js +219 -106
- package/dist/index.js.map +1 -1
- 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 +297 -128
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +92 -9
- package/dist/worker/index.d.ts +92 -9
- package/dist/worker/index.js +297 -128
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-PTC43J7F.js.map +0 -1
- package/dist/chunk-TEHCORNC.js.map +0 -1
package/dist/worker/index.cjs
CHANGED
|
@@ -36,6 +36,12 @@ __export(worker_exports, {
|
|
|
36
36
|
deliverMembershipChapterEffect: () => deliverMembershipChapterEffect,
|
|
37
37
|
deliverSignupControl: () => deliverSignupControl,
|
|
38
38
|
drainMembershipEffects: () => drainMembershipEffects,
|
|
39
|
+
grantIncludedNamedSeat: () => grantIncludedNamedSeat,
|
|
40
|
+
liftSeatPurchaseWindow: () => liftSeatPurchaseWindow,
|
|
41
|
+
lowerSeatPrice: () => lowerSeatPrice,
|
|
42
|
+
namedSeatIncluded: () => namedSeatIncluded,
|
|
43
|
+
namedSeatRecipientState: () => namedSeatRecipientState,
|
|
44
|
+
readNamedSeatOffer: () => readNamedSeatOffer,
|
|
39
45
|
refundNamedSeatByOperator: () => refundNamedSeatByOperator,
|
|
40
46
|
renderActiveSignupControlHtml: () => renderActiveSignupControlHtml,
|
|
41
47
|
resolveNetworkTargetTransport: () => resolveNetworkTargetTransport,
|
|
@@ -1803,8 +1809,8 @@ var object = (value) => {
|
|
|
1803
1809
|
var id = (value) => typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9_.:-]{2,159}$/.test(value) ? value : fail2("id");
|
|
1804
1810
|
var integer2 = (value, min, max) => typeof value === "number" && Number.isSafeInteger(value) && value >= min && value <= max ? value : fail2("integer");
|
|
1805
1811
|
var time = (value) => {
|
|
1806
|
-
const
|
|
1807
|
-
return integer2(
|
|
1812
|
+
const at2 = typeof value === "number" ? value : typeof value === "string" ? Date.parse(value) : NaN;
|
|
1813
|
+
return integer2(at2, 0, 864e13);
|
|
1808
1814
|
};
|
|
1809
1815
|
var ids = (value, allowEmpty = false) => {
|
|
1810
1816
|
if (!Array.isArray(value) || value.length > 100 || !allowEmpty && value.length === 0) return fail2("ids");
|
|
@@ -1848,7 +1854,7 @@ function membershipOfferingVersion(value) {
|
|
|
1848
1854
|
term: normalizedTerm,
|
|
1849
1855
|
autoRenew: true,
|
|
1850
1856
|
paymentGraceDays: integer2(input.paymentGraceDays, 0, 60),
|
|
1851
|
-
seatPriceCents: integer2(input.seatPriceCents,
|
|
1857
|
+
seatPriceCents: integer2(input.seatPriceCents, 0, Number.MAX_SAFE_INTEGER),
|
|
1852
1858
|
...input.seatPurchaseWindowCalendarMonths === void 0 || input.seatPurchaseWindowCalendarMonths === null ? {} : { seatPurchaseWindowCalendarMonths: integer2(input.seatPurchaseWindowCalendarMonths, 1, 120) },
|
|
1853
1859
|
requiredDisclosures: ids(input.requiredDisclosures)
|
|
1854
1860
|
};
|
|
@@ -2936,7 +2942,7 @@ async function checkoutVariables(db, group, app) {
|
|
|
2936
2942
|
const frozen = await frozenCheckoutVariables(db, app);
|
|
2937
2943
|
return frozen.tierName ? frozen : { ...frozen, tierName: await catalogTierName(db, group, app) };
|
|
2938
2944
|
}
|
|
2939
|
-
function formatInstant(
|
|
2945
|
+
function formatInstant(at2, timeZone) {
|
|
2940
2946
|
return new Intl.DateTimeFormat("en-US", {
|
|
2941
2947
|
weekday: "long",
|
|
2942
2948
|
month: "long",
|
|
@@ -2946,7 +2952,7 @@ function formatInstant(at, timeZone) {
|
|
|
2946
2952
|
minute: "2-digit",
|
|
2947
2953
|
timeZoneName: "short",
|
|
2948
2954
|
timeZone
|
|
2949
|
-
}).format(new Date(
|
|
2955
|
+
}).format(new Date(at2));
|
|
2950
2956
|
}
|
|
2951
2957
|
var adminUrlFor = (origin) => `${origin}/admin/`;
|
|
2952
2958
|
var membersUrlFor = (origin) => `${origin}/members/`;
|
|
@@ -3894,6 +3900,181 @@ async function refundNamedSeatStripePayment(secretKey, seat) {
|
|
|
3894
3900
|
return { refundedCents, refundIds };
|
|
3895
3901
|
}
|
|
3896
3902
|
|
|
3903
|
+
// src/membership-seat-reservation.ts
|
|
3904
|
+
async function recipientKey(input, email) {
|
|
3905
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify([input.appId, input.chapterId, input.environment, input.runtime, email])));
|
|
3906
|
+
return `seat-recipient:${[...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
3907
|
+
}
|
|
3908
|
+
async function reserveNamedMembershipSeat(db, input, now) {
|
|
3909
|
+
membershipClock(now);
|
|
3910
|
+
const name = typeof input.recipientName === "string" ? input.recipientName.trim().replace(/\s+/g, " ") : "";
|
|
3911
|
+
const email = normalizeChapterEmail(typeof input.recipientEmail === "string" ? input.recipientEmail : "");
|
|
3912
|
+
const purchaser = normalizeChapterEmail(typeof input.purchaserEmail === "string" ? input.purchaserEmail : "");
|
|
3913
|
+
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");
|
|
3914
|
+
const state = await loadMembershipAuthorityState(db, input);
|
|
3915
|
+
const memberId = await networkMembershipId(input), member = await membershipLoad(db, "networkMembership", memberId);
|
|
3916
|
+
requireMembershipScope(member, input);
|
|
3917
|
+
if (!Number.isSafeInteger(member.revision) || !/^price_[A-Za-z0-9_]+$/.test(String(member.providerPriceId)) || member.providerPriceId === input.priceId) membershipFail("named_seat_purchase_unavailable");
|
|
3918
|
+
const id6 = (await membershipOfferIdForRequest({ ...input, requestId: "named_seat" })).replace(/^offer_/, "seat_");
|
|
3919
|
+
let seat = await membershipLoad(db, "networkSeat", id6);
|
|
3920
|
+
if (!seat) {
|
|
3921
|
+
const offering = membershipOfferingVersion(member.offeringSnapshot), claimId = await recipientKey(input, email);
|
|
3922
|
+
const purchaseEndsAt = seatPurchaseDeadline(offering, member);
|
|
3923
|
+
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) {
|
|
3924
|
+
membershipFail("named_seat_purchase_unavailable");
|
|
3925
|
+
}
|
|
3926
|
+
const attrs = {
|
|
3927
|
+
id: id6,
|
|
3928
|
+
...membershipScope(input),
|
|
3929
|
+
primaryMembershipId: memberId,
|
|
3930
|
+
recipientName: name,
|
|
3931
|
+
recipientEmailNormalized: email,
|
|
3932
|
+
purchaserUserId: input.purchaserUserId,
|
|
3933
|
+
recipientClaimId: claimId,
|
|
3934
|
+
priceId: input.priceId,
|
|
3935
|
+
primaryPriceId: member.providerPriceId,
|
|
3936
|
+
amountCents: offering.seatPriceCents,
|
|
3937
|
+
currency: offering.currency,
|
|
3938
|
+
interval: offering.interval,
|
|
3939
|
+
providerCustomerId: member.providerCustomerId,
|
|
3940
|
+
providerSubscriptionId: member.providerSubscriptionId,
|
|
3941
|
+
status: "payment_pending",
|
|
3942
|
+
preparedAt: now,
|
|
3943
|
+
termEndsAt: member.termEndsAt,
|
|
3944
|
+
seatPurchaseEndsAt: purchaseEndsAt,
|
|
3945
|
+
offeringSnapshot: offering,
|
|
3946
|
+
revision: 1,
|
|
3947
|
+
createdAt: now,
|
|
3948
|
+
updatedAt: now
|
|
3949
|
+
};
|
|
3950
|
+
try {
|
|
3951
|
+
await db.transact([
|
|
3952
|
+
membershipUpdate("networkSeat", id6, attrs),
|
|
3953
|
+
membershipUpdate("networkSeatRecipient", claimId, {
|
|
3954
|
+
id: claimId,
|
|
3955
|
+
appId: input.appId,
|
|
3956
|
+
chapterId: input.chapterId,
|
|
3957
|
+
environment: input.environment,
|
|
3958
|
+
runtime: input.runtime,
|
|
3959
|
+
seatId: id6,
|
|
3960
|
+
recipientEmailNormalized: email,
|
|
3961
|
+
createdAt: now
|
|
3962
|
+
}),
|
|
3963
|
+
membershipUpdate("networkMembership", memberId, { namedSeatId: id6, revision: Number(member.revision) + 1, updatedAt: now })
|
|
3964
|
+
], { mutationId: `named-seat-reserve:${id6}`, guards: [
|
|
3965
|
+
{ ns: "networkSeat", id: id6, exists: false },
|
|
3966
|
+
{ ns: "networkSeatRecipient", id: claimId, exists: false },
|
|
3967
|
+
{ ns: "networkMembership", id: memberId, exists: true, equals: { revision: member.revision }, absent: ["namedSeatId"] },
|
|
3968
|
+
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
|
|
3969
|
+
] });
|
|
3970
|
+
} catch (error) {
|
|
3971
|
+
if (error.code !== "transact_guard_failed") throw error;
|
|
3972
|
+
}
|
|
3973
|
+
seat = await membershipLoad(db, "networkSeat", id6);
|
|
3974
|
+
if (!seat && await membershipLoad(db, "networkSeatRecipient", claimId)) membershipFail("named_seat_recipient_unavailable");
|
|
3975
|
+
}
|
|
3976
|
+
requireMembershipScope(seat, input);
|
|
3977
|
+
if (seat.primaryMembershipId !== memberId || seat.recipientName !== name || seat.recipientEmailNormalized !== email || seat.priceId !== input.priceId || seat.purchaserUserId !== input.purchaserUserId) membershipFail("named_seat_purchase_conflict");
|
|
3978
|
+
return seat;
|
|
3979
|
+
}
|
|
3980
|
+
|
|
3981
|
+
// src/membership-seat-included.ts
|
|
3982
|
+
var namedSeatIncluded = (seat) => Number(seat.amountCents) === 0;
|
|
3983
|
+
var namedSeatSettled = (seat) => namedSeatIncluded(seat) ? seat.purchasedAt !== void 0 : Boolean(seat.providerPaymentIntentId && seat.providerSubscriptionItemId);
|
|
3984
|
+
function namedSeatRecipientState(seat) {
|
|
3985
|
+
switch (String(seat.status)) {
|
|
3986
|
+
case "renewal_pending":
|
|
3987
|
+
case "pending_acceptance":
|
|
3988
|
+
return "invited";
|
|
3989
|
+
case "pending_approval":
|
|
3990
|
+
case "pending_primary":
|
|
3991
|
+
return "accepted";
|
|
3992
|
+
case "active":
|
|
3993
|
+
return "joined";
|
|
3994
|
+
case "refund_pending":
|
|
3995
|
+
case "refunded":
|
|
3996
|
+
return seat.refundReason === "declined" ? "declined" : "refunded";
|
|
3997
|
+
case "canceled":
|
|
3998
|
+
return "canceled";
|
|
3999
|
+
default:
|
|
4000
|
+
return "payment_pending";
|
|
4001
|
+
}
|
|
4002
|
+
}
|
|
4003
|
+
var OPEN = ["pending_acceptance", "pending_approval", "pending_primary", "active"];
|
|
4004
|
+
async function grantIncludedNamedSeat(db, input, now) {
|
|
4005
|
+
membershipClock(now);
|
|
4006
|
+
const seat = await reserveNamedMembershipSeat(db, input, now);
|
|
4007
|
+
if (!namedSeatIncluded(seat)) membershipFail("named_seat_not_included");
|
|
4008
|
+
const state = await loadMembershipAuthorityState(db, input);
|
|
4009
|
+
const member = await membershipLoad(db, "networkMembership", seat.primaryMembershipId);
|
|
4010
|
+
requireMembershipScope(member, input);
|
|
4011
|
+
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)) {
|
|
4012
|
+
membershipFail("named_seat_purchase_unavailable");
|
|
4013
|
+
}
|
|
4014
|
+
if (seat.status === "payment_pending") {
|
|
4015
|
+
const entitlementId = `${seat.id}:entitlement`, inviteId = `${seat.id}:invite`, source = `included:${seat.id}`;
|
|
4016
|
+
try {
|
|
4017
|
+
await db.transact([
|
|
4018
|
+
membershipUpdate("networkSeat", seat.id, {
|
|
4019
|
+
status: "pending_acceptance",
|
|
4020
|
+
purchasedAt: now,
|
|
4021
|
+
termStartedAt: now,
|
|
4022
|
+
termEndsAt: member.termEndsAt,
|
|
4023
|
+
acceptanceEndsAt: Number(member.termEndsAt),
|
|
4024
|
+
entitlementId,
|
|
4025
|
+
revision: Number(seat.revision) + 1,
|
|
4026
|
+
updatedAt: now
|
|
4027
|
+
}),
|
|
4028
|
+
membershipUpdate("networkEntitlement", entitlementId, {
|
|
4029
|
+
id: entitlementId,
|
|
4030
|
+
...membershipScope(input),
|
|
4031
|
+
membershipId: member.id,
|
|
4032
|
+
seatId: seat.id,
|
|
4033
|
+
kind: "named_seat",
|
|
4034
|
+
status: "pending_acceptance",
|
|
4035
|
+
termStartedAt: now,
|
|
4036
|
+
termEndsAt: member.termEndsAt,
|
|
4037
|
+
offeringSnapshot: member.offeringSnapshot,
|
|
4038
|
+
sourceEventId: source,
|
|
4039
|
+
revision: 1,
|
|
4040
|
+
createdAt: now,
|
|
4041
|
+
updatedAt: now
|
|
4042
|
+
}),
|
|
4043
|
+
membershipUpdate("membershipEffect", inviteId, {
|
|
4044
|
+
id: inviteId,
|
|
4045
|
+
chapterId: input.chapterId,
|
|
4046
|
+
environment: input.environment,
|
|
4047
|
+
runtime: input.runtime,
|
|
4048
|
+
membershipId: member.id,
|
|
4049
|
+
seatId: seat.id,
|
|
4050
|
+
kind: "invite_named_recipient",
|
|
4051
|
+
status: "pending",
|
|
4052
|
+
attempts: 0,
|
|
4053
|
+
createdAt: now,
|
|
4054
|
+
updatedAt: now
|
|
4055
|
+
})
|
|
4056
|
+
], { mutationId: `named-seat-included:${seat.id}`, guards: [
|
|
4057
|
+
{
|
|
4058
|
+
ns: "networkSeat",
|
|
4059
|
+
id: seat.id,
|
|
4060
|
+
exists: true,
|
|
4061
|
+
equals: { status: "payment_pending", revision: seat.revision, amountCents: 0 },
|
|
4062
|
+
absent: ["purchasedAt", "providerPaymentIntentId"]
|
|
4063
|
+
},
|
|
4064
|
+
{ ns: "networkEntitlement", id: entitlementId, exists: false },
|
|
4065
|
+
{ ns: "networkMembership", id: String(member.id), exists: true, equals: { revision: member.revision, namedSeatId: seat.id } },
|
|
4066
|
+
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
|
|
4067
|
+
] });
|
|
4068
|
+
} catch (error) {
|
|
4069
|
+
if (error.code !== "transact_guard_failed") throw error;
|
|
4070
|
+
}
|
|
4071
|
+
}
|
|
4072
|
+
const current = await membershipLoad(db, "networkSeat", seat.id);
|
|
4073
|
+
requireMembershipScope(current, input);
|
|
4074
|
+
if (!namedSeatIncluded(current) || current.purchasedAt === void 0 || !OPEN.includes(String(current.status))) membershipFail("named_seat_purchase_unavailable");
|
|
4075
|
+
return { seatId: seat.id, amountCents: 0, currency: seat.currency, termEndsAt: Number(current.termEndsAt), status: String(current.status), included: true };
|
|
4076
|
+
}
|
|
4077
|
+
|
|
3897
4078
|
// src/membership-seat-refund.ts
|
|
3898
4079
|
async function requestNamedSeatRefund(db, input, now) {
|
|
3899
4080
|
membershipClock(now);
|
|
@@ -3903,7 +4084,7 @@ async function requestNamedSeatRefund(db, input, now) {
|
|
|
3903
4084
|
const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
|
|
3904
4085
|
requireMembershipScope(member, input);
|
|
3905
4086
|
if (seat.status === "refunded" || seat.status === "refund_pending") return;
|
|
3906
|
-
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");
|
|
4087
|
+
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");
|
|
3907
4088
|
const entitlement = seat.entitlementId ? await membershipLoad(db, "networkEntitlement", String(seat.entitlementId)) : void 0;
|
|
3908
4089
|
await db.transact([
|
|
3909
4090
|
membershipUpdate("networkSeat", input.seatId, { status: "refund_pending", refundReason: input.reason, revision: Number(seat.revision) + 1, updatedAt: now }),
|
|
@@ -3948,7 +4129,7 @@ async function completeNamedSeatRefund(db, seatId, secretKey, now) {
|
|
|
3948
4129
|
await loadMembershipAuthorityState(db, seat);
|
|
3949
4130
|
if (seat.status === "refunded") return;
|
|
3950
4131
|
if (seat.status !== "refund_pending") membershipFail("named_seat_refund_not_ready");
|
|
3951
|
-
const result = await refundNamedSeatStripePayment(secretKey, seat);
|
|
4132
|
+
const result = namedSeatIncluded(seat) ? { refundedCents: 0, refundIds: [] } : await refundNamedSeatStripePayment(secretKey, seat);
|
|
3952
4133
|
await db.transact([
|
|
3953
4134
|
membershipUpdate("networkSeat", seatId, {
|
|
3954
4135
|
status: "refunded",
|
|
@@ -3970,7 +4151,7 @@ async function refundNamedSeatByOperator(db, input, stripeSecretKey, now) {
|
|
|
3970
4151
|
await requestNamedSeatRefund(db, { ...input, reason: "operator" }, now);
|
|
3971
4152
|
await completeNamedSeatRefund(db, input.seatId, stripeSecretKey, now);
|
|
3972
4153
|
const refunded = await membershipLoad(db, "networkSeat", input.seatId);
|
|
3973
|
-
if (refunded?.status !== "refunded" || !Number.isSafeInteger(refunded.refundedCents) || Number(refunded.refundedCents)
|
|
4154
|
+
if (refunded?.status !== "refunded" || !Number.isSafeInteger(refunded.refundedCents) || Number(refunded.refundedCents) !== Number(seat.amountCents)) {
|
|
3974
4155
|
throw new Error("named seat refund unconfirmed");
|
|
3975
4156
|
}
|
|
3976
4157
|
return { seatId: String(seat.id), status: "refunded", refundedCents: Number(refunded.refundedCents), subscriptionCanceled: false };
|
|
@@ -4002,9 +4183,56 @@ async function liftSeatPurchaseWindow(db, scope2, now, limit = 100) {
|
|
|
4002
4183
|
return { lifted, more: rows5.length > limit };
|
|
4003
4184
|
}
|
|
4004
4185
|
|
|
4186
|
+
// src/membership-seat-reprice.ts
|
|
4187
|
+
async function lowerSeatPrice(db, scope2, now, limit = 100) {
|
|
4188
|
+
membershipClock(now);
|
|
4189
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200) throw new TypeError("invalid reprice limit");
|
|
4190
|
+
const state = await loadMembershipAuthorityState(db, scope2);
|
|
4191
|
+
const versions = (await db.query({ membershipOfferingVersion: { $: { where: { chapterId: scope2.chapterId }, limit: 200 } } })).membershipOfferingVersion ?? [];
|
|
4192
|
+
if (versions.length >= 200) throw Object.assign(new Error("policy_history_incomplete"), { code: "policy_history_incomplete" });
|
|
4193
|
+
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];
|
|
4194
|
+
if (!current) throw Object.assign(new Error("ambiguous_membership_policy"), { code: "ambiguous_membership_policy" });
|
|
4195
|
+
const target = current.seatPriceCents;
|
|
4196
|
+
const rows5 = (await db.query({ networkMembership: { $: { where: { ...scope2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkMembership ?? [];
|
|
4197
|
+
const receipt = { seatPriceCents: target, repriced: 0, skipped: [], more: rows5.length > limit };
|
|
4198
|
+
for (const member of rows5.slice(0, limit)) {
|
|
4199
|
+
if (Object.entries(scope2).some(([key3, value]) => member[key3] !== value)) throw new Error("membership reprice scope mismatch");
|
|
4200
|
+
const id6 = String(member.id), skip = (reason) => {
|
|
4201
|
+
receipt.skipped.push({ membershipId: id6, reason });
|
|
4202
|
+
};
|
|
4203
|
+
const offering = membershipOfferingVersion(member.offeringSnapshot);
|
|
4204
|
+
if (offering.seatPriceCents <= target) {
|
|
4205
|
+
skip("seat price already at or below the current offering");
|
|
4206
|
+
continue;
|
|
4207
|
+
}
|
|
4208
|
+
if (!["paid_pending_approval", "active", "cancel_at_term_end", "payment_grace"].includes(String(member.status))) {
|
|
4209
|
+
skip(`membership is ${String(member.status)}`);
|
|
4210
|
+
continue;
|
|
4211
|
+
}
|
|
4212
|
+
const seat = member.namedSeatId ? await membershipLoad(db, "networkSeat", String(member.namedSeatId)) : void 0;
|
|
4213
|
+
if (seat && (seat.status !== "payment_pending" || seat.providerPaymentIntentId !== void 0)) {
|
|
4214
|
+
skip(`seat is ${String(seat.status)}`);
|
|
4215
|
+
continue;
|
|
4216
|
+
}
|
|
4217
|
+
const ops = [membershipUpdate("networkMembership", id6, { offeringSnapshot: { ...offering, seatPriceCents: target }, revision: Number(member.revision) + 1, updatedAt: now })];
|
|
4218
|
+
const guards = [
|
|
4219
|
+
{ ns: "networkMembership", id: id6, exists: true, equals: { revision: member.revision } },
|
|
4220
|
+
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative" } }
|
|
4221
|
+
];
|
|
4222
|
+
if (seat) {
|
|
4223
|
+
ops.push(membershipUpdate("networkSeat", String(seat.id), { amountCents: target, offeringSnapshot: { ...offering, seatPriceCents: target }, revision: Number(seat.revision) + 1, updatedAt: now }));
|
|
4224
|
+
guards.push({ ns: "networkSeat", id: String(seat.id), exists: true, equals: { revision: seat.revision, status: "payment_pending" } });
|
|
4225
|
+
}
|
|
4226
|
+
await db.transact(ops, { mutationId: `seat-reprice:${id6}:${member.revision}`, guards });
|
|
4227
|
+
receipt.repriced++;
|
|
4228
|
+
}
|
|
4229
|
+
return receipt;
|
|
4230
|
+
}
|
|
4231
|
+
|
|
4005
4232
|
// src/worker-routes-admin-membership-seats.ts
|
|
4006
4233
|
var LIST = "/api/admin/membership-seats";
|
|
4007
4234
|
var LIFT = "/api/admin/membership-seats/lift-purchase-window";
|
|
4235
|
+
var REPRICE = "/api/admin/membership-seats/reprice";
|
|
4008
4236
|
var ONE = /^\/api\/admin\/membership-seats\/([A-Za-z0-9][A-Za-z0-9_.:-]{2,159})\/refund$/;
|
|
4009
4237
|
var SCOPE = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
4010
4238
|
var PUBLIC = [
|
|
@@ -4031,11 +4259,11 @@ var PUBLIC = [
|
|
|
4031
4259
|
];
|
|
4032
4260
|
var project = (row2) => Object.fromEntries(PUBLIC.filter((key3) => row2[key3] !== void 0).map((key3) => [key3, row2[key3]]));
|
|
4033
4261
|
var handleAdminMembershipSeats = async (req, url, env, ctx) => {
|
|
4034
|
-
const one = url.pathname.match(ONE), lift = url.pathname === LIFT;
|
|
4035
|
-
if (url.pathname !== LIST && !one && !lift) return null;
|
|
4262
|
+
const one = url.pathname.match(ONE), lift = url.pathname === LIFT, reprice = url.pathname === REPRICE;
|
|
4263
|
+
if (url.pathname !== LIST && !one && !lift && !reprice) return null;
|
|
4036
4264
|
const operator = ctx.membershipAuthorityOperator;
|
|
4037
4265
|
if (!operator) return json({ error: "not found" }, 404);
|
|
4038
|
-
if (req.method !== (one || lift ? "POST" : "GET")) return json({ error: "method not allowed" }, 405);
|
|
4266
|
+
if (req.method !== (one || lift || reprice ? "POST" : "GET")) return json({ error: "method not allowed" }, 405);
|
|
4039
4267
|
const raw2 = ctx.makeDb(env), user = await ctx.verifyUser(req, env);
|
|
4040
4268
|
if (!user) return json({ error: "unauthorized" }, 401);
|
|
4041
4269
|
if (!await ctx.isSuperAdminEmail(raw2, user.email, user.userId)) return json({ error: "super-admin required", code: "super_admin_required" }, 403);
|
|
@@ -4045,7 +4273,7 @@ var handleAdminMembershipSeats = async (req, url, env, ctx) => {
|
|
|
4045
4273
|
} catch {
|
|
4046
4274
|
return json({ error: "the membership authority is not hosted by this deployment" }, 404);
|
|
4047
4275
|
}
|
|
4048
|
-
if (lift) {
|
|
4276
|
+
if (lift || reprice) {
|
|
4049
4277
|
let body;
|
|
4050
4278
|
try {
|
|
4051
4279
|
body = await req.json();
|
|
@@ -4053,14 +4281,16 @@ var handleAdminMembershipSeats = async (req, url, env, ctx) => {
|
|
|
4053
4281
|
return json({ error: "chapterId and runtime are required" }, 400);
|
|
4054
4282
|
}
|
|
4055
4283
|
const appId = body.appId ?? body.chapterId;
|
|
4056
|
-
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)) {
|
|
4057
|
-
return json({ error: "chapterId and runtime are required" }, 400);
|
|
4284
|
+
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))) {
|
|
4285
|
+
return json({ error: reprice ? "chapterId, runtime and skuId are required" : "chapterId and runtime are required" }, 400);
|
|
4058
4286
|
}
|
|
4287
|
+
const scope2 = { appId, chapterId: body.chapterId, environment: env.ODLA_ENV, runtime: body.runtime };
|
|
4059
4288
|
try {
|
|
4060
|
-
return json({ ok: true, ...await
|
|
4289
|
+
if (reprice) return json({ ok: true, ...await lowerSeatPrice(db, { ...scope2, skuId: String(body.skuId) }, Date.now()) });
|
|
4290
|
+
return json({ ok: true, ...await liftSeatPurchaseWindow(db, scope2, Date.now()) });
|
|
4061
4291
|
} catch (error) {
|
|
4062
4292
|
const code = error.code;
|
|
4063
|
-
return json({ error: "purchase window lift is pending; retry to confirm
|
|
4293
|
+
return json({ error: `${reprice ? "seat reprice" : "purchase window lift"} is pending; retry to confirm`, ...code ? { code } : {} }, code ? 409 : 503);
|
|
4064
4294
|
}
|
|
4065
4295
|
}
|
|
4066
4296
|
if (!one) {
|
|
@@ -4096,8 +4326,8 @@ var handleAdminMembershipSeats = async (req, url, env, ctx) => {
|
|
|
4096
4326
|
var LIMIT = 200;
|
|
4097
4327
|
var text2 = (value) => typeof value === "string" && value.length > 0 && value.length <= 200 ? value : void 0;
|
|
4098
4328
|
var instant = (value) => {
|
|
4099
|
-
const
|
|
4100
|
-
return Number.isSafeInteger(
|
|
4329
|
+
const at2 = typeof value === "number" ? value : typeof value === "string" ? Date.parse(value) : NaN;
|
|
4330
|
+
return Number.isSafeInteger(at2) && at2 >= 0 ? at2 : void 0;
|
|
4101
4331
|
};
|
|
4102
4332
|
function provenance(row2) {
|
|
4103
4333
|
const source = text2(row2.source), actor = text2(row2.actor ?? row2.$createdBy), recordedAt = instant(row2.recordedAt ?? row2.createdAt ?? row2.$createdAt);
|
|
@@ -6470,7 +6700,7 @@ var handleMe = async (req, url, env, ctx) => {
|
|
|
6470
6700
|
} catch {
|
|
6471
6701
|
}
|
|
6472
6702
|
}
|
|
6473
|
-
return json({ ...base, memberAccess, membershipRestart, application: member?.application ?? null, namedSeatsEnabled: Boolean(
|
|
6703
|
+
return json({ ...base, memberAccess, membershipRestart, application: member?.application ?? null, namedSeatsEnabled: Boolean(ctx.membershipAuthority?.namedSeats && member?.application.paid && member.source.status !== "submitted") });
|
|
6474
6704
|
};
|
|
6475
6705
|
var handleCrm = async (req, url, env, ctx) => {
|
|
6476
6706
|
const crmBase = ctx.crmBase;
|
|
@@ -9468,6 +9698,7 @@ async function namedSeatOfferDigest(offer) {
|
|
|
9468
9698
|
])));
|
|
9469
9699
|
return [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
9470
9700
|
}
|
|
9701
|
+
var at = (value) => Number.isSafeInteger(value) ? Number(value) : void 0;
|
|
9471
9702
|
async function readNamedSeatOffer(db, input, now) {
|
|
9472
9703
|
membershipClock(now);
|
|
9473
9704
|
await loadMembershipAuthorityState(db, input);
|
|
@@ -9490,10 +9721,19 @@ async function readNamedSeatOffer(db, input, now) {
|
|
|
9490
9721
|
seatId: String(seat.id),
|
|
9491
9722
|
status: String(seat.status),
|
|
9492
9723
|
recipientName: String(seat.recipientName),
|
|
9493
|
-
recipientEmail: String(seat.recipientEmailNormalized)
|
|
9724
|
+
recipientEmail: String(seat.recipientEmailNormalized),
|
|
9725
|
+
recipientState: namedSeatRecipientState(seat),
|
|
9726
|
+
...at(seat.purchasedAt) !== void 0 ? { purchasedAt: at(seat.purchasedAt) } : {},
|
|
9727
|
+
...at(seat.acceptedAt) !== void 0 ? { acceptedAt: at(seat.acceptedAt) } : {},
|
|
9728
|
+
...seat.status === "active" && at(seat.journeyApprovedAt) !== void 0 ? { joinedAt: at(seat.journeyApprovedAt) } : {}
|
|
9494
9729
|
} } : {}
|
|
9495
9730
|
};
|
|
9496
|
-
return {
|
|
9731
|
+
return {
|
|
9732
|
+
...offer,
|
|
9733
|
+
digest: await namedSeatOfferDigest(offer),
|
|
9734
|
+
included: offering.seatPriceCents === 0,
|
|
9735
|
+
purchaseWindowed: offering.seatPurchaseWindowCalendarMonths !== void 0
|
|
9736
|
+
};
|
|
9497
9737
|
}
|
|
9498
9738
|
|
|
9499
9739
|
// src/named-seat-request.ts
|
|
@@ -9569,8 +9809,9 @@ var handleNamedSeat = async (req, url, env, ctx) => {
|
|
|
9569
9809
|
if (recipientName.length < 2 || /[\u0000-\u001f\u007f]/.test(body.recipientName) || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(recipientEmail) || recipientEmail === normalizeChapterEmail(user.email)) {
|
|
9570
9810
|
return json({ error: "a valid name and email for someone other than the purchaser are required" }, 400);
|
|
9571
9811
|
}
|
|
9572
|
-
|
|
9573
|
-
if (!
|
|
9812
|
+
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);
|
|
9813
|
+
if (!offer.eligible && !granted || body.quoteDigest !== offer.digest) return json({ error: "seat offer changed or is unavailable; refresh before paying" }, 409);
|
|
9814
|
+
if (!group?.refundPolicyText || !group.merchantDisclosureText || !offer.included && !group.stripePublishableKey) return json({ error: "seat checkout disclosures unavailable" }, 503);
|
|
9574
9815
|
const consentId = `${offer.membershipId}:named-seat-consent`;
|
|
9575
9816
|
const prior = await membershipLoad(db, "namedSeatConsents", consentId);
|
|
9576
9817
|
const attrs = {
|
|
@@ -9609,7 +9850,7 @@ var handleNamedSeat = async (req, url, env, ctx) => {
|
|
|
9609
9850
|
}
|
|
9610
9851
|
return json({
|
|
9611
9852
|
...receipt,
|
|
9612
|
-
publishableKey: String(group.stripePublishableKey),
|
|
9853
|
+
...group.stripePublishableKey ? { publishableKey: String(group.stripePublishableKey) } : {},
|
|
9613
9854
|
lineItems: { standardCents: offer.amountCents, discountCents: 0, dueTodayCents: offer.amountCents }
|
|
9614
9855
|
});
|
|
9615
9856
|
} catch {
|
|
@@ -11406,7 +11647,7 @@ async function networkApplicationAction(opts) {
|
|
|
11406
11647
|
if (app.status !== input.expectedStage || !applicationActionStages(app, env, ctx).includes(input.stage)) {
|
|
11407
11648
|
return json({ error: "application action is no longer available; reload", code: "invalid_application_action" }, 409);
|
|
11408
11649
|
}
|
|
11409
|
-
const
|
|
11650
|
+
const at2 = Date.now();
|
|
11410
11651
|
const meta = { [ACTION_META]: {
|
|
11411
11652
|
version: 1,
|
|
11412
11653
|
state: "pending",
|
|
@@ -11426,7 +11667,7 @@ async function networkApplicationAction(opts) {
|
|
|
11426
11667
|
recordId: record4.id,
|
|
11427
11668
|
kind: "note",
|
|
11428
11669
|
authorId: `network:${sourceId}`,
|
|
11429
|
-
createdAt:
|
|
11670
|
+
createdAt: at2,
|
|
11430
11671
|
body: `Requested ${input.expectedStage} \u2192 ${input.stage} from ${sourceId}`,
|
|
11431
11672
|
meta
|
|
11432
11673
|
} },
|
|
@@ -11435,7 +11676,7 @@ async function networkApplicationAction(opts) {
|
|
|
11435
11676
|
recordId: record4.id,
|
|
11436
11677
|
kind: "note",
|
|
11437
11678
|
authorId: `network:${sourceId}`,
|
|
11438
|
-
createdAt:
|
|
11679
|
+
createdAt: at2,
|
|
11439
11680
|
body: "Application operation reconciliation",
|
|
11440
11681
|
meta: { state: "pending", activityId }
|
|
11441
11682
|
} }
|
|
@@ -12282,9 +12523,9 @@ function normalizeNetworkIdentity(kind, value) {
|
|
|
12282
12523
|
return trimmed;
|
|
12283
12524
|
}
|
|
12284
12525
|
function emailLooksReal(value) {
|
|
12285
|
-
const
|
|
12286
|
-
if (
|
|
12287
|
-
const domain = value.slice(
|
|
12526
|
+
const at2 = value.lastIndexOf("@");
|
|
12527
|
+
if (at2 <= 0 || at2 === value.length - 1) return false;
|
|
12528
|
+
const domain = value.slice(at2 + 1).toLowerCase();
|
|
12288
12529
|
return !domain.endsWith(".invalid") && domain !== "example.com" && domain !== "example.test";
|
|
12289
12530
|
}
|
|
12290
12531
|
function validateNetworkPersonImport(input, allowedChapterIds) {
|
|
@@ -14603,7 +14844,9 @@ async function reconcileMembershipRenewal(db, input, now) {
|
|
|
14603
14844
|
if (start !== member.termEndsAt || start > now + 3e4) membershipFail("membership_renewal_period_gap");
|
|
14604
14845
|
const seat = member.namedSeatId ? await membershipLoad(db, "networkSeat", String(member.namedSeatId)) : void 0;
|
|
14605
14846
|
if (seat) requireMembershipScope(seat, input);
|
|
14606
|
-
const
|
|
14847
|
+
const openSeat = seat && !["refunded", "refund_pending", "canceled"].includes(String(seat.status)) ? seat : void 0;
|
|
14848
|
+
const billedSeat = openSeat && openSeat.providerSubscriptionId === member.providerSubscriptionId && openSeat.providerSubscriptionItemId ? openSeat : void 0;
|
|
14849
|
+
const carriedSeat = billedSeat ?? (openSeat && namedSeatIncluded(openSeat) && openSeat.purchasedAt !== void 0 ? openSeat : void 0);
|
|
14607
14850
|
const prices = [String(member.providerPriceId), ...billedSeat ? [String(billedSeat.priceId)] : []].sort();
|
|
14608
14851
|
const amount = Number(member.renewalAmountCents) + Number(billedSeat?.amountCents ?? 0);
|
|
14609
14852
|
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");
|
|
@@ -14643,19 +14886,19 @@ async function reconcileMembershipRenewal(db, input, now) {
|
|
|
14643
14886
|
evidenceGuard
|
|
14644
14887
|
];
|
|
14645
14888
|
if (seat) guards.push({ ns: "networkSeat", id: String(seat.id), exists: true, equals: { revision: seat.revision } });
|
|
14646
|
-
if (
|
|
14647
|
-
const seatEntitlement = await membershipLoad(db, "networkEntitlement", String(
|
|
14889
|
+
if (carriedSeat?.entitlementId) {
|
|
14890
|
+
const seatEntitlement = await membershipLoad(db, "networkEntitlement", String(carriedSeat.entitlementId));
|
|
14648
14891
|
requireMembershipScope(seatEntitlement, input);
|
|
14649
14892
|
guards.push({ ns: "networkEntitlement", id: String(seatEntitlement.id), exists: true, equals: { revision: seatEntitlement.revision } });
|
|
14650
14893
|
ops.push(
|
|
14651
|
-
membershipUpdate("networkSeat", String(
|
|
14894
|
+
membershipUpdate("networkSeat", String(carriedSeat.id), {
|
|
14652
14895
|
...paid ? { termStartedAt: start, termEndsAt: end } : {},
|
|
14653
|
-
revision: Number(
|
|
14896
|
+
revision: Number(carriedSeat.revision) + 1,
|
|
14654
14897
|
updatedAt: now
|
|
14655
14898
|
}),
|
|
14656
14899
|
membershipUpdate("networkEntitlement", String(seatEntitlement.id), {
|
|
14657
14900
|
...paid ? { termStartedAt: start, termEndsAt: end } : {},
|
|
14658
|
-
status:
|
|
14901
|
+
status: carriedSeat.journeyApprovedAt ? entitlementStatus : seatEntitlement.status,
|
|
14659
14902
|
revision: Number(seatEntitlement.revision) + 1,
|
|
14660
14903
|
updatedAt: now
|
|
14661
14904
|
})
|
|
@@ -14765,84 +15008,6 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
14765
15008
|
return membershipFail("membership_billing_retry_required");
|
|
14766
15009
|
}
|
|
14767
15010
|
|
|
14768
|
-
// src/membership-seat-reservation.ts
|
|
14769
|
-
async function recipientKey(input, email) {
|
|
14770
|
-
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify([input.appId, input.chapterId, input.environment, input.runtime, email])));
|
|
14771
|
-
return `seat-recipient:${[...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
14772
|
-
}
|
|
14773
|
-
async function reserveNamedMembershipSeat(db, input, now) {
|
|
14774
|
-
membershipClock(now);
|
|
14775
|
-
const name = typeof input.recipientName === "string" ? input.recipientName.trim().replace(/\s+/g, " ") : "";
|
|
14776
|
-
const email = normalizeChapterEmail(typeof input.recipientEmail === "string" ? input.recipientEmail : "");
|
|
14777
|
-
const purchaser = normalizeChapterEmail(typeof input.purchaserEmail === "string" ? input.purchaserEmail : "");
|
|
14778
|
-
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");
|
|
14779
|
-
const state = await loadMembershipAuthorityState(db, input);
|
|
14780
|
-
const memberId = await networkMembershipId(input), member = await membershipLoad(db, "networkMembership", memberId);
|
|
14781
|
-
requireMembershipScope(member, input);
|
|
14782
|
-
if (!Number.isSafeInteger(member.revision) || !/^price_[A-Za-z0-9_]+$/.test(String(member.providerPriceId)) || member.providerPriceId === input.priceId) membershipFail("named_seat_purchase_unavailable");
|
|
14783
|
-
const id6 = (await membershipOfferIdForRequest({ ...input, requestId: "named_seat" })).replace(/^offer_/, "seat_");
|
|
14784
|
-
let seat = await membershipLoad(db, "networkSeat", id6);
|
|
14785
|
-
if (!seat) {
|
|
14786
|
-
const offering = membershipOfferingVersion(member.offeringSnapshot), claimId = await recipientKey(input, email);
|
|
14787
|
-
const purchaseEndsAt = seatPurchaseDeadline(offering, member);
|
|
14788
|
-
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) {
|
|
14789
|
-
membershipFail("named_seat_purchase_unavailable");
|
|
14790
|
-
}
|
|
14791
|
-
const attrs = {
|
|
14792
|
-
id: id6,
|
|
14793
|
-
...membershipScope(input),
|
|
14794
|
-
primaryMembershipId: memberId,
|
|
14795
|
-
recipientName: name,
|
|
14796
|
-
recipientEmailNormalized: email,
|
|
14797
|
-
purchaserUserId: input.purchaserUserId,
|
|
14798
|
-
recipientClaimId: claimId,
|
|
14799
|
-
priceId: input.priceId,
|
|
14800
|
-
primaryPriceId: member.providerPriceId,
|
|
14801
|
-
amountCents: offering.seatPriceCents,
|
|
14802
|
-
currency: offering.currency,
|
|
14803
|
-
interval: offering.interval,
|
|
14804
|
-
providerCustomerId: member.providerCustomerId,
|
|
14805
|
-
providerSubscriptionId: member.providerSubscriptionId,
|
|
14806
|
-
status: "payment_pending",
|
|
14807
|
-
preparedAt: now,
|
|
14808
|
-
termEndsAt: member.termEndsAt,
|
|
14809
|
-
seatPurchaseEndsAt: purchaseEndsAt,
|
|
14810
|
-
offeringSnapshot: offering,
|
|
14811
|
-
revision: 1,
|
|
14812
|
-
createdAt: now,
|
|
14813
|
-
updatedAt: now
|
|
14814
|
-
};
|
|
14815
|
-
try {
|
|
14816
|
-
await db.transact([
|
|
14817
|
-
membershipUpdate("networkSeat", id6, attrs),
|
|
14818
|
-
membershipUpdate("networkSeatRecipient", claimId, {
|
|
14819
|
-
id: claimId,
|
|
14820
|
-
appId: input.appId,
|
|
14821
|
-
chapterId: input.chapterId,
|
|
14822
|
-
environment: input.environment,
|
|
14823
|
-
runtime: input.runtime,
|
|
14824
|
-
seatId: id6,
|
|
14825
|
-
recipientEmailNormalized: email,
|
|
14826
|
-
createdAt: now
|
|
14827
|
-
}),
|
|
14828
|
-
membershipUpdate("networkMembership", memberId, { namedSeatId: id6, revision: Number(member.revision) + 1, updatedAt: now })
|
|
14829
|
-
], { mutationId: `named-seat-reserve:${id6}`, guards: [
|
|
14830
|
-
{ ns: "networkSeat", id: id6, exists: false },
|
|
14831
|
-
{ ns: "networkSeatRecipient", id: claimId, exists: false },
|
|
14832
|
-
{ ns: "networkMembership", id: memberId, exists: true, equals: { revision: member.revision }, absent: ["namedSeatId"] },
|
|
14833
|
-
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
|
|
14834
|
-
] });
|
|
14835
|
-
} catch (error) {
|
|
14836
|
-
if (error.code !== "transact_guard_failed") throw error;
|
|
14837
|
-
}
|
|
14838
|
-
seat = await membershipLoad(db, "networkSeat", id6);
|
|
14839
|
-
if (!seat && await membershipLoad(db, "networkSeatRecipient", claimId)) membershipFail("named_seat_recipient_unavailable");
|
|
14840
|
-
}
|
|
14841
|
-
requireMembershipScope(seat, input);
|
|
14842
|
-
if (seat.primaryMembershipId !== memberId || seat.recipientName !== name || seat.recipientEmailNormalized !== email || seat.priceId !== input.priceId || seat.purchaserUserId !== input.purchaserUserId) membershipFail("named_seat_purchase_conflict");
|
|
14843
|
-
return seat;
|
|
14844
|
-
}
|
|
14845
|
-
|
|
14846
15011
|
// src/membership-seat-stripe.ts
|
|
14847
15012
|
var import_stripe15 = require("@odla-ai/stripe");
|
|
14848
15013
|
var reject7 = () => {
|
|
@@ -14930,6 +15095,7 @@ async function attachNamedSeatStripeRenewal(sk, seat) {
|
|
|
14930
15095
|
// src/membership-seat-checkout.ts
|
|
14931
15096
|
async function prepareNamedMembershipSeatPayment(db, input, secretKey, now) {
|
|
14932
15097
|
let seat = await reserveNamedMembershipSeat(db, input, now);
|
|
15098
|
+
if (namedSeatIncluded(seat)) membershipFail("named_seat_included");
|
|
14933
15099
|
const state = await loadMembershipAuthorityState(db, input);
|
|
14934
15100
|
const member = await membershipLoad(db, "networkMembership", seat.primaryMembershipId);
|
|
14935
15101
|
requireMembershipScope(member, input);
|
|
@@ -15136,7 +15302,7 @@ async function recordRecipient(db, input, now, approve) {
|
|
|
15136
15302
|
requireMembershipScope(member, input);
|
|
15137
15303
|
const entitlement = await membershipLoad(db, "networkEntitlement", String(seat.entitlementId));
|
|
15138
15304
|
requireMembershipScope(entitlement, input);
|
|
15139
|
-
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
|
|
15305
|
+
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");
|
|
15140
15306
|
if (seat.acceptedAt === void 0) {
|
|
15141
15307
|
if (approve || normalizeChapterEmail(input.verifiedRecipientEmail) !== seat.recipientEmailNormalized) {
|
|
15142
15308
|
membershipFail("named_seat_recipient_verification_required");
|
|
@@ -15323,6 +15489,15 @@ async function handleMembershipSeatAction(action, db, peer, input, now, stripeSe
|
|
|
15323
15489
|
};
|
|
15324
15490
|
if (action === "seat-accept") return acceptNamedSeatRecipient(db, recipientInput, now);
|
|
15325
15491
|
if (action === "seat-approve") return approveNamedSeatRecipient(db, recipientInput, now);
|
|
15492
|
+
const checkout = {
|
|
15493
|
+
...target,
|
|
15494
|
+
priceId: peer.seatPriceId,
|
|
15495
|
+
recipientName: String(input.recipientName),
|
|
15496
|
+
recipientEmail: String(input.recipientEmail),
|
|
15497
|
+
purchaserEmail: String(input.purchaserEmail),
|
|
15498
|
+
purchaserUserId: String(input.purchaserUserId)
|
|
15499
|
+
};
|
|
15500
|
+
if (action === "seat-checkouts" && namedSeatIncluded(await reserveNamedMembershipSeat(db, checkout, now))) return grantIncludedNamedSeat(db, checkout, now);
|
|
15326
15501
|
const secret = await getVaultSecret(db, stripeSecretName);
|
|
15327
15502
|
if (!secret) throw new Error("network Stripe provider is unavailable");
|
|
15328
15503
|
if (action === "seat-decline") {
|
|
@@ -15338,14 +15513,7 @@ async function handleMembershipSeatAction(action, db, peer, input, now, stripeSe
|
|
|
15338
15513
|
return { seatId: seat.id, status: "refunded", refundedCents: refunded.refundedCents, subscriptionCanceled: false };
|
|
15339
15514
|
}
|
|
15340
15515
|
if (action === "seat-operator-refund") return refundNamedSeatByOperator(db, { ...target, seatId: String(input.seatId) }, secret, now);
|
|
15341
|
-
if (action === "seat-checkouts") return prepareNamedMembershipSeatPayment(db,
|
|
15342
|
-
...target,
|
|
15343
|
-
priceId: peer.seatPriceId,
|
|
15344
|
-
recipientName: String(input.recipientName),
|
|
15345
|
-
recipientEmail: String(input.recipientEmail),
|
|
15346
|
-
purchaserEmail: String(input.purchaserEmail),
|
|
15347
|
-
purchaserUserId: String(input.purchaserUserId)
|
|
15348
|
-
}, secret, now);
|
|
15516
|
+
if (action === "seat-checkouts") return prepareNamedMembershipSeatPayment(db, checkout, secret, now);
|
|
15349
15517
|
const evidence = { ...target, seatId: String(input.seatId), eventId: String(input.eventId) };
|
|
15350
15518
|
if (action === "seat-payments") {
|
|
15351
15519
|
const receipt = await confirmNamedMembershipSeatPayment(db, evidence, now);
|
|
@@ -16201,13 +16369,13 @@ async function requestNamedSeatDecline(options) {
|
|
|
16201
16369
|
verifiedRecipientEmail: options.verifiedRecipientEmail
|
|
16202
16370
|
});
|
|
16203
16371
|
const seat = result.seat;
|
|
16204
|
-
if (!seat || seat.seatId !== options.seatId || seat.status !== "refunded" || seat.subscriptionCanceled !== false || !Number.isSafeInteger(seat.refundedCents) || seat.refundedCents
|
|
16372
|
+
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");
|
|
16205
16373
|
return seat;
|
|
16206
16374
|
}
|
|
16207
16375
|
async function requestNamedSeatOperatorRefund(options) {
|
|
16208
16376
|
const result = await requestAuthority(options, "seat-operator-refund", { applicationId: options.applicationId, skuId: options.skuId, seatId: options.seatId });
|
|
16209
16377
|
const seat = result.seat;
|
|
16210
|
-
if (!seat || seat.seatId !== options.seatId || seat.status !== "refunded" || seat.subscriptionCanceled !== false || !Number.isSafeInteger(seat.refundedCents) || seat.refundedCents
|
|
16378
|
+
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");
|
|
16211
16379
|
return seat;
|
|
16212
16380
|
}
|
|
16213
16381
|
async function requestNamedSeatReservation(options) {
|
|
@@ -16221,13 +16389,13 @@ async function requestNamedSeatReservation(options) {
|
|
|
16221
16389
|
});
|
|
16222
16390
|
const seat = result.seat;
|
|
16223
16391
|
const expected = (await membershipOfferIdForRequest({ ...options, requestId: "named_seat" })).replace(/^offer_/, "seat_");
|
|
16224
|
-
if (!seat || seat.seatId !== expected || !Number.isSafeInteger(seat.amountCents) || seat.amountCents
|
|
16392
|
+
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");
|
|
16225
16393
|
return { seatId: seat.seatId, amountCents: seat.amountCents, currency: seat.currency, termEndsAt: seat.termEndsAt };
|
|
16226
16394
|
}
|
|
16227
16395
|
async function requestNamedSeatOffer(options) {
|
|
16228
16396
|
const result = await requestAuthority(options, "seat-offer", { applicationId: options.applicationId, skuId: options.skuId });
|
|
16229
16397
|
const offer = result.seat;
|
|
16230
|
-
if (!offer || offer.membershipId !== await networkMembershipId(options) || !Number.isSafeInteger(offer.amountCents) || offer.amountCents
|
|
16398
|
+
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");
|
|
16231
16399
|
return offer;
|
|
16232
16400
|
}
|
|
16233
16401
|
async function requestNamedSeatInvitation(options) {
|
|
@@ -16263,7 +16431,8 @@ async function requestNamedSeatCheckout(options) {
|
|
|
16263
16431
|
});
|
|
16264
16432
|
const seat = result.seat;
|
|
16265
16433
|
const expected = (await membershipOfferIdForRequest({ ...options, requestId: "named_seat" })).replace(/^offer_/, "seat_");
|
|
16266
|
-
|
|
16434
|
+
const included = seat?.amountCents === 0;
|
|
16435
|
+
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))) {
|
|
16267
16436
|
throw new Error("invalid named seat checkout receipt");
|
|
16268
16437
|
}
|
|
16269
16438
|
return seat;
|