P PhilocalyLuxury & Ethically Sourced Human Hair Extensions
Theme: Philocaly (Shopify) · by Signifly

Philocaly Documentation

Storefront Documentation

How the Philocaly Shopify storefront is configured — product pages and cards, the metafields and metaobjects that power them, the B2B loyalty program, course video management, preorders & split shipments, the Chatters portal, pro vs. regular pricing, and the developer apps (checkout validation, checkout UI extension, Chatters API). Use the sidebar to jump to a topic.


Guides

Pick a topic to get started. Each guide maps directly to how things are configured in the Shopify admin.

01

Product Setup Guide

How products, the PDP and product cards are configured — images, info blocks, and the metafields & metaobjects behind them.

Read guide →
02

Metaobject Entry Guide

Step-by-step instructions for creating each metaobject entry and linking it to a product or variant.

Read guide →
03

B2B Loyalty Program

Tiers, benefits and how points, discounts, shipping and birthday rewards are handled across Loyalty Lion and Shopify.

Read guide →
04

Course Video Management

The Shopify + Vimeo workflow — upload videos, register them, and organise them into ordered course groupings.

Read guide →
05

Preorders & Split Shipments

How native preorders work — what customers see, split fulfillment behind the scenes, and the shipping setup.

Read guide →
06

Chatters

The Chatters x Philocaly portal — custom pricing, payment-free checkout, monthly order consolidation and HQ invoicing.

Read guide →
07

Pro Pricing & Hidden Prices

Why and how regular (D2C) prices are hidden for professional-only products, and how the B2B pro discount is applied.

Read guide →
08

Checkout Validation Dev

A Shopify Function that blocks checkout when a non-professional customer tries to buy professional-only (B2B) products.

Read guide →
09

Checkout UI Extension Dev

A checkout extension that clears the cart on logout, so a shared device never inherits a previous (B2B) shopper's cart.

Read guide →
10

Chatters API Dev

A Node/Express service that creates Shopify orders for ambassadors with discounts, shipping and net payment terms.

Read guide →

Product Setup Guide

01 Key concepts: metafields vs. metaobjects

Most of what appears on a Philocaly product page is not typed into the standard Shopify product fields. It comes from custom data structures. Understanding these two building blocks makes the rest of this document straightforward.

Metafield

A single custom field on a product or variant

A metafield stores one piece of extra information attached directly to a product (or to a single variant). Examples: a badge, a b2b_discount percentage, a "professional only" toggle. You edit these in the Shopify admin under the Metafields section at the bottom of a product page.

Metaobject

A reusable, structured content record

A metaobject is a small, repeatable "mini record" with several fields of its own (e.g. a Badge with a label + colour, or a Variant Images set holding a gallery). You build them once under Settings → Custom data → Metaobjects, then link them to products through a metafield.

How they work together: A product has a metafield (e.g. custom.badge) that points to a metaobject entry (the actual "New" badge with its colour). This keeps content consistent and editable in one place, then reused across many products.
Custom metafield Metaobject Variant-level Shopify standard / app

02 The Product Page (PDP) anatomy

The product page is a two-column layout. The left column is the image gallery; the right column is a stack of content blocks that merchandisers can arrange in the theme editor. Source: sections/main-product.liquid

Philocaly product page anatomy

Each block on the right is optional and added per-template in the theme editor. The blocks read their content from the metafields and metaobjects described below.

03 PDP — Images & media

Philocaly products use a dual image system. Simpler products fall back to the standard Shopify media gallery, while colour/length products use a dedicated Variant Images metaobject so each colour shows its own curated gallery.

How the gallery decides what to show

   Does the product have BOTH a "Color" option AND a "Length" option?
            │
      ┌─────┴───────────────────────────┐
     YES                                NO
      │                                 │
      ▼                                  ▼
  Use each variant's                Use the standard product
  custom.product_variant_images     media gallery
  metaobject  →  its .files          (product.media — images & video
  gallery (images + video),          uploaded directly on the product)
  shown/hidden based on the
  selected colour (+ texture)

1 · Variant Images metaobject (primary system)

For extension products (Colour + Length, optionally Texture), each variant links to a Variant Images metaobject via the variant metafield custom.product_variant_images. The theme groups variants by colour (and texture) and renders one gallery per unique combination — thumbnails on the left, large images/video in the main viewer. Switching colour swaps the gallery instantly.

2 · Standard media gallery (fallback)

Products without the Colour+Length combination simply use the images and videos uploaded directly to the product in Shopify (product.media). Images render at up to 1000px; videos autoplay, muted and looping.

3 · Product badge (overlay on the image)

A small uppercase label (e.g. "New", "Best Seller") can overlay the top-left of the main image. It is driven by the Badge metaobject linked through custom.badge, which carries both a label and a background colour. The same badge also appears on the product card.

WhatWhere it comes fromNotes
Variant galleries (per colour)variant.metafields.custom.product_variant_imagesVariant ImagesColour + Length products. Images + video.
Standard galleryProduct media (uploaded on the product)Fallback for all other products.
Badge overlayproduct.metafields.custom.badgeBadge metaobject (label, color)Optional. Shown on both PDP & card.
Colour swatch imageShopify color-pattern metaobject (label, color, image)Used for the colour picker thumbnails.

04 PDP — Product information

The right-hand column is assembled from content blocks. Below is what each block displays and which field feeds it.

Product header — title, price, payment

  • Title — the standard Shopify product title.
  • Product type label — a small label above the title, set on the section ("Professional Extension" by default).
  • Price — standard variant price, with automatic handling of compare-at (sale) price, collection-based automatic discounts, and B2B discounts (see §09).
  • Sezzle / pay monthly — optional "or pay monthly" line.

Source: snippets/product-header.liquid

Description & specifications

The description block is variant-aware. It can show a different description per variant and falls back to a product-level one, then to the standard Shopify description.

ElementFieldBehaviour
Per-variant descriptionvariant.metafields.custom.product_description → metaobject (title, description)Swaps when a variant is selected.
Default descriptionproduct.metafields.custom.product_description → metaobject (title, description)Shown when no variant-specific one exists.
Plain fallbackStandard product descriptionUsed only if no metaobject is set.
Specificationsproduct.metafields.custom.product_specifications → metaobject (title, specifications)Rendered as a collapsible accordion.

Source: snippets/product-description.liquid · product-specifications.liquid

