@odla-ai/chapter 0.41.0 → 0.42.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.
@@ -1,8 +1,8 @@
1
1
  import * as preact from 'preact';
2
2
  import { ComponentChildren, JSX } from 'preact';
3
3
  import { CrmClient, Crm, CrmRecord } from '@odla-ai/crm';
4
- import { a as Chapter, b as ChapterCopy, M as MemberContentDocument, E as EffectiveMemberContent, c as MemberContentTierAuthority } from '../../copy-context-B14OOiYq.js';
5
- export { C as ChapterCopyProvider, u as useChapterCopy } from '../../copy-context-B14OOiYq.js';
4
+ import { a as Chapter, b as ChapterCopy, M as MemberContentDocument, E as EffectiveMemberContent, c as MemberContentTierAuthority } from '../../copy-context-DuJkH4U9.js';
5
+ export { C as ChapterCopyProvider, u as useChapterCopy } from '../../copy-context-DuJkH4U9.js';
6
6
  import { CrmLifecycleAdapter, CrmWorkspaceMasterContext, CrmWorkspaceRecordContext, RecordPanelTab } from '@odla-ai/crm/ui';
7
7
 
8
8
  /** Normalized public auth configuration consumed by the Clerk gate. */
@@ -33,11 +33,11 @@ import {
33
33
  networkRecordsWorkspace,
34
34
  peopleSection,
35
35
  useAdminResource
36
- } from "../../chunk-O22RRPBT.js";
36
+ } from "../../chunk-MMDWXTJU.js";
37
37
  import {
38
38
  ChapterCopyProvider,
39
39
  useChapterCopy
40
- } from "../../chunk-IAWBE5PX.js";
40
+ } from "../../chunk-JXFYOUDL.js";
41
41
  export {
42
42
  AdminNote,
43
43
  AdminPage,
@@ -1,6 +1,6 @@
1
1
  export { AdminAccountMenuProps, AdminBuilderLibraryData, AdminChrome, AdminMemberContentData, AdminNote, AdminPage, AdminResource, AdminRouteState, AdminRouteTarget, AdminRouting, AdminSection, AdminSectionContext, AdminWorkspace, AdminWorkspaceNavProps, AdmissionGrantsWorkspace, AvailabilitySectionOptions, BillingSectionOptions, BuilderLibraryAdmin, ChapterAdmin, ChapterAdminAuthAdapter, ChapterAdminAuthConfig, ChapterAdminAuthLoadContext, ChapterAdminProps, ChapterAdminUser, CollectionSectionOptions, DashboardSectionOptions, EmailSectionOptions, LeaderNetworkOverview, MeetingsSectionOptions, MemberContentAdmin, NetworkRecordsWorkspace, NetworkShareActions, NetworkShareActionsProps, Panel, PeopleSectionOptions, RecordActions, RecordActionsProps, ThemeWarning, adminFetch, adminRouteFromUrl, adminRouteHref, adminSectionFromUrl, adminSectionHref, admissionGrantsWorkspace, applicationLifecycleAdapter, availabilitySection, billingSection, builderLibrarySection, collectionSection, dashboardSection, defaultAdminSections, defaultAdminWorkspaces, emailSection, loadChapterAdminConfig, loadChapterAdminUser, meetingsSection, memberContentSection, networkRecordsWorkspace, peopleSection, useAdminResource } from './admin/index.js';
2
- export { C as ChapterCopyProvider, u as useChapterCopy } from '../copy-context-B14OOiYq.js';
3
- export { ApiFn, BrandStyle, BrandStyleProps, GiftCodeRedeemer, GiftCodeRedeemerCopy, GiftCodeRedeemerProps, GiftOfferCards, GiftOfferCardsMode, GiftOfferCardsProps, JoinBookingSimulation, JoinConfig, JoinFlowState, JoinIsland, JoinIslandProps, JoinSimulation, JoinStepRenderContext, JoinSubmitRenderContext, JoinTier, JoinTierRenderContext, MEMBERSHIP_CARD_TOKEN_DEFAULTS, MemberContentArea, MemberContentAreaProps, MemberGifts, MemberProvisionalRenderContext, MembersArea, MembersAreaProps, MembershipCardTokenName, MembershipCards, MembershipCardsMode, MembershipCardsProps, PaymentPriceLines, PaymentStep, PaymentStepProps, PaymentStepSimulation, RescheduleProps, Rescheduler, Slot, SlotPicker, SlotPickerClasses, SlotPickerProps, SubResponse, TierPlaceholder, dayKey, dayLabel, fmtDate, fmtMoney, fullLabel, groupSlotsByDay, loadJoinResume, membershipApplicationHref, tierNeedsPayment, timeLabel, tzShort, useTierSelection } from './member/index.js';
2
+ export { C as ChapterCopyProvider, u as useChapterCopy } from '../copy-context-DuJkH4U9.js';
3
+ export { ApiFn, BrandStyle, BrandStyleProps, GiftCodeRedeemResult, GiftCodeRedeemer, GiftCodeRedeemerCopy, GiftCodeRedeemerProps, GiftOfferCards, GiftOfferCardsMode, GiftOfferCardsProps, GiftPurchases, GiftPurchasesProps, JoinBookingSimulation, JoinConfig, JoinFlowState, JoinIsland, JoinIslandProps, JoinSimulation, JoinStepRenderContext, JoinSubmitRenderContext, JoinTier, JoinTierRenderContext, MEMBERSHIP_CARD_TOKEN_DEFAULTS, MemberContentArea, MemberContentAreaProps, MemberGifts, MemberProvisionalRenderContext, MembersArea, MembersAreaProps, MembershipCardTokenName, MembershipCards, MembershipCardsMode, MembershipCardsProps, PaymentPriceLines, PaymentStep, PaymentStepProps, PaymentStepSimulation, RescheduleProps, Rescheduler, Slot, SlotPicker, SlotPickerClasses, SlotPickerProps, SubResponse, TierPlaceholder, dayKey, dayLabel, fmtDate, fmtMoney, fullLabel, groupSlotsByDay, loadJoinResume, membershipApplicationHref, tierNeedsPayment, timeLabel, tzShort, useTierSelection } from './member/index.js';
4
4
  import 'preact';
5
5
  import '@odla-ai/crm';
6
6
  import '@odla-ai/crm/ui';
package/dist/ui/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  GiftCodeRedeemer,
3
3
  GiftOfferCards,
4
+ GiftPurchases,
4
5
  JoinIsland,
5
6
  MEMBERSHIP_CARD_TOKEN_DEFAULTS,
6
7
  MemberContentArea,
@@ -23,7 +24,7 @@ import {
23
24
  timeLabel,
24
25
  tzShort,
25
26
  useTierSelection
26
- } from "../chunk-JLLRZO7B.js";
27
+ } from "../chunk-S7CND3R5.js";
27
28
  import {
28
29
  AdminNote,
29
30
  AdminPage,
@@ -59,12 +60,12 @@ import {
59
60
  networkRecordsWorkspace,
60
61
  peopleSection,
61
62
  useAdminResource
62
- } from "../chunk-O22RRPBT.js";
63
+ } from "../chunk-MMDWXTJU.js";
63
64
  import {
64
65
  BrandStyle,
65
66
  ChapterCopyProvider,
66
67
  useChapterCopy
67
- } from "../chunk-IAWBE5PX.js";
68
+ } from "../chunk-JXFYOUDL.js";
68
69
  import "../chunk-IVXVECKR.js";
69
70
  export {
70
71
  AdminNote,
@@ -76,6 +77,7 @@ export {
76
77
  ChapterCopyProvider,
77
78
  GiftCodeRedeemer,
78
79
  GiftOfferCards,
80
+ GiftPurchases,
79
81
  JoinIsland,
80
82
  LeaderNetworkOverview,
81
83
  MEMBERSHIP_CARD_TOKEN_DEFAULTS,
@@ -1,7 +1,7 @@
1
1
  import * as preact from 'preact';
2
2
  import { ComponentChildren, JSX } from 'preact';
3
- import { d as MemberContentProjection, e as MemberContentPageProjection, b as ChapterCopy, f as ChapterBrand } from '../../copy-context-B14OOiYq.js';
4
- export { C as ChapterCopyProvider, u as useChapterCopy } from '../../copy-context-B14OOiYq.js';
3
+ import { d as MemberContentProjection, e as MemberContentPageProjection, b as ChapterCopy, f as ChapterBrand } from '../../copy-context-DuJkH4U9.js';
4
+ export { C as ChapterCopyProvider, u as useChapterCopy } from '../../copy-context-DuJkH4U9.js';
5
5
  import { FieldType, FieldConditions } from '@odla-ai/crm';
6
6
 
7
7
  /** A bookable slot: a start instant in epoch milliseconds. */
@@ -16,7 +16,7 @@ declare function tzShort(tz: string): string;
16
16
  declare function dayKey(ms: number, tz: string): string;
17
17
  /** Short day label for a day chip, e.g. "Mon, Jun 3". */
18
18
  declare function dayLabel(ms: number, tz: string): string;
19
- /** Time-of-day label for a slot button, e.g. "2:30 PM". */
19
+ /** Time-of-day label for a slot button, e.g. "2:30 PM PST". */
20
20
  declare function timeLabel(ms: number, tz: string): string;
21
21
  /** Full, human confirmation label, e.g. "Monday, June 3, 2:30 PM PST". */
22
22
  declare function fullLabel(ms: number, tz: string): string;
@@ -359,6 +359,7 @@ type JoinFlowState = {
359
359
  } | {
360
360
  step: "payment";
361
361
  applicationId: string;
362
+ giftPurchase?: boolean;
362
363
  } | {
363
364
  step: "paymentPending";
364
365
  applicationId: string;
@@ -443,6 +444,11 @@ interface JoinIslandProps {
443
444
  }) => ComponentChildren;
444
445
  children?: ComponentChildren;
445
446
  };
447
+ /**
448
+ * Authenticated API adapter used to claim a received gift against the current
449
+ * application. When absent, JoinIsland cannot offer recipient claiming.
450
+ */
451
+ giftClaimApi?: ApiFn;
446
452
  /** Run the canonical journey without network, Stripe, or calendar writes. */
447
453
  simulation?: JoinSimulation;
448
454
  }
@@ -553,13 +559,28 @@ interface GiftOfferCardsProps {
553
559
  declare function GiftOfferCards(props: GiftOfferCardsProps): JSX.Element;
554
560
 
555
561
  /** Resolved Chapter copy consumed by {@link GiftCodeRedeemer}. */
556
- type GiftCodeRedeemerCopy = Pick<ChapterCopy["members"]["gifts"], "code" | "redeem" | "redeemed" | "redeemFailed">;
562
+ interface GiftCodeRedeemerCopy {
563
+ code: string;
564
+ redeem: string;
565
+ redeemed: string;
566
+ redeemFailed: string;
567
+ }
568
+ /** Canonical continuation returned after a recipient claim is committed. */
569
+ interface GiftCodeRedeemResult {
570
+ ok: true;
571
+ kind: "application";
572
+ id: string;
573
+ applicationId: string;
574
+ state?: "payment" | "paymentPending" | "booking" | "accepted";
575
+ }
557
576
  /** Props for the canonical Chapter member gift-code redemption control. */
558
577
  interface GiftCodeRedeemerProps {
559
578
  api: ApiFn;
579
+ /** Exact application whose payment requirement the gift must satisfy. */
580
+ applicationId: string;
560
581
  copy: GiftCodeRedeemerCopy;
561
582
  id?: string;
562
- onRedeemed?: () => void | Promise<void>;
583
+ onRedeemed?: (result: GiftCodeRedeemResult) => void | Promise<void>;
563
584
  }
564
585
  /**
565
586
  * Redeems one Chapter gift code through the canonical member API.
@@ -569,14 +590,21 @@ interface GiftCodeRedeemerProps {
569
590
  * browser capitalization and spellcheck are disabled, while
570
591
  * `autocomplete="one-time-code"` allows platform code suggestions.
571
592
  */
572
- declare function GiftCodeRedeemer({ api, copy, id, onRedeemed }: GiftCodeRedeemerProps): preact.JSX.Element;
593
+ declare function GiftCodeRedeemer({ api, applicationId, copy, id, onRedeemed }: GiftCodeRedeemerProps): preact.JSX.Element;
573
594
 
574
- /** Authenticated buyer ledger, checkout, code rotation, and beneficiary redemption. */
575
- declare function MemberGifts(props: {
595
+ /** Props for the authenticated gift purchaser ledger and checkout surface. */
596
+ interface GiftPurchasesProps {
576
597
  api: ApiFn;
577
598
  copy: ChapterCopy["members"]["gifts"];
578
599
  paymentCopy: ChapterCopy["join"]["payment"];
579
- }): preact.JSX.Element | null;
600
+ }
601
+ /** Authenticated buyer offers, purchase history, and code rotation. */
602
+ declare function GiftPurchases(props: GiftPurchasesProps): preact.JSX.Element | null;
603
+ /**
604
+ * Compatibility name for the authenticated gift purchaser surface.
605
+ * @deprecated Use {@link GiftPurchases}; recipient claiming belongs in JoinIsland.
606
+ */
607
+ declare const MemberGifts: typeof GiftPurchases;
580
608
 
581
609
  /** Props for {@link BrandStyle}. */
582
610
  interface BrandStyleProps {
@@ -588,4 +616,4 @@ interface BrandStyleProps {
588
616
  * there is no brand to theme). */
589
617
  declare function BrandStyle(props: BrandStyleProps): preact.JSX.Element | null;
590
618
 
591
- export { type ApiFn, BrandStyle, type BrandStyleProps, GiftCodeRedeemer, type GiftCodeRedeemerCopy, type GiftCodeRedeemerProps, GiftOfferCards, type GiftOfferCardsMode, type GiftOfferCardsProps, type JoinBookingSimulation, type JoinConfig, type JoinFlowState, JoinIsland, type JoinIslandProps, type JoinSimulation, type JoinStepRenderContext, type JoinSubmitRenderContext, type JoinTier, type JoinTierRenderContext, MEMBERSHIP_CARD_TOKEN_DEFAULTS, MemberContentArea, type MemberContentAreaProps, MemberGifts, type MemberProvisionalRenderContext, MembersArea, type MembersAreaProps, type MembershipCardTokenName, MembershipCards, type MembershipCardsMode, type MembershipCardsProps, type PaymentPriceLines, PaymentStep, type PaymentStepProps, type PaymentStepSimulation, type RescheduleProps, Rescheduler, type Slot, SlotPicker, type SlotPickerClasses, type SlotPickerProps, type SubResponse, TierPlaceholder, dayKey, dayLabel, fmtDate, fmtMoney, fullLabel, groupSlotsByDay, loadJoinResume, membershipApplicationHref, tierNeedsPayment, timeLabel, tzShort, useTierSelection };
619
+ export { type ApiFn, BrandStyle, type BrandStyleProps, type GiftCodeRedeemResult, GiftCodeRedeemer, type GiftCodeRedeemerCopy, type GiftCodeRedeemerProps, GiftOfferCards, type GiftOfferCardsMode, type GiftOfferCardsProps, GiftPurchases, type GiftPurchasesProps, type JoinBookingSimulation, type JoinConfig, type JoinFlowState, JoinIsland, type JoinIslandProps, type JoinSimulation, type JoinStepRenderContext, type JoinSubmitRenderContext, type JoinTier, type JoinTierRenderContext, MEMBERSHIP_CARD_TOKEN_DEFAULTS, MemberContentArea, type MemberContentAreaProps, MemberGifts, type MemberProvisionalRenderContext, MembersArea, type MembersAreaProps, type MembershipCardTokenName, MembershipCards, type MembershipCardsMode, type MembershipCardsProps, type PaymentPriceLines, PaymentStep, type PaymentStepProps, type PaymentStepSimulation, type RescheduleProps, Rescheduler, type Slot, SlotPicker, type SlotPickerClasses, type SlotPickerProps, type SubResponse, TierPlaceholder, dayKey, dayLabel, fmtDate, fmtMoney, fullLabel, groupSlotsByDay, loadJoinResume, membershipApplicationHref, tierNeedsPayment, timeLabel, tzShort, useTierSelection };
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  GiftCodeRedeemer,
3
3
  GiftOfferCards,
4
+ GiftPurchases,
4
5
  JoinIsland,
5
6
  MEMBERSHIP_CARD_TOKEN_DEFAULTS,
6
7
  MemberContentArea,
@@ -23,18 +24,19 @@ import {
23
24
  timeLabel,
24
25
  tzShort,
25
26
  useTierSelection
26
- } from "../../chunk-JLLRZO7B.js";
27
+ } from "../../chunk-S7CND3R5.js";
27
28
  import {
28
29
  BrandStyle,
29
30
  ChapterCopyProvider,
30
31
  useChapterCopy
31
- } from "../../chunk-IAWBE5PX.js";
32
+ } from "../../chunk-JXFYOUDL.js";
32
33
  import "../../chunk-IVXVECKR.js";
33
34
  export {
34
35
  BrandStyle,
35
36
  ChapterCopyProvider,
36
37
  GiftCodeRedeemer,
37
38
  GiftOfferCards,
39
+ GiftPurchases,
38
40
  JoinIsland,
39
41
  MEMBERSHIP_CARD_TOKEN_DEFAULTS,
40
42
  MemberContentArea,
@@ -2009,7 +2009,12 @@ var handleMember = async (req, url, env, ctx) => {
2009
2009
  const group = Array.isArray(groups) ? groups[0] : void 0;
2010
2010
  if (!group) return json({ error: "not found" }, 404);
2011
2011
  const state = await continueJoin(db, ctx, env, url.origin, app, group);
2012
- if (state) return json({ state, applicationId, ...state === "gift" ? { claimId: app.giftClaimId } : {} });
2012
+ if (state) return json({
2013
+ state,
2014
+ applicationId,
2015
+ ...state === "gift" ? { claimId: app.giftClaimId } : {},
2016
+ ...state === "payment" && typeof app.giftOfferId === "string" ? { giftPurchase: true } : {}
2017
+ });
2013
2018
  return json({ error: `application cannot resume from status "${String(app.status ?? "")}"` }, 409);
2014
2019
  }
2015
2020
  if (req.method === "POST" && url.pathname === "/api/applications") {
@@ -2058,6 +2063,7 @@ var handleMember = async (req, url, env, ctx) => {
2058
2063
  duplicate: result.duplicate,
2059
2064
  status: result.status,
2060
2065
  ...state ? { state } : {},
2066
+ ...state === "payment" && requestedGiftId ? { giftPurchase: true } : {},
2061
2067
  // Echoed so a site can see (and assert in an integration test) whether its
2062
2068
  // join page actually posted the ack. Absent consent is otherwise invisible.
2063
2069
  disclaimerAckAt: result.disclaimerAckAt
@@ -3756,9 +3762,15 @@ async function redeemGiftClaim(db, claim, beneficiary, now) {
3756
3762
  const target = claim.target;
3757
3763
  const ops = [];
3758
3764
  let result;
3765
+ let applicationGuard;
3759
3766
  if (target.type === "tier") {
3760
- const apps = (await db.query({ applications: { $: { where: { emailNormalized }, limit: 20 } } })).applications;
3761
- const existing = apps?.filter((row) => row.groupId === chapterId).sort((a, b) => Number(b.createdAt ?? 0) - Number(a.createdAt ?? 0))[0];
3767
+ const apps = beneficiary.applicationId ? (await db.query({ applications: { $: { where: { id: beneficiary.applicationId }, limit: 1 } } })).applications : (await db.query({ applications: { $: { where: { emailNormalized }, limit: 20 } } })).applications;
3768
+ const existing = beneficiary.applicationId ? apps?.[0] : apps?.filter((row) => row.groupId === chapterId).sort((a, b) => Number(b.createdAt ?? 0) - Number(a.createdAt ?? 0))[0];
3769
+ if (beneficiary.applicationId && (!existing || String(existing.groupId ?? "") !== chapterId || String(existing.emailNormalized ?? "") !== emailNormalized || String(existing.tierId ?? "") !== target.tierId || String(existing.status ?? "") !== "submitted" || typeof existing.stripeSubscriptionId === "string" || typeof existing.giftClaimId === "string" || typeof existing.clerkUserId === "string" && existing.clerkUserId !== beneficiary.userId)) return null;
3770
+ if (beneficiary.applicationId) applicationGuard = {
3771
+ tierId: target.tierId,
3772
+ ...typeof existing?.clerkUserId === "string" ? { clerkUserId: existing.clerkUserId } : {}
3773
+ };
3762
3774
  const id = existing ? String(existing.id) : `gift-application:${String(claim.id)}`;
3763
3775
  const status = existing && existing.status !== "submitted" ? String(existing.status) : "paid_pending_vetting";
3764
3776
  ops.push({ t: "update", ns: "applications", id, attrs: {
@@ -3775,6 +3787,7 @@ async function redeemGiftClaim(db, claim, beneficiary, now) {
3775
3787
  } });
3776
3788
  result = { kind: "application", id };
3777
3789
  } else {
3790
+ if (beneficiary.applicationId) return null;
3778
3791
  const id = `gift-entitlement:${String(claim.id)}`;
3779
3792
  ops.push({ t: "update", ns: "giftEntitlements", id, attrs: {
3780
3793
  id,
@@ -3798,13 +3811,27 @@ async function redeemGiftClaim(db, claim, beneficiary, now) {
3798
3811
  attemptCount: Number(claim.attemptCount ?? 0) + 1
3799
3812
  } });
3800
3813
  try {
3814
+ const guards = [{ ns: "giftClaims", id: String(claim.id), exists: true, equals: {
3815
+ status: "pending",
3816
+ codeVersion: Number(claim.codeVersion),
3817
+ codeHash: String(claim.codeHash)
3818
+ } }];
3819
+ if (beneficiary.applicationId) guards.push({
3820
+ ns: "applications",
3821
+ id: beneficiary.applicationId,
3822
+ exists: true,
3823
+ equals: {
3824
+ status: "submitted",
3825
+ groupId: chapterId,
3826
+ emailNormalized,
3827
+ tierId: applicationGuard.tierId,
3828
+ ...applicationGuard?.clerkUserId ? { clerkUserId: applicationGuard.clerkUserId } : {}
3829
+ },
3830
+ absent: ["stripeSubscriptionId", "giftClaimId"]
3831
+ });
3801
3832
  await db.transact(ops, {
3802
3833
  mutationId: `gift-redeem:${String(claim.id)}:${beneficiary.userId}`,
3803
- guards: [{ ns: "giftClaims", id: String(claim.id), exists: true, equals: {
3804
- status: "pending",
3805
- codeVersion: Number(claim.codeVersion),
3806
- codeHash: String(claim.codeHash)
3807
- } }]
3834
+ guards
3808
3835
  });
3809
3836
  return result;
3810
3837
  } catch {
@@ -4319,15 +4346,35 @@ var handleGifts = async (req, url, env, ctx) => {
4319
4346
  return json({ error: "too many attempts" }, 429);
4320
4347
  }
4321
4348
  let code = "";
4349
+ let applicationId = "";
4322
4350
  try {
4323
- code = String(JSON.parse(await req.text()).code ?? "").trim();
4351
+ const body = JSON.parse(await req.text());
4352
+ code = String(body.code ?? "").trim();
4353
+ applicationId = typeof body.applicationId === "string" ? body.applicationId : "";
4324
4354
  } catch {
4325
4355
  return invalid();
4326
4356
  }
4327
- if (!/^gift1_[A-Za-z0-9_-]{43}$/.test(code)) return invalid();
4357
+ if (!/^gift1_[A-Za-z0-9_-]{43}$/.test(code) || !applicationId) return invalid();
4358
+ const app = first3((await db.query({ applications: { $: { where: { id: applicationId }, limit: 1 } } })).applications);
4359
+ const emailNormalized = normalizeChapterEmail(user.email);
4360
+ if (!app || String(app.groupId ?? "") !== ctx.chapter.id || String(app.emailNormalized ?? "") !== emailNormalized || typeof app.clerkUserId === "string" && app.clerkUserId !== user.userId) return invalid();
4328
4361
  const codeHash = await hashGiftCode(ctx.chapter.id, code);
4329
4362
  const claim = first3((await db.query({ giftClaims: { $: { where: { codeHash }, limit: 1 } } })).giftClaims);
4330
4363
  if (!claim || claim.chapterId !== ctx.chapter.id) return invalid();
4364
+ const sameRedemption = (candidate) => candidate.status === "redeemed" && candidate.applicationId === applicationId && candidate.beneficiaryEmailNormalized === emailNormalized && candidate.beneficiaryUserId === user.userId;
4365
+ const success = async () => {
4366
+ const current = first3((await db.query({ applications: { $: { where: { id: applicationId }, limit: 1 } } })).applications);
4367
+ const group2 = first3((await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups);
4368
+ const state = current && group2 ? await continueJoin(db, ctx, env, url.origin, current, group2) : null;
4369
+ return json({
4370
+ ok: true,
4371
+ kind: "application",
4372
+ id: applicationId,
4373
+ applicationId,
4374
+ ...state ? { state } : {}
4375
+ });
4376
+ };
4377
+ if (sameRedemption(claim)) return success();
4331
4378
  if (claim.status !== "pending" || Number(claim.codeExpiresAt) <= now) {
4332
4379
  await db.transact([{ t: "update", ns: "giftClaims", id: String(claim.id), attrs: {
4333
4380
  attemptCount: Number(claim.attemptCount ?? 0) + 1,
@@ -4335,8 +4382,18 @@ var handleGifts = async (req, url, env, ctx) => {
4335
4382
  } }]).catch(() => void 0);
4336
4383
  return invalid();
4337
4384
  }
4338
- const redeemed = await redeemGiftClaim(db, claim, { email: user.email, userId: user.userId }, now);
4339
- return redeemed ? json({ ok: true, ...redeemed }) : invalid();
4385
+ const target = claim.target;
4386
+ const group = first3((await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups);
4387
+ const tier = group ? findTier(await loadTiers(db, group), typeof app.tierId === "string" ? app.tierId : null) : void 0;
4388
+ 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();
4389
+ const redeemed = await redeemGiftClaim(db, claim, {
4390
+ email: user.email,
4391
+ userId: user.userId,
4392
+ applicationId
4393
+ }, now);
4394
+ if (redeemed) return success();
4395
+ const raced = await loadClaim(db, String(claim.id));
4396
+ return raced && sameRedemption(raced) ? success() : invalid();
4340
4397
  }
4341
4398
  return null;
4342
4399
  };