@odla-ai/chapter 0.47.7 → 0.47.9

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.
@@ -1495,12 +1495,12 @@ function validateControlledApplication(chapter, document) {
1495
1495
  return [...document.application.required, ...document.application.optional].filter((field) => !declared.has(field)).map((field) => `document.application field "${field}" is not declared by this chapter schema`);
1496
1496
  }
1497
1497
  function groupAttrs(document) {
1498
- const first4 = [...document.tiers].filter((tier) => tier.available).sort((a, b) => a.sortOrder - b.sortOrder)[0];
1498
+ const first5 = [...document.tiers].filter((tier) => tier.available).sort((a, b) => a.sortOrder - b.sortOrder)[0];
1499
1499
  const firstPaid = [...document.tiers].find((tier) => tier.available && tier.priceCents > 0);
1500
1500
  return {
1501
- ...first4 ? { standardPriceCents: first4.priceCents } : {},
1502
- currency: first4?.currency ?? "usd",
1503
- interval: first4?.interval ?? "year",
1501
+ ...first5 ? { standardPriceCents: first5.priceCents } : {},
1502
+ currency: first5?.currency ?? "usd",
1503
+ interval: first5?.interval ?? "year",
1504
1504
  foundingDiscountCents: 0,
1505
1505
  ...firstPaid?.stripePriceId ? { stripePriceId: firstPaid.stripePriceId } : {},
1506
1506
  ...document.policy ?? {},
@@ -4468,12 +4468,130 @@ async function resolveStripeWebhookSecret(db, runtime) {
4468
4468
 
4469
4469
  // src/payments-route-support.ts
4470
4470
  var paymentErrorCode = (error) => error && typeof error === "object" && typeof error.code === "string" ? error.code : "unknown";
4471
- function paymentLineItems(group) {
4471
+ function paymentLineItems(group, tier) {
4472
4472
  const standard = Number(group.standardPriceCents ?? 0);
4473
4473
  const discount = Number(group.foundingDiscountCents ?? 0);
4474
+ const legacyTier = !tier || tier.id === "standard" && tier.priceCents === standard && tier.stripePriceId === group.stripePriceId;
4475
+ if (!legacyTier) {
4476
+ return { standardCents: tier.priceCents, discountCents: 0, dueTodayCents: tier.priceCents };
4477
+ }
4474
4478
  return { standardCents: standard, discountCents: discount, dueTodayCents: standard - discount };
4475
4479
  }
4476
4480
 
4481
+ // src/subscription-checkout.ts
4482
+ var first3 = (value) => Array.isArray(value) ? value[0] : void 0;
4483
+ function mismatch() {
4484
+ const error = new Error("subscription checkout intent does not match this checkout");
4485
+ error.code = "checkout_intent_mismatch";
4486
+ throw error;
4487
+ }
4488
+ function intentAttrs(input) {
4489
+ return {
4490
+ id: input.applicationId,
4491
+ applicationId: input.applicationId,
4492
+ appId: input.appId,
4493
+ chapterId: input.chapterId,
4494
+ environment: input.environment,
4495
+ ...input.runtime ? { runtime: input.runtime } : {},
4496
+ groupId: input.groupId,
4497
+ tierId: input.tierId,
4498
+ priceId: input.priceId,
4499
+ priceCents: input.priceCents,
4500
+ currency: input.currency,
4501
+ interval: input.interval,
4502
+ purchaserEmailNormalized: input.emailNormalized,
4503
+ ...input.existingCustomerId ? { requestedCustomerId: input.existingCustomerId } : {},
4504
+ status: "created",
4505
+ createdAt: input.now,
4506
+ updatedAt: input.now
4507
+ };
4508
+ }
4509
+ function matchesIntent(stored, expected) {
4510
+ return [
4511
+ "id",
4512
+ "applicationId",
4513
+ "appId",
4514
+ "chapterId",
4515
+ "environment",
4516
+ "runtime",
4517
+ "groupId",
4518
+ "tierId",
4519
+ "priceId",
4520
+ "priceCents",
4521
+ "currency",
4522
+ "interval",
4523
+ "purchaserEmailNormalized",
4524
+ "requestedCustomerId"
4525
+ ].every((key) => stored[key] === expected[key]);
4526
+ }
4527
+ async function loadIntent(db, id) {
4528
+ return first3((await db.query({
4529
+ subscriptionCheckoutIntents: { $: { where: { id }, limit: 1 } }
4530
+ })).subscriptionCheckoutIntents);
4531
+ }
4532
+ async function startSubscriptionCheckout(db, provider, input) {
4533
+ const expected = intentAttrs(input);
4534
+ let stored = await loadIntent(db, input.applicationId);
4535
+ if (!stored) {
4536
+ const write = await db.transact([{
4537
+ t: "update",
4538
+ ns: "subscriptionCheckoutIntents",
4539
+ id: input.applicationId,
4540
+ attrs: expected
4541
+ }], {
4542
+ mutationId: `subscription-checkout-intent:${input.applicationId}`,
4543
+ guards: [{ ns: "subscriptionCheckoutIntents", id: input.applicationId, exists: false }]
4544
+ });
4545
+ stored = write.duplicate ? await loadIntent(db, input.applicationId) : expected;
4546
+ }
4547
+ if (!stored || !matchesIntent(stored, expected)) mismatch();
4548
+ if (stored.status !== "created" && stored.status !== "checkout_ready") mismatch();
4549
+ const result = await provider.createSubscription({
4550
+ appId: input.appId,
4551
+ chapterId: input.chapterId,
4552
+ environment: input.environment,
4553
+ ...input.runtime ? { runtime: input.runtime } : {},
4554
+ applicationId: input.applicationId,
4555
+ groupId: input.groupId,
4556
+ email: input.email,
4557
+ name: input.name,
4558
+ priceId: input.priceId,
4559
+ existingCustomerId: typeof stored.providerCustomerId === "string" ? stored.providerCustomerId : input.existingCustomerId
4560
+ });
4561
+ if (stored.providerCustomerId && stored.providerCustomerId !== result.customerId || stored.providerSubscriptionId && stored.providerSubscriptionId !== result.subscriptionId) mismatch();
4562
+ await db.transact([
4563
+ {
4564
+ t: "update",
4565
+ ns: "subscriptionCheckoutIntents",
4566
+ id: input.applicationId,
4567
+ attrs: {
4568
+ status: "checkout_ready",
4569
+ providerCustomerId: result.customerId,
4570
+ providerSubscriptionId: result.subscriptionId,
4571
+ updatedAt: input.now
4572
+ }
4573
+ },
4574
+ {
4575
+ t: "update",
4576
+ ns: "applications",
4577
+ id: input.applicationId,
4578
+ attrs: {
4579
+ stripeCustomerId: result.customerId,
4580
+ stripeSubscriptionId: result.subscriptionId,
4581
+ ...input.runtime ? { stripeRuntime: input.runtime } : {},
4582
+ refundPolicyAckAt: input.now
4583
+ }
4584
+ }
4585
+ ], {
4586
+ mutationId: `subscription-checkout-ready:${input.applicationId}:${result.subscriptionId}`,
4587
+ guards: [
4588
+ { ns: "subscriptionCheckoutIntents", id: input.applicationId, exists: true, equals: { status: stored.status } },
4589
+ { ns: "applications", id: input.applicationId, exists: true, equals: { status: "submitted" } }
4590
+ ]
4591
+ });
4592
+ return result;
4593
+ }
4594
+
4477
4595
  // src/member-system-contract.ts
4478
4596
  function classifyMemberStripeEvent(identity, target, hasProvenLegacyLocalLink) {
4479
4597
  const keys = ["appId", "environment", "dataEnvironment", "runtime", "chapterId"];
@@ -4642,35 +4760,29 @@ async function startSubscription(req, env, ctx) {
4642
4760
  const provider = createStripeProvider({ secretKey });
4643
4761
  let result;
4644
4762
  try {
4645
- result = await provider.createSubscription({
4763
+ result = await startSubscriptionCheckout(db, provider, {
4646
4764
  appId: env.ODLA_APP_ID,
4647
4765
  chapterId: ctx.chapter.id,
4648
4766
  environment: env.ODLA_ENV,
4649
4767
  runtime: stripeRuntimeIdentity(env.ODLA_RUNTIME),
4650
4768
  applicationId,
4651
4769
  groupId: String(group.id),
4770
+ tierId: tier.id,
4771
+ priceCents: tier.priceCents,
4772
+ currency: tier.currency ?? String(group.currency ?? "usd"),
4773
+ interval: tier.interval ?? (group.interval === "month" ? "month" : "year"),
4652
4774
  email: String(app.email ?? ""),
4775
+ emailNormalized: normalizeChapterEmail(String(app.email ?? "")),
4653
4776
  name: `${app.firstName ?? ""} ${app.lastName ?? ""}`.trim(),
4654
4777
  priceId: String(priceId),
4655
- existingCustomerId: typeof app.stripeCustomerId === "string" ? app.stripeCustomerId : void 0
4778
+ existingCustomerId: typeof app.stripeCustomerId === "string" ? app.stripeCustomerId : void 0,
4779
+ now: Date.now()
4656
4780
  });
4657
4781
  } catch (err) {
4658
- return json({ error: "payment setup failed", code: paymentErrorCode(err) }, 502);
4782
+ const code = paymentErrorCode(err);
4783
+ return json({ error: "payment setup failed", code }, code === "checkout_intent_mismatch" ? 409 : 502);
4659
4784
  }
4660
- await db.transact([
4661
- {
4662
- t: "update",
4663
- ns: "applications",
4664
- id: applicationId,
4665
- attrs: {
4666
- stripeCustomerId: result.customerId,
4667
- stripeSubscriptionId: result.subscriptionId,
4668
- stripeRuntime: stripeRuntimeIdentity(env.ODLA_RUNTIME),
4669
- refundPolicyAckAt: Date.now()
4670
- }
4671
- }
4672
- ]);
4673
- return json({ clientSecret: result.clientSecret, publishableKey: group.stripePublishableKey ?? null, lineItems: paymentLineItems(group) });
4785
+ return json({ clientSecret: result.clientSecret, publishableKey: group.stripePublishableKey ?? null, lineItems: paymentLineItems(group, tier) });
4674
4786
  }
4675
4787
  async function ingestWebhook(req, env, ctx) {
4676
4788
  const db = ctx.makeDb(env);
@@ -4782,18 +4894,18 @@ var handlePayments = async (req, url, env, ctx) => {
4782
4894
  };
4783
4895
 
4784
4896
  // src/worker-routes-gifts.ts
4785
- var first3 = (value) => Array.isArray(value) ? value[0] : void 0;
4897
+ var first4 = (value) => Array.isArray(value) ? value[0] : void 0;
4786
4898
  var rows2 = (value) => Array.isArray(value) ? value : [];
4787
4899
  var invalid = () => json({ error: "gift code is invalid or unavailable" }, 400);
4788
4900
  var userFor = async (req, env, ctx) => {
4789
4901
  const user = await ctx.verifyUser(req, env);
4790
4902
  return user?.email ? user : null;
4791
4903
  };
4792
- var loadClaim = async (db, id) => first3((await db.query({ giftClaims: { $: { where: { id }, limit: 1 } } })).giftClaims);
4904
+ var loadClaim = async (db, id) => first4((await db.query({ giftClaims: { $: { where: { id }, limit: 1 } } })).giftClaims);
4793
4905
  async function consumeRateLimit(db, chapterId, requester, now) {
4794
4906
  const bucket = await giftRequesterBucket(chapterId, requester);
4795
4907
  const id = `gift-rate:${chapterId}:${bucket}`;
4796
- const current = first3((await db.query({ giftRedemptionAttempts: { $: { where: { id }, limit: 1 } } })).giftRedemptionAttempts);
4908
+ const current = first4((await db.query({ giftRedemptionAttempts: { $: { where: { id }, limit: 1 } } })).giftRedemptionAttempts);
4797
4909
  const within = Boolean(current && now - Number(current.windowStartedAt) < 15 * 6e4);
4798
4910
  const attemptCount = within ? Number(current?.attemptCount ?? 0) + 1 : 1;
4799
4911
  if (attemptCount > 10) return false;
@@ -4818,7 +4930,7 @@ var handleGifts = async (req, url, env, ctx) => {
4818
4930
  purchaserEmailNormalized: normalizeChapterEmail(user.email),
4819
4931
  chapterId: ctx.chapter.id
4820
4932
  }, limit: 200 } } });
4821
- const group = first3((await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups);
4933
+ const group = first4((await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups);
4822
4934
  return json({
4823
4935
  offers: publicGiftOffers(control3?.document.giftOffers, Date.now()),
4824
4936
  claims: buyerGiftClaims(rows2(found.giftClaims).sort((a, b) => Number(b.createdAt) - Number(a.createdAt)), Date.now()),
@@ -4840,7 +4952,7 @@ var handleGifts = async (req, url, env, ctx) => {
4840
4952
  const control3 = await loadActiveSignupControl(db, ctx.chapter.id, env.ODLA_ENV);
4841
4953
  const offer = findGiftOffer(control3?.document.giftOffers, typeof body.offerId === "string" ? body.offerId : "", Date.now());
4842
4954
  if (!offer) return json({ error: "gift offer is unavailable" }, 409);
4843
- const group = first3((await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups);
4955
+ const group = first4((await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups);
4844
4956
  const stripeSecret = await getVaultSecret(db, "stripe_secret_key");
4845
4957
  if (!group || !stripeSecret || typeof group.stripePublishableKey !== "string") return json({ error: "payments not configured" }, 503);
4846
4958
  const emailNormalized = normalizeChapterEmail(user.email);
@@ -4909,16 +5021,16 @@ var handleGifts = async (req, url, env, ctx) => {
4909
5021
  return invalid();
4910
5022
  }
4911
5023
  if (!/^gift1_[A-Za-z0-9_-]{43}$/.test(code) || !applicationId) return invalid();
4912
- const app = first3((await db.query({ applications: { $: { where: { id: applicationId }, limit: 1 } } })).applications);
5024
+ const app = first4((await db.query({ applications: { $: { where: { id: applicationId }, limit: 1 } } })).applications);
4913
5025
  const emailNormalized = normalizeChapterEmail(user.email);
4914
5026
  if (!app || String(app.groupId ?? "") !== ctx.chapter.id || String(app.emailNormalized ?? "") !== emailNormalized || typeof app.clerkUserId === "string" && app.clerkUserId !== user.userId) return invalid();
4915
5027
  const codeHash = await hashGiftCode(ctx.chapter.id, code);
4916
- const claim = first3((await db.query({ giftClaims: { $: { where: { codeHash }, limit: 1 } } })).giftClaims);
5028
+ const claim = first4((await db.query({ giftClaims: { $: { where: { codeHash }, limit: 1 } } })).giftClaims);
4917
5029
  if (!claim || claim.chapterId !== ctx.chapter.id) return invalid();
4918
5030
  const sameRedemption = (candidate) => candidate.status === "redeemed" && candidate.applicationId === applicationId && candidate.beneficiaryEmailNormalized === emailNormalized && candidate.beneficiaryUserId === user.userId;
4919
5031
  const success = async () => {
4920
- const current = first3((await db.query({ applications: { $: { where: { id: applicationId }, limit: 1 } } })).applications);
4921
- const group2 = first3((await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups);
5032
+ const current = first4((await db.query({ applications: { $: { where: { id: applicationId }, limit: 1 } } })).applications);
5033
+ const group2 = first4((await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups);
4922
5034
  const state = current && group2 ? await continueJoin(db, ctx, env, url.origin, current, group2) : null;
4923
5035
  return json({
4924
5036
  ok: true,
@@ -4937,7 +5049,7 @@ var handleGifts = async (req, url, env, ctx) => {
4937
5049
  return invalid();
4938
5050
  }
4939
5051
  const target = claim.target;
4940
- const group = first3((await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups);
5052
+ const group = first4((await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups);
4941
5053
  const tier = group ? findTier(await loadTiers(db, group), typeof app.tierId === "string" ? app.tierId : null) : void 0;
4942
5054
  if (String(app.status ?? "") !== "submitted" || typeof app.stripeSubscriptionId === "string" || typeof app.giftClaimId === "string" || !tier || tierIsFree(tier) || target?.type !== "tier" || target.tierId !== app.tierId) return invalid();
4943
5055
  const redeemed = await redeemGiftClaim(db, claim, {