@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/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 at = typeof value === "number" ? value : typeof value === "string" ? Date.parse(value) : NaN;
3568
- return integer2(at, 0, 864e13);
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, 1, Number.MAX_SAFE_INTEGER),
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 couponId(input, policyId) {
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 couponId(input, contract.discountPolicy?.id)
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 couponId(input, policy?.id)
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 couponId(input, policy?.id)
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 billedSeat = seat && seat.providerSubscriptionId === member.providerSubscriptionId && seat.providerSubscriptionItemId && !["refunded", "refund_pending"].includes(String(seat.status)) ? seat : void 0;
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 (billedSeat?.entitlementId) {
4460
- const seatEntitlement = await membershipLoad(db, "networkEntitlement", String(billedSeat.entitlementId));
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(billedSeat.id), {
4642
+ membershipUpdate("networkSeat", String(carriedSeat.id), {
4465
4643
  ...paid ? { termStartedAt: start, termEndsAt: end } : {},
4466
- revision: Number(billedSeat.revision) + 1,
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: billedSeat.journeyApprovedAt ? entitlementStatus : seatEntitlement.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 couponId2 = typeof couponRef === "string" ? couponRef : couponRef?.id;
4853
- if (!couponId2 || typeof couponId2 !== "string") reject3();
4854
- const coupon = await read(sk, `/v1/coupons/${encodeURIComponent(couponId2)}`, { "expand[0]": "applies_to" });
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.providerPaymentIntentId || !seat.providerSubscriptionItemId) membershipFail("named_seat_acceptance_unavailable");
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) <= 0) {
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 { ...offer, digest: await namedSeatOfferDigest(offer), purchaseWindowed: offering.seatPurchaseWindowCalendarMonths !== void 0 };
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 <= 0) throw new Error("invalid named seat refund receipt");
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 <= 0) throw new Error("invalid named seat refund receipt");
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 <= 0 || !/^[a-z]{3}$/.test(seat.currency) || !Number.isSafeInteger(seat.termEndsAt)) throw new Error("invalid seat reservation receipt");
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 <= 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");
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
- if (!seat || seat.seatId !== expected || !/^pi_[A-Za-z0-9_]+$/.test(seat.paymentIntentId) || typeof seat.clientSecret !== "string" || !Number.isSafeInteger(seat.amountCents) || seat.amountCents <= 0 || !/^[a-z]{3}$/.test(seat.currency) || !Number.isSafeInteger(seat.termEndsAt) || !["payment_processing", "renewal_pending", "pending_acceptance", "pending_approval", "active"].includes(seat.status)) {
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;