@odla-ai/chapter 0.49.0 → 0.51.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -2
- package/dist/{chunk-KXIGZWG5.js → chunk-4EA5V4JE.js} +855 -614
- package/dist/chunk-4EA5V4JE.js.map +1 -0
- package/dist/{chunk-N2JIKINY.js → chunk-JXX4BFDP.js} +5 -2
- package/dist/chunk-JXX4BFDP.js.map +1 -0
- package/dist/{chunk-KHDO2QFA.js → chunk-UAXWA5BL.js} +36 -24
- package/dist/chunk-UAXWA5BL.js.map +1 -0
- package/dist/{copy-context-By4Nq7yB.d.ts → copy-context-Ddat__H5.d.ts} +10 -2
- package/dist/index.cjs +129 -54
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +59 -7
- package/dist/index.d.ts +59 -7
- package/dist/index.js +129 -54
- package/dist/index.js.map +1 -1
- package/dist/ui/admin/index.d.ts +54 -6
- package/dist/ui/admin/index.js +10 -2
- package/dist/ui/index.d.ts +2 -2
- package/dist/ui/index.js +11 -3
- package/dist/ui/member/index.d.ts +10 -2
- package/dist/ui/member/index.js +2 -2
- package/dist/worker/index.cjs +588 -272
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +86 -6
- package/dist/worker/index.d.ts +86 -6
- package/dist/worker/index.js +582 -266
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-KHDO2QFA.js.map +0 -1
- package/dist/chunk-KXIGZWG5.js.map +0 -1
- package/dist/chunk-N2JIKINY.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1442,6 +1442,7 @@ var DEFAULT_ADMIN_COPY = {
|
|
|
1442
1442
|
},
|
|
1443
1443
|
records: {
|
|
1444
1444
|
workflowMissing: "This record is not linked to an application workflow.",
|
|
1445
|
+
invitation: "Invitation",
|
|
1445
1446
|
comms: "Comms",
|
|
1446
1447
|
commsHistory: "Comms history",
|
|
1447
1448
|
scheduling: "Scheduling",
|
|
@@ -1526,7 +1527,9 @@ var DEFAULT_JOIN_COPY = {
|
|
|
1526
1527
|
processing: "Processing\u2026",
|
|
1527
1528
|
payAndContinue: "Pay and continue",
|
|
1528
1529
|
incomplete: "The payment did not complete. Please try again.",
|
|
1529
|
-
consent: "I have read and agree to the pricing and refund policy above."
|
|
1530
|
+
consent: "I have read and agree to the pricing and refund policy above.",
|
|
1531
|
+
foundingMember: "Founding member rate",
|
|
1532
|
+
foundingDiscount: "Founding discount"
|
|
1530
1533
|
},
|
|
1531
1534
|
done: {
|
|
1532
1535
|
label: "You're booked",
|
|
@@ -3083,7 +3086,7 @@ function findTier(tiers2, tierId) {
|
|
|
3083
3086
|
if (!tierId) return tiers2.length === 1 ? tiers2[0] : null;
|
|
3084
3087
|
return tiers2.find((tier) => tier.id === tierId) ?? null;
|
|
3085
3088
|
}
|
|
3086
|
-
function joinConfigTiers(tiers2, defaults = {}) {
|
|
3089
|
+
function joinConfigTiers(tiers2, defaults = {}, founding = /* @__PURE__ */ new Map()) {
|
|
3087
3090
|
return tiers2.map((tier) => ({
|
|
3088
3091
|
id: tier.id,
|
|
3089
3092
|
name: tier.name,
|
|
@@ -3091,7 +3094,8 @@ function joinConfigTiers(tiers2, defaults = {}) {
|
|
|
3091
3094
|
currency: tier.currency ?? defaults.currency ?? "usd",
|
|
3092
3095
|
interval: tier.interval ?? defaults.interval ?? "year",
|
|
3093
3096
|
blurb: tier.blurb ?? "",
|
|
3094
|
-
free: tierIsFree(tier)
|
|
3097
|
+
free: tierIsFree(tier),
|
|
3098
|
+
...founding.has(tier.id) ? { founding: founding.get(tier.id) } : {}
|
|
3095
3099
|
}));
|
|
3096
3100
|
}
|
|
3097
3101
|
|
|
@@ -3194,6 +3198,11 @@ function createChapterIntegration(chapter, options = {}) {
|
|
|
3194
3198
|
}
|
|
3195
3199
|
|
|
3196
3200
|
// src/membership-policy.ts
|
|
3201
|
+
function seatPurchaseDeadline(offering, member) {
|
|
3202
|
+
if (offering.seatPurchaseWindowCalendarMonths === void 0) return Number(member.termEndsAt);
|
|
3203
|
+
if (Number.isSafeInteger(member.seatPurchaseEndsAt)) return Number(member.seatPurchaseEndsAt);
|
|
3204
|
+
return addMembershipCalendarMonths(Number(member.initialTermStartedAt ?? member.termStartedAt), offering.seatPurchaseWindowCalendarMonths);
|
|
3205
|
+
}
|
|
3197
3206
|
var fail2 = (field2) => {
|
|
3198
3207
|
throw new TypeError(`invalid membership policy: ${field2}`);
|
|
3199
3208
|
};
|
|
@@ -3250,7 +3259,7 @@ function membershipOfferingVersion(value) {
|
|
|
3250
3259
|
autoRenew: true,
|
|
3251
3260
|
paymentGraceDays: integer2(input.paymentGraceDays, 0, 60),
|
|
3252
3261
|
seatPriceCents: integer2(input.seatPriceCents, 1, Number.MAX_SAFE_INTEGER),
|
|
3253
|
-
seatPurchaseWindowCalendarMonths: integer2(input.seatPurchaseWindowCalendarMonths, 1, 120),
|
|
3262
|
+
...input.seatPurchaseWindowCalendarMonths === void 0 || input.seatPurchaseWindowCalendarMonths === null ? {} : { seatPurchaseWindowCalendarMonths: integer2(input.seatPurchaseWindowCalendarMonths, 1, 120) },
|
|
3254
3263
|
requiredDisclosures: ids(input.requiredDisclosures)
|
|
3255
3264
|
};
|
|
3256
3265
|
}
|
|
@@ -3377,24 +3386,63 @@ async function verifyIssuedMembershipOffer(value, expected) {
|
|
|
3377
3386
|
if (!value || typeof value !== "object" || Array.isArray(value)) return reject("invalid_offer_snapshot");
|
|
3378
3387
|
return restore({ ...value, approvedFoundingCount: 0 }, expected);
|
|
3379
3388
|
}
|
|
3380
|
-
async function
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3389
|
+
async function previewMembershipCommercialOffer(db, input) {
|
|
3390
|
+
for (const key3 of ["appId", "chapterId", "environment", "runtime", "skuId"]) {
|
|
3391
|
+
if (typeof input[key3] !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_.:-]{1,159}$/.test(input[key3])) reject("invalid_offer_identity");
|
|
3392
|
+
}
|
|
3393
|
+
if (!Number.isSafeInteger(input.now) || input.now < 0) reject("invalid_offer_window");
|
|
3394
|
+
const state = await authoritativeState(db, input);
|
|
3395
|
+
const { offering, policy } = await currentPolicy(db, input, false, state);
|
|
3396
|
+
const approvedFoundingCount = policy ? state.cohortCounts[policy.cohortLabel] ?? 0 : 0;
|
|
3397
|
+
if (!Number.isSafeInteger(approvedFoundingCount) || Number(approvedFoundingCount) < 0) reject("invalid_cohort_count");
|
|
3398
|
+
const contract = createMembershipCommercialOffer({
|
|
3399
|
+
offering,
|
|
3400
|
+
discountPolicy: policy,
|
|
3401
|
+
approvedFoundingCount: Number(approvedFoundingCount),
|
|
3402
|
+
now: input.now,
|
|
3403
|
+
couponId: await couponId(input, policy?.id)
|
|
3404
|
+
});
|
|
3405
|
+
const discount = contract.billingTerms.discount;
|
|
3406
|
+
return {
|
|
3407
|
+
skuId: input.skuId,
|
|
3408
|
+
offeringVersionId: offering.id,
|
|
3409
|
+
basePriceCents: offering.basePriceCents,
|
|
3410
|
+
currency: offering.currency,
|
|
3411
|
+
interval: offering.interval,
|
|
3412
|
+
founding: contract.founding,
|
|
3413
|
+
...contract.founding && contract.discountPolicy ? { cohortLabel: contract.discountPolicy.cohortLabel } : {},
|
|
3414
|
+
dueTodayCents: contract.dueTodayCents,
|
|
3415
|
+
renewalCents: contract.renewalCents,
|
|
3416
|
+
...discount ? { discountDuration: discount.duration === "once" ? "first_term" : "while_active" } : {},
|
|
3417
|
+
...contract.initialTermEndsAt !== void 0 ? { initialTermEndsAt: contract.initialTermEndsAt } : {}
|
|
3418
|
+
};
|
|
3419
|
+
}
|
|
3420
|
+
async function authoritativeState(db, input) {
|
|
3421
|
+
const state = await load(db, "membershipAuthorityState", `${input.chapterId}:${input.environment}`);
|
|
3384
3422
|
if (!state || state.mode !== "authoritative" || state.chapterId !== input.chapterId || state.environment !== input.environment || !Number.isSafeInteger(state.revision) || Number(state.revision) < 1 || !state.cohortCounts || typeof state.cohortCounts !== "object" || Array.isArray(state.cohortCounts)) return reject("membership_authority_unavailable");
|
|
3385
|
-
|
|
3386
|
-
|
|
3423
|
+
return state;
|
|
3424
|
+
}
|
|
3425
|
+
async function currentPolicy(db, input, restartAware, state) {
|
|
3387
3426
|
const rows3 = await db.query({
|
|
3388
3427
|
membershipOfferingVersion: { $: { where: { chapterId: input.chapterId }, limit: 200 } },
|
|
3389
3428
|
discountPolicyVersion: { $: { where: { chapterId: input.chapterId }, limit: 200 } }
|
|
3390
3429
|
});
|
|
3391
3430
|
if (!Array.isArray(rows3.membershipOfferingVersion) || !Array.isArray(rows3.discountPolicyVersion) || rows3.membershipOfferingVersion.length >= 200 || rows3.discountPolicyVersion.length >= 200) return reject("policy_history_incomplete");
|
|
3392
|
-
const offerings = rows3.membershipOfferingVersion.map((
|
|
3393
|
-
const policies = rows3.discountPolicyVersion.map((
|
|
3431
|
+
const offerings = rows3.membershipOfferingVersion.map((row2) => membershipOfferingVersion(row2.snapshot)).filter((offer) => offer.chapterId === input.chapterId && offer.baseSkuId === input.skuId && offer.status === "published" && input.now >= offer.effectiveFrom && (offer.effectiveUntil === void 0 || input.now < offer.effectiveUntil));
|
|
3432
|
+
const policies = rows3.discountPolicyVersion.map((row2) => membershipDiscountPolicyVersion(row2.snapshot)).filter((policy2) => policy2.chapterId === input.chapterId && policy2.status === "published" && policy2.cohortEligibleSkuIds.includes(input.skuId) && input.now >= policy2.qualifyingStartsAt && input.now < policy2.qualifyingEndsAt);
|
|
3394
3433
|
const offering = offerings.sort((a, b) => b.version - a.version)[0];
|
|
3395
|
-
const restart = await load(db, "membershipRestart", (await membershipOfferIdForRequest({ ...input, requestId: "checkout_binding" })).replace(/^offer_/, "membership_"));
|
|
3434
|
+
const restart = restartAware && input.applicationId ? await load(db, "membershipRestart", (await membershipOfferIdForRequest({ ...input, applicationId: input.applicationId, requestId: "checkout_binding" })).replace(/^offer_/, "membership_")) : void 0;
|
|
3396
3435
|
const policy = restart ? void 0 : policies.sort((a, b) => b.version - a.version)[0];
|
|
3397
3436
|
if (!offering || offerings.filter((item) => item.version === offering.version).length !== 1 || policy && policies.filter((item) => item.version === policy.version).length !== 1) reject("ambiguous_membership_policy");
|
|
3437
|
+
return { offering, policy };
|
|
3438
|
+
}
|
|
3439
|
+
async function issueMembershipCommercialOffer(db, input) {
|
|
3440
|
+
validate(input);
|
|
3441
|
+
const stateId = `${input.chapterId}:${input.environment}`;
|
|
3442
|
+
const state = await authoritativeState(db, input);
|
|
3443
|
+
const existing = await load(db, "membershipCheckoutOffer", input.offerId);
|
|
3444
|
+
if (existing) return restore(existing, input);
|
|
3445
|
+
const { offering, policy } = await currentPolicy(db, input, true, state);
|
|
3398
3446
|
const approvedFoundingCount = policy ? state.cohortCounts[policy.cohortLabel] ?? 0 : 0;
|
|
3399
3447
|
if (!Number.isSafeInteger(approvedFoundingCount) || Number(approvedFoundingCount) < 0) reject("invalid_cohort_count");
|
|
3400
3448
|
const contract = createMembershipCommercialOffer({
|
|
@@ -3605,7 +3653,7 @@ async function confirmMembershipPayment(db, input, now) {
|
|
|
3605
3653
|
initialTermEndsAt: endsAt,
|
|
3606
3654
|
termStartedAt: paidAt,
|
|
3607
3655
|
termEndsAt: endsAt,
|
|
3608
|
-
seatPurchaseEndsAt:
|
|
3656
|
+
seatPurchaseEndsAt: seatPurchaseDeadline(offering, { initialTermStartedAt: paidAt, termEndsAt: endsAt }),
|
|
3609
3657
|
autoRenew: true,
|
|
3610
3658
|
refundedCents: 0,
|
|
3611
3659
|
revision: 1,
|
|
@@ -4033,7 +4081,11 @@ async function reconcileMembershipRenewal(db, input, now) {
|
|
|
4033
4081
|
membershipUpdate("networkMembership", id10, {
|
|
4034
4082
|
status: status2,
|
|
4035
4083
|
graceEndsAt,
|
|
4036
|
-
...paid ? {
|
|
4084
|
+
...paid ? {
|
|
4085
|
+
termStartedAt: start,
|
|
4086
|
+
termEndsAt: end,
|
|
4087
|
+
...membershipOfferingVersion(member.offeringSnapshot).seatPurchaseWindowCalendarMonths === void 0 ? { seatPurchaseEndsAt: end } : {}
|
|
4088
|
+
} : {},
|
|
4037
4089
|
revision: Number(member.revision) + 1,
|
|
4038
4090
|
updatedAt: now
|
|
4039
4091
|
}),
|
|
@@ -4335,10 +4387,11 @@ async function reserveNamedMembershipSeat(db, input, now) {
|
|
|
4335
4387
|
const id10 = (await membershipOfferIdForRequest({ ...input, requestId: "named_seat" })).replace(/^offer_/, "seat_");
|
|
4336
4388
|
let seat = await membershipLoad(db, "networkSeat", id10);
|
|
4337
4389
|
if (!seat) {
|
|
4338
|
-
|
|
4390
|
+
const offering = membershipOfferingVersion(member.offeringSnapshot), claimId = await recipientKey(input, email);
|
|
4391
|
+
const purchaseEndsAt = seatPurchaseDeadline(offering, member);
|
|
4392
|
+
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) {
|
|
4339
4393
|
membershipFail("named_seat_purchase_unavailable");
|
|
4340
4394
|
}
|
|
4341
|
-
const offering = membershipOfferingVersion(member.offeringSnapshot), claimId = await recipientKey(input, email);
|
|
4342
4395
|
const attrs = {
|
|
4343
4396
|
id: id10,
|
|
4344
4397
|
...membershipScope(input),
|
|
@@ -4357,7 +4410,7 @@ async function reserveNamedMembershipSeat(db, input, now) {
|
|
|
4357
4410
|
status: "payment_pending",
|
|
4358
4411
|
preparedAt: now,
|
|
4359
4412
|
termEndsAt: member.termEndsAt,
|
|
4360
|
-
seatPurchaseEndsAt:
|
|
4413
|
+
seatPurchaseEndsAt: purchaseEndsAt,
|
|
4361
4414
|
offeringSnapshot: offering,
|
|
4362
4415
|
revision: 1,
|
|
4363
4416
|
createdAt: now,
|
|
@@ -4574,14 +4627,14 @@ async function confirmNamedMembershipSeatPayment(db, input, now) {
|
|
|
4574
4627
|
}
|
|
4575
4628
|
if (seat.purchasedAt === void 0) {
|
|
4576
4629
|
if (!["payment_processing", "payment_canceling"].includes(String(seat.status))) membershipFail("named_seat_payment_not_expected");
|
|
4577
|
-
const eligible = seat.status !== "payment_canceling" && ["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) && Number(member.refundedCents) === 0 && paidAt <=
|
|
4630
|
+
const eligible = seat.status !== "payment_canceling" && ["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) && Number(member.refundedCents) === 0 && paidAt <= seatPurchaseDeadline(membershipOfferingVersion(member.offeringSnapshot), member) && now < Number(member.termEndsAt);
|
|
4578
4631
|
const status2 = eligible ? "renewal_pending" : "refund_pending", effectId = `${seat.id}:${eligible ? "attach-renewal" : "refund"}`;
|
|
4579
4632
|
try {
|
|
4580
4633
|
await db.transact([
|
|
4581
4634
|
membershipUpdate("networkSeat", input.seatId, {
|
|
4582
4635
|
status: status2,
|
|
4583
4636
|
purchasedAt: paidAt,
|
|
4584
|
-
acceptanceEndsAt:
|
|
4637
|
+
acceptanceEndsAt: Number(member.termEndsAt),
|
|
4585
4638
|
termStartedAt: paidAt,
|
|
4586
4639
|
termEndsAt: member.termEndsAt,
|
|
4587
4640
|
paymentEventId: input.eventId,
|
|
@@ -4629,7 +4682,7 @@ async function completeNamedSeatRenewal(db, seatId, secretKey, now) {
|
|
|
4629
4682
|
if (seat.status !== "renewal_pending" || !seat.purchasedAt) membershipFail("named_seat_renewal_not_ready");
|
|
4630
4683
|
const member = await membershipLoad(db, "networkMembership", target.primaryMembershipId);
|
|
4631
4684
|
requireMembershipScope(member, target);
|
|
4632
|
-
if (now >= Number(
|
|
4685
|
+
if (now >= Number(member.termEndsAt) || Number(member.refundedCents) !== 0 || !["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status))) membershipFail("named_seat_refund_required");
|
|
4633
4686
|
const itemId = await attachNamedSeatStripeRenewal(secretKey, target);
|
|
4634
4687
|
const entitlementId = `${seatId}:entitlement`, effectId = `${seatId}:attach-renewal`;
|
|
4635
4688
|
try {
|
|
@@ -4704,7 +4757,7 @@ async function recordRecipient(db, input, now, approve) {
|
|
|
4704
4757
|
requireMembershipScope(entitlement, input);
|
|
4705
4758
|
if (!["pending_acceptance", "pending_approval", "pending_primary", "active"].includes(String(seat.status)) || !["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) || Number(member.refundedCents) !== 0 || now >= Number(member.termEndsAt) || !seat.providerPaymentIntentId || !seat.providerSubscriptionItemId) membershipFail("named_seat_acceptance_unavailable");
|
|
4706
4759
|
if (seat.acceptedAt === void 0) {
|
|
4707
|
-
if (approve ||
|
|
4760
|
+
if (approve || normalizeChapterEmail(input.verifiedRecipientEmail) !== seat.recipientEmailNormalized) {
|
|
4708
4761
|
membershipFail("named_seat_recipient_verification_required");
|
|
4709
4762
|
}
|
|
4710
4763
|
} else if (seat.recipientUserId !== input.recipientUserId || seat.recipientApplicationId !== input.recipientApplicationId) {
|
|
@@ -4817,7 +4870,7 @@ async function requestNamedSeatRefund(db, input, now) {
|
|
|
4817
4870
|
const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
|
|
4818
4871
|
requireMembershipScope(member, input);
|
|
4819
4872
|
if (seat.status === "refunded" || seat.status === "refund_pending") return;
|
|
4820
|
-
if (!seat.purchasedAt || !seat.providerPaymentIntentId || input.reason === "declined" && seat.journeyApprovedAt !== void 0 || input.reason === "
|
|
4873
|
+
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");
|
|
4821
4874
|
const entitlement = seat.entitlementId ? await membershipLoad(db, "networkEntitlement", String(seat.entitlementId)) : void 0;
|
|
4822
4875
|
await db.transact([
|
|
4823
4876
|
membershipUpdate("networkSeat", input.seatId, { status: "refund_pending", refundReason: input.reason, revision: Number(seat.revision) + 1, updatedAt: now }),
|
|
@@ -4889,6 +4942,19 @@ async function confirmNamedSeatProviderRefund(db, input, now) {
|
|
|
4889
4942
|
await requestNamedSeatRefund(db, { ...input, reason: "provider_refund" }, now);
|
|
4890
4943
|
}
|
|
4891
4944
|
|
|
4945
|
+
// src/membership-seat-operator-refund.ts
|
|
4946
|
+
async function refundNamedSeatByOperator(db, input, stripeSecretKey, now) {
|
|
4947
|
+
const seat = await membershipLoad(db, "networkSeat", input.seatId);
|
|
4948
|
+
requireMembershipScope(seat, input);
|
|
4949
|
+
await requestNamedSeatRefund(db, { ...input, reason: "operator" }, now);
|
|
4950
|
+
await completeNamedSeatRefund(db, input.seatId, stripeSecretKey, now);
|
|
4951
|
+
const refunded = await membershipLoad(db, "networkSeat", input.seatId);
|
|
4952
|
+
if (refunded?.status !== "refunded" || !Number.isSafeInteger(refunded.refundedCents) || Number(refunded.refundedCents) <= 0) {
|
|
4953
|
+
throw new Error("named seat refund unconfirmed");
|
|
4954
|
+
}
|
|
4955
|
+
return { seatId: String(seat.id), status: "refunded", refundedCents: Number(refunded.refundedCents), subscriptionCanceled: false };
|
|
4956
|
+
}
|
|
4957
|
+
|
|
4892
4958
|
// src/membership-seat-offer.ts
|
|
4893
4959
|
async function namedSeatOfferDigest(offer) {
|
|
4894
4960
|
const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify([
|
|
@@ -4901,7 +4967,7 @@ async function namedSeatOfferDigest(offer) {
|
|
|
4901
4967
|
"named-only",
|
|
4902
4968
|
"full-upfront",
|
|
4903
4969
|
"auto-renew-coterm",
|
|
4904
|
-
"
|
|
4970
|
+
"no-automatic-refund"
|
|
4905
4971
|
])));
|
|
4906
4972
|
return [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
4907
4973
|
}
|
|
@@ -4913,15 +4979,16 @@ async function readNamedSeatOffer(db, input, now) {
|
|
|
4913
4979
|
const offering = membershipOfferingVersion(member.offeringSnapshot);
|
|
4914
4980
|
const seat = member.namedSeatId ? await membershipLoad(db, "networkSeat", String(member.namedSeatId)) : void 0;
|
|
4915
4981
|
if (seat) requireMembershipScope(seat, input);
|
|
4982
|
+
const purchaseEndsAt = seatPurchaseDeadline(offering, member);
|
|
4916
4983
|
const offer = {
|
|
4917
4984
|
membershipId: id10,
|
|
4918
4985
|
amountCents: offering.seatPriceCents,
|
|
4919
4986
|
currency: offering.currency,
|
|
4920
4987
|
interval: offering.interval,
|
|
4921
4988
|
termEndsAt: Number(member.termEndsAt),
|
|
4922
|
-
purchaseEndsAt
|
|
4989
|
+
purchaseEndsAt,
|
|
4923
4990
|
autoRenew: member.autoRenew === true,
|
|
4924
|
-
eligible: ["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) && member.refundedCents === 0 && now >= Number(member.initialTermStartedAt) && now < Number(member.termEndsAt) && now <=
|
|
4991
|
+
eligible: ["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) && member.refundedCents === 0 && now >= Number(member.initialTermStartedAt) && now < Number(member.termEndsAt) && now <= purchaseEndsAt && (!seat || ["payment_pending", "payment_processing"].includes(String(seat.status))),
|
|
4925
4992
|
...seat ? { seat: {
|
|
4926
4993
|
seatId: String(seat.id),
|
|
4927
4994
|
status: String(seat.status),
|
|
@@ -4929,7 +4996,7 @@ async function readNamedSeatOffer(db, input, now) {
|
|
|
4929
4996
|
recipientEmail: String(seat.recipientEmailNormalized)
|
|
4930
4997
|
} } : {}
|
|
4931
4998
|
};
|
|
4932
|
-
return { ...offer, digest: await namedSeatOfferDigest(offer) };
|
|
4999
|
+
return { ...offer, digest: await namedSeatOfferDigest(offer), purchaseWindowed: offering.seatPurchaseWindowCalendarMonths !== void 0 };
|
|
4933
5000
|
}
|
|
4934
5001
|
|
|
4935
5002
|
// src/membership-seat-invitation.ts
|
|
@@ -4941,7 +5008,7 @@ async function readNamedSeatInvitation(db, input, now) {
|
|
|
4941
5008
|
await loadMembershipAuthorityState(db, target);
|
|
4942
5009
|
const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
|
|
4943
5010
|
requireMembershipScope(member, target);
|
|
4944
|
-
if (!["pending_acceptance", "pending_approval", "pending_primary", "active"].includes(String(seat.status)) || !["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) || now >= Number(member.termEndsAt)
|
|
5011
|
+
if (!["pending_acceptance", "pending_approval", "pending_primary", "active"].includes(String(seat.status)) || !["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) || now >= Number(member.termEndsAt)) membershipFail("named_seat_invitation_unavailable");
|
|
4945
5012
|
return {
|
|
4946
5013
|
seatId: input.seatId,
|
|
4947
5014
|
primaryApplicationId: target.applicationId,
|
|
@@ -4971,7 +5038,7 @@ async function readMembershipChapterEffect(db, input, now) {
|
|
|
4971
5038
|
requireMembershipScope(member, input);
|
|
4972
5039
|
if (!effect || effect.kind !== input.kind || effect.seatId !== seat.id || effect.membershipId !== member.id || ["chapterId", "environment", "runtime"].some((key3) => effect[key3] !== seat[key3]) || !["invite_named_recipient", "grant_named_recipient_access", "revoke_named_recipient_access"].includes(input.kind)) throw new Error("invalid chapter effect");
|
|
4973
5040
|
let applicable;
|
|
4974
|
-
if (input.kind === "invite_named_recipient") applicable = seat.status === "pending_acceptance" && seat.acceptedAt === void 0 && now < Number(
|
|
5041
|
+
if (input.kind === "invite_named_recipient") applicable = seat.status === "pending_acceptance" && seat.acceptedAt === void 0 && now < Number(member.termEndsAt) && ["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status));
|
|
4975
5042
|
else if (input.kind === "revoke_named_recipient_access") applicable = ["refund_pending", "refunded"].includes(String(seat.status));
|
|
4976
5043
|
else applicable = (await readNamedSeatAccess(db, {
|
|
4977
5044
|
...input,
|
|
@@ -5000,6 +5067,7 @@ var MEMBERSHIP_SEAT_ACTION_FIELDS = {
|
|
|
5000
5067
|
"seat-reservations": ["recipientName", "recipientEmail", "purchaserEmail", "purchaserUserId"],
|
|
5001
5068
|
"seat-payments": ["seatId", "eventId"],
|
|
5002
5069
|
"seat-refunds": ["seatId", "eventId"],
|
|
5070
|
+
"seat-operator-refund": ["seatId"],
|
|
5003
5071
|
"seat-accept": recipient,
|
|
5004
5072
|
"seat-approve": recipient,
|
|
5005
5073
|
"seat-decline": recipient,
|
|
@@ -5068,6 +5136,7 @@ async function handleMembershipSeatAction(action, db, peer, input, now, stripeSe
|
|
|
5068
5136
|
if (refunded?.status !== "refunded" || refunded.refundedCents !== seat.amountCents) throw new Error("named seat refund unconfirmed");
|
|
5069
5137
|
return { seatId: seat.id, status: "refunded", refundedCents: refunded.refundedCents, subscriptionCanceled: false };
|
|
5070
5138
|
}
|
|
5139
|
+
if (action === "seat-operator-refund") return refundNamedSeatByOperator(db, { ...target, seatId: String(input.seatId) }, secret, now);
|
|
5071
5140
|
if (action === "seat-checkouts") return prepareNamedMembershipSeatPayment(db, {
|
|
5072
5141
|
...target,
|
|
5073
5142
|
priceId: peer.seatPriceId,
|
|
@@ -5156,7 +5225,7 @@ function createMembershipAuthorityHandler(options) {
|
|
|
5156
5225
|
if (req.method !== "POST") return respond({ error: "method_not_allowed" }, 405);
|
|
5157
5226
|
const sender = req.headers.get(FEDERATION_HEADERS.sender);
|
|
5158
5227
|
const action = url.pathname.split("/").at(-1);
|
|
5159
|
-
if (!["offers", "checkouts", "payments", "approvals", "refunds", "renewals", "access", "restarts", "cancel-renewal", ...Object.keys(MEMBERSHIP_SEAT_ACTION_FIELDS)].includes(action ?? "")) return respond({ error: "unauthorized" }, 401);
|
|
5228
|
+
if (!["offers", "offer-preview", "checkouts", "payments", "approvals", "refunds", "renewals", "access", "restarts", "cancel-renewal", ...Object.keys(MEMBERSHIP_SEAT_ACTION_FIELDS)].includes(action ?? "")) return respond({ error: "unauthorized" }, 401);
|
|
5160
5229
|
const peer = peers.find((candidate) => candidate.appId === sender && url.pathname === `${MEMBERSHIP_AUTHORITY_PATH}/${candidate.runtime}/${action}` && !url.search);
|
|
5161
5230
|
if (!peer) return respond({ error: "unauthorized" }, 401);
|
|
5162
5231
|
let body;
|
|
@@ -5178,13 +5247,23 @@ function createMembershipAuthorityHandler(options) {
|
|
|
5178
5247
|
let input;
|
|
5179
5248
|
try {
|
|
5180
5249
|
input = JSON.parse(verified.body);
|
|
5181
|
-
const fields = action === "seat-invitation" ? MEMBERSHIP_SEAT_ACTION_FIELDS[action] : ["applicationId", "skuId", ...action === "offers" ? ["requestId"] : action === "checkouts" ? ["offerId", "offerDigest", "quoteDigest", "priceId"] : action === "payments" ? ["eventId", "providerSubscriptionId", "providerCustomerId"] : action === "renewals" ? ["eventId"] : action === "restarts" ? ["previousSubscriptionId"] : MEMBERSHIP_SEAT_ACTION_FIELDS[action] ?? []];
|
|
5250
|
+
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] ?? []];
|
|
5182
5251
|
if (!input || typeof input !== "object" || Array.isArray(input) || Object.keys(input).some((key3) => !fields.includes(key3)) || !fields.every((key3) => typeof input[key3] === "string" && (["recipientName", "recipientEmail", "purchaserEmail", "verifiedRecipientEmail"].includes(key3) ? input[key3].length > 0 && input[key3].length <= 254 && !/[\u0000-\u001f\u007f]/.test(input[key3]) : SAFE.test(input[key3])))) {
|
|
5183
5252
|
throw new TypeError("invalid request");
|
|
5184
5253
|
}
|
|
5185
5254
|
} catch {
|
|
5186
5255
|
return respond({ error: "invalid_offer_request" }, 400);
|
|
5187
5256
|
}
|
|
5257
|
+
if (action === "offer-preview") {
|
|
5258
|
+
return respond({ ok: true, preview: await previewMembershipCommercialOffer(db, {
|
|
5259
|
+
appId: peer.appId,
|
|
5260
|
+
chapterId: peer.chapterId,
|
|
5261
|
+
environment: peer.environment,
|
|
5262
|
+
runtime: peer.runtime,
|
|
5263
|
+
skuId: String(input.skuId),
|
|
5264
|
+
now: options.now?.() ?? Date.now()
|
|
5265
|
+
}) });
|
|
5266
|
+
}
|
|
5188
5267
|
if (Object.hasOwn(MEMBERSHIP_SEAT_ACTION_FIELDS, action)) {
|
|
5189
5268
|
return respond({ ok: true, seat: await handleMembershipSeatAction(
|
|
5190
5269
|
action,
|
|
@@ -5335,6 +5414,12 @@ async function requestMembershipCommercialOffer(options) {
|
|
|
5335
5414
|
lifetimeMs: 6e4
|
|
5336
5415
|
});
|
|
5337
5416
|
}
|
|
5417
|
+
async function requestMembershipOfferPreview(options) {
|
|
5418
|
+
const result = await requestAuthority(options, "offer-preview", { skuId: options.skuId });
|
|
5419
|
+
const preview = result.preview;
|
|
5420
|
+
if (!preview || preview.skuId !== options.skuId || typeof preview.founding !== "boolean" || !Number.isSafeInteger(preview.basePriceCents) || !Number.isSafeInteger(preview.dueTodayCents) || preview.dueTodayCents < 0 || preview.dueTodayCents > preview.basePriceCents || !Number.isSafeInteger(preview.renewalCents) || !/^[a-z]{3}$/.test(preview.currency) || !["month", "year"].includes(preview.interval) || preview.cohortLabel !== void 0 && typeof preview.cohortLabel !== "string" || preview.discountDuration !== void 0 && !["first_term", "while_active"].includes(preview.discountDuration)) throw new Error("invalid offer preview");
|
|
5421
|
+
return preview;
|
|
5422
|
+
}
|
|
5338
5423
|
async function requestMembershipCheckoutBinding(options) {
|
|
5339
5424
|
const result = await requestAuthority(options, "checkouts", {
|
|
5340
5425
|
applicationId: options.applicationId,
|
|
@@ -5373,6 +5458,12 @@ async function requestNamedSeatDecline(options) {
|
|
|
5373
5458
|
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");
|
|
5374
5459
|
return seat;
|
|
5375
5460
|
}
|
|
5461
|
+
async function requestNamedSeatOperatorRefund(options) {
|
|
5462
|
+
const result = await requestAuthority(options, "seat-operator-refund", { applicationId: options.applicationId, skuId: options.skuId, seatId: options.seatId });
|
|
5463
|
+
const seat = result.seat;
|
|
5464
|
+
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");
|
|
5465
|
+
return seat;
|
|
5466
|
+
}
|
|
5376
5467
|
async function requestNamedSeatReservation(options) {
|
|
5377
5468
|
const result = await requestAuthority(options, "seat-reservations", {
|
|
5378
5469
|
applicationId: options.applicationId,
|
|
@@ -5390,7 +5481,7 @@ async function requestNamedSeatReservation(options) {
|
|
|
5390
5481
|
async function requestNamedSeatOffer(options) {
|
|
5391
5482
|
const result = await requestAuthority(options, "seat-offer", { applicationId: options.applicationId, skuId: options.skuId });
|
|
5392
5483
|
const offer = result.seat;
|
|
5393
|
-
if (!offer || offer.membershipId !== await networkMembershipId(options) || !Number.isSafeInteger(offer.amountCents) || offer.amountCents <= 0 || !/^[a-z]{3}$/.test(offer.currency) || !["month", "year"].includes(offer.interval) || typeof offer.eligible !== "boolean" || typeof offer.autoRenew !== "boolean" || !Number.isSafeInteger(offer.termEndsAt) || !Number.isSafeInteger(offer.purchaseEndsAt) || offer.digest !== await namedSeatOfferDigest(offer)) throw new Error("invalid named seat offer");
|
|
5484
|
+
if (!offer || offer.membershipId !== await networkMembershipId(options) || !Number.isSafeInteger(offer.amountCents) || offer.amountCents <= 0 || !/^[a-z]{3}$/.test(offer.currency) || !["month", "year"].includes(offer.interval) || typeof offer.eligible !== "boolean" || typeof offer.autoRenew !== "boolean" || !Number.isSafeInteger(offer.termEndsAt) || !Number.isSafeInteger(offer.purchaseEndsAt) || offer.purchaseWindowed !== void 0 && typeof offer.purchaseWindowed !== "boolean" || offer.digest !== await namedSeatOfferDigest(offer)) throw new Error("invalid named seat offer");
|
|
5394
5485
|
return offer;
|
|
5395
5486
|
}
|
|
5396
5487
|
async function requestNamedSeatInvitation(options) {
|
|
@@ -5617,7 +5708,7 @@ async function drainMembershipEffects(db, options, now) {
|
|
|
5617
5708
|
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100 || !options.stripeSecretKey) throw new TypeError("invalid membership recovery configuration");
|
|
5618
5709
|
await loadMembershipAuthorityState(db, scope2);
|
|
5619
5710
|
const matches2 = (row2) => Object.entries(scope2).every(([key3, value]) => row2[key3] === value);
|
|
5620
|
-
let
|
|
5711
|
+
let pending = 0, complete = 0, more = false;
|
|
5621
5712
|
for (const status2 of ["payment_pending", "payment_processing", "payment_canceling"]) {
|
|
5622
5713
|
const seats = (await db.query({ networkSeat: { $: { where: { ...scope2, status: status2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkSeat ?? [];
|
|
5623
5714
|
more ||= seats.length > limit;
|
|
@@ -5635,26 +5726,6 @@ async function drainMembershipEffects(db, options, now) {
|
|
|
5635
5726
|
}
|
|
5636
5727
|
}
|
|
5637
5728
|
}
|
|
5638
|
-
for (const status2 of ["renewal_pending", "pending_acceptance"]) {
|
|
5639
|
-
const seats = (await db.query({ networkSeat: { $: { where: { ...scope2, status: status2 }, order: { acceptanceEndsAt: "asc" }, limit: limit + 1 } } })).networkSeat ?? [];
|
|
5640
|
-
more ||= seats.length > limit;
|
|
5641
|
-
for (const seat of seats.slice(0, limit)) {
|
|
5642
|
-
if (!matches2(seat)) throw new Error("membership recovery scope mismatch");
|
|
5643
|
-
if (seat.acceptedAt !== void 0 || now < Number(seat.acceptanceEndsAt)) continue;
|
|
5644
|
-
try {
|
|
5645
|
-
await requestNamedSeatRefund(db, {
|
|
5646
|
-
...scope2,
|
|
5647
|
-
applicationId: String(seat.applicationId),
|
|
5648
|
-
skuId: String(seat.skuId),
|
|
5649
|
-
seatId: String(seat.id),
|
|
5650
|
-
reason: "expired"
|
|
5651
|
-
}, now);
|
|
5652
|
-
expired++;
|
|
5653
|
-
} catch {
|
|
5654
|
-
pending++;
|
|
5655
|
-
}
|
|
5656
|
-
}
|
|
5657
|
-
}
|
|
5658
5729
|
const effects = (await db.query({ membershipEffect: { $: { where: {
|
|
5659
5730
|
chapterId: scope2.chapterId,
|
|
5660
5731
|
environment: scope2.environment,
|
|
@@ -5716,7 +5787,7 @@ async function drainMembershipEffects(db, options, now) {
|
|
|
5716
5787
|
pending++;
|
|
5717
5788
|
}
|
|
5718
5789
|
}
|
|
5719
|
-
return { complete, pending,
|
|
5790
|
+
return { complete, pending, more };
|
|
5720
5791
|
}
|
|
5721
5792
|
|
|
5722
5793
|
// src/schema-membership-offers.ts
|
|
@@ -9500,6 +9571,7 @@ export {
|
|
|
9500
9571
|
planMemberSystemEventReplay,
|
|
9501
9572
|
planMemberSystemMigration,
|
|
9502
9573
|
planPrivateRoleMigration,
|
|
9574
|
+
previewMembershipCommercialOffer,
|
|
9503
9575
|
projectApplicant,
|
|
9504
9576
|
projectBuilderLibraryDelivery,
|
|
9505
9577
|
projectMemberContent,
|
|
@@ -9532,6 +9604,7 @@ export {
|
|
|
9532
9604
|
requestMembershipChapterEffect,
|
|
9533
9605
|
requestMembershipCheckoutBinding,
|
|
9534
9606
|
requestMembershipCommercialOffer,
|
|
9607
|
+
requestMembershipOfferPreview,
|
|
9535
9608
|
requestMembershipPayment,
|
|
9536
9609
|
requestMembershipRefund,
|
|
9537
9610
|
requestMembershipRenewal,
|
|
@@ -9543,6 +9616,7 @@ export {
|
|
|
9543
9616
|
requestNamedSeatDecline,
|
|
9544
9617
|
requestNamedSeatInvitation,
|
|
9545
9618
|
requestNamedSeatOffer,
|
|
9619
|
+
requestNamedSeatOperatorRefund,
|
|
9546
9620
|
requestNamedSeatPayment,
|
|
9547
9621
|
requestNamedSeatRefund2 as requestNamedSeatRefund,
|
|
9548
9622
|
requestNamedSeatReservation,
|
|
@@ -9560,6 +9634,7 @@ export {
|
|
|
9560
9634
|
resolveSignupControl,
|
|
9561
9635
|
resolveTiers,
|
|
9562
9636
|
roleFromClaim,
|
|
9637
|
+
seatPurchaseDeadline,
|
|
9563
9638
|
sendTemplated,
|
|
9564
9639
|
sharedPersonInput,
|
|
9565
9640
|
sharedRecordFromCrm,
|