@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.cjs
CHANGED
|
@@ -176,6 +176,7 @@ __export(src_exports, {
|
|
|
176
176
|
planMemberSystemEventReplay: () => planMemberSystemEventReplay,
|
|
177
177
|
planMemberSystemMigration: () => planMemberSystemMigration,
|
|
178
178
|
planPrivateRoleMigration: () => planPrivateRoleMigration,
|
|
179
|
+
previewMembershipCommercialOffer: () => previewMembershipCommercialOffer,
|
|
179
180
|
projectApplicant: () => projectApplicant,
|
|
180
181
|
projectBuilderLibraryDelivery: () => projectBuilderLibraryDelivery,
|
|
181
182
|
projectMemberContent: () => projectMemberContent,
|
|
@@ -208,6 +209,7 @@ __export(src_exports, {
|
|
|
208
209
|
requestMembershipChapterEffect: () => requestMembershipChapterEffect,
|
|
209
210
|
requestMembershipCheckoutBinding: () => requestMembershipCheckoutBinding,
|
|
210
211
|
requestMembershipCommercialOffer: () => requestMembershipCommercialOffer,
|
|
212
|
+
requestMembershipOfferPreview: () => requestMembershipOfferPreview,
|
|
211
213
|
requestMembershipPayment: () => requestMembershipPayment,
|
|
212
214
|
requestMembershipRefund: () => requestMembershipRefund,
|
|
213
215
|
requestMembershipRenewal: () => requestMembershipRenewal,
|
|
@@ -219,6 +221,7 @@ __export(src_exports, {
|
|
|
219
221
|
requestNamedSeatDecline: () => requestNamedSeatDecline,
|
|
220
222
|
requestNamedSeatInvitation: () => requestNamedSeatInvitation,
|
|
221
223
|
requestNamedSeatOffer: () => requestNamedSeatOffer,
|
|
224
|
+
requestNamedSeatOperatorRefund: () => requestNamedSeatOperatorRefund,
|
|
222
225
|
requestNamedSeatPayment: () => requestNamedSeatPayment,
|
|
223
226
|
requestNamedSeatRefund: () => requestNamedSeatRefund2,
|
|
224
227
|
requestNamedSeatReservation: () => requestNamedSeatReservation,
|
|
@@ -236,6 +239,7 @@ __export(src_exports, {
|
|
|
236
239
|
resolveSignupControl: () => resolveSignupControl,
|
|
237
240
|
resolveTiers: () => resolveTiers,
|
|
238
241
|
roleFromClaim: () => roleFromClaim,
|
|
242
|
+
seatPurchaseDeadline: () => seatPurchaseDeadline,
|
|
239
243
|
sendTemplated: () => sendTemplated,
|
|
240
244
|
sharedPersonInput: () => sharedPersonInput,
|
|
241
245
|
sharedRecordFromCrm: () => sharedRecordFromCrm,
|
|
@@ -1713,6 +1717,7 @@ var DEFAULT_ADMIN_COPY = {
|
|
|
1713
1717
|
},
|
|
1714
1718
|
records: {
|
|
1715
1719
|
workflowMissing: "This record is not linked to an application workflow.",
|
|
1720
|
+
invitation: "Invitation",
|
|
1716
1721
|
comms: "Comms",
|
|
1717
1722
|
commsHistory: "Comms history",
|
|
1718
1723
|
scheduling: "Scheduling",
|
|
@@ -1797,7 +1802,9 @@ var DEFAULT_JOIN_COPY = {
|
|
|
1797
1802
|
processing: "Processing\u2026",
|
|
1798
1803
|
payAndContinue: "Pay and continue",
|
|
1799
1804
|
incomplete: "The payment did not complete. Please try again.",
|
|
1800
|
-
consent: "I have read and agree to the pricing and refund policy above."
|
|
1805
|
+
consent: "I have read and agree to the pricing and refund policy above.",
|
|
1806
|
+
foundingMember: "Founding member rate",
|
|
1807
|
+
foundingDiscount: "Founding discount"
|
|
1801
1808
|
},
|
|
1802
1809
|
done: {
|
|
1803
1810
|
label: "You're booked",
|
|
@@ -3354,7 +3361,7 @@ function findTier(tiers2, tierId) {
|
|
|
3354
3361
|
if (!tierId) return tiers2.length === 1 ? tiers2[0] : null;
|
|
3355
3362
|
return tiers2.find((tier) => tier.id === tierId) ?? null;
|
|
3356
3363
|
}
|
|
3357
|
-
function joinConfigTiers(tiers2, defaults = {}) {
|
|
3364
|
+
function joinConfigTiers(tiers2, defaults = {}, founding = /* @__PURE__ */ new Map()) {
|
|
3358
3365
|
return tiers2.map((tier) => ({
|
|
3359
3366
|
id: tier.id,
|
|
3360
3367
|
name: tier.name,
|
|
@@ -3362,7 +3369,8 @@ function joinConfigTiers(tiers2, defaults = {}) {
|
|
|
3362
3369
|
currency: tier.currency ?? defaults.currency ?? "usd",
|
|
3363
3370
|
interval: tier.interval ?? defaults.interval ?? "year",
|
|
3364
3371
|
blurb: tier.blurb ?? "",
|
|
3365
|
-
free: tierIsFree(tier)
|
|
3372
|
+
free: tierIsFree(tier),
|
|
3373
|
+
...founding.has(tier.id) ? { founding: founding.get(tier.id) } : {}
|
|
3366
3374
|
}));
|
|
3367
3375
|
}
|
|
3368
3376
|
|
|
@@ -3465,6 +3473,11 @@ function createChapterIntegration(chapter, options = {}) {
|
|
|
3465
3473
|
}
|
|
3466
3474
|
|
|
3467
3475
|
// src/membership-policy.ts
|
|
3476
|
+
function seatPurchaseDeadline(offering, member) {
|
|
3477
|
+
if (offering.seatPurchaseWindowCalendarMonths === void 0) return Number(member.termEndsAt);
|
|
3478
|
+
if (Number.isSafeInteger(member.seatPurchaseEndsAt)) return Number(member.seatPurchaseEndsAt);
|
|
3479
|
+
return addMembershipCalendarMonths(Number(member.initialTermStartedAt ?? member.termStartedAt), offering.seatPurchaseWindowCalendarMonths);
|
|
3480
|
+
}
|
|
3468
3481
|
var fail2 = (field2) => {
|
|
3469
3482
|
throw new TypeError(`invalid membership policy: ${field2}`);
|
|
3470
3483
|
};
|
|
@@ -3521,7 +3534,7 @@ function membershipOfferingVersion(value) {
|
|
|
3521
3534
|
autoRenew: true,
|
|
3522
3535
|
paymentGraceDays: integer2(input.paymentGraceDays, 0, 60),
|
|
3523
3536
|
seatPriceCents: integer2(input.seatPriceCents, 1, Number.MAX_SAFE_INTEGER),
|
|
3524
|
-
seatPurchaseWindowCalendarMonths: integer2(input.seatPurchaseWindowCalendarMonths, 1, 120),
|
|
3537
|
+
...input.seatPurchaseWindowCalendarMonths === void 0 || input.seatPurchaseWindowCalendarMonths === null ? {} : { seatPurchaseWindowCalendarMonths: integer2(input.seatPurchaseWindowCalendarMonths, 1, 120) },
|
|
3525
3538
|
requiredDisclosures: ids(input.requiredDisclosures)
|
|
3526
3539
|
};
|
|
3527
3540
|
}
|
|
@@ -3648,24 +3661,63 @@ async function verifyIssuedMembershipOffer(value, expected) {
|
|
|
3648
3661
|
if (!value || typeof value !== "object" || Array.isArray(value)) return reject("invalid_offer_snapshot");
|
|
3649
3662
|
return restore({ ...value, approvedFoundingCount: 0 }, expected);
|
|
3650
3663
|
}
|
|
3651
|
-
async function
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3664
|
+
async function previewMembershipCommercialOffer(db, input) {
|
|
3665
|
+
for (const key3 of ["appId", "chapterId", "environment", "runtime", "skuId"]) {
|
|
3666
|
+
if (typeof input[key3] !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_.:-]{1,159}$/.test(input[key3])) reject("invalid_offer_identity");
|
|
3667
|
+
}
|
|
3668
|
+
if (!Number.isSafeInteger(input.now) || input.now < 0) reject("invalid_offer_window");
|
|
3669
|
+
const state = await authoritativeState(db, input);
|
|
3670
|
+
const { offering, policy } = await currentPolicy(db, input, false, state);
|
|
3671
|
+
const approvedFoundingCount = policy ? state.cohortCounts[policy.cohortLabel] ?? 0 : 0;
|
|
3672
|
+
if (!Number.isSafeInteger(approvedFoundingCount) || Number(approvedFoundingCount) < 0) reject("invalid_cohort_count");
|
|
3673
|
+
const contract = createMembershipCommercialOffer({
|
|
3674
|
+
offering,
|
|
3675
|
+
discountPolicy: policy,
|
|
3676
|
+
approvedFoundingCount: Number(approvedFoundingCount),
|
|
3677
|
+
now: input.now,
|
|
3678
|
+
couponId: await couponId(input, policy?.id)
|
|
3679
|
+
});
|
|
3680
|
+
const discount = contract.billingTerms.discount;
|
|
3681
|
+
return {
|
|
3682
|
+
skuId: input.skuId,
|
|
3683
|
+
offeringVersionId: offering.id,
|
|
3684
|
+
basePriceCents: offering.basePriceCents,
|
|
3685
|
+
currency: offering.currency,
|
|
3686
|
+
interval: offering.interval,
|
|
3687
|
+
founding: contract.founding,
|
|
3688
|
+
...contract.founding && contract.discountPolicy ? { cohortLabel: contract.discountPolicy.cohortLabel } : {},
|
|
3689
|
+
dueTodayCents: contract.dueTodayCents,
|
|
3690
|
+
renewalCents: contract.renewalCents,
|
|
3691
|
+
...discount ? { discountDuration: discount.duration === "once" ? "first_term" : "while_active" } : {},
|
|
3692
|
+
...contract.initialTermEndsAt !== void 0 ? { initialTermEndsAt: contract.initialTermEndsAt } : {}
|
|
3693
|
+
};
|
|
3694
|
+
}
|
|
3695
|
+
async function authoritativeState(db, input) {
|
|
3696
|
+
const state = await load(db, "membershipAuthorityState", `${input.chapterId}:${input.environment}`);
|
|
3655
3697
|
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");
|
|
3656
|
-
|
|
3657
|
-
|
|
3698
|
+
return state;
|
|
3699
|
+
}
|
|
3700
|
+
async function currentPolicy(db, input, restartAware, state) {
|
|
3658
3701
|
const rows3 = await db.query({
|
|
3659
3702
|
membershipOfferingVersion: { $: { where: { chapterId: input.chapterId }, limit: 200 } },
|
|
3660
3703
|
discountPolicyVersion: { $: { where: { chapterId: input.chapterId }, limit: 200 } }
|
|
3661
3704
|
});
|
|
3662
3705
|
if (!Array.isArray(rows3.membershipOfferingVersion) || !Array.isArray(rows3.discountPolicyVersion) || rows3.membershipOfferingVersion.length >= 200 || rows3.discountPolicyVersion.length >= 200) return reject("policy_history_incomplete");
|
|
3663
|
-
const offerings = rows3.membershipOfferingVersion.map((
|
|
3664
|
-
const policies = rows3.discountPolicyVersion.map((
|
|
3706
|
+
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));
|
|
3707
|
+
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);
|
|
3665
3708
|
const offering = offerings.sort((a, b) => b.version - a.version)[0];
|
|
3666
|
-
const restart = await load(db, "membershipRestart", (await membershipOfferIdForRequest({ ...input, requestId: "checkout_binding" })).replace(/^offer_/, "membership_"));
|
|
3709
|
+
const restart = restartAware && input.applicationId ? await load(db, "membershipRestart", (await membershipOfferIdForRequest({ ...input, applicationId: input.applicationId, requestId: "checkout_binding" })).replace(/^offer_/, "membership_")) : void 0;
|
|
3667
3710
|
const policy = restart ? void 0 : policies.sort((a, b) => b.version - a.version)[0];
|
|
3668
3711
|
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");
|
|
3712
|
+
return { offering, policy };
|
|
3713
|
+
}
|
|
3714
|
+
async function issueMembershipCommercialOffer(db, input) {
|
|
3715
|
+
validate(input);
|
|
3716
|
+
const stateId = `${input.chapterId}:${input.environment}`;
|
|
3717
|
+
const state = await authoritativeState(db, input);
|
|
3718
|
+
const existing = await load(db, "membershipCheckoutOffer", input.offerId);
|
|
3719
|
+
if (existing) return restore(existing, input);
|
|
3720
|
+
const { offering, policy } = await currentPolicy(db, input, true, state);
|
|
3669
3721
|
const approvedFoundingCount = policy ? state.cohortCounts[policy.cohortLabel] ?? 0 : 0;
|
|
3670
3722
|
if (!Number.isSafeInteger(approvedFoundingCount) || Number(approvedFoundingCount) < 0) reject("invalid_cohort_count");
|
|
3671
3723
|
const contract = createMembershipCommercialOffer({
|
|
@@ -3876,7 +3928,7 @@ async function confirmMembershipPayment(db, input, now) {
|
|
|
3876
3928
|
initialTermEndsAt: endsAt,
|
|
3877
3929
|
termStartedAt: paidAt,
|
|
3878
3930
|
termEndsAt: endsAt,
|
|
3879
|
-
seatPurchaseEndsAt:
|
|
3931
|
+
seatPurchaseEndsAt: seatPurchaseDeadline(offering, { initialTermStartedAt: paidAt, termEndsAt: endsAt }),
|
|
3880
3932
|
autoRenew: true,
|
|
3881
3933
|
refundedCents: 0,
|
|
3882
3934
|
revision: 1,
|
|
@@ -4304,7 +4356,11 @@ async function reconcileMembershipRenewal(db, input, now) {
|
|
|
4304
4356
|
membershipUpdate("networkMembership", id10, {
|
|
4305
4357
|
status: status2,
|
|
4306
4358
|
graceEndsAt,
|
|
4307
|
-
...paid ? {
|
|
4359
|
+
...paid ? {
|
|
4360
|
+
termStartedAt: start,
|
|
4361
|
+
termEndsAt: end,
|
|
4362
|
+
...membershipOfferingVersion(member.offeringSnapshot).seatPurchaseWindowCalendarMonths === void 0 ? { seatPurchaseEndsAt: end } : {}
|
|
4363
|
+
} : {},
|
|
4308
4364
|
revision: Number(member.revision) + 1,
|
|
4309
4365
|
updatedAt: now
|
|
4310
4366
|
}),
|
|
@@ -4606,10 +4662,11 @@ async function reserveNamedMembershipSeat(db, input, now) {
|
|
|
4606
4662
|
const id10 = (await membershipOfferIdForRequest({ ...input, requestId: "named_seat" })).replace(/^offer_/, "seat_");
|
|
4607
4663
|
let seat = await membershipLoad(db, "networkSeat", id10);
|
|
4608
4664
|
if (!seat) {
|
|
4609
|
-
|
|
4665
|
+
const offering = membershipOfferingVersion(member.offeringSnapshot), claimId = await recipientKey(input, email);
|
|
4666
|
+
const purchaseEndsAt = seatPurchaseDeadline(offering, member);
|
|
4667
|
+
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) {
|
|
4610
4668
|
membershipFail("named_seat_purchase_unavailable");
|
|
4611
4669
|
}
|
|
4612
|
-
const offering = membershipOfferingVersion(member.offeringSnapshot), claimId = await recipientKey(input, email);
|
|
4613
4670
|
const attrs = {
|
|
4614
4671
|
id: id10,
|
|
4615
4672
|
...membershipScope(input),
|
|
@@ -4628,7 +4685,7 @@ async function reserveNamedMembershipSeat(db, input, now) {
|
|
|
4628
4685
|
status: "payment_pending",
|
|
4629
4686
|
preparedAt: now,
|
|
4630
4687
|
termEndsAt: member.termEndsAt,
|
|
4631
|
-
seatPurchaseEndsAt:
|
|
4688
|
+
seatPurchaseEndsAt: purchaseEndsAt,
|
|
4632
4689
|
offeringSnapshot: offering,
|
|
4633
4690
|
revision: 1,
|
|
4634
4691
|
createdAt: now,
|
|
@@ -4845,14 +4902,14 @@ async function confirmNamedMembershipSeatPayment(db, input, now) {
|
|
|
4845
4902
|
}
|
|
4846
4903
|
if (seat.purchasedAt === void 0) {
|
|
4847
4904
|
if (!["payment_processing", "payment_canceling"].includes(String(seat.status))) membershipFail("named_seat_payment_not_expected");
|
|
4848
|
-
const eligible = seat.status !== "payment_canceling" && ["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) && Number(member.refundedCents) === 0 && paidAt <=
|
|
4905
|
+
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);
|
|
4849
4906
|
const status2 = eligible ? "renewal_pending" : "refund_pending", effectId = `${seat.id}:${eligible ? "attach-renewal" : "refund"}`;
|
|
4850
4907
|
try {
|
|
4851
4908
|
await db.transact([
|
|
4852
4909
|
membershipUpdate("networkSeat", input.seatId, {
|
|
4853
4910
|
status: status2,
|
|
4854
4911
|
purchasedAt: paidAt,
|
|
4855
|
-
acceptanceEndsAt:
|
|
4912
|
+
acceptanceEndsAt: Number(member.termEndsAt),
|
|
4856
4913
|
termStartedAt: paidAt,
|
|
4857
4914
|
termEndsAt: member.termEndsAt,
|
|
4858
4915
|
paymentEventId: input.eventId,
|
|
@@ -4900,7 +4957,7 @@ async function completeNamedSeatRenewal(db, seatId, secretKey, now) {
|
|
|
4900
4957
|
if (seat.status !== "renewal_pending" || !seat.purchasedAt) membershipFail("named_seat_renewal_not_ready");
|
|
4901
4958
|
const member = await membershipLoad(db, "networkMembership", target.primaryMembershipId);
|
|
4902
4959
|
requireMembershipScope(member, target);
|
|
4903
|
-
if (now >= Number(
|
|
4960
|
+
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");
|
|
4904
4961
|
const itemId = await attachNamedSeatStripeRenewal(secretKey, target);
|
|
4905
4962
|
const entitlementId = `${seatId}:entitlement`, effectId = `${seatId}:attach-renewal`;
|
|
4906
4963
|
try {
|
|
@@ -4975,7 +5032,7 @@ async function recordRecipient(db, input, now, approve) {
|
|
|
4975
5032
|
requireMembershipScope(entitlement, input);
|
|
4976
5033
|
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");
|
|
4977
5034
|
if (seat.acceptedAt === void 0) {
|
|
4978
|
-
if (approve ||
|
|
5035
|
+
if (approve || normalizeChapterEmail(input.verifiedRecipientEmail) !== seat.recipientEmailNormalized) {
|
|
4979
5036
|
membershipFail("named_seat_recipient_verification_required");
|
|
4980
5037
|
}
|
|
4981
5038
|
} else if (seat.recipientUserId !== input.recipientUserId || seat.recipientApplicationId !== input.recipientApplicationId) {
|
|
@@ -5088,7 +5145,7 @@ async function requestNamedSeatRefund(db, input, now) {
|
|
|
5088
5145
|
const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
|
|
5089
5146
|
requireMembershipScope(member, input);
|
|
5090
5147
|
if (seat.status === "refunded" || seat.status === "refund_pending") return;
|
|
5091
|
-
if (!seat.purchasedAt || !seat.providerPaymentIntentId || input.reason === "declined" && seat.journeyApprovedAt !== void 0 || input.reason === "
|
|
5148
|
+
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");
|
|
5092
5149
|
const entitlement = seat.entitlementId ? await membershipLoad(db, "networkEntitlement", String(seat.entitlementId)) : void 0;
|
|
5093
5150
|
await db.transact([
|
|
5094
5151
|
membershipUpdate("networkSeat", input.seatId, { status: "refund_pending", refundReason: input.reason, revision: Number(seat.revision) + 1, updatedAt: now }),
|
|
@@ -5160,6 +5217,19 @@ async function confirmNamedSeatProviderRefund(db, input, now) {
|
|
|
5160
5217
|
await requestNamedSeatRefund(db, { ...input, reason: "provider_refund" }, now);
|
|
5161
5218
|
}
|
|
5162
5219
|
|
|
5220
|
+
// src/membership-seat-operator-refund.ts
|
|
5221
|
+
async function refundNamedSeatByOperator(db, input, stripeSecretKey, now) {
|
|
5222
|
+
const seat = await membershipLoad(db, "networkSeat", input.seatId);
|
|
5223
|
+
requireMembershipScope(seat, input);
|
|
5224
|
+
await requestNamedSeatRefund(db, { ...input, reason: "operator" }, now);
|
|
5225
|
+
await completeNamedSeatRefund(db, input.seatId, stripeSecretKey, now);
|
|
5226
|
+
const refunded = await membershipLoad(db, "networkSeat", input.seatId);
|
|
5227
|
+
if (refunded?.status !== "refunded" || !Number.isSafeInteger(refunded.refundedCents) || Number(refunded.refundedCents) <= 0) {
|
|
5228
|
+
throw new Error("named seat refund unconfirmed");
|
|
5229
|
+
}
|
|
5230
|
+
return { seatId: String(seat.id), status: "refunded", refundedCents: Number(refunded.refundedCents), subscriptionCanceled: false };
|
|
5231
|
+
}
|
|
5232
|
+
|
|
5163
5233
|
// src/membership-seat-offer.ts
|
|
5164
5234
|
async function namedSeatOfferDigest(offer) {
|
|
5165
5235
|
const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify([
|
|
@@ -5172,7 +5242,7 @@ async function namedSeatOfferDigest(offer) {
|
|
|
5172
5242
|
"named-only",
|
|
5173
5243
|
"full-upfront",
|
|
5174
5244
|
"auto-renew-coterm",
|
|
5175
|
-
"
|
|
5245
|
+
"no-automatic-refund"
|
|
5176
5246
|
])));
|
|
5177
5247
|
return [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
5178
5248
|
}
|
|
@@ -5184,15 +5254,16 @@ async function readNamedSeatOffer(db, input, now) {
|
|
|
5184
5254
|
const offering = membershipOfferingVersion(member.offeringSnapshot);
|
|
5185
5255
|
const seat = member.namedSeatId ? await membershipLoad(db, "networkSeat", String(member.namedSeatId)) : void 0;
|
|
5186
5256
|
if (seat) requireMembershipScope(seat, input);
|
|
5257
|
+
const purchaseEndsAt = seatPurchaseDeadline(offering, member);
|
|
5187
5258
|
const offer = {
|
|
5188
5259
|
membershipId: id10,
|
|
5189
5260
|
amountCents: offering.seatPriceCents,
|
|
5190
5261
|
currency: offering.currency,
|
|
5191
5262
|
interval: offering.interval,
|
|
5192
5263
|
termEndsAt: Number(member.termEndsAt),
|
|
5193
|
-
purchaseEndsAt
|
|
5264
|
+
purchaseEndsAt,
|
|
5194
5265
|
autoRenew: member.autoRenew === true,
|
|
5195
|
-
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 <=
|
|
5266
|
+
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))),
|
|
5196
5267
|
...seat ? { seat: {
|
|
5197
5268
|
seatId: String(seat.id),
|
|
5198
5269
|
status: String(seat.status),
|
|
@@ -5200,7 +5271,7 @@ async function readNamedSeatOffer(db, input, now) {
|
|
|
5200
5271
|
recipientEmail: String(seat.recipientEmailNormalized)
|
|
5201
5272
|
} } : {}
|
|
5202
5273
|
};
|
|
5203
|
-
return { ...offer, digest: await namedSeatOfferDigest(offer) };
|
|
5274
|
+
return { ...offer, digest: await namedSeatOfferDigest(offer), purchaseWindowed: offering.seatPurchaseWindowCalendarMonths !== void 0 };
|
|
5204
5275
|
}
|
|
5205
5276
|
|
|
5206
5277
|
// src/membership-seat-invitation.ts
|
|
@@ -5212,7 +5283,7 @@ async function readNamedSeatInvitation(db, input, now) {
|
|
|
5212
5283
|
await loadMembershipAuthorityState(db, target);
|
|
5213
5284
|
const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
|
|
5214
5285
|
requireMembershipScope(member, target);
|
|
5215
|
-
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)
|
|
5286
|
+
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");
|
|
5216
5287
|
return {
|
|
5217
5288
|
seatId: input.seatId,
|
|
5218
5289
|
primaryApplicationId: target.applicationId,
|
|
@@ -5242,7 +5313,7 @@ async function readMembershipChapterEffect(db, input, now) {
|
|
|
5242
5313
|
requireMembershipScope(member, input);
|
|
5243
5314
|
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");
|
|
5244
5315
|
let applicable;
|
|
5245
|
-
if (input.kind === "invite_named_recipient") applicable = seat.status === "pending_acceptance" && seat.acceptedAt === void 0 && now < Number(
|
|
5316
|
+
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));
|
|
5246
5317
|
else if (input.kind === "revoke_named_recipient_access") applicable = ["refund_pending", "refunded"].includes(String(seat.status));
|
|
5247
5318
|
else applicable = (await readNamedSeatAccess(db, {
|
|
5248
5319
|
...input,
|
|
@@ -5271,6 +5342,7 @@ var MEMBERSHIP_SEAT_ACTION_FIELDS = {
|
|
|
5271
5342
|
"seat-reservations": ["recipientName", "recipientEmail", "purchaserEmail", "purchaserUserId"],
|
|
5272
5343
|
"seat-payments": ["seatId", "eventId"],
|
|
5273
5344
|
"seat-refunds": ["seatId", "eventId"],
|
|
5345
|
+
"seat-operator-refund": ["seatId"],
|
|
5274
5346
|
"seat-accept": recipient,
|
|
5275
5347
|
"seat-approve": recipient,
|
|
5276
5348
|
"seat-decline": recipient,
|
|
@@ -5339,6 +5411,7 @@ async function handleMembershipSeatAction(action, db, peer, input, now, stripeSe
|
|
|
5339
5411
|
if (refunded?.status !== "refunded" || refunded.refundedCents !== seat.amountCents) throw new Error("named seat refund unconfirmed");
|
|
5340
5412
|
return { seatId: seat.id, status: "refunded", refundedCents: refunded.refundedCents, subscriptionCanceled: false };
|
|
5341
5413
|
}
|
|
5414
|
+
if (action === "seat-operator-refund") return refundNamedSeatByOperator(db, { ...target, seatId: String(input.seatId) }, secret, now);
|
|
5342
5415
|
if (action === "seat-checkouts") return prepareNamedMembershipSeatPayment(db, {
|
|
5343
5416
|
...target,
|
|
5344
5417
|
priceId: peer.seatPriceId,
|
|
@@ -5427,7 +5500,7 @@ function createMembershipAuthorityHandler(options) {
|
|
|
5427
5500
|
if (req.method !== "POST") return respond({ error: "method_not_allowed" }, 405);
|
|
5428
5501
|
const sender = req.headers.get(import_db.FEDERATION_HEADERS.sender);
|
|
5429
5502
|
const action = url.pathname.split("/").at(-1);
|
|
5430
|
-
if (!["offers", "checkouts", "payments", "approvals", "refunds", "renewals", "access", "restarts", "cancel-renewal", ...Object.keys(MEMBERSHIP_SEAT_ACTION_FIELDS)].includes(action ?? "")) return respond({ error: "unauthorized" }, 401);
|
|
5503
|
+
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);
|
|
5431
5504
|
const peer = peers.find((candidate) => candidate.appId === sender && url.pathname === `${MEMBERSHIP_AUTHORITY_PATH}/${candidate.runtime}/${action}` && !url.search);
|
|
5432
5505
|
if (!peer) return respond({ error: "unauthorized" }, 401);
|
|
5433
5506
|
let body;
|
|
@@ -5449,13 +5522,23 @@ function createMembershipAuthorityHandler(options) {
|
|
|
5449
5522
|
let input;
|
|
5450
5523
|
try {
|
|
5451
5524
|
input = JSON.parse(verified.body);
|
|
5452
|
-
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] ?? []];
|
|
5525
|
+
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] ?? []];
|
|
5453
5526
|
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])))) {
|
|
5454
5527
|
throw new TypeError("invalid request");
|
|
5455
5528
|
}
|
|
5456
5529
|
} catch {
|
|
5457
5530
|
return respond({ error: "invalid_offer_request" }, 400);
|
|
5458
5531
|
}
|
|
5532
|
+
if (action === "offer-preview") {
|
|
5533
|
+
return respond({ ok: true, preview: await previewMembershipCommercialOffer(db, {
|
|
5534
|
+
appId: peer.appId,
|
|
5535
|
+
chapterId: peer.chapterId,
|
|
5536
|
+
environment: peer.environment,
|
|
5537
|
+
runtime: peer.runtime,
|
|
5538
|
+
skuId: String(input.skuId),
|
|
5539
|
+
now: options.now?.() ?? Date.now()
|
|
5540
|
+
}) });
|
|
5541
|
+
}
|
|
5459
5542
|
if (Object.hasOwn(MEMBERSHIP_SEAT_ACTION_FIELDS, action)) {
|
|
5460
5543
|
return respond({ ok: true, seat: await handleMembershipSeatAction(
|
|
5461
5544
|
action,
|
|
@@ -5606,6 +5689,12 @@ async function requestMembershipCommercialOffer(options) {
|
|
|
5606
5689
|
lifetimeMs: 6e4
|
|
5607
5690
|
});
|
|
5608
5691
|
}
|
|
5692
|
+
async function requestMembershipOfferPreview(options) {
|
|
5693
|
+
const result = await requestAuthority(options, "offer-preview", { skuId: options.skuId });
|
|
5694
|
+
const preview = result.preview;
|
|
5695
|
+
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");
|
|
5696
|
+
return preview;
|
|
5697
|
+
}
|
|
5609
5698
|
async function requestMembershipCheckoutBinding(options) {
|
|
5610
5699
|
const result = await requestAuthority(options, "checkouts", {
|
|
5611
5700
|
applicationId: options.applicationId,
|
|
@@ -5644,6 +5733,12 @@ async function requestNamedSeatDecline(options) {
|
|
|
5644
5733
|
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");
|
|
5645
5734
|
return seat;
|
|
5646
5735
|
}
|
|
5736
|
+
async function requestNamedSeatOperatorRefund(options) {
|
|
5737
|
+
const result = await requestAuthority(options, "seat-operator-refund", { applicationId: options.applicationId, skuId: options.skuId, seatId: options.seatId });
|
|
5738
|
+
const seat = result.seat;
|
|
5739
|
+
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");
|
|
5740
|
+
return seat;
|
|
5741
|
+
}
|
|
5647
5742
|
async function requestNamedSeatReservation(options) {
|
|
5648
5743
|
const result = await requestAuthority(options, "seat-reservations", {
|
|
5649
5744
|
applicationId: options.applicationId,
|
|
@@ -5661,7 +5756,7 @@ async function requestNamedSeatReservation(options) {
|
|
|
5661
5756
|
async function requestNamedSeatOffer(options) {
|
|
5662
5757
|
const result = await requestAuthority(options, "seat-offer", { applicationId: options.applicationId, skuId: options.skuId });
|
|
5663
5758
|
const offer = result.seat;
|
|
5664
|
-
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");
|
|
5759
|
+
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");
|
|
5665
5760
|
return offer;
|
|
5666
5761
|
}
|
|
5667
5762
|
async function requestNamedSeatInvitation(options) {
|
|
@@ -5888,7 +5983,7 @@ async function drainMembershipEffects(db, options, now) {
|
|
|
5888
5983
|
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100 || !options.stripeSecretKey) throw new TypeError("invalid membership recovery configuration");
|
|
5889
5984
|
await loadMembershipAuthorityState(db, scope2);
|
|
5890
5985
|
const matches2 = (row2) => Object.entries(scope2).every(([key3, value]) => row2[key3] === value);
|
|
5891
|
-
let
|
|
5986
|
+
let pending = 0, complete = 0, more = false;
|
|
5892
5987
|
for (const status2 of ["payment_pending", "payment_processing", "payment_canceling"]) {
|
|
5893
5988
|
const seats = (await db.query({ networkSeat: { $: { where: { ...scope2, status: status2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkSeat ?? [];
|
|
5894
5989
|
more ||= seats.length > limit;
|
|
@@ -5906,26 +6001,6 @@ async function drainMembershipEffects(db, options, now) {
|
|
|
5906
6001
|
}
|
|
5907
6002
|
}
|
|
5908
6003
|
}
|
|
5909
|
-
for (const status2 of ["renewal_pending", "pending_acceptance"]) {
|
|
5910
|
-
const seats = (await db.query({ networkSeat: { $: { where: { ...scope2, status: status2 }, order: { acceptanceEndsAt: "asc" }, limit: limit + 1 } } })).networkSeat ?? [];
|
|
5911
|
-
more ||= seats.length > limit;
|
|
5912
|
-
for (const seat of seats.slice(0, limit)) {
|
|
5913
|
-
if (!matches2(seat)) throw new Error("membership recovery scope mismatch");
|
|
5914
|
-
if (seat.acceptedAt !== void 0 || now < Number(seat.acceptanceEndsAt)) continue;
|
|
5915
|
-
try {
|
|
5916
|
-
await requestNamedSeatRefund(db, {
|
|
5917
|
-
...scope2,
|
|
5918
|
-
applicationId: String(seat.applicationId),
|
|
5919
|
-
skuId: String(seat.skuId),
|
|
5920
|
-
seatId: String(seat.id),
|
|
5921
|
-
reason: "expired"
|
|
5922
|
-
}, now);
|
|
5923
|
-
expired++;
|
|
5924
|
-
} catch {
|
|
5925
|
-
pending++;
|
|
5926
|
-
}
|
|
5927
|
-
}
|
|
5928
|
-
}
|
|
5929
6004
|
const effects = (await db.query({ membershipEffect: { $: { where: {
|
|
5930
6005
|
chapterId: scope2.chapterId,
|
|
5931
6006
|
environment: scope2.environment,
|
|
@@ -5987,7 +6062,7 @@ async function drainMembershipEffects(db, options, now) {
|
|
|
5987
6062
|
pending++;
|
|
5988
6063
|
}
|
|
5989
6064
|
}
|
|
5990
|
-
return { complete, pending,
|
|
6065
|
+
return { complete, pending, more };
|
|
5991
6066
|
}
|
|
5992
6067
|
|
|
5993
6068
|
// src/schema-membership-offers.ts
|