OxifyOxify

Notification emails

Show a bundle as one grouped row with its SKU and total in order confirmation, shipping confirmation, and invoice emails.

By default, an order confirmation email lists every component of a box as its own row with its own price. This block regroups them into one row: the bundle's name and SKU, the components indented underneath, and one combined price.

Where to edit: Shopify admin → SettingsNotifications → pick a notification → Edit code.

The same block works in Order confirmation, Shipping confirmation, Order invoice, and any other notification that loops over line_items. Replace the existing {% for line in line_items %} loop with everything below.

Back it up first

Copy the original template into a text file before editing. Shopify's Revert to default button exists, but it throws away all your other customizations too.

The snippet

{%- comment -%} Collect the distinct bundle ids on this order {%- endcomment -%}
{%- assign bundle_ids = "" -%}
{%- for line in line_items -%}
  {%- if line.properties._bundle_id != blank -%}
    {%- assign bundle_ids = bundle_ids | append: line.properties._bundle_id | append: "~" -%}
  {%- endif -%}
{%- endfor -%}
{%- assign bundle_ids = bundle_ids | split: "~" | uniq -%}
 
{%- comment -%} 1. Non-bundle items render exactly as before {%- endcomment -%}
{%- for line in line_items -%}
  {%- if line.properties._bundle_id == blank -%}
    <tr>
      <td><img src="{{ line.image | image_url: width: 200 }}" width="100" alt=""></td>
      <td>
        {{ line.title }}<br>
        <span>x {{ line.quantity }}</span>
      </td>
      <td>{{ line.final_line_price | money }}</td>
    </tr>
  {%- endif -%}
{%- endfor -%}
 
{%- comment -%} 2. One grouped block per bundle {%- endcomment -%}
{%- for bid in bundle_ids -%}
  {%- assign bundle_name  = "" -%}
  {%- assign bundle_sku   = "" -%}
  {%- assign bundle_image = "" -%}
  {%- assign bundle_total = 0 -%}
  {%- assign bundle_was   = 0 -%}
 
  {%- for line in line_items -%}
    {%- if line.properties._bundle_id == bid -%}
      {%- if bundle_sku == "" -%}
        {%- assign bundle_sku = line.properties["Bundle SKU"] -%}
      {%- endif -%}
      {%- if bundle_name == "" -%}
        {%- if line.properties._oxify_builder != blank -%}
          {%- assign name_parts = line.properties._oxify_builder | split: '"n":"' -%}
          {%- if name_parts.size > 1 -%}
            {%- assign bundle_name = name_parts[1] | split: '"' | first -%}
          {%- endif -%}
        {%- endif -%}
        {%- if bundle_name == "" -%}
          {%- assign bundle_name = line.properties._bundle_name -%}
        {%- endif -%}
        {%- if line.properties._bundle_image_url != blank -%}
          {%- assign bundle_image = line.properties._bundle_image_url -%}
        {%- else -%}
          {%- assign bundle_image = line.image | image_url: width: 200 -%}
        {%- endif -%}
      {%- endif -%}
      {%- assign bundle_total = bundle_total | plus: line.final_line_price -%}
      {%- assign bundle_was   = bundle_was   | plus: line.original_line_price -%}
    {%- endif -%}
  {%- endfor -%}
 
  <tr>
    <td><img src="{{ bundle_image }}" width="100" alt=""></td>
    <td>
      <strong>{{ bundle_name | default: "Bundle" }}</strong>
      {%- if bundle_sku != blank -%}
        <div style="color:#6b7177; font-size:12px;">SKU {{ bundle_sku }}</div>
      {%- endif -%}
      <div style="margin-top:6px; padding-left:12px; border-left:2px solid #e3e3e3;">
        {%- for line in line_items -%}
          {%- if line.properties._bundle_id == bid -%}
            <div style="color:#6b7177; font-size:13px;">
              {{ line.title }} &times; {{ line.quantity }}
            </div>
          {%- endif -%}
        {%- endfor -%}
      </div>
    </td>
    <td>
      {%- if bundle_was > bundle_total -%}
        <s>{{ bundle_was | money }}</s><br>
      {%- endif -%}
      {{ bundle_total | money }}
    </td>
  </tr>
{%- endfor -%}

How it works

  • _bundle_id is the grouping key. Two boxes from the same builder in one order have different ids, so they render as two separate groups. That's intentional.
  • Totals are summed from the components, not read from the bundle parent — the parent doesn't exist on the order. final_line_price already includes every discount that applied (bundle pricing and any of your own automatic discounts), so the total in the email always matches what was charged.
  • The strikethrough price only renders when the components were actually discounted, so a no-discount bundle shows a single clean price.
  • The SKU comes from the visible Bundle SKU property and is printed once in the header rather than on every child row.

Gotchas

  • Don't sort children by _slot_index with Liquid's sort — it's a string, so slot 10 would sort before slot 2. line_items already comes back in cart order.
  • Per-item discount labels (your own "BUY 4 → 20% OFF" and similar) are dropped by this layout by design, since the group shows one combined price. To keep them, iterate line.discount_allocations inside the child loop.
  • Test with a real order. Shopify's notification preview uses fake line items with no properties, so nothing groups in the preview. Place a test order, then use Orders → the order → Resend email.

Next

Order Printer — invoices, pick lists, and return forms.

On this page