Variant pickers — Colour, Texture, Size

  • Colour select — swatches built from the Shopify color-pattern metaobject (each colour's swatch image + name). Colours can be filtered by colour category tabs sourced from the Color Categories metaobject. An optional per-colour blurb is read from variant.metafields.custom.color_description.
  • Texture select — shown only when the product has a Texture option.
  • Size / Length select — uses the product's Size or Length option.
  • Help block — an optional editable note under the colour picker (e.g. "order a Color Ring for matching").

Source: snippets/color-select.liquid · texture-select.liquid · size-select.liquid

Product USPs

A row of small uppercase chips (e.g. "100% Remy Hair", "Reusable"). Sourced from a list of Product USP metaobjects via custom.product_usps, each with a title.

Pair It With

A curated mini list of recommended variants to add alongside the product. Sourced from variant.metafields.custom.pair_it_with (a list of variant references). Each entry shows its image, option name and price.

Source: snippets/pair-it-with.liquid

Complementary products ("Buy it with")

Uses Shopify's Search & Discovery app recommendations — metafields['shopify--discovery--product_recommendation'].complementary_products — limited to 5 available products. This is managed in the Search & Discovery app, not as a custom metafield.

Source: snippets/complementary-products.liquid

Add to Cart

The add-to-cart area renders the quantity selector and the price-aware button. It also handles pre-order messaging, "notify when in stock", and login-gated purchasing for professional-only products.

SEO / structured data

Every product outputs JSON-LD (Product + AggregateRating) for Google. Ratings come from the Stamped review app metafields (stamped.reviews_average, stamped.reviews_count).

Source: snippets/structured-data-product.liquid

05 The Product Card anatomy

The product card is the compact tile used in collections, carousels and "you may also like" rows. Source: snippets/product-card.liquid

BADGE Optional overlay (metaobject)
Main image · hover image · colour images
SWATCH ● ● ● ● +3 colour swatches
TITLE Product title
INFO Available in: textures / sizes
PRICE $XX  $YY
BTN Quick shop / Add to bag
  • Badgecustom.badge metaobject (label + colour), same as PDP.
  • Main + hover imageMain image is the product/variant image; an optional second image on hover comes from custom.hover_image.
  • Colour swatchesUp to 4 colours from the color-pattern metaobject; "+N" link for the rest. Hovering a swatch swaps the card image.
  • Title & "Available in"Title is standard; for extensions, a line lists available textures (or sizes).
  • PriceStandard price with sale, automatic-discount and B2B logic mirrored from the PDP.
  • Button"Quick shop", "Add to bag" or "See details" depending on context.
Card elementField / source
Hover imageproduct.metafields.custom.hover_image image
Badgeproduct.metafields.custom.badgeBadge
Colour swatchesShopify metafields.shopify['color-pattern'] standard
B2B priceproduct.metafields.custom.b2b_discount number
Title / type / optionsStandard Shopify product fields

06 Metaobjects reference

These are the reusable structured records used across product pages. Build and edit them under Settings → Custom data → Metaobjects.

MetaobjectFieldsUsed forLinked via
Badge label (text), color (colour) Overlay label on PDP image & product card custom.badge
Variant Images files (list of images/videos) Per-colour image gallery on the PDP variant.custom.product_variant_images
Product Description title (text), description (rich text) Product / per-variant description block custom.product_description (product & variant)
Product Specifications title (text), specifications (rich text) Spec accordion under the description custom.product_specifications
Product USP title (text) USP chips on the PDP custom.product_usps (list)
Color Description content (rich text) Per-colour blurb under the colour picker variant.custom.color_description
Color Categories category_name (text), colors (list of color-pattern) Filter tabs in the colour picker Shop-level metaobject
Course Information title (text), content (rich text) FAQ-style accordions on course products custom.course_information (list)
Color Pattern Shopify standard label, color, image Colour swatches (PDP picker & card) metafields.shopify['color-pattern']
Digital Content Video / Document Content references Gated digital content for course/digital products custom.digital_content_videos / ...documents

07 Metafields reference

Custom fields attached to products and variants. Edit them in the Metafields panel at the bottom of a product (or variant) in the Shopify admin.

Product-level — custom namespace

MetafieldTypePurpose
badgeMetaobject refBadge overlay on image & card.
hover_imageImageSecond image shown on card hover.
product_descriptionMetaobject refDefault description block.
product_specificationsMetaobject refSpecs accordion.
product_uspsList of metaobject refsUSP chips.
b2b_discountNumber (percent)Discount % applied for professional/B2B customers.
is_a_professional_only_productBooleanRestricts purchase to professional accounts.
subtitleTextSupplementary title text.
course_informationList of metaobject refsCourse accordions.
course_bullet_listList (text)"Why you'll love it" bullets (courses).
related_coursesProduct refsRelated course products.
spots_leftText"X spots left" label (courses).
faqs / courses_faqsMetaobject / listFAQ content.
digital_content_videos / digital_content_documentsMetaobject refsGated digital downloads.
it_s_privateBooleanPrivacy / visibility flag.

Variant-level — custom namespace

MetafieldTypePurpose
product_variant_imagesMetaobject refPer-colour image gallery (Colour + Length products).
product_descriptionMetaobject refVariant-specific description.
color_descriptionMetaobject refPer-colour blurb under the picker.
pair_it_withList of variant refs"Pair It With" recommendations.

Course-specific — course_details namespace

MetafieldPurpose
date · time · locationShown in the course details panel.
methods · avatarCourse methods & instructor avatar.

App / standard metafields (managed by their apps)

SourceField(s)Used for
Stampedstamped.reviews_average, stamped.reviews_countStar rating + JSON-LD structured data.
Search & Discoveryshopify--discovery--product_recommendation.complementary_products"Buy it with" products.
Shopifyshopify['color-pattern']Colour swatches.
The theme also contains compatibility hooks for several other review apps (Yotpo, Loox, Okendo, Opinew, etc.), but Stamped is the active source for ratings and structured data.

08 Course products (special case)

Academy / course products reuse the same product page but surface a different set of blocks.

  • Course Details panel — date, time and location from the course_details metafields.
  • "Why you'll love it" — bullet list from custom.course_bullet_list.
  • Course Information — accordions from the Course Information metaobject list.
  • Spots left — a highlighted availability label from custom.spots_left.
  • Participants quantity — the add-to-cart quantity is labelled "participants".

Source: snippets/product-page-course-details.liquid · course-information-accordions.liquid

09 B2B pricing & access rules

Philocaly sells to both consumers and professionals, so pricing and access adapt to the logged-in customer.

RuleDriven byEffect
Professional-only productcustom.is_a_professional_only_product + customer tagsNon-professional customers can't purchase; sees a login/help prompt.
B2B discountcustom.b2b_discount (%) + professional tagsDiscounted price shown to professionals; suggested retail price shown alongside.
Automatic discount collectionsTheme setting (collection list + %)Applies a storewide collection discount to price display.
Chatters customersCustomer tags + price markupRedirected to account; markup logic applied to displayed prices.
Note: These rules affect displayed prices and access in the theme. The professional tags and discount collections are configured in Theme settings, while the per-product discount lives on the b2b_discount metafield.

10 Setup checklist per product

A practical order of operations when configuring a new extension product.

  • Basics — title, standard description (fallback), product type, options (Colour / Texture / Length), variants & prices.
  • Colours — ensure every colour value maps to a Shopify color-pattern metaobject (swatch image + colour) so swatches render.
  • Variant images — create/link a Variant Images metaobject per colour and connect it on each variant via product_variant_images.
  • Description & specs — fill the Product Description and Product Specifications metaobjects; link via the product metafields.
  • USPs — attach the relevant Product USP entries to product_usps.
  • Badge (optional) — link a Badge metaobject for "New", "Best Seller", etc.
  • Hover image (optional) — set hover_image for the card.
  • Pair It With (optional) — add variant references on pair_it_with.
  • Complementary products — set "Buy it with" in the Search & Discovery app.
  • B2B (if applicable) — set b2b_discount and/or is_a_professional_only_product.
  • Reviews — managed automatically by Stamped once the product has reviews.
Rule of thumb: if a piece of content needs to be reused, structured, or swapped per colour/variant, it lives in a metaobject. If it's a single switch or value on one product, it's a metafield.

Metaobject Entry Guide

00 The basics: where & how

A step-by-step companion to the Product Setup Guide — how to create a new entry for each metaobject and connect it to a product. Every metaobject entry is created in the same place; once you know the flow, each entry below is just a matter of filling in different fields.

Philocaly metaobject entry guide Philocaly metaobject entry guide
Philocaly metaobject entry guide
Two-step idea: creating the entry (the content) and linking it to a product (through a metafield) are separate actions.
Key in admin: when you save an entry, Shopify generates a handle (a unique id). The theme finds entries by their linked metafield, so you normally don't need to touch the handle — just make sure the entry is connected on the product/variant.

1 · Badge

Metaobject
Shows as: overlay label on the product image & card · Linked via product.metafields.custom.badge
Fields to fill
FieldTypeExample / guidance
labelSingle line texte.g. New, Best Seller — kept short, shown in uppercase.
colorColorBackground colour of the badge (e.g. a brand accent). Text is rendered over it.
  1. Go to Settings → Custom data → Metaobjects → Badge.
  2. Click Add entry.
  3. Type the label (e.g. "New").
  4. Pick the color for the background.
  5. Click Save.
  6. Open the product → Metafields → Badge → select this entry.
Philocaly metaobject entry guide
Tip: choose a background colour with enough contrast — badge text is a fixed colour, so very light backgrounds can be hard to read.

2 · Variant Images

MetaobjectPer variant
Shows as: the per-colour image gallery on the PDP · Linked via variant.metafields.custom.product_variant_images
Fields to fill
FieldTypeExample / guidance
filesList of files (images & videos)The ordered gallery for one colour — the first file is the lead image. Both images and video are supported.
  1. Go to Settings → Custom data → Metaobjects → Variant Images.
  2. Click Add entry.
  3. Give it a clear name/handle so you can find it later (e.g. the colour name, like caramel-blonde).
  4. Add the images/videos to the files field in the order you want them shown.
  5. Click Save.
  6. Open the product → click the variantMetafields → Variant Images → select this entry.
  7. Repeat for each colour, then link the same entry to every length/size that shares that colour.
Philocaly metaobject entry guide
Why: this is what makes the gallery swap when a shopper changes colour. Variants of the same colour (different lengths) should point at the same entry — the theme automatically de-duplicates them.

3 · Product Description

Metaobject
Shows as: the description block on the PDP · Linked via custom.product_description (on the product, or per variant)
Fields to fill
FieldTypeExample / guidance
titleSingle line textOptional heading shown in italics above the text.
descriptionRich textThe body copy — supports bold, links, lists, etc.
  1. Go to Settings → Custom data → Metaobjects → Product Description.
  2. Click Add entry.
  3. Optionally add a title.
  4. Write the description in the rich-text editor.
  5. Click Save.
  6. Link it on the product (Metafields → Product Description) for the default, or on a specific variant for a colour-specific description.
Philocaly metaobject entry guide
Variant override: if a variant has its own Product Description entry, it replaces the default whenever that variant is selected. If none is set, the product-level entry (then the plain Shopify description) is used.

4 · Product Specifications

Metaobject
Shows as: a collapsible "Specifications" accordion under the description · Linked via custom.product_specifications
Fields to fill
FieldTypeExample / guidance
titleSingle line textThe accordion heading, e.g. "Specifications".
specificationsRich textThe spec content — typically a list (weight, length, hair type, etc.).
  1. Go to Settings → Custom data → Metaobjects → Product Specifications.
  2. Click Add entry.
  3. Set the title (the accordion label).
  4. Fill the specifications rich text (a bulleted list works well).
  5. Click Save.
  6. Open the product → Metafields → Product Specifications → select this entry.
Philocaly metaobject entry guide

5 · Product USP

MetaobjectList
Shows as: a row of small uppercase chips on the PDP · Linked via custom.product_usps (a list)
Fields to fill
FieldTypeExample / guidance
titleSingle line textOne selling point, e.g. 100% Remy Hair, Reusable.
  1. Go to Settings → Custom data → Metaobjects → Product USP.
  2. Click Add entry, set the title, and Save. Create one entry per USP you want available.
  3. Open the product → Metafields → Product USPs.
  4. Because this is a list, click Add and select each USP entry you want shown.
  5. Re-order them if needed, then save the product.
Philocaly metaobject entry guide
Reusable: the same USP (e.g. "Reusable") can be attached to many products — create it once, link it everywhere.

6 · Color Description

MetaobjectPer variant
Shows as: a short blurb under the colour picker when that colour is selected · Linked via variant.metafields.custom.color_description
Fields to fill
FieldTypeExample / guidance
contentRich textA sentence or two describing the colour/shade.
  1. Go to Settings → Custom data → Metaobjects → Color Description.
  2. Click Add entry, write the content, and Save.
  3. Open the product → click the variant for that colour → Metafields → Color Description → select this entry.
  4. Link the same entry to every variant of that colour (e.g. all lengths).
Philocaly metaobject entry guide

7 · Color Categories

MetaobjectList of colours
Shows as: filter tabs above the colour picker ("All colours", "Blondes", "Browns"…) · Shop-level (not linked per product)
Fields to fill
FieldTypeExample / guidance
category_nameSingle line textThe tab label, e.g. Blondes.
colorsList of Color PatternThe colours that belong to this category (referenced from the Color Pattern metaobject).
  1. Go to Settings → Custom data → Metaobjects → Color Categories.
  2. Click Add entry and set the category_name.
  3. In the colors list, add each Color Pattern that belongs to this category.
  4. Click Save. The category tab appears automatically on any product that has at least one matching colour.
Philocaly metaobject entry guide
No product linking needed: these are global. A product simply shows whichever category tabs match its own colours — so adding a new category benefits every relevant product at once.

8 · Course Information

MetaobjectList
Shows as: FAQ-style accordions on course products · Linked via custom.course_information (a list)
Fields to fill
FieldTypeExample / guidance
titleSingle line textThe accordion heading / question.
contentRich textThe answer / details revealed when expanded.
  1. Go to Settings → Custom data → Metaobjects → Course Information.
  2. Click Add entry, fill the title and content, and Save. Create one entry per accordion.
  3. Open the course product → Metafields → Course Information.
  4. Since it's a list, add each entry in the order you want the accordions to appear.
Philocaly metaobject entry guide

9 · Color Pattern

Shopify standard
Shows as: the colour swatches in the picker & on the card · Provided by Shopify at metafields.shopify['color-pattern']
Fields to fill
FieldTypeExample / guidance
labelTextThe colour name — must match the product's Color option value exactly.
colorColorA solid fallback colour for the swatch.
imageImageA small swatch photo of the actual shade (preferred over the solid colour).
  1. This is a built-in Shopify metaobject. Go to Settings → Custom data → Metaobjects and open Color pattern (it may appear under "Shopify" standard definitions).
  2. Click Add entry.
  3. Set the label to exactly match the option value used on products (e.g. Caramel Blonde).
  4. Add a swatch image and/or pick a fallback color.
  5. Click Save.
  6. Connect it to a product's Color option (Shopify links swatches to option values via this metaobject).
Philocaly metaobject entry guide
Exact match matters: if the label doesn't match the product's Color option value letter-for-letter, the swatch won't render for that colour.

10 · Digital Content (Video / Document)

Metaobject
Shows as: gated downloads/videos on course & digital products · Linked via custom.digital_content_videos / custom.digital_content_documents
Fields to fill (typical)
FieldTypeExample / guidance
titleTextName of the lesson / document.
file / videoFile or URLThe downloadable document or the video source.
  1. Go to Settings → Custom data → Metaobjects and open Digital Content Video or Digital Content Document.
  2. Click Add entry and fill in the title and the file/video.
  3. Click Save.
  4. Open the product → Metafields → Digital Content Videos / Documents → add the entry (these are reference fields).
Philocaly metaobject entry guide Philocaly metaobject entry guide
Field names can vary slightly per definition — open the metaobject in admin to see the exact fields available, then fill them in the same way.

Quick reference

MetaobjectMain fieldsLinked through
Badgelabel, colorcustom.badge
Variant Imagesfilesvariant.custom.product_variant_images
Product Descriptiontitle, descriptioncustom.product_description (product/variant)
Product Specificationstitle, specificationscustom.product_specifications
Product USPtitlecustom.product_usps (list)
Color Descriptioncontentvariant.custom.color_description
Color Categoriescategory_name, colorsShop-level (global)
Course Informationtitle, contentcustom.course_information (list)
Color Patternlabel, color, imageProduct Color option (Shopify standard)
Digital Contenttitle, file/videocustom.digital_content_videos / ...documents
Golden rule: create the entry under Custom data → Metaobjects, then link it on the product (or variant) via the matching metafield. For list fields (USPs, Course Information) you add several entries; for single fields you pick one.

B2B Loyalty Program

01 Overview

How the rewards program works, what's live, and where the moving parts live.

Once a customer is approved for a B2B account, they're automatically enrolled in the rewards program. All new members start in the Bronze tier and earn points on every dollar spent. Tier progression is based on annual spend, and points can be redeemed at checkout through the Loyalty Lion checkout extension, which handles the automatic conversion.

Points & tiers: point accumulation, multipliers and tier logic are owned entirely by Loyalty Lion — there is no Shopify-side configuration for them.

02 Tiers and benefits

There are four tiers. Member counts below reflect the current state of the program.

TierMembersPointsDiscount ShippingBirthdayAdditional benefits
Bronze
Starting tier
24,107 1 pt / $ 10% off Tools & Accessories (one use/month)
Silver
$2,500 annual spend
285 2 pts / $ 10% off all courses, year-round 10% off Tools & Accessories (one use/month)
Gold
$8,000 annual spend
74 2 pts / $ 10% off all courses, year-round 50% off standard shipping 20% off Tools & Supplies (one use/month)
Platinum
$20,000 annual spend
8 5 pts / $ 15% off all education Free express shipping 10% off Pro Collection (one use/month) Dedicated concierge account manager + first access to new launches, exclusives & masterclasses

03 Implementation notes

How each benefit type is handled technically.

Points multiplier

Handled entirely by Loyalty Lion. No Shopify-side config is needed for points accumulation or multipliers — Loyalty Lion owns this logic.

Discount benefits (Education)

Managed via Shopify discounts connected to dynamic customer segments. Those segments are created and maintained by Loyalty Lion automatically. Discounts are applied automatically at checkout — no coupon code required from the customer.

Discount nameStatusMethodApplies to
Loyalty Lion: 15% Off Education – Year RoundActiveAutomaticMembers + Platinum customers (2 segments)
Loyalty Lion: 10% Off Education – Year RoundActiveAutomaticSilver + Gold customers (2 segments)

Shipping benefits

Same mechanic as discount benefits — Shopify discounts tied to Loyalty Lion-managed segments, applied automatically at checkout.

Discount nameStatusMethodUsage
Platinum Membership: Free ShippingActiveAutomaticUsed 8 times
Gold Member: 50% off ShippingActiveAutomaticUsed 89 times

Birthday benefits

Birthday rewards are handled via Loyalty Lion. The flow works like this:

  1. On the member's birthday, Loyalty Lion sends them an email letting them know a reward is waiting.
  2. Clicking the link takes them to the store, where they're prompted to sign in.
  3. After signing in, they land on the Reward Collection page and see their tier-specific discount code.
  4. The code is valid for one month and applies to any order that meets the reward requirements.

Quick reference

Where does each piece live?

PieceWhere it lives
Points & tier logicLoyalty Lion dashboard
Discount codes (education + shipping)Shopify Admin → Discounts (automatically applied)
Customer segmentsShopify Admin → Customers → Segments (created dynamically by Loyalty Lion)
Birthday rewardsLoyalty Lion (email trigger + reward collection page)
Checkout redemptionLoyalty Lion checkout extension (automatic points conversion)

Course Video Management

01 Overview

Shopify + Vimeo workflow for managing course videos. Last updated April 2026

This guide explains how to manage course videos for the Philocaly Shopify store. The workflow is split into two main parts: uploading videos to Vimeo, then organising them inside Shopify using two metaobjects.

The reason for using Vimeo is straightforward: video files are large, and hosting them directly inside Shopify wastes storage. Vimeo handles delivery, while Shopify holds the metadata and structure.

Two metaobjects power this: Course Videos (individual video entries) and Course Video Groupings (ordered lists of those videos).

02 Step 1 — Upload videos to Vimeo

Before anything touches Shopify, every course video must be uploaded to Vimeo.

Once uploaded, Vimeo provides a unique Video ID for each video. You'll need this ID in the next step.

Where to find the Vimeo ID: open the video in Vimeo and look at the URL in your browser. It will look something like vimeo.com/123456789 — the number at the end is your Vimeo ID.

03 Step 2 — Add videos to "Course Videos"

Once a video is on Vimeo, register it inside Shopify's Course Videos metaobject — this is where all individual video entries live.

  1. In your Shopify admin, navigate to Content → Metaobjects and open Course Videos.
  2. Click Add entry to create a new record.
  3. Enter the title of the video in the Title field.
  4. Add a short description in the Description field. Keep it concise — a sentence or two is ideal.
  5. Paste the Vimeo ID (the number from the Vimeo URL) into the Vimeo ID field.
  6. Save the entry.
Tip: fill in all three fields for every entry. Missing a Vimeo ID means the video will not play on the storefront.

04 Step 3 — Organize videos in "Course Video Groupings"

With all individual videos created, the next step is to group them. The groupings in Course Video Groupings have already been set up — the team simply needs to select which videos belong to each one.

  1. Navigate to Content → Metaobjects and open Course Video Groupings.
  2. Click on the grouping you want to configure.
  3. In the Videos field, click Add to start selecting videos from the Course Videos metaobject.
  4. Select all videos that belong to this grouping.
  5. Arrange the videos into the correct order. Each grouping is a list, so you can drag and reorder freely.
  6. Save the grouping.
  7. Repeat for each grouping.
Important: the order videos appear in the list is the order they display on the storefront. Take care when arranging them, especially for structured courses where sequence matters.

05 A note on tagging

The existing tagging rules remain in place and have not changed. The Course Video Groupings step is an addition to the workflow, not a replacement for tagging.

Quick reference

StepAction
1. Vimeo uploadUpload the video to Vimeo and note the Vimeo ID from the URL.
2. "Course Videos" entryAdd a new entry with title, description, and Vimeo ID.
3. "Course Video Groupings"Open each grouping, select the relevant videos, arrange them in order, and save.

Preorders & Split Shipments

01 Overview

How preorders and split shipments work on Shopify. Created October 7, 2025

Shopify supports native preorders, which let you sell products before they're in stock. Customers can purchase preorder items in the same cart as regular items, and Shopify automatically separates those into different shipments.

If you set up a dedicated shipping profile for preorder items and make it free shipping, customers will see that at checkout, and your team won't need to manage those rates manually.

[Client approval needed] — this describes the proposed native preorder + multi-shipping-profile setup before it is enabled.

02 What customers experience

On the product page

  • Customers see a "Preorder" or "Ships later" message on any item that's not yet in stock.
  • They can add preorder items to their cart like any other product.

In the cart

  • If a customer has both in-stock and preorder items, Shopify keeps them in one cart but labels preorder items as shipping separately.
  • This ensures the customer knows they'll get multiple deliveries.

At checkout

  • Shopify automatically groups shipping options by availability.
  • Regular items show normal shipping choices.
  • Preorder items show "Free Shipping" (based on your preorder shipping profile).
  • The customer pays for the full order upfront unless you've configured preorders to charge later (a merchant choice).

After checkout

  • The customer receives one order confirmation but will later get separate shipping notifications when each part ships.
  • The experience is smooth — they never need to place separate orders.

03 What happens behind the scenes

One order, multiple fulfillments

Shopify creates a single order in your admin, but automatically separates the items into fulfillment groups.

  • Group 1 — in-stock products (ready to ship now).
  • Group 2 — preorder products (waiting for restock).

Automatic split shipments

When the preorder items arrive and are marked as in stock, your team can fulfill that part of the order without touching the rest.

  • You can do this manually or connect your fulfillment app or 3PL to fulfill automatically once inventory updates.
  • Shopify sends a new shipping confirmation to the customer automatically.

Inventory updates trigger fulfillment readiness

When your preorder stock lands and the inventory quantity increases, Shopify flags those items as ready to fulfill.

  • You can filter orders in the Admin by "Unfulfilled (preorder)" to process them in batches.

04 Shipping setup for preorders

You'll create a separate shipping profile for preorder items.

  • Assign only your preorder products to that profile.
  • Add one shipping rate: "Free Shipping – $0."
  • Shopify will automatically show that option for preorder items at checkout.
Because these products are in their own shipping group, customers won't pay twice for shipping — regular items use your normal rates, and preorders show as free.

05 Internal benefits

  • No manual workarounds — the system automatically manages which items ship when.
  • Clear visibility — orders are clearly labeled in the admin so your fulfillment team can see what's preorder vs. ready-to-ship.
  • Reduced customer confusion — customers receive separate shipping updates automatically, improving communication and reducing "Where's my order?" messages.
  • Marketing alignment — you can promote upcoming launches while letting customers lock in their purchase early.

06 Operational considerations

  • Charging timing — you can choose to collect payment at the time of order or only when the item ships (depending on your preorder setup).
  • Fulfillment workflow — your team fulfills the in-stock shipment immediately. When preorder stock arrives, you simply open the same order and fulfill the remaining items.
  • Customer service — be transparent about preorder timelines in product descriptions and order confirmations.
  • Returns — returns and refunds process normally, regardless of when the product shipped.

07 Example

A customer orders:

  • 1 in-stock CW Genius Weft ($300, ships now)
  • 1 preorder Curly CW Genius Weft ($400, ships next month)
StageWhat happens
At checkoutCW Genius Weft → standard shipping ($8); Curly CW Genius Weft → free shipping (preorder profile). Customer pays $708 total.
In Shopify AdminOne order appears with two fulfillment groups. You fulfill the CW Genius Weft immediately; the Curly CW Genius Weft stays "unfulfilled" until it's back in stock.
When stock arrivesYou click "Fulfill." Shopify emails the customer automatically with tracking.

08 Summary

Customers can buy future-release products and current items in a single, simple checkout. Shopify handles split shipments, free preorder shipping, and separate fulfillment automatically. Internally, your team doesn't need to create separate orders or worry about shipping overcharges — it's all handled by the native preorder + multi-shipping-profile system.

Chatters

01 Overview

The Philocaly x Chatters portal — global overview of how it works.

The Chatters x Philocaly portal enables individual Chatters stores to access a curated selection of products at a custom price, with streamlined ordering and a monthly payment arrangement through Chatters HQ.

The system is designed to:

  • Give each store its own login and account tagging for easy identification.
  • Provide a custom checkout flow that does not require payment at the time of order.
  • Apply Chatters-specific pricing and markup rules.
  • Automate order fulfillment and monthly invoicing.
Confidential — internal use only. This guide documents back-end automations (Mechanic flows, custom code) and contains a live HQ discount code. Keep it internal.

02 Portal functionality

Store access & login

  • Each Chatters store has its own Shopify account (created by Philocaly), under the segment "Chatters Corporate".
  • Accounts are tagged with the chatters tag in Shopify (by Philocaly).
  • When logged in, stores are restricted to an account page that includes only the curated set of approved products — they cannot leave this section without logging out.

Pricing rules

  • Chatters stores see a 30% markup on standard B2B prices in the portal (this is a displayed markup, not the actual HQ invoice rate). Controlled by the customizer theme settings (section name: Chatters).
  • Chatters HQ is invoiced at a 10% discount on standard B2B prices for all orders. This is handled with a discount code applied automatically on the back end via custom code.
SettingValue
Portal markup (displayed to stores)+30% on standard B2B price
HQ invoice rate−10% on standard B2B price
HQ discount codeXSWA2GT4FRP78-CHATTERS-HQ
Theme settings sectionChatters (customizer)

Checkout process

  • Orders placed through the portal do not require payment at checkout.
  • Shipping is or is not included per the store's shipping rules, and the order is sent to the address associated with the store's account.
  • Taxes are not visible to stores. Taxes are applied only on the HQ invoice and calculated based on the individual store's shipping location.

03 Order processing workflow

Once a draft order is placed by a Chatters store, the following automated flows are triggered in Shopify.

1 · Order tagging by month

  • Any order from an account tagged chatters or chatters HQ receives an order tag in the format YYYY-MM based on the order date. Handled via a custom back-end API. This tag is used for monthly aggregation and invoicing.
  • There are 2 Mechanic tasks:
    • For regular Chatters users — consolidate all orders placed by Chatters customers.
    • For Chatters HQ users — consolidate all orders placed by Chatters HQ customers.

2 · Auto fulfillment + invoicing

  • Draft orders from tagged accounts are automatically marked as paid, even if payment hasn't been collected. Handled via custom back-end API.
    Mechanic Flow: "Convert Draft Consolidation Orders to Real Orders - DO NOT DELETE" · runs every 30 minutes
  • Individual order confirmation emails are sent to the stores as orders are placed. Prices are not shown in confirmation emails to Chatters-tagged stores (custom code over the native Shopify confirmation emails).
  • On the first day of every month, a flow searches for all orders with the previous month's tag and consolidates them into a single draft order / invoice.
    Mechanic Flow: "ORDER CONSOLIDATION - DO NOT DELETE"
Mechanic Flow — ORDER CONSOLIDATION (regular Chatters) Liquid
{% if options.hq_customer_id != blank %}
  {%- assign format = options.month_tag_format | default: "%Y-%m" -%}
  {%- if options.target_month_tag != blank -%}
    {%- assign month_tag = options.target_month_tag -%}
  {%- else -%}
    {%- assign month_tag = "now" | date: format -%}
  {%- endif -%}
  {%- assign current_date = "now" | date -%}
  {%- assign month_tag_array = month_tag | split: '-' -%}
  {%- assign month_tag_array_month = month_tag_array | last | times: 1 -%}
  {%- assign month_tag_array_month = month_tag_array_month | minus: 1 -%}
  {% if month_tag_array_month == 0 %}
    {%- assign month_tag_array_month = 12 -%}
  {% endif %}
  {% if month_tag_array_month < 10 %}
    {%- assign month_tag_array_month = '0' | append: month_tag_array_month -%}
  {% endif %}
{% assign month_tag_year = month_tag_array | first %}
{% assign month_tag = month_tag_year | append:'-' | append: month_tag_array_month %}
  {%- capture order_search -%}tag:'{{ month_tag }}' AND tag:'chatters' AND cancelReason:null AND -financial_status:refunded AND -financial_status:partially_refunded AND -status:cancelled{%- endcapture -%}
  {% comment %} we need to know the payment terms id for NET 45 {% endcomment %}
  {%- capture query -%}
  query paymentTerms {
    paymentTermsTemplates (paymentTermsType:NET) {
      id
      description
      name
      paymentTermsType
    }
  }
  {%- endcapture -%}
  {%- assign result = query | shopify -%}
  {%- if result.errors -%}
    {%- log level: "error", message: "Payment Terms not found", errors: result.errors -%}
    {%- break -%}
  {%- endif -%}
  {%- assign result_info = result.data.paymentTermsTemplates -%}
  {%- assign result_info = result_info | where: 'name','Net 45' | first -%}
  {%- if result_info == nil -%}
    {%- break -%}
  {%- endif -%}
  {%- assign payment_terms_id = result_info.id -%}
  {%- assign orders = array -%}
  {%- assign cursor = nil -%}
  {% comment %} loop orders to get the one we want to consolidate {% endcomment %}
  {%- for n in (1..250) -%}
    {%- capture query -%}
    query($query: String!, $cursor: String) {
      orders(first: 250, after: $cursor, query: $query, sortKey: CREATED_AT, reverse: true) {
        pageInfo { hasNextPage }
        edges {
          cursor
          node {
            id
            name
            createdAt
            totalPriceSet { shopMoney { amount currencyCode } }
            customer {
              id
              displayName
              defaultEmailAddress { emailAddress }
            }
          }
        }
      }
    }
    {%- endcapture -%}
    {%- capture vars_json -%}{
      "query": {{ order_search | json }},
      "cursor": {{ cursor | json }}
    }{%- endcapture -%}
    {%- assign vars = vars_json | parse_json -%}
    {%- assign result = query | shopify: variables: vars -%}
    {%- if result.errors -%}
      {%- log level: "error", message: "GraphQL error while querying orders", errors: result.errors, query: order_search -%}
      {%- break -%}
    {%- endif -%}
    {%- assign edges = result.data.orders.edges -%}
    {%- if edges == blank -%}
      {%- break -%}
    {%- endif -%}
    {%- for edge in edges -%}
      {%- assign orders = orders | push: edge.node -%}
    {%- endfor -%}
    {% comment %} get all pages {% endcomment %}
    {%- if result.data.orders.pageInfo.hasNextPage -%}
      {%- assign cursor = edges.last.cursor -%}
    {%- else -%}
      {%- assign cursor = nil -%}
      {%- break -%}
    {%- endif -%}
  {%- endfor -%}
  {%- if orders == blank -%}
    {%- log message: "No Chatters orders found for month", month: month_tag, query: order_search -%}
    {%- break -%}
  {%- endif -%}
  {% comment %} group the orders by customer id / salon {% endcomment %}
  {%- assign customer_ids = orders | map: "customer" | map: "id" -%}
  {%- assign unique_customer_ids = customer_ids | uniq -%}
  {%- assign line_items_list = array -%}
  {%- for one_customer in unique_customer_ids -%}
    {%- assign customer_name = nil -%}
    {%- for o in orders -%}
      {%- if o.customer == nil or o.customer.id != one_customer -%}
        {%- continue -%}
      {%- endif -%}
      {%- assign company_name = blank -%}
      {%- if o.customer.current_company != blank -%}
        {%- assign company_name = o.customer.current_company.name -%}
      {%- endif -%}
      {%- assign customer_name = o.customer.displayName | append: ' ' | append: company_name -%}
      {%- assign title = "Order " | append: o.name | append: " (" | append: o.createdAt | append: ") " | append: customer_name -%}
      {%- assign amount = o.totalPriceSet.shopMoney.amount -%}
      {%- capture item_json -%}{
        title: {{ title | json }},
        quantity: 1,
        originalUnitPriceWithCurrency: {
          amount: {{ amount | json }},
          currencyCode: {{ o.totalPriceSet.shopMoney.currencyCode }}
        },
        requiresShipping: false,
        taxable: false,
        customAttributes: [
          { key: "order-id", value: {{ o.id | json }} },
          { key: "order-number", value: "{{ o.name}}" }
        ]
      }{%- endcapture -%}
      {%- assign item = item_json -%}
      {%- assign line_items_list = line_items_list | push: item -%}
    {%- endfor -%}
  {% endfor %}
  {%- assign note_prefix = "Consolidated invoice for: " -%}
  {%- assign hq_customer = "gid://shopify/Customer/" | append: options.hq_customer_id -%}
  {%- capture input_json -%}{
    purchasingEntity: { customerId: "{{ hq_customer }}" },
    useCustomerDefaultAddress: true,
    tags: {{ month_tag | append: ',portal-orders,ready-to-complete,consolidated-order' | json }},
    note: {{ note_prefix | append: month_tag | json }},
    lineItems: [{{ line_items_list }}],
    paymentTerms: {
      paymentTermsTemplateId: "{{payment_terms_id}}",
      paymentSchedules: { issuedAt: "{{ current_date }}" }
    }
  }{%- endcapture -%}
  {%- assign input = input_json -%}
  {%- capture vars_json -%}{ "input": {{ input }} }{%- endcapture -%}
  {%- assign vars = vars_json -%}
  {%- action "shopify" -%}
  mutation draftOrderCreate {
    draftOrderCreate(input: {{input }}) {
      draftOrder { id name invoiceUrl }
      userErrors { field message }
    }
  }
  {%- endaction -%}
{% action "mechanic/run" %}
{ "task_id": "c13f3dbc-610f-40d7-b377-9af6da7fe7a3" }
{% endaction %}
{% else %}
  {% log "You must enter the HQ Customer ID" %}
{% endif %}
Mechanic Flow — ORDER CONSOLIDATION (for Chatters HQ) Liquid
Same as the regular flow above, with two differences: the order search filters on tag:'exclude' instead of tag:'chatters', and the consolidated draft tags include an extra empty segment. The differing lines are shown below.
// order search — uses tag:'exclude' instead of tag:'chatters'
{%- capture order_search -%}tag:'{{ month_tag }}' AND tag:'exclude' AND cancelReason:null AND -financial_status:refunded AND -financial_status:partially_refunded AND -status:cancelled{%- endcapture -%}

// consolidated draft tags — note the extra ',' segment
tags: {{ month_tag | append: ',' | append: ',portal-orders,ready-to-complete,consolidated-order' | json }},

3 · Post payment

  • Once the consolidated order is marked as paid, a script automatically updates all included orders as "Paid" in Shopify. Runs daily.
    Mechanic Flow: "MARK ORDER INSIDE CONSOLIDATE AS PAID - DO NOT DELETE"
Mechanic Flow — MARK ORDER INSIDE CONSOLIDATE AS PAID Liquid
{%- comment -%} STEP 1 — Gather all matching open drafts with both tags {%- endcomment -%}
{%- assign orders = array -%}
{%- assign cursor = nil -%}
{%- for n in (1..20) -%}
  {%- capture query -%}
  query($cursor: String) {
    orders(
      first: 250
      query: "financial_status:PENDING AND tag:'chatters' AND tag:'ready-to-complete'"
      reverse: true
      after: $cursor
      sortKey: CREATED_AT
    ) {
      pageInfo { hasNextPage }
      edges {
        cursor
        node {
          id
          name
          createdAt
          lineItems(first: 250) {
            edges { node { id customAttributes { value key } } }
          }
        }
      }
    }
  }
  {%- endcapture -%}
  {%- capture vars_json -%}{ "cursor": {{ cursor | json }} }{%- endcapture -%}
  {%- assign vars = vars_json | parse_json -%}
  {%- assign result = query | shopify: variables: vars -%}
  {%- if result.errors -%}
    {%- log level:"error", message:"GraphQL error while querying draftOrders", errors: result.errors -%}
    {%- break -%}
  {%- endif -%}
  {%- assign edges = result.data.orders.edges -%}
  {%- if edges == blank -%}{% break %}{%- endif -%}
  {%- for edge in edges -%}
    {%- assign orders = orders | push: edge.node -%}
  {%- endfor -%}
  {%- if result.data.orders.pageInfo.hasNextPage -%}
    {%- assign cursor = edges.last.cursor -%}
  {%- else -%}
    {%- assign cursor = nil -%}
    {%- break -%}
  {%- endif -%}
{%- endfor -%}
{%- assign orders_to_mark = array -%}
{%- for one_order in orders -%}
  {%- for line_item in one_order.lineItems.edges -%}
    {% assign line_item_info = line_item.node.customAttributes %}
    {%- for one_item in line_item_info -%}
      {% if one_item.key == 'order-id' %}
        {%- assign orders_to_mark = orders_to_mark | push: one_item.value -%}
      {% endif %}
    {%- endfor -%}
  {%- endfor -%}
{%- endfor -%}
{% for order_pay in orders_to_mark %}
  {%- capture input -%}{ id: "{{order_pay}}" }{%- endcapture -%}
  {%- capture vars_json -%}{ "input": {{input}} }{%- endcapture -%}
  {%- action "shopify" -%}
  mutation MarkPaid {
    orderMarkAsPaid(input: {{input}}) {
      order { id }
      userErrors { field message }
    }
  }
  {% endaction %}
{% endfor %}

04 Payment

  • Terms — Chatters HQ pays for all orders in a single transaction at the end of each month (Net 45).
  • Consolidated invoice contents:
    • Consolidated list of orders by store.
    • Taxes applied by province (based on shipping location). These taxes are included in the "Unit Price" of the consolidated invoice.
    • Total monthly payment due.

05 Key notes & considerations

  • Shipping is dictated by the store's standard shipping rules.
  • Taxes are paid by Chatters HQ and calculated dynamically based on shipping location.
  • Portal access is restricted to tagged chatters accounts.
  • Monthly tagging ensures accurate reconciliation and reporting.
  • All flows operate automatically once configured.

06 Appendix — invoice example

A sample consolidated invoice sent to Chatters HQ.

FieldValue
Invoice #81841
Issue dateMay 1, 2026
FromPhilocaly Hair Extensions · 246 Stewart Green SW, Suite 2047, Calgary AB T3H 3C8, Canada · GST/HST 797462710RT0001
Customer / shippingChatters Distribution Center · 271 Burnt Park Drive, Red Deer AB T4S 0K7, Canada · education@chatters.ca
ItemQtyUnit priceTaxTotal
Order #81725 (2026-04-30) Chatters Support Centre11,192.80$0.00$1,192.80
Order #81576 (2026-04-28) CHATTERS Crossroads1544.99$0.00$544.99
Order #81560 (2026-04-28) CHATTERS Village Green Mall (Vernon)1280.90$0.00$280.90
Order #81547 (2026-04-28) CHATTERS Grandview Corners1137.76$0.00$137.76
Order #81423 (2026-04-27) CHATTERS Grandview Corners1280.90$0.00$280.90
Order #81416 (2026-04-27) CHATTERS Camrose Commons1510.93$0.00$510.93
Order #81358 (2026-04-26) CHATTERS Willoughby Centre1237.75$0.00$237.75
Order #79542 (2026-04-05) CHATTERS Willoughby Centre11,056.38$0.00$1,056.38
Order #81288 (2026-04-25) CHATTERS Sudbury Centre1150.18$0.00$150.18
Order #81255 (2026-04-24) CHATTERS Polo Park1500.64$0.00$500.64
Order #81239 (2026-04-24) CHATTERS Bower Place1129.15$0.00$129.15
Tax shows as $0.00 per line because provincial taxes are rolled into the consolidated unit prices on the HQ invoice rather than charged to the individual stores.

Pro Pricing & Hidden Prices

01 Why prices are hidden

Why prices and purchase options are hidden from non-professional ("non-pro") customers in this theme.

This is a hybrid B2C / B2B (wholesale / "stylist") store. Some products are professional-only — they should only be visible and purchasable by qualified pros (stylists, salons, B2B accounts). For regular direct-to-consumer ("D2C") shoppers, these products must:

  • Not show a price — wholesale pricing is confidential / not relevant to D2C.
  • Not be purchasable — instead of an "Add to cart" button, the shopper sees a prompt to log in or register for a pro account.
  • Optionally show a help callout steering D2C shoppers toward the right alternative ("Not a professional? Check out our Clip-In Extensions…").

Eligible pros, once logged in and tagged, see the price and can buy — and may additionally receive a B2B discount off the retail price.

02 How it works

The system has two independent layers: an eligibility gate (controls whether price + purchase are shown at all), and pro pricing (for eligible pros, applies a B2B discount).

The key inputs

InputWhere it livesPurpose
Customer tags (e.g. professional, b2b)Customer record in Shopify AdminIdentifies who is a pro
professional_tags theme settingTheme settings → Professional / Students SettingsDefines which tags count as "pro"
is_a_professional_only_product metafield (boolean)Product → custom.is_a_professional_only_productFlags a product as pro-only
b2b_discount metafield (number, %)Product → custom.b2b_discountDiscount % applied for pros

Layer 1 — Eligibility gate

The gate is computed at the top of the product section in main-product.liquid:

{% assign is_eligible_to_purchase = true %}

{% if product.metafields.custom.is_a_professional_only_product %}
  {% assign is_eligible_to_purchase = false %}

  {% assign professional_tags = settings.professional_tags | downcase %}

  {% for tag in customer.tags %}
    {% assign downcased_tag = tag | downcase %}
    {% if professional_tags contains downcased_tag %}
      {% assign is_eligible_to_purchase = true %}
      {% break %}
    {% endif %}
  {% endfor %}
{% endif %}

Logic:

  • If the product is not flagged pro-only → is_eligible_to_purchase = true for everyone (normal product, price shown to all).
  • If the product is pro-only → start with false, then flip to true only if one of the customer's tags matches a tag in the professional_tags setting (case-insensitive).

This is_eligible_to_purchase flag is then passed down to every snippet that renders price or purchase UI.

  • Price is hidden — the entire price block in product-header.liquid is wrapped in {% if is_eligible_to_purchase %} … {% endif %}. If the shopper isn't eligible, no price markup is rendered at all.
  • Purchase is blocked — in main-product.liquid, when the flag is false the "Add to cart" button is replaced with a login/register button (label from settings.not_logged_in_add_to_cart_label, linking to the account login URL), plus the disclaimer (settings.not_logged_in_disclaimer) and the D2C help callout (settings.d2c_help_callout_title / d2c_help_callout_content).

The same gate is enforced everywhere a price/buy action could leak through:

SurfaceBehaviour
add-to-cart-popup.liquidEntire popup wrapped in the eligibility check.
product-card.liquidCollection cards only show price when allowed.
head-assets.liquidPrice is omitted from JSON-LD structured data for pro-only products (so it doesn't leak into SEO / search results).

Layer 2 — Pro (B2B) pricing

For eligible pros, an optional B2B discount is applied. First the snippet determines if the logged-in customer is a B2B customer (product-header.liquid):

{% assign is_b2b_customer = false %}
{% assign professional_tags = settings.professional_tags | downcase %}

{% for tag in customer.tags %}
  {% assign downcased_tag = tag | downcase %}
  {% if professional_tags contains downcased_tag %}
    {% assign is_b2b_customer = true %}
    {% break %}
  {% endif %}
{% endfor %}

Then, if the product has a b2b_discount metafield and the customer is a pro, the displayed price is discounted:

{% if product.metafields.custom.b2b_discount != blank and is_b2b_customer %}
  {% assign b2b_discount = 100 | minus: product.metafields.custom.b2b_discount %}
  {% assign price = b2b_discount | times: product.selected_or_first_available_variant.price | divided_by: 100 %}
  ...
{% endif %}

So a b2b_discount of 15 shows the price at 85% of retail. The original retail price is still shown to pros as a "Suggested Retail Price" line.

Related: special-tag price markup (Chatters)

Separately from the pro gate, customers tagged as Chatters / Chatters HQ have a markup (or discount) applied to all displayed prices via price-markup.liquid, controlled by the chatters_markup_percentage / chatters_hq_markup_percentage theme settings. This is a price adjustment, not a hiding mechanism, but it shares the same customer-tag pattern. See the Chatters guide.

Decision flow

Shopper opens a product page
  │
  ├─ Is product flagged custom.is_a_professional_only_product?
  │    ├─ NO  → show price + Add to cart to everyone
  │    └─ YES → does customer.tags match a professional_tags entry?
  │              ├─ NO  → HIDE price, replace buy button with
  │              │         "Login / Register to shop" + disclaimer + help callout
  │              └─ YES → show price + Add to cart
  │
  └─ For eligible pros: if product has custom.b2b_discount,
       displayed price = retail × (100 − b2b_discount) / 100
       (retail still shown as "Suggested Retail Price")

03 Merchant setup

1 · Configure the theme settings

In the Shopify theme editor → Theme settingsProfessional / Students Settings (settings_schema.json):

SettingDefaultNotes
Professional Tagsprofessional, b2bComma-separated tags that count as a pro
Student TagsStudentTags that count as a student
Label on add to cart button for not logged in usersLogin / Register to shopButton text shown to non-eligible shoppers
Disclaimer for not logged in usersStylist exclusive. Login or register for a pro account today!Shown under the button
Title for D2C Help CalloutNot a professional?Help section heading
Content for D2C Help CalloutCheck out our Clip-In Extensions instead or Find a StylistHelp section body

2 · Tag your pro customers

In Shopify Admin → Customers, open each professional/stylist/B2B account and add a tag that matches one of the values in Professional Tags (e.g. professional or b2b). Tag matching is case-insensitive.

Customers create these accounts via the storefront login/register flow; you then approve them by adding the tag.

3 · Flag the pro-only products

For each product that should be hidden from D2C shoppers, set the metafield:

  • Namespace / key: custom.is_a_professional_only_product
  • Type: Boolean (true/false)
  • Value: true

Products without this metafield (or set to false) remain visible and purchasable by everyone.

4 · (Optional) Set a B2B discount per product

To give pros a discount off retail, set the metafield:

  • Namespace / key: custom.b2b_discount
  • Type: Number (integer percentage, e.g. 15 for 15% off)

Pros will see the discounted price plus the original as "Suggested Retail Price".

Quick verification

  1. Open a pro-only product in an incognito window (logged out) → you should see no price and a Login / Register to shop button + disclaimer.
  2. Log in as a customer without a pro tag → same hidden behaviour.
  3. Log in as a customer with a pro tag → price and Add to cart appear; if a b2b_discount is set, the discounted price shows with the retail price as "Suggested Retail Price".

04 File reference

ConcernFile
Eligibility gate + login/buy blocksrc/sections/main-product.liquid
Price display (gated) + B2B discountsrc/snippets/product-header.liquid
Add-to-cart popup (gated)src/snippets/add-to-cart-popup.liquid
Collection card price (gated)src/snippets/product-card.liquid
SEO / structured data price suppressionsrc/snippets/head-assets.liquid
Special-tag price markup (Chatters)src/snippets/price-markup.liquid
Theme settingssrc/config/settings_schema.json

Checkout Validation Developer app

01 Overview

A Shopify Cart & Checkout Validation function that blocks checkout when a non-professional customer tries to buy products reserved for professional (B2B) accounts.

This is an extension-only Shopify app — it has no admin UI and no backend server. It ships a single Shopify Function that runs at the cart.validations.generate.run target. Shopify executes the function automatically as the customer moves toward checkout.

The rule it enforces: if the cart contains at least one product from a designated "Professional" collection, and the customer is not tagged as a professional, checkout is blocked with a clear error message. Everyone else checks out normally.

Typical use case: a store sells some items only to verified B2B / trade customers. Those items live in a "Professional" collection. Retail shoppers can browse and add them to the cart, but cannot complete the purchase unless their account is approved.

02 How it works

On each cart/checkout evaluation, the function receives the cart contents and buyer identity, then applies this logic.

  1. Read the cart lines and check whether any product is a member of one of the configured collection IDs (inCollections).
  2. Read the buyer identity and check whether the customer has the b2b or professional tag.
  3. Decision: is there a professional product in the cart and the customer is not a professional?
  4. Yes → block. A validation error is added targeting the cart. Shopify disables the "Pay now" / "Place order" button and shows the message.
  5. No → allow. The function returns no operations and checkout proceeds normally.

The error shown to the customer is:

You need to have a professional account to place this order.
Make sure the order email matches your professional account email.

Core logic: extensions/philocaly-cart-checkout-validation/src/cart_validations_generate_run.js — and the data it reads is defined in the sibling .graphql input query.

03 Requirements & setup

Requirements

  • Node.js installed.
  • A Shopify Partner account.
  • A test store — a development store or a Plus sandbox store.
  • The Shopify CLI.

Install & run locally

From the root of the app, start a development session. The CLI connects to your app in the Partners dashboard, provides environment variables, and runs the function in dev mode:

yarn install
yarn dev

Whenever you change the main function file cart_validations_generate_run.js, regenerate the TypeScript types used for JSDoc type-safety:

shopify app function typegen

Deploy

Push the function to Shopify:

shopify app deploy

After deploying, the validation function appears in your store under Settings → Checkout → Checkout rules, where it can be enabled.

04 Configuration

The function reads its list of "professional" collections from a metafield, so you can change which collections are gated without redeploying code. There are two metafields to be aware of.

1 · The validation config metafield

The extension is bound (in shopify.extension.toml) to read a metafield with:

PropertyValue
Namespacephilocaly
Keycheckout_validation_config
Typejson
Value{"collectionIds": ["gid://shopify/Collection/280538153094"]}

Replace the collection GID with the ID of the collection you want to gate. You can list multiple collection IDs in the array.

2 · Attach the config to the validation

The metafield must be owned by the Validation object that Shopify creates for your function. First, find the validation ID by running this query (e.g. in the GraphiQL app or via the Admin API):

{
  validations(first: 10) {
    nodes {
      id
      title
      shopifyFunction { title }
    }
  }
}

Then set the config metafield on that validation. Replace the ownerId with your validation's GID and the collection ID with your collection's GID:

mutation {
  metafieldsSet(metafields: [
    {
      ownerId: "gid://shopify/Validation/34373766",
      namespace: "philocaly",
      key: "checkout_validation_config",
      type: "json",
      value: "{\"collectionIds\": [\"gid://shopify/Collection/280538153094\"]}"
    }
  ]) {
    metafields { id }
  }
}
Important: the metafield's owner must be the Validation object (not the shop or a product), so the function can read it via its bound input variables. Get the collection GID from the collection page URL in the admin, or from a collections GraphQL query.

3 · Tag your professional customers

The function treats a customer as a professional if their account carries either of these tags: b2b or professional.

Add the tag to approved customers in Admin → Customers → (customer) → Tags. A tagged customer whose order email matches their account can check out with professional products; everyone else is blocked.

05 Testing the behavior

  1. Make sure a product belongs to the configured "Professional" collection.
  2. As a guest / untagged customer: add that product to the cart and proceed to checkout — the "Place order" button should be blocked with the professional-account message.
  3. As a customer tagged b2b or professional: log in and repeat — checkout should now succeed.
  4. A cart with no professional-collection products always checks out normally, regardless of tags.

06 Reference

Key files

PathPurpose
extensions/philocaly-cart-checkout-validation/src/cart_validations_generate_run.jsThe validation logic.
…/src/cart_validations_generate_run.graphqlInput query — the cart & buyer data the function receives.
…/shopify.extension.tomlExtension config: target, input metafield namespace/key.
shopify.app.tomlApp-level configuration.

What the function reads (input query)

Per cart, the function receives: the customer's numberOfOrders and whether they have the b2b / professional tags; for each line, the product and which of the configured collections it belongs to (inCollections); and the cart's total cost.

07 Troubleshooting

SymptomCheck
Checkout is never blockedConfirm the validation metafield is set on the correct Validation owner, that the collection GID is correct, and that a cart product actually belongs to that collection.
Checkout is always blocked, even for B2B customersVerify the customer is logged in and carries the exact tag b2b or professional (tags are evaluated against the logged-in customer).
Changes to the JS aren't reflectedRe-run shopify app function typegen if you changed inputs, then shopify app deploy.
Function not appearing in checkout rulesMake sure the app is installed on the store and the function was deployed successfully.

Checkout UI Extension Developer app

01 Purpose

A checkout extension that automatically clears the shopping cart when a shopper logs out during checkout — preventing the next person on the same device from inheriting (and purchasing) a previous user's cart.

This extension enhances security and privacy at checkout. Its primary business goal is to stop B2B customers from buying products at retail prices: when a B2B (wholesale) user logs out, their negotiated cart must not remain available to a subsequent, unauthenticated retail shopper using the same browser or device.

It does this silently in the background — it renders no visible UI. It watches the customer's authentication state and, the moment a logged-in session becomes logged out, it empties the cart.

Privacy

No leftover cart contents are exposed to the next user of a shared device.

Price integrity

Prevents wholesale carts from being checked out at retail pricing after logout.

02 How it works

The extension is rendered at the purchase.checkout.block.render target. On each render it compares the shopper's current login state against the state it stored last time, using Shopify's per-extension shopify.storage API.

Read previous state  →  Read current customer  →  Write current state
        →  logged in → logged out?  →  Remove every cart line

The core logic lives in extensions/philocaly-ui/src/Checkout.jsx:

const customer = useCustomer();
const cartLines = useCartLines();
const applyCartLinesChange = useApplyCartLinesChange();

const { storage } = shopify;
const isLoggedIn = !!customer;

// Compare the previous state with the current one
const wasLoggedIn = await storage.read('isLoggedIn');
await storage.write('isLoggedIn', isLoggedIn);

// On a logged-in → logged-out transition, empty the cart
if (wasLoggedIn === true && isLoggedIn === false) {
  for (const line of cartLines) {
    await applyCartLinesChange({
      type: 'removeCartLine',
      id: line.id,
      quantity: line.quantity,
    });
  }
}
Why shopify.storage? It persists a small flag (isLoggedIn) scoped to this extension, so the extension can detect a transition between renders rather than just reading a single point-in-time value.

03 Project structure

PathWhat it is
shopify.app.tomlApp-level config: client ID, name, access scopes (read_customers, unauthenticated_read_customers).
package.jsonRoot workspace and Shopify CLI scripts (dev, build, deploy).
extensions/philocaly-ui/The checkout UI extension itself.
…/shopify.extension.tomlExtension config: target, API access capability, API version.
…/src/Checkout.jsxExtension source — the cart-clearing logic (Preact).
…/locales/Translation strings (en.default.json, fr.json).

04 Setup & development

Prerequisites

  • Node.js and npm installed.
  • The Shopify CLI (invoked via npm run scripts / npx shopify).
  • A Shopify Partner account and a development store with checkout extensibility enabled.
  • Access to the Partner organization that owns the app client ID.

Setup

git clone <repo-url>
cd philocaly-ui-extension
npm install

Local development

Start the Shopify development server. The CLI prompts you to choose a Partner organization and a development store, then gives you a preview URL.

npm run dev
# equivalent to: shopify app dev
  1. Select your Partner organization when prompted.
  2. Select (or create) a development store.
  3. Open the preview URL the CLI prints, then go through checkout.
  4. Log in as a customer, then log out — confirm the cart empties. Check the browser console for the Philocaly UI Extension log line.
Tip: because this extension renders no visible UI, the easiest way to verify it is the cart behaviour itself plus the console logs from Checkout.jsx.

Deployment

npm run deploy
# equivalent to: shopify app deploy
  1. Release the version in the Partner Dashboard so it becomes live for merchants/stores.
  2. In the store's checkout editor, place the extension block where it should run (the default target lets the merchant choose placement).

05 Configuration reference

SettingValueFile
Extension targetpurchase.checkout.block.rendershopify.extension.toml
Checkout API version2025-10shopify.extension.toml
Webhooks API version2026-01shopify.app.toml
API accessenabled (api_access = true)shopify.extension.toml
Access scopesread_customers, unauthenticated_read_customersshopify.app.toml

Available helper scripts: npm run dev · build · deploy · generate · info

06 Troubleshooting

SymptomCheck
The cart doesn't clear on logoutConfirm the extension block is actually placed in the checkout editor, and that you transitioned from a logged-in to a logged-out state (the trigger only fires on that specific transition, not on a plain anonymous session).
Extension not appearing in checkoutMake sure the dev server is running (npm run dev), you opened the CLI's preview URL, and the store has checkout extensibility enabled.
"Failed to remove line" in the consoleA removeCartLine change returned an error. Verify the checkout supports cart line edits at the current step and that API access is enabled in shopify.extension.toml.

Chatters API Developer app

01 Overview

A small Node/Express service that creates Shopify orders for the Philocaly "Chatters" ambassador program — with deferred (net-terms) payment.

Chatters are Philocaly's brand ambassadors / sales reps. This API lets the storefront place orders on a chatter's behalf programmatically. Instead of charging the customer immediately, each order is created in Shopify as a draft, has shipping and an ambassador discount applied, is assigned net payment terms (the invoice is due on the first day of the month three months out), and is then completed into a real, unpaid order. This lets ambassadors order stock now and settle the invoice later.

What it does

Turns a cart payload into a completed Shopify order with discounts, calculated shipping and net payment terms.

Who calls it

The Philocaly storefront (CORS-locked to the philocalyhairextensions.com domains).

Where it runs

Deployed as a serverless function on Vercel; runs locally on port 4000 for dev.

02 How it works

When the storefront posts a cart to /create-order, the service performs a multi-step Shopify Admin GraphQL flow.

  1. Create draft order — builds a DraftOrderInput from the payload (customer email, line items, shipping/billing address) and applies the correct discount code. Shopify calculates taxes.
  2. Calculate shipping — runs draftOrderCalculate against the shipping address and selects the first available shipping rate.
  3. Fetch payment terms — queries the store's fixed/net payment-term templates and picks one.
  4. Update the draft — attaches the payment terms with a due date (1st of the month, +3 months), sets the chosen shipping line, and hides the draft from the customer.
  5. Complete the draft — converts it into a real order via draftOrderComplete and returns the order to the caller.

Every order is automatically tagged so downstream automation can find it:

YYYY-MMhq-orderAuto fulfilled chattersexclude

The chatters / exclude tag depends on whether the request is flagged as an HQ customer (isChattersHQCustomer), which also decides which discount code is applied.

03 Tech stack

ConcernChoice
RuntimeNode.js (ES Modules — "type": "module")
FrameworkExpress 4
Shopify@shopify/admin-api-client (Admin GraphQL)
Configdotenv
CORScors — locked to Philocaly domains
HostingVercel serverless (vercel.json)
AutomationShopify Mechanic tasks (the .liquid files)

04 Setup & environment

Prerequisites

  • Node.js 18+ and npm (or yarn — a yarn.lock is committed).
  • A Shopify store with a custom/private app that has Admin API access (draft orders, orders, customers, metafields).
  • The Shopify Admin access token and the relevant discount codes.

Install & run

# 1. Clone and enter the project
git clone <repo-url>
cd philocaly-chatters-api-node

# 2. Install dependencies
npm install        # or: yarn

# 3. Create your .env file (see below)

# 4. Start the server
npm run start      # plain node
npm run watch      # auto-reload with nodemon (dev)

Locally the server listens on http://localhost:4000. In production (NODE_ENV=production) it does not call app.listen — it exports the Express app for Vercel to invoke as a serverless function.

Environment variables

Create a .env file in the project root:

SHOPIFY_STORE_DOMAIN=your-store.myshopify.com
SHOPIFY_API_VERSION=2025-01
SHOPIFY_ADMIN_ACCESS_TOKEN=shpat_xxxxxxxxxxxxxxxx
SHOPIFY_DISCOUNT_CODE=chatters-discount
SHOPIFY_DISCOUNT_CODE_HQ=chatters-hq-discount
SHOPIFY_DISCOUNT_PERCENTAGE=20
VariablePurpose
SHOPIFY_STORE_DOMAINThe *.myshopify.com domain of the store.
SHOPIFY_API_VERSIONAdmin API version, e.g. 2025-01.
SHOPIFY_ADMIN_ACCESS_TOKENAdmin API access token for the private app.
SHOPIFY_DISCOUNT_CODEDiscount applied to standard chatter orders.
SHOPIFY_DISCOUNT_CODE_HQDiscount applied when isChattersHQCustomer is true.
SHOPIFY_DISCOUNT_PERCENTAGEDiscount percentage (reference value).
Heads up: never commit your real .env or access token. On Vercel, set these under Project → Settings → Environment Variables.

Deployment

Deployment targets Vercel. The vercel.json builds index.js with @vercel/node and rewrites every route to that function:

{
  "version": 2,
  "builds": [{ "src": "index.js", "use": "@vercel/node" }],
  "rewrites": [{ "source": "/(.*)", "destination": "/index.js" }]
}

Push to the connected Git branch (or run vercel from the CLI) and add the environment variables in the Vercel dashboard.

05 API endpoints

All requests must come from an allowed origin (CORS): philocalyhairextensions.com, www.philocalyhairextensions.com, or philocaly-hair.myshopify.com. Request bodies are JSON.

POST /create-order

Creates and completes a Shopify order with shipping, discount and net payment terms.

Request body

{
  "customer": { "toAssociate": { "email": "rep@example.com" } },
  "isChattersHQCustomer": "false",
  "shippingAddress": {
    "address1": "123 Main St",
    "city": "Austin",
    "province": "TX",
    "country": "United States",
    "zip": "78701"
  },
  "lineItems": [
    { "variantId": "gid://shopify/ProductVariant/123456789", "quantity": 2 }
  ]
}
FieldNotes
customer.toAssociate.emailEmail attached to the draft order.
isChattersHQCustomerString "true"/"false". Selects HQ vs standard discount and the exclude/chatters tag.
shippingAddressUsed for both shipping and billing, and to calculate shipping rates.
lineItems[]Each item needs a variantId (GID) and quantity. Catalog price is used.

Success — 200

{
  "message": "Order created",
  "order": {
    "id": "gid://shopify/Order/...",
    "totalTaxSet": { "shopMoney": { "amount": "12.34", "currencyCode": "USD" } }
  }
}

Errors400 with { error, details } for GraphQL/user errors or when no shipping rates are available; 500 on unexpected failures.

POST /add-birthdate

Stores a customer's birthday as a metafield (loyaltylion.birthday, type date) — used for loyalty / LoyaltyLion.

Request body

{
  "customerId": "1234567890",
  "birthdate": "1990-05-21"   // YYYY-MM-DD
}

Returns 200 with the updated metafields, or 400 for an invalid date format / metafield errors.

06 Companion Mechanic tasks

The repo also ships Shopify Mechanic task templates (.liquid). These run inside Shopify (not in this API) and handle the back-office side of the program — consolidating, invoicing and notifying.

FileRole
mechanic-consolidate.liquidFinds open chatters orders for a month tag and consolidates them (looks up the NET payment-terms template).
mechanic-convert-to-order.liquidGathers open drafts tagged chatters + ready-to-complete and converts them to orders.
mechanic-mark-inner-consolidate-paid.liquidMarks the inner/consolidated orders as paid.
mechanic-send-email.liquidSends notification email(s), optionally filtered by a product tag on the order.
Related: these tasks are the runnable templates behind the flows described in the Chatters → Order processing workflow section.

07 Troubleshooting

SymptomCheck
CORS errorsThe calling origin must be in the allow-list in index.js. Add new domains there.
"No shipping rates available"The store has no shipping rate matching the address; check Shopify shipping zones.
GraphQL / draft errorsInspect the details array in the 400 response; usually a bad variantId, missing scope, or invalid discount code.
Auth failuresVerify SHOPIFY_ADMIN_ACCESS_TOKEN, SHOPIFY_STORE_DOMAIN and SHOPIFY_API_VERSION match the store/app.