@odla-ai/chapter 0.52.1 → 0.54.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -1
- package/dist/{chunk-WWF2TO3H.js → chunk-NDZCR3OW.js} +43 -9
- package/dist/chunk-NDZCR3OW.js.map +1 -0
- package/dist/{chunk-TEHCORNC.js → chunk-SPEL4OGE.js} +57 -23
- package/dist/chunk-SPEL4OGE.js.map +1 -0
- package/dist/index.cjs +227 -113
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +33 -10
- package/dist/index.d.ts +33 -10
- package/dist/index.js +227 -113
- package/dist/index.js.map +1 -1
- package/dist/ui/admin/index.d.ts +4 -3
- package/dist/ui/admin/index.js +1 -1
- package/dist/ui/index.d.ts +1 -1
- package/dist/ui/index.js +8 -2
- package/dist/ui/member/index.d.ts +32 -1
- package/dist/ui/member/index.js +7 -1
- package/dist/worker/index.cjs +595 -212
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +116 -9
- package/dist/worker/index.d.ts +116 -9
- package/dist/worker/index.js +588 -205
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-TEHCORNC.js.map +0 -1
- package/dist/chunk-WWF2TO3H.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -155,6 +155,7 @@ __export(src_exports, {
|
|
|
155
155
|
membershipAuthorityEntities: () => membershipAuthorityEntities,
|
|
156
156
|
membershipAuthorityRules: () => membershipAuthorityRules,
|
|
157
157
|
membershipCheckoutBindingId: () => membershipCheckoutBindingId,
|
|
158
|
+
membershipDiscountCouponId: () => membershipDiscountCouponId,
|
|
158
159
|
membershipDiscountPolicyVersion: () => membershipDiscountPolicyVersion,
|
|
159
160
|
membershipOfferEntities: () => membershipOfferEntities,
|
|
160
161
|
membershipOfferIdForRequest: () => membershipOfferIdForRequest,
|
|
@@ -3564,8 +3565,8 @@ var object = (value) => {
|
|
|
3564
3565
|
var id6 = (value) => typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9_.:-]{2,159}$/.test(value) ? value : fail2("id");
|
|
3565
3566
|
var integer2 = (value, min, max) => typeof value === "number" && Number.isSafeInteger(value) && value >= min && value <= max ? value : fail2("integer");
|
|
3566
3567
|
var time = (value) => {
|
|
3567
|
-
const
|
|
3568
|
-
return integer2(
|
|
3568
|
+
const at2 = typeof value === "number" ? value : typeof value === "string" ? Date.parse(value) : NaN;
|
|
3569
|
+
return integer2(at2, 0, 864e13);
|
|
3569
3570
|
};
|
|
3570
3571
|
var ids = (value, allowEmpty = false) => {
|
|
3571
3572
|
if (!Array.isArray(value) || value.length > 100 || !allowEmpty && value.length === 0) return fail2("ids");
|
|
@@ -3609,7 +3610,7 @@ function membershipOfferingVersion(value) {
|
|
|
3609
3610
|
term: normalizedTerm,
|
|
3610
3611
|
autoRenew: true,
|
|
3611
3612
|
paymentGraceDays: integer2(input.paymentGraceDays, 0, 60),
|
|
3612
|
-
seatPriceCents: integer2(input.seatPriceCents,
|
|
3613
|
+
seatPriceCents: integer2(input.seatPriceCents, 0, Number.MAX_SAFE_INTEGER),
|
|
3613
3614
|
...input.seatPurchaseWindowCalendarMonths === void 0 || input.seatPurchaseWindowCalendarMonths === null ? {} : { seatPurchaseWindowCalendarMonths: integer2(input.seatPurchaseWindowCalendarMonths, 1, 120) },
|
|
3614
3615
|
requiredDisclosures: ids(input.requiredDisclosures)
|
|
3615
3616
|
};
|
|
@@ -3694,7 +3695,7 @@ async function hash(value) {
|
|
|
3694
3695
|
const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify(value)));
|
|
3695
3696
|
return [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
3696
3697
|
}
|
|
3697
|
-
async function
|
|
3698
|
+
async function membershipDiscountCouponId(input, policyId) {
|
|
3698
3699
|
return policyId ? `odla_m_${await hash([input.appId, input.chapterId, input.environment, input.runtime, input.skuId, policyId])}` : void 0;
|
|
3699
3700
|
}
|
|
3700
3701
|
async function load(db, ns, id10) {
|
|
@@ -3720,7 +3721,7 @@ async function restore(row2, input) {
|
|
|
3720
3721
|
discountPolicy: contract.discountPolicy,
|
|
3721
3722
|
approvedFoundingCount: Number(row2.approvedFoundingCount),
|
|
3722
3723
|
now: Number(row2.issuedAt),
|
|
3723
|
-
couponId: await
|
|
3724
|
+
couponId: await membershipDiscountCouponId(input, contract.discountPolicy?.id)
|
|
3724
3725
|
});
|
|
3725
3726
|
if (expected.offering.chapterId !== input.chapterId || expected.offering.baseSkuId !== input.skuId) reject("invalid_offer_snapshot");
|
|
3726
3727
|
const result = {
|
|
@@ -3751,7 +3752,7 @@ async function previewMembershipCommercialOffer(db, input) {
|
|
|
3751
3752
|
discountPolicy: policy,
|
|
3752
3753
|
approvedFoundingCount: Number(approvedFoundingCount),
|
|
3753
3754
|
now: input.now,
|
|
3754
|
-
couponId: await
|
|
3755
|
+
couponId: await membershipDiscountCouponId(input, policy?.id)
|
|
3755
3756
|
});
|
|
3756
3757
|
const discount = contract.billingTerms.discount;
|
|
3757
3758
|
return {
|
|
@@ -3801,7 +3802,7 @@ async function issueMembershipCommercialOffer(db, input) {
|
|
|
3801
3802
|
discountPolicy: policy,
|
|
3802
3803
|
approvedFoundingCount: Number(approvedFoundingCount),
|
|
3803
3804
|
now: input.now,
|
|
3804
|
-
couponId: await
|
|
3805
|
+
couponId: await membershipDiscountCouponId(input, policy?.id)
|
|
3805
3806
|
});
|
|
3806
3807
|
const value = {
|
|
3807
3808
|
id: input.offerId,
|
|
@@ -4381,6 +4382,181 @@ async function refundNetworkMembership(db, input, secretKey, now) {
|
|
|
4381
4382
|
// src/membership-provider-lifecycle.ts
|
|
4382
4383
|
var import_stripe4 = require("@odla-ai/stripe");
|
|
4383
4384
|
|
|
4385
|
+
// src/membership-seat-reservation.ts
|
|
4386
|
+
async function recipientKey(input, email) {
|
|
4387
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify([input.appId, input.chapterId, input.environment, input.runtime, email])));
|
|
4388
|
+
return `seat-recipient:${[...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
4389
|
+
}
|
|
4390
|
+
async function reserveNamedMembershipSeat(db, input, now) {
|
|
4391
|
+
membershipClock(now);
|
|
4392
|
+
const name = typeof input.recipientName === "string" ? input.recipientName.trim().replace(/\s+/g, " ") : "";
|
|
4393
|
+
const email = normalizeChapterEmail(typeof input.recipientEmail === "string" ? input.recipientEmail : "");
|
|
4394
|
+
const purchaser = normalizeChapterEmail(typeof input.purchaserEmail === "string" ? input.purchaserEmail : "");
|
|
4395
|
+
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");
|
|
4396
|
+
const state = await loadMembershipAuthorityState(db, input);
|
|
4397
|
+
const memberId = await networkMembershipId(input), member = await membershipLoad(db, "networkMembership", memberId);
|
|
4398
|
+
requireMembershipScope(member, input);
|
|
4399
|
+
if (!Number.isSafeInteger(member.revision) || !/^price_[A-Za-z0-9_]+$/.test(String(member.providerPriceId)) || member.providerPriceId === input.priceId) membershipFail("named_seat_purchase_unavailable");
|
|
4400
|
+
const id10 = (await membershipOfferIdForRequest({ ...input, requestId: "named_seat" })).replace(/^offer_/, "seat_");
|
|
4401
|
+
let seat = await membershipLoad(db, "networkSeat", id10);
|
|
4402
|
+
if (!seat) {
|
|
4403
|
+
const offering = membershipOfferingVersion(member.offeringSnapshot), claimId = await recipientKey(input, email);
|
|
4404
|
+
const purchaseEndsAt = seatPurchaseDeadline(offering, member);
|
|
4405
|
+
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) {
|
|
4406
|
+
membershipFail("named_seat_purchase_unavailable");
|
|
4407
|
+
}
|
|
4408
|
+
const attrs = {
|
|
4409
|
+
id: id10,
|
|
4410
|
+
...membershipScope(input),
|
|
4411
|
+
primaryMembershipId: memberId,
|
|
4412
|
+
recipientName: name,
|
|
4413
|
+
recipientEmailNormalized: email,
|
|
4414
|
+
purchaserUserId: input.purchaserUserId,
|
|
4415
|
+
recipientClaimId: claimId,
|
|
4416
|
+
priceId: input.priceId,
|
|
4417
|
+
primaryPriceId: member.providerPriceId,
|
|
4418
|
+
amountCents: offering.seatPriceCents,
|
|
4419
|
+
currency: offering.currency,
|
|
4420
|
+
interval: offering.interval,
|
|
4421
|
+
providerCustomerId: member.providerCustomerId,
|
|
4422
|
+
providerSubscriptionId: member.providerSubscriptionId,
|
|
4423
|
+
status: "payment_pending",
|
|
4424
|
+
preparedAt: now,
|
|
4425
|
+
termEndsAt: member.termEndsAt,
|
|
4426
|
+
seatPurchaseEndsAt: purchaseEndsAt,
|
|
4427
|
+
offeringSnapshot: offering,
|
|
4428
|
+
revision: 1,
|
|
4429
|
+
createdAt: now,
|
|
4430
|
+
updatedAt: now
|
|
4431
|
+
};
|
|
4432
|
+
try {
|
|
4433
|
+
await db.transact([
|
|
4434
|
+
membershipUpdate("networkSeat", id10, attrs),
|
|
4435
|
+
membershipUpdate("networkSeatRecipient", claimId, {
|
|
4436
|
+
id: claimId,
|
|
4437
|
+
appId: input.appId,
|
|
4438
|
+
chapterId: input.chapterId,
|
|
4439
|
+
environment: input.environment,
|
|
4440
|
+
runtime: input.runtime,
|
|
4441
|
+
seatId: id10,
|
|
4442
|
+
recipientEmailNormalized: email,
|
|
4443
|
+
createdAt: now
|
|
4444
|
+
}),
|
|
4445
|
+
membershipUpdate("networkMembership", memberId, { namedSeatId: id10, revision: Number(member.revision) + 1, updatedAt: now })
|
|
4446
|
+
], { mutationId: `named-seat-reserve:${id10}`, guards: [
|
|
4447
|
+
{ ns: "networkSeat", id: id10, exists: false },
|
|
4448
|
+
{ ns: "networkSeatRecipient", id: claimId, exists: false },
|
|
4449
|
+
{ ns: "networkMembership", id: memberId, exists: true, equals: { revision: member.revision }, absent: ["namedSeatId"] },
|
|
4450
|
+
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
|
|
4451
|
+
] });
|
|
4452
|
+
} catch (error) {
|
|
4453
|
+
if (error.code !== "transact_guard_failed") throw error;
|
|
4454
|
+
}
|
|
4455
|
+
seat = await membershipLoad(db, "networkSeat", id10);
|
|
4456
|
+
if (!seat && await membershipLoad(db, "networkSeatRecipient", claimId)) membershipFail("named_seat_recipient_unavailable");
|
|
4457
|
+
}
|
|
4458
|
+
requireMembershipScope(seat, input);
|
|
4459
|
+
if (seat.primaryMembershipId !== memberId || seat.recipientName !== name || seat.recipientEmailNormalized !== email || seat.priceId !== input.priceId || seat.purchaserUserId !== input.purchaserUserId) membershipFail("named_seat_purchase_conflict");
|
|
4460
|
+
return seat;
|
|
4461
|
+
}
|
|
4462
|
+
|
|
4463
|
+
// src/membership-seat-included.ts
|
|
4464
|
+
var namedSeatIncluded = (seat) => Number(seat.amountCents) === 0;
|
|
4465
|
+
var namedSeatSettled = (seat) => namedSeatIncluded(seat) ? seat.purchasedAt !== void 0 : Boolean(seat.providerPaymentIntentId && seat.providerSubscriptionItemId);
|
|
4466
|
+
function namedSeatRecipientState(seat) {
|
|
4467
|
+
switch (String(seat.status)) {
|
|
4468
|
+
case "renewal_pending":
|
|
4469
|
+
case "pending_acceptance":
|
|
4470
|
+
return "invited";
|
|
4471
|
+
case "pending_approval":
|
|
4472
|
+
case "pending_primary":
|
|
4473
|
+
return "accepted";
|
|
4474
|
+
case "active":
|
|
4475
|
+
return "joined";
|
|
4476
|
+
case "refund_pending":
|
|
4477
|
+
case "refunded":
|
|
4478
|
+
return seat.refundReason === "declined" ? "declined" : "refunded";
|
|
4479
|
+
case "canceled":
|
|
4480
|
+
return "canceled";
|
|
4481
|
+
default:
|
|
4482
|
+
return "payment_pending";
|
|
4483
|
+
}
|
|
4484
|
+
}
|
|
4485
|
+
var OPEN = ["pending_acceptance", "pending_approval", "pending_primary", "active"];
|
|
4486
|
+
async function grantIncludedNamedSeat(db, input, now) {
|
|
4487
|
+
membershipClock(now);
|
|
4488
|
+
const seat = await reserveNamedMembershipSeat(db, input, now);
|
|
4489
|
+
if (!namedSeatIncluded(seat)) membershipFail("named_seat_not_included");
|
|
4490
|
+
const state = await loadMembershipAuthorityState(db, input);
|
|
4491
|
+
const member = await membershipLoad(db, "networkMembership", seat.primaryMembershipId);
|
|
4492
|
+
requireMembershipScope(member, input);
|
|
4493
|
+
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)) {
|
|
4494
|
+
membershipFail("named_seat_purchase_unavailable");
|
|
4495
|
+
}
|
|
4496
|
+
if (seat.status === "payment_pending") {
|
|
4497
|
+
const entitlementId = `${seat.id}:entitlement`, inviteId = `${seat.id}:invite`, source = `included:${seat.id}`;
|
|
4498
|
+
try {
|
|
4499
|
+
await db.transact([
|
|
4500
|
+
membershipUpdate("networkSeat", seat.id, {
|
|
4501
|
+
status: "pending_acceptance",
|
|
4502
|
+
purchasedAt: now,
|
|
4503
|
+
termStartedAt: now,
|
|
4504
|
+
termEndsAt: member.termEndsAt,
|
|
4505
|
+
acceptanceEndsAt: Number(member.termEndsAt),
|
|
4506
|
+
entitlementId,
|
|
4507
|
+
revision: Number(seat.revision) + 1,
|
|
4508
|
+
updatedAt: now
|
|
4509
|
+
}),
|
|
4510
|
+
membershipUpdate("networkEntitlement", entitlementId, {
|
|
4511
|
+
id: entitlementId,
|
|
4512
|
+
...membershipScope(input),
|
|
4513
|
+
membershipId: member.id,
|
|
4514
|
+
seatId: seat.id,
|
|
4515
|
+
kind: "named_seat",
|
|
4516
|
+
status: "pending_acceptance",
|
|
4517
|
+
termStartedAt: now,
|
|
4518
|
+
termEndsAt: member.termEndsAt,
|
|
4519
|
+
offeringSnapshot: member.offeringSnapshot,
|
|
4520
|
+
sourceEventId: source,
|
|
4521
|
+
revision: 1,
|
|
4522
|
+
createdAt: now,
|
|
4523
|
+
updatedAt: now
|
|
4524
|
+
}),
|
|
4525
|
+
membershipUpdate("membershipEffect", inviteId, {
|
|
4526
|
+
id: inviteId,
|
|
4527
|
+
chapterId: input.chapterId,
|
|
4528
|
+
environment: input.environment,
|
|
4529
|
+
runtime: input.runtime,
|
|
4530
|
+
membershipId: member.id,
|
|
4531
|
+
seatId: seat.id,
|
|
4532
|
+
kind: "invite_named_recipient",
|
|
4533
|
+
status: "pending",
|
|
4534
|
+
attempts: 0,
|
|
4535
|
+
createdAt: now,
|
|
4536
|
+
updatedAt: now
|
|
4537
|
+
})
|
|
4538
|
+
], { mutationId: `named-seat-included:${seat.id}`, guards: [
|
|
4539
|
+
{
|
|
4540
|
+
ns: "networkSeat",
|
|
4541
|
+
id: seat.id,
|
|
4542
|
+
exists: true,
|
|
4543
|
+
equals: { status: "payment_pending", revision: seat.revision, amountCents: 0 },
|
|
4544
|
+
absent: ["purchasedAt", "providerPaymentIntentId"]
|
|
4545
|
+
},
|
|
4546
|
+
{ ns: "networkEntitlement", id: entitlementId, exists: false },
|
|
4547
|
+
{ ns: "networkMembership", id: String(member.id), exists: true, equals: { revision: member.revision, namedSeatId: seat.id } },
|
|
4548
|
+
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
|
|
4549
|
+
] });
|
|
4550
|
+
} catch (error) {
|
|
4551
|
+
if (error.code !== "transact_guard_failed") throw error;
|
|
4552
|
+
}
|
|
4553
|
+
}
|
|
4554
|
+
const current = await membershipLoad(db, "networkSeat", seat.id);
|
|
4555
|
+
requireMembershipScope(current, input);
|
|
4556
|
+
if (!namedSeatIncluded(current) || current.purchasedAt === void 0 || !OPEN.includes(String(current.status))) membershipFail("named_seat_purchase_unavailable");
|
|
4557
|
+
return { seatId: seat.id, amountCents: 0, currency: seat.currency, termEndsAt: Number(current.termEndsAt), status: String(current.status), included: true };
|
|
4558
|
+
}
|
|
4559
|
+
|
|
4384
4560
|
// src/membership-renewal-authority.ts
|
|
4385
4561
|
async function reconcileMembershipRenewal(db, input, now) {
|
|
4386
4562
|
membershipClock(now);
|
|
@@ -4416,7 +4592,9 @@ async function reconcileMembershipRenewal(db, input, now) {
|
|
|
4416
4592
|
if (start !== member.termEndsAt || start > now + 3e4) membershipFail("membership_renewal_period_gap");
|
|
4417
4593
|
const seat = member.namedSeatId ? await membershipLoad(db, "networkSeat", String(member.namedSeatId)) : void 0;
|
|
4418
4594
|
if (seat) requireMembershipScope(seat, input);
|
|
4419
|
-
const
|
|
4595
|
+
const openSeat = seat && !["refunded", "refund_pending", "canceled"].includes(String(seat.status)) ? seat : void 0;
|
|
4596
|
+
const billedSeat = openSeat && openSeat.providerSubscriptionId === member.providerSubscriptionId && openSeat.providerSubscriptionItemId ? openSeat : void 0;
|
|
4597
|
+
const carriedSeat = billedSeat ?? (openSeat && namedSeatIncluded(openSeat) && openSeat.purchasedAt !== void 0 ? openSeat : void 0);
|
|
4420
4598
|
const prices = [String(member.providerPriceId), ...billedSeat ? [String(billedSeat.priceId)] : []].sort();
|
|
4421
4599
|
const amount = Number(member.renewalAmountCents) + Number(billedSeat?.amountCents ?? 0);
|
|
4422
4600
|
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");
|
|
@@ -4456,19 +4634,19 @@ async function reconcileMembershipRenewal(db, input, now) {
|
|
|
4456
4634
|
evidenceGuard
|
|
4457
4635
|
];
|
|
4458
4636
|
if (seat) guards.push({ ns: "networkSeat", id: String(seat.id), exists: true, equals: { revision: seat.revision } });
|
|
4459
|
-
if (
|
|
4460
|
-
const seatEntitlement = await membershipLoad(db, "networkEntitlement", String(
|
|
4637
|
+
if (carriedSeat?.entitlementId) {
|
|
4638
|
+
const seatEntitlement = await membershipLoad(db, "networkEntitlement", String(carriedSeat.entitlementId));
|
|
4461
4639
|
requireMembershipScope(seatEntitlement, input);
|
|
4462
4640
|
guards.push({ ns: "networkEntitlement", id: String(seatEntitlement.id), exists: true, equals: { revision: seatEntitlement.revision } });
|
|
4463
4641
|
ops.push(
|
|
4464
|
-
membershipUpdate("networkSeat", String(
|
|
4642
|
+
membershipUpdate("networkSeat", String(carriedSeat.id), {
|
|
4465
4643
|
...paid ? { termStartedAt: start, termEndsAt: end } : {},
|
|
4466
|
-
revision: Number(
|
|
4644
|
+
revision: Number(carriedSeat.revision) + 1,
|
|
4467
4645
|
updatedAt: now
|
|
4468
4646
|
}),
|
|
4469
4647
|
membershipUpdate("networkEntitlement", String(seatEntitlement.id), {
|
|
4470
4648
|
...paid ? { termStartedAt: start, termEndsAt: end } : {},
|
|
4471
|
-
status:
|
|
4649
|
+
status: carriedSeat.journeyApprovedAt ? entitlementStatus : seatEntitlement.status,
|
|
4472
4650
|
revision: Number(seatEntitlement.revision) + 1,
|
|
4473
4651
|
updatedAt: now
|
|
4474
4652
|
})
|
|
@@ -4720,84 +4898,6 @@ async function verifyMembershipAccessReceipt(value, input, now) {
|
|
|
4720
4898
|
return { membershipId: row2.membershipId, allowed: row2.allowed, status: row2.status, accessEndsAt: row2.accessEndsAt, revision: row2.revision, ...row2.restartEligible === true && !row2.allowed ? { restartEligible: true } : {} };
|
|
4721
4899
|
}
|
|
4722
4900
|
|
|
4723
|
-
// src/membership-seat-reservation.ts
|
|
4724
|
-
async function recipientKey(input, email) {
|
|
4725
|
-
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify([input.appId, input.chapterId, input.environment, input.runtime, email])));
|
|
4726
|
-
return `seat-recipient:${[...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
4727
|
-
}
|
|
4728
|
-
async function reserveNamedMembershipSeat(db, input, now) {
|
|
4729
|
-
membershipClock(now);
|
|
4730
|
-
const name = typeof input.recipientName === "string" ? input.recipientName.trim().replace(/\s+/g, " ") : "";
|
|
4731
|
-
const email = normalizeChapterEmail(typeof input.recipientEmail === "string" ? input.recipientEmail : "");
|
|
4732
|
-
const purchaser = normalizeChapterEmail(typeof input.purchaserEmail === "string" ? input.purchaserEmail : "");
|
|
4733
|
-
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");
|
|
4734
|
-
const state = await loadMembershipAuthorityState(db, input);
|
|
4735
|
-
const memberId = await networkMembershipId(input), member = await membershipLoad(db, "networkMembership", memberId);
|
|
4736
|
-
requireMembershipScope(member, input);
|
|
4737
|
-
if (!Number.isSafeInteger(member.revision) || !/^price_[A-Za-z0-9_]+$/.test(String(member.providerPriceId)) || member.providerPriceId === input.priceId) membershipFail("named_seat_purchase_unavailable");
|
|
4738
|
-
const id10 = (await membershipOfferIdForRequest({ ...input, requestId: "named_seat" })).replace(/^offer_/, "seat_");
|
|
4739
|
-
let seat = await membershipLoad(db, "networkSeat", id10);
|
|
4740
|
-
if (!seat) {
|
|
4741
|
-
const offering = membershipOfferingVersion(member.offeringSnapshot), claimId = await recipientKey(input, email);
|
|
4742
|
-
const purchaseEndsAt = seatPurchaseDeadline(offering, member);
|
|
4743
|
-
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) {
|
|
4744
|
-
membershipFail("named_seat_purchase_unavailable");
|
|
4745
|
-
}
|
|
4746
|
-
const attrs = {
|
|
4747
|
-
id: id10,
|
|
4748
|
-
...membershipScope(input),
|
|
4749
|
-
primaryMembershipId: memberId,
|
|
4750
|
-
recipientName: name,
|
|
4751
|
-
recipientEmailNormalized: email,
|
|
4752
|
-
purchaserUserId: input.purchaserUserId,
|
|
4753
|
-
recipientClaimId: claimId,
|
|
4754
|
-
priceId: input.priceId,
|
|
4755
|
-
primaryPriceId: member.providerPriceId,
|
|
4756
|
-
amountCents: offering.seatPriceCents,
|
|
4757
|
-
currency: offering.currency,
|
|
4758
|
-
interval: offering.interval,
|
|
4759
|
-
providerCustomerId: member.providerCustomerId,
|
|
4760
|
-
providerSubscriptionId: member.providerSubscriptionId,
|
|
4761
|
-
status: "payment_pending",
|
|
4762
|
-
preparedAt: now,
|
|
4763
|
-
termEndsAt: member.termEndsAt,
|
|
4764
|
-
seatPurchaseEndsAt: purchaseEndsAt,
|
|
4765
|
-
offeringSnapshot: offering,
|
|
4766
|
-
revision: 1,
|
|
4767
|
-
createdAt: now,
|
|
4768
|
-
updatedAt: now
|
|
4769
|
-
};
|
|
4770
|
-
try {
|
|
4771
|
-
await db.transact([
|
|
4772
|
-
membershipUpdate("networkSeat", id10, attrs),
|
|
4773
|
-
membershipUpdate("networkSeatRecipient", claimId, {
|
|
4774
|
-
id: claimId,
|
|
4775
|
-
appId: input.appId,
|
|
4776
|
-
chapterId: input.chapterId,
|
|
4777
|
-
environment: input.environment,
|
|
4778
|
-
runtime: input.runtime,
|
|
4779
|
-
seatId: id10,
|
|
4780
|
-
recipientEmailNormalized: email,
|
|
4781
|
-
createdAt: now
|
|
4782
|
-
}),
|
|
4783
|
-
membershipUpdate("networkMembership", memberId, { namedSeatId: id10, revision: Number(member.revision) + 1, updatedAt: now })
|
|
4784
|
-
], { mutationId: `named-seat-reserve:${id10}`, guards: [
|
|
4785
|
-
{ ns: "networkSeat", id: id10, exists: false },
|
|
4786
|
-
{ ns: "networkSeatRecipient", id: claimId, exists: false },
|
|
4787
|
-
{ ns: "networkMembership", id: memberId, exists: true, equals: { revision: member.revision }, absent: ["namedSeatId"] },
|
|
4788
|
-
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
|
|
4789
|
-
] });
|
|
4790
|
-
} catch (error) {
|
|
4791
|
-
if (error.code !== "transact_guard_failed") throw error;
|
|
4792
|
-
}
|
|
4793
|
-
seat = await membershipLoad(db, "networkSeat", id10);
|
|
4794
|
-
if (!seat && await membershipLoad(db, "networkSeatRecipient", claimId)) membershipFail("named_seat_recipient_unavailable");
|
|
4795
|
-
}
|
|
4796
|
-
requireMembershipScope(seat, input);
|
|
4797
|
-
if (seat.primaryMembershipId !== memberId || seat.recipientName !== name || seat.recipientEmailNormalized !== email || seat.priceId !== input.priceId || seat.purchaserUserId !== input.purchaserUserId) membershipFail("named_seat_purchase_conflict");
|
|
4798
|
-
return seat;
|
|
4799
|
-
}
|
|
4800
|
-
|
|
4801
4901
|
// src/membership-seat-stripe.ts
|
|
4802
4902
|
var import_stripe7 = require("@odla-ai/stripe");
|
|
4803
4903
|
|
|
@@ -4849,9 +4949,9 @@ async function subscriptionForSeat(sk, seat) {
|
|
|
4849
4949
|
if (!Array.isArray(discounts) || discounts.length > 5) reject3();
|
|
4850
4950
|
for (const discount of discounts) {
|
|
4851
4951
|
const couponRef = discount.source?.coupon ?? discount.coupon;
|
|
4852
|
-
const
|
|
4853
|
-
if (!
|
|
4854
|
-
const coupon = await read(sk, `/v1/coupons/${encodeURIComponent(
|
|
4952
|
+
const couponId = typeof couponRef === "string" ? couponRef : couponRef?.id;
|
|
4953
|
+
if (!couponId || typeof couponId !== "string") reject3();
|
|
4954
|
+
const coupon = await read(sk, `/v1/coupons/${encodeURIComponent(couponId)}`, { "expand[0]": "applies_to" });
|
|
4855
4955
|
if (coupon.livemode !== (seat.environment === "prod") || !Array.isArray(coupon.applies_to?.products) || coupon.applies_to.products.length !== 1 || coupon.applies_to.products[0] !== primary.price.product) reject3();
|
|
4856
4956
|
}
|
|
4857
4957
|
return sub;
|
|
@@ -4900,6 +5000,7 @@ async function attachNamedSeatStripeRenewal(sk, seat) {
|
|
|
4900
5000
|
// src/membership-seat-checkout.ts
|
|
4901
5001
|
async function prepareNamedMembershipSeatPayment(db, input, secretKey, now) {
|
|
4902
5002
|
let seat = await reserveNamedMembershipSeat(db, input, now);
|
|
5003
|
+
if (namedSeatIncluded(seat)) membershipFail("named_seat_included");
|
|
4903
5004
|
const state = await loadMembershipAuthorityState(db, input);
|
|
4904
5005
|
const member = await membershipLoad(db, "networkMembership", seat.primaryMembershipId);
|
|
4905
5006
|
requireMembershipScope(member, input);
|
|
@@ -5106,7 +5207,7 @@ async function recordRecipient(db, input, now, approve) {
|
|
|
5106
5207
|
requireMembershipScope(member, input);
|
|
5107
5208
|
const entitlement = await membershipLoad(db, "networkEntitlement", String(seat.entitlementId));
|
|
5108
5209
|
requireMembershipScope(entitlement, input);
|
|
5109
|
-
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
|
|
5210
|
+
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");
|
|
5110
5211
|
if (seat.acceptedAt === void 0) {
|
|
5111
5212
|
if (approve || normalizeChapterEmail(input.verifiedRecipientEmail) !== seat.recipientEmailNormalized) {
|
|
5112
5213
|
membershipFail("named_seat_recipient_verification_required");
|
|
@@ -5221,7 +5322,7 @@ async function requestNamedSeatRefund(db, input, now) {
|
|
|
5221
5322
|
const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
|
|
5222
5323
|
requireMembershipScope(member, input);
|
|
5223
5324
|
if (seat.status === "refunded" || seat.status === "refund_pending") return;
|
|
5224
|
-
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");
|
|
5325
|
+
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");
|
|
5225
5326
|
const entitlement = seat.entitlementId ? await membershipLoad(db, "networkEntitlement", String(seat.entitlementId)) : void 0;
|
|
5226
5327
|
await db.transact([
|
|
5227
5328
|
membershipUpdate("networkSeat", input.seatId, { status: "refund_pending", refundReason: input.reason, revision: Number(seat.revision) + 1, updatedAt: now }),
|
|
@@ -5266,7 +5367,7 @@ async function completeNamedSeatRefund(db, seatId, secretKey, now) {
|
|
|
5266
5367
|
await loadMembershipAuthorityState(db, seat);
|
|
5267
5368
|
if (seat.status === "refunded") return;
|
|
5268
5369
|
if (seat.status !== "refund_pending") membershipFail("named_seat_refund_not_ready");
|
|
5269
|
-
const result = await refundNamedSeatStripePayment(secretKey, seat);
|
|
5370
|
+
const result = namedSeatIncluded(seat) ? { refundedCents: 0, refundIds: [] } : await refundNamedSeatStripePayment(secretKey, seat);
|
|
5270
5371
|
await db.transact([
|
|
5271
5372
|
membershipUpdate("networkSeat", seatId, {
|
|
5272
5373
|
status: "refunded",
|
|
@@ -5300,7 +5401,7 @@ async function refundNamedSeatByOperator(db, input, stripeSecretKey, now) {
|
|
|
5300
5401
|
await requestNamedSeatRefund(db, { ...input, reason: "operator" }, now);
|
|
5301
5402
|
await completeNamedSeatRefund(db, input.seatId, stripeSecretKey, now);
|
|
5302
5403
|
const refunded = await membershipLoad(db, "networkSeat", input.seatId);
|
|
5303
|
-
if (refunded?.status !== "refunded" || !Number.isSafeInteger(refunded.refundedCents) || Number(refunded.refundedCents)
|
|
5404
|
+
if (refunded?.status !== "refunded" || !Number.isSafeInteger(refunded.refundedCents) || Number(refunded.refundedCents) !== Number(seat.amountCents)) {
|
|
5304
5405
|
throw new Error("named seat refund unconfirmed");
|
|
5305
5406
|
}
|
|
5306
5407
|
return { seatId: String(seat.id), status: "refunded", refundedCents: Number(refunded.refundedCents), subscriptionCanceled: false };
|
|
@@ -5322,6 +5423,7 @@ async function namedSeatOfferDigest(offer) {
|
|
|
5322
5423
|
])));
|
|
5323
5424
|
return [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
5324
5425
|
}
|
|
5426
|
+
var at = (value) => Number.isSafeInteger(value) ? Number(value) : void 0;
|
|
5325
5427
|
async function readNamedSeatOffer(db, input, now) {
|
|
5326
5428
|
membershipClock(now);
|
|
5327
5429
|
await loadMembershipAuthorityState(db, input);
|
|
@@ -5344,10 +5446,19 @@ async function readNamedSeatOffer(db, input, now) {
|
|
|
5344
5446
|
seatId: String(seat.id),
|
|
5345
5447
|
status: String(seat.status),
|
|
5346
5448
|
recipientName: String(seat.recipientName),
|
|
5347
|
-
recipientEmail: String(seat.recipientEmailNormalized)
|
|
5449
|
+
recipientEmail: String(seat.recipientEmailNormalized),
|
|
5450
|
+
recipientState: namedSeatRecipientState(seat),
|
|
5451
|
+
...at(seat.purchasedAt) !== void 0 ? { purchasedAt: at(seat.purchasedAt) } : {},
|
|
5452
|
+
...at(seat.acceptedAt) !== void 0 ? { acceptedAt: at(seat.acceptedAt) } : {},
|
|
5453
|
+
...seat.status === "active" && at(seat.journeyApprovedAt) !== void 0 ? { joinedAt: at(seat.journeyApprovedAt) } : {}
|
|
5348
5454
|
} } : {}
|
|
5349
5455
|
};
|
|
5350
|
-
return {
|
|
5456
|
+
return {
|
|
5457
|
+
...offer,
|
|
5458
|
+
digest: await namedSeatOfferDigest(offer),
|
|
5459
|
+
included: offering.seatPriceCents === 0,
|
|
5460
|
+
purchaseWindowed: offering.seatPurchaseWindowCalendarMonths !== void 0
|
|
5461
|
+
};
|
|
5351
5462
|
}
|
|
5352
5463
|
|
|
5353
5464
|
// src/membership-seat-invitation.ts
|
|
@@ -5473,6 +5584,15 @@ async function handleMembershipSeatAction(action, db, peer, input, now, stripeSe
|
|
|
5473
5584
|
};
|
|
5474
5585
|
if (action === "seat-accept") return acceptNamedSeatRecipient(db, recipientInput, now);
|
|
5475
5586
|
if (action === "seat-approve") return approveNamedSeatRecipient(db, recipientInput, now);
|
|
5587
|
+
const checkout = {
|
|
5588
|
+
...target,
|
|
5589
|
+
priceId: peer.seatPriceId,
|
|
5590
|
+
recipientName: String(input.recipientName),
|
|
5591
|
+
recipientEmail: String(input.recipientEmail),
|
|
5592
|
+
purchaserEmail: String(input.purchaserEmail),
|
|
5593
|
+
purchaserUserId: String(input.purchaserUserId)
|
|
5594
|
+
};
|
|
5595
|
+
if (action === "seat-checkouts" && namedSeatIncluded(await reserveNamedMembershipSeat(db, checkout, now))) return grantIncludedNamedSeat(db, checkout, now);
|
|
5476
5596
|
const secret = await getVaultSecret(db, stripeSecretName);
|
|
5477
5597
|
if (!secret) throw new Error("network Stripe provider is unavailable");
|
|
5478
5598
|
if (action === "seat-decline") {
|
|
@@ -5488,14 +5608,7 @@ async function handleMembershipSeatAction(action, db, peer, input, now, stripeSe
|
|
|
5488
5608
|
return { seatId: seat.id, status: "refunded", refundedCents: refunded.refundedCents, subscriptionCanceled: false };
|
|
5489
5609
|
}
|
|
5490
5610
|
if (action === "seat-operator-refund") return refundNamedSeatByOperator(db, { ...target, seatId: String(input.seatId) }, secret, now);
|
|
5491
|
-
if (action === "seat-checkouts") return prepareNamedMembershipSeatPayment(db,
|
|
5492
|
-
...target,
|
|
5493
|
-
priceId: peer.seatPriceId,
|
|
5494
|
-
recipientName: String(input.recipientName),
|
|
5495
|
-
recipientEmail: String(input.recipientEmail),
|
|
5496
|
-
purchaserEmail: String(input.purchaserEmail),
|
|
5497
|
-
purchaserUserId: String(input.purchaserUserId)
|
|
5498
|
-
}, secret, now);
|
|
5611
|
+
if (action === "seat-checkouts") return prepareNamedMembershipSeatPayment(db, checkout, secret, now);
|
|
5499
5612
|
const evidence = { ...target, seatId: String(input.seatId), eventId: String(input.eventId) };
|
|
5500
5613
|
if (action === "seat-payments") {
|
|
5501
5614
|
const receipt = await confirmNamedMembershipSeatPayment(db, evidence, now);
|
|
@@ -5806,13 +5919,13 @@ async function requestNamedSeatDecline(options) {
|
|
|
5806
5919
|
verifiedRecipientEmail: options.verifiedRecipientEmail
|
|
5807
5920
|
});
|
|
5808
5921
|
const seat = result.seat;
|
|
5809
|
-
if (!seat || seat.seatId !== options.seatId || seat.status !== "refunded" || seat.subscriptionCanceled !== false || !Number.isSafeInteger(seat.refundedCents) || seat.refundedCents
|
|
5922
|
+
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");
|
|
5810
5923
|
return seat;
|
|
5811
5924
|
}
|
|
5812
5925
|
async function requestNamedSeatOperatorRefund(options) {
|
|
5813
5926
|
const result = await requestAuthority(options, "seat-operator-refund", { applicationId: options.applicationId, skuId: options.skuId, seatId: options.seatId });
|
|
5814
5927
|
const seat = result.seat;
|
|
5815
|
-
if (!seat || seat.seatId !== options.seatId || seat.status !== "refunded" || seat.subscriptionCanceled !== false || !Number.isSafeInteger(seat.refundedCents) || seat.refundedCents
|
|
5928
|
+
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");
|
|
5816
5929
|
return seat;
|
|
5817
5930
|
}
|
|
5818
5931
|
async function requestNamedSeatReservation(options) {
|
|
@@ -5826,13 +5939,13 @@ async function requestNamedSeatReservation(options) {
|
|
|
5826
5939
|
});
|
|
5827
5940
|
const seat = result.seat;
|
|
5828
5941
|
const expected = (await membershipOfferIdForRequest({ ...options, requestId: "named_seat" })).replace(/^offer_/, "seat_");
|
|
5829
|
-
if (!seat || seat.seatId !== expected || !Number.isSafeInteger(seat.amountCents) || seat.amountCents
|
|
5942
|
+
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");
|
|
5830
5943
|
return { seatId: seat.seatId, amountCents: seat.amountCents, currency: seat.currency, termEndsAt: seat.termEndsAt };
|
|
5831
5944
|
}
|
|
5832
5945
|
async function requestNamedSeatOffer(options) {
|
|
5833
5946
|
const result = await requestAuthority(options, "seat-offer", { applicationId: options.applicationId, skuId: options.skuId });
|
|
5834
5947
|
const offer = result.seat;
|
|
5835
|
-
if (!offer || offer.membershipId !== await networkMembershipId(options) || !Number.isSafeInteger(offer.amountCents) || offer.amountCents
|
|
5948
|
+
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");
|
|
5836
5949
|
return offer;
|
|
5837
5950
|
}
|
|
5838
5951
|
async function requestNamedSeatInvitation(options) {
|
|
@@ -5868,7 +5981,8 @@ async function requestNamedSeatCheckout(options) {
|
|
|
5868
5981
|
});
|
|
5869
5982
|
const seat = result.seat;
|
|
5870
5983
|
const expected = (await membershipOfferIdForRequest({ ...options, requestId: "named_seat" })).replace(/^offer_/, "seat_");
|
|
5871
|
-
|
|
5984
|
+
const included = seat?.amountCents === 0;
|
|
5985
|
+
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))) {
|
|
5872
5986
|
throw new Error("invalid named seat checkout receipt");
|
|
5873
5987
|
}
|
|
5874
5988
|
return seat;
|