Skip to main content

Products and Commerce Feature Context

Source: .agents/references/features/products-and-commerce.md

Content

Products and Commerce Feature Context

Purpose

Products and commerce cover the catalog and detail experiences for courses, events, challenges, memberships, digital files, forms, sessions, physical products, unified product cards, and affiliate commerce surfaces. The domain connects public discovery, product detail pages, availability, enrollment, pricing, and checkout entry.

User-Facing Workflows

  • Visitors browse product cards on community pages and open product detail pages.
  • Buyers choose product variants or tiers, inspect pricing and metadata, then enter checkout.
  • Members see enrolled/access states, resource view pages, event attendance, course/file access, and challenge participation.
  • Creators/admin users may configure product behavior through related admin/pro surfaces, but public product rendering should stay decoupled from admin-only assumptions.
  • Physical products include image carousels, variant pages, shipping/location availability, and inquiry analytics.
  • Product and business-setting media forms can open Magic Content V2 in place from AI Everywhere media actions, including challenge, event, course, 1:1 session, digital file/folder, physical product, and community/business settings cover uploads.

Key Entrypoints

  • Routes:
    • src/pages/[communityLink]/products/[productSlug]/
    • src/pages/[communityLink]/products.page.js
    • src/pages/[communityLink]/courses/[courseSlug].page.tsx
    • src/pages/[communityLink]/courses/[courseSlug]/view.page.tsx
    • src/pages/[communityLink]/events/[eventSlug]/index.page.js
    • src/pages/[communityLink]/challenges/[challengeSlug]/
    • src/pages/[communityLink]/digital-files/[digitalFileSlug].page.tsx
    • src/pages/[communityLink]/digital-files/[digitalFileSlug]/view.page.tsx
    • src/pages/[communityLink]/forms/[formSlug].page.tsx
    • src/pages/[communityLink]/sessions/
    • src/pages/[communityLink]/physical-products/
  • Components/features:
    • src/components/common/EntityMarketingSection/
    • src/components/features/CommunityPage/components/allProducts/
    • src/features/CommunityProductPage/
    • src/features/EventPublicPage/
    • src/features/ResourcePage/
    • src/features/1on1Sessions/
    • src/features/Affiliates/
    • src/pages/portal/components/MultimediaUpload/
    • src/pages/[communityLink]/physical-products/components/
    • src/pages/portal/physical-products/create/components/steps/RearrangeImagesStep/
  • Hooks:
    • src/hooks/useEntityPrice.ts
    • src/hooks/useMembershipTiers.tsx
    • src/hooks/useManageCommunityEvents.js
    • src/hooks/member/useCommunityEventsData.js
    • src/pages/[communityLink]/physical-products/usePhysicalProductLandingPage.ts
  • Services:
    • src/services/unifiedProduct/unifiedProductServices.ts
    • src/services/courseService.js
    • src/services/calendarService.js
    • src/services/leadFormsService/
    • src/services/referral/referralService.ts
  • Utilities:
    • src/utility/productUtils.ts
    • src/utility/eventUtils.js
    • src/utility/checkoutUtils.js
    • src/pages/[communityLink]/physical-products/utils.ts

Data Flow and Service Boundaries

  • Unified product data powers product cards and many discovery views. Check src/services/unifiedProduct/ and product card helpers before changing labels, CTA state, or pricing.
  • Lead forms are a unified product type for public discovery and creator display management, but they are not priced purchase cards: public lead form cards route to the form public page with a view CTA and should not show price UI; reorder/display-management rows can omit product details for lead forms.
  • Detail pages may use route-level static props, client hooks, and product-specific services. Inspect the page file before adding fetch logic to a nested component.
  • Product access state is not the same as public visibility. A product can be visible but unavailable, sold out, enrolled, pending, inactive, location-restricted, or gated by membership.
  • For public TICKET_TYPES events, default selection uses the first non-archived ticket that is open and not sold out. Registration availability follows the selected ticket rather than legacy event-level sold-out state.
  • Event attendee message drafts retain the active ticket-type filter. Ticket-scoped registration email fetch, update, reset, and test requests all carry the same ticket type ID.
  • Legacy event and ticket discounts with no isActive field remain visible; only explicit isActive: false entries are hidden.
  • Event creation at /portal/event/add renders every start/end time option with the same zero-padded hh:mm AM/PM label while preserving the existing minute values, disabled-option rules, and submitted scheduling data.
  • Event management Pending approval, Approved, and Rejected email editors present an existing serialized lockSection as Event summary with an editable CTA label, defaulting missing button text to View event. Content without a lockSection node must not render or synthesize the protected block. Existing custom CTA text and backend preview metadata remain intact; other event mail types keep their current behavior.
  • Unsaved membership tiers have no canonical tier ID, so Welcome message editing reuses EmailEditorSideSheet as a single local deferred draft. Editor Save validates and updates the tier's local subject/content, while saveSelectedTier performs the community-mail update after pricing creates the tier.
  • Every successful membership-tier pricing mutation (save, enable/disable, reorder, or delete) refreshes both the shared tier query and the active-community snapshot. These independent refreshes run in parallel, and the active-community refresh stays silent so membership updates do not trigger a portal-wide loading state.
  • Persisted Edit Tier sheets load every tier-scoped COMMUNITY_ENROLMENT row in backend response order. Each row uses the shared Magic Reach detail/update/reset/test flow with title, timing, subject, and body; the Membership caller omits enablement and deletion UI, preserves row isEnabled, and retains Member name / Business name variables plus the CUSTOM_PRODUCT_EMAILS restriction UI. A failed empty sequence response falls back to the legacy single-message entry so editing is not blocked. Both unsaved and persisted Welcome rows use the established labeled neutral-primary small Edit button; Magic Reach rows remain icon-only. Both Membership editor paths use {name} as Member name and {community_name} as Business name for subject/body suggestions and the legend, refreshing serialized variable labels on initial load and reset.
  • The membership tier “Add another email” handoff targets /portal/magic-reach?activeTab=automatedEmails&tierId=<tierId>. New and existing tiers both show Save tier to continue and must pass the tier validation/save pipeline first, so current form and welcome-message edits persist before navigation. Redirect only after the shared membership-tier list refreshes and the save returns the canonical tier ID.
  • Membership Add/Edit Tier uses the same compact add-action geometry for Add another plan and both new-tier and persisted-tier Add another email controls: small NPL button with plus-circle lead icon and yellow plain_accent hierarchy. Copy and click behavior remain action-specific.
  • New-event Tickets preserve the existing membership-tier access gate: show All members / By tiers only when more than one tier satisfies isActive || hasActiveSubscription; derive the selected mode from tieredPrices.isActive, retain both ticket drafts and tier-price drafts when switching modes, and keep one shared capacity, bulk-registration, questions, and approval block beneath the tier rows in By tiers.
  • New-event All members Flexible pricing seeds Minimum and Suggested with real controlled values of 5 and 10 only on first enable, synchronizing legacy amounts as 500 and 1000 cents. After a user clears either field, preserve the empty controlled value and undefined legacy amount across off/on toggles, showing 0 as the placeholder. Existing typed values remain intact across toggles.
  • Edit-event All members Tickets keep ticket names editable, always expose Add ticket, and show Delete on every card when more than one ticket draft exists; the final remaining ticket cannot be deleted.
  • Animated event-ticket cards keep their rounded 1px visual edge on the height-clipping motion wrapper, while measured inner content remains borderless so add/reflow animations cannot clip the card edge.
  • Edit-event pricing always uses the new Tickets experience, including membership-tier and legacy events without pricingMode. The All members / By tiers segment is create-only; tier edits render a static By Tiers editor.
  • Edit-event dirty state shows the 112px sticky gradient footer used by create-event: 24px horizontal and bottom padding, 40px top fade, 12px gap, equal-width Cancel then Save buttons.
  • Edit-event ticket deletion uses the admin-event goingAttendees count for the persisted legacy ticket: tickets with sales show the non-destructive blocked notice, while zero-sale and newly added ticket drafts show a confirmation before removal. Replace this legacy count with per-ticket sales data when the backend exposes persisted multiple ticket types; do not infer per-ticket sales by splitting the event total.
  • Manage-event Tickets has separate closure scopes: Close ticket registration stays inside the ticket card and updates the event ticket contract with the complete ticket-type list; it must not mutate the event-level isRegistrationClosed field. Serialize rapid ticket toggles from cumulative desired state so no concurrent stale full-list PUT can revert another ticket, then authoritatively revalidate after the final intent. Closing or reopening it immediately shows a localized toast. All-members tickets include the displayed ticket name; By Tiers uses generic ticket-registration copy without a ticket name. A ticket-toggle action atomically replaces the prior toast with its latest state, so rapid toggles never stack or suppress messages. Ticket Edit remains enabled regardless of ticket-level or event-level registration closure. Flexible-price summaries show the localized Suggested label and pricingConfig.suggestedAmount; they must not interpret the absent fixed pricingConfig.amount as Free. Close event registration remains under More and owns the existing isRegistrationClosed mutation.
  • Manage-event attendee tables show the Ticket column only when multiple non-archived ticket types exist. The attendee API must provide both the event-level multiple-ticket indicator and each attendee's purchased ticket name; keep any pre-contract UI preview explicitly mocked and isolated for later replacement.
  • Manage-event Add attendees sends root ticketTypeObjectId for every pricingMode=TICKET_TYPES event. Show the Figma ticket selector only when more than one non-archived ticket accepts registrations; with one eligible ticket, hide the selector and submit that sole ticket ID. Single-ticket and membership-tier events retain the legacy attendee-only payload.
  • Manage-event attendee removal offers a refund only when the selected attendee is not manually added and the attendee response's authoritative paidAmount.amount is finite and greater than zero. Zero, negative, missing, or non-finite paid amounts keep the remove-only flow; legacy flat/local amount fields do not establish refund eligibility. The backend resolves the exact transaction from the attendee ID. Removal requests send only requestRefund and the optional trimmed note; the note is included in the removal email for both refund and remove-only flows. For refunds, the backend also uses a non-empty note as the internal refund reason and defaults an omitted note to Attendee removed by community manager; new frontend code must not send the backward-compatible refundReason field. Removal confirmation copy uses the attendee email when the attendee name is null, missing, or blank.
  • Public event ticket-viewer badges prefer each incoming ticket's purchased ticketName in array order, then the purchase-level ticketType.name. When both are absent, render the localized generic Ticket label; never fabricate a specific ticket-type name.
  • Public event attendee copy follows the authoritative sold-out state, not event-level remaining capacity. Unlimited-capacity events use Join <count> other people; sold-out events use <count> people are going.
  • Public multi-ticket registration rows render flexible ticket pricing as localized Pay what you want; suggested and minimum amounts remain checkout guidance and must not be formatted as a fixed ticket price.
  • Public multi-ticket quantity controls cap each selection at the lower of that ticket's maxQuantityPerPurchase and known remaining capacity. Disable increment at that effective limit and retain the state-level clamp; when remaining capacity is unknown, use the per-purchase limit alone.
  • Event checkout carries the selected ticket type ID to pricing/signup APIs without duplicating the ticket name in URL metadata. Checkout resolves the selected ticket name from the public event API by that ID; the quantity row falls back to localized Tickets only when the API ticket name is unavailable.
  • Public TICKET_TYPES registration derives checkout routing from the selected ticket rather than the legacy event-level price. Paid fixed, flexible, approval-required, and event-application selections use checkout; a selected zero-amount fixed ticket can register directly. A free sibling ticket must never bypass payment for a paid selection.
  • Public TICKET_TYPES events with exactly one non-archived ticket use the legacy-style single-ticket CTA instead of a redundant selector, while Register still submits that ticket's ID and selected quantity. Events with zero or multiple non-archived tickets retain the multi-ticket CTA.
  • Purchase-history event cards append the purchased ticketType.name from the member purchase-history response to the event date/time metadata with a separator. Render the API's required quantity as <ticket type name> x<quantity>, including x1; preserve the legacy date/time-only label when ticket type is absent.
  • Registered public-event attendees always see the registered card instead of the inline ticket selector, including pricingMode=TICKET_TYPES events. When another purchase is available, Buy more tickets opens a separate selector and preserves the selected ticket type ID and quantity through checkout. Free-ticket registration keeps this modal open with Register loading while the request is pending, closes it after a successful signup response, and leaves it open after failure so the attendee can retry. Upcoming registered attendees see View my ticket(s) as the first action only for in-person events; online events omit the ticket-view action. Its count sums every active attendee purchase quantity, and opening the viewer loads each purchase's private attendee record in parallel before displaying every returned ticket reference in purchase order. Legacy single-attendee responses remain a fallback.
  • Public legacy single-ticket and membership-tier CTA summaries display the first non-archived ticketTypes[].name, then the trimmed legacy ticketName, falling back to localized General Admission only when both are missing or blank. Production ticket UI must not accept development mock-ticket overrides.
  • Public event attendance cancellation is transaction-scoped: keep duplicate ticket types from separate checkouts as distinct rows, target actions by transactionId, and cancel all represented tickets in that transaction together without changing other transactions. Until the authoritative transaction API exists, keep permutation fixtures and their switcher development-only and preserve the production attendee flow.
  • Manage-event Close ticket registration uses the exact tooltip copy This ticket will no longer accept new registrations; keep both its info trigger and toggle programmatically named while preserving optimistic ticket-local state during the serialized backend update.
  • Manage-event headers for pricingMode=TICKET_TYPES replace the legacy event access row with one 40px ticket-icon row per non-archived ticket. Preserve the creator's exact ticket-name casing; show fixed price, flexible suggested price plus the localized Suggested badge, or Free. Join localized approval and capacity subtitles with ; preserve the legacy row for single-ticket and membership-tier events.
  • Event-ticket discount creation keeps the disabled scope select's nested event-and-ticket pill border fully visible while preserving truncation for its text and for unrelated shared selects.
  • Event-ticket discount creation persists immediately through the existing community discount API while editing an event; new-event discount creation remains a local ticket draft until the event is created.
  • New-event submit excludes event-level and ticket-local discount drafts from the event request, waits for the successful event response, then creates discounts through the existing community discount API using the returned event ID and matching returned ticket-type ID. Event creation failure must not issue discount requests; a later discount failure must not cause the already-created event to be submitted again.
  • Persisted event-ticket discounts include the selected ticket's _id as ticketTypeObjectId alongside the event linked entity so the discount does not apply to sibling ticket types.
  • In edit-event ticket cards, removing any persisted discount opens the shared disable confirmation; discounts without a persisted _id remain locally removable drafts.
  • Confirming persisted ticket-discount removal disables the discount immediately through the shared discount mutation and removes its _id from ticket cards only after success; failures preserve the UI.
  • Event edit ticket cards hide fetched discounts with isActive: false for both legacy and ticket-type responses; missing flags remain visible for backward compatibility.
  • Discount applies-to event rows render the event name and each selected active ticket name separated by ; comma-separated ticket lists and generated ticket / tickets suffixes are not part of the label.
  • Event-form accordions use CSS transitions with coordinated panel/grid opacity timing: 220ms open, 170ms close, a 180ms transform-only chevron, and instant state changes under prefers-reduced-motion.
  • Physical product pages have separate layout, carousel, checkout, and variant concerns under src/pages/[communityLink]/physical-products/.
  • Physical-product email sidesheets use mail-type-specific variables through the shared email editor:
    • MEMBER_PHYSICAL_PRODUCT_ORDER_CREATED: &#123;name&#125; (Member name), &#123;product_title&#125; (Product title), &#123;product_variant&#125; (Product variant), &#123;community_name&#125; (Business name).
    • MEMBER_PHYSICAL_PRODUCT_ORDER_SHIPPED: the same ordered variables with &#123;order_number&#125; (Order number) before &#123;community_name&#125;. The resolved list must drive subject and body suggestions/highlights plus the ordered guidance legend; saves keep the raw placeholder identifiers.
  • Persisted digital products under /portal/digital-files/manage and courses under /portal/courses/manage expose an Emails tab immediately after Marketing; shared-folder variants retain their existing tabs. The tab contains the product-scoped Order Confirmation email for COMMUNITY_FOLDER_PURCHASE_MEMBER and reuses the existing community-mail permission, load, reset, test, save, discard, and responsive sidesheet behavior. Its ordered variable list drives subject/body highlighting and suggestions plus the guidance legend: &#123;name&#125; (Member name), &#123;product_title&#125; (Product title), and &#123;community_name&#125; (Business name). Saves preserve these raw placeholder identifiers and pass the digital-product or course ID through the existing mailCourseOffer mapping. The email list uses the same responsive 960px horizontal grid as Content. Serialized lockSection nodes are permanent protected email structure: editing their CTA label remains allowed, but editor deletion must not remove the node.
  • One-on-one session email sidesheets use one ordered variable list for subject/body tokens and the single-bullet localized guidance rows: &#123;name&#125; (Member name), &#123;community_name&#125; (Business name), and &#123;session_title&#125; (Session title). Display-label changes must not change the raw placeholders sent to the backend. The localized row template already supplies ; do not add CSS list markers. Its locked Session summary uses the NPL 1-1-session icon.
  • Challenge Confirmation email uses an explicit ordered legend and variable list: &#123;challenge_title&#125; (Challenge title), &#123;participant_name&#125; (Participant name), &#123;checkpoint_count&#125; (Number of checkpoints), and &#123;community_name&#125; (Business name). Do not show the generic &#123;name&#125; legend. Scheduled Challenge start/end date and time variables remain available after these four confirmation variables.
  • Challenge Checkpoint start email uses the ordered base legend &#123;challenge_title&#125; (Challenge title), &#123;checkpoint_title&#125; (Checkpoint title), and &#123;participant_name&#125; (Participant name). The same labels drive subject/body tokens. Fixed-date challenges insert &#123;checkpoint_start_date&#125; (Checkpoint start date), &#123;checkpoint_end_date&#125; (Checkpoint end date), &#123;checkpoint_start_time&#125; (Checkpoint start time), and &#123;checkpoint_end_time&#125; (Checkpoint end time) before Participant name in both editor options and the legend. Challenge email lists must resolve guidance by selected mail type and challenge schedule instead of falling back to generic member/business guidance. Variable suggestions must expose every supplied option and scope cached results to the active variable set so switching email types cannot reuse stale choices.
  • Challenge Declared winners email uses one ordered variable list for editor tokens and its legend: &#123;challenge_title&#125; (Challenge title), &#123;participant_name&#125; (Participant name), and &#123;completed_checkpoint_count&#125; (Completed checkpoint count). Do not show generic &#123;name&#125; or &#123;community_name&#125; guidance for this mail type. Leaderboard-specific editor variables remain governed by the existing leaderboard behavior and do not alter this three-row legend. Its serialized lockSection uses the button-only locked wrapper: keep the editable CTA and omit the Challenge summary card. Other Challenge email variants retain their summary cards.
  • Challenge “Marked participants as failed” email uses the failed-participant button-only locked variant. Its subject/body tokens and legend share the ordered variables &#123;challenge_title&#125; (Challenge title), &#123;participant_name&#125; (Participant name), and &#123;failed_reason&#125; (Failed reason); do not show generic member/business guidance or change the raw placeholders. Its serialized lockSection.buttonText renders and edits through the shared button-label modal even when the response omits lockedSection preview metadata; other locked summary variants still require preview data. If the node label is absent on initial load or reset, initialize it to localized Open Challenge; preserve existing custom labels and keep the node as the editable/saveable source. Preserve the full-width CTA, dotted underline, edit-04 tail icon, and backend save, reset, and test behavior.
  • Persisted physical-product Details exposes Emails between Shipping & Return policy and Collect additional info from InventoryShippingCard; pre-create forms without _id do not show the entry. Details action rows are semantic full-width buttons and must retain their established 48px geometry, text alignment, hover treatment, and keyboard activation. The 480px page-local list sidesheet reuses CustomizeEmailMessages for Order Confirmation (MEMBER_PHYSICAL_PRODUCT_ORDER_CREATED) and Shipping Confirmation (MEMBER_PHYSICAL_PRODUCT_ORDER_SHIPPED), passing the physical-product ID as mailCourseOffer through the existing community-mail service flow. Per-email saves happen in the shared 560px editor; the parent list footer only closes navigation and must not introduce an aggregate draft/save API. Keep CUSTOM_PRODUCT_EMAILS gating, backend reset/test behavior, locked content, and responsive full-width narrow sheets intact. Product email editors never render the Enable this email toggle, even when a backend response exposes allowDisableToggle; preserve the response isEnabled value in update payloads. Physical-product Figma variants stay caller-scoped: the list uses a black-25 backdrop, Order Confirmation and Shipping Confirmation both use a 500px editable area, and any other physical mail type retains the 278px caller fallback. Variable-guidance rows indent 8px, and skeleton title and description lines are 20px and 22px. Shared SideSheet and email-editor defaults remain black-50 and 420px for every other caller. On initial load and reset, normalize legacy physical-product &#123;product_name&#125; subject placeholders, body variable nodes, and body plain placeholders to &#123;product_title&#125; with the Product title label before opening the editor; saves must not preserve the legacy placeholder identity.
  • Product/media upload flows use shared Magic Content V2 handoffs. EntityMarketingRunAdsAssetModal owns the AI Everywhere asset modal; its New Product option is local UI state only and must not be inserted into product-list API data or productRefs. Selecting that option and clicking Create with AI opens MagicContentCreationSteps in start-from-scratch mode. Selecting an existing product preserves the product-linked Magic Content flow.
  • Magic Content prompt generation must identify EXTERNAL_PRODUCT items with externalProductObjectId and omit entityObjectId, including external products returned through the NAS product list and external-product reuse through Generate Another. Internal NAS products continue to use entityObjectId.
  • Magic Content generated media is the only product/media cover media eligible for AI edit. Use the shared MultimediaUpload helpers before showing edit affordances; non-Magic Content uploaded or saved media should not expose AI edit.
  • Affiliate behavior can affect product CTAs and referral codes. Search Affiliates and referral before changing join/purchase links.

Conventions and Gotchas

  • Product type labels and CTA labels should use existing helpers where available. Avoid introducing parallel string switches.
  • Keep pricing and availability checks close to the helper/hook that already owns product state.
  • Do not assume all product types share the same slug or ID shape.
  • After client resource and community-role data confirm access, public digital-product and folder landing pages replace their URL with /&#123;communityLink&#125;/products/&#123;resourceSlug&#125;/view. Use the canonical products member-view route for both automatic redirects and Access CTAs; do not redirect from cached static data.
  • Preserve access-control redirects and view pages for member-only product content.
  • Use exact Figma spacing and NPL typography tokens for design-driven product cards.
  • Avoid deriving product display state in useEffect; derive from product, member, and enrollment data during render unless synchronizing with an external system.
  • Processing Magic Content image/video cover media should render the shared Magic Content processing visual. Product and business-setting forms should warn before closing when unsaved processing Magic Content media would be discarded, while ordinary uploaded media keeps the existing generic unsaved-change warning.
  • .agents/rules/clean-react.mdc
  • .agents/rules/clean-typescript.mdc
  • .agents/rules/no-try-catch-service-api.mdc
  • .agents/rules/direct-translation-and-component-files.mdc
  • .agents/references/coding-standard/figma-guidance.md
  • .agents/references/coding-standard/useeffect-guidance.md

Useful Graph Queries and Fallback Searches

Use the terms below with search_graph/search_code, then run trace_path on the central product or commerce symbols. Run these shell searches only as documented fallbacks.

rtk rg "UnifiedProduct|productSlug|entityPrice|membershipTier|physicalProduct|leadForms|variant|MagicContentCreationSteps|isMagicContentGeneratedMedia" src/pages/[communityLink] src/components/features src/features src/hooks src/services src/pages/portal src/components/common
rtk rg "ProductCardCTA|ProductCardPriceDisplay|ProductCardSubtitle|ProductFilter|LEAD_FORM" src/components/features/CommunityPage src/pages/portal/products/unified-product
rtk find src/pages/[communityLink]/physical-products -maxdepth 5 -type f

Update Triggers

Update this reference when product type behavior, unified product contracts, product card CTAs, pricing, access checks, creator display ordering/visibility, physical product flows, product/media Magic Content handoffs, or cover-media edit and discard-warning rules change.