Vache prompts. Claude codes.How it works

Linesheet Ships Eight Features in One Commit: From Buyer Segments to PO PDFs

·5 min read·by Vache Sarkissian
Updated June 3, 2026
·
Reviewed March 30, 2026
linesheetconvextypescriptreact-pdffeature
📚Top of Funnel

Written by Claude (Opus 4.6) Vache prompted, reviewed, and published. The data and benchmarks are real; the prose is AI-generated.

Eight Features in One Commit

Linesheet deployed 8 new features in a single 2,839-line commit across 30 files. Two waves: buyer-facing (wave 1) and seller operations (wave 2).

Wave 1 — Buyer Experience (ships-now filter, sustainability metadata, order confirmation emails, multi-currency display): Improves the product showcase experience for wholesale buyers, with clearer shipping timelines, environmental impact transparency, formal order receipts, and flexible pricing in buyer home currencies.

Wave 2 — Seller Operations (season archives, buyer segments, PO PDF generation, enhanced analytics): Gives sellers new tools to manage inventory lifecycles, segment buyers for custom pricing, automate purchase order workflows, and track sales trends.

Buyer Segments with Price Modifiers

Buyer segments allow sellers to create audience-specific share links with custom pricing. Each segment has a price modifier (a multiplier applied to base prices), optional style include/exclude lists, and a description.

// convex/buyerSegments.ts
export const create = mutation({
  args: {
    name: v.string(),
    description: v.optional(v.string()),
    priceModifier: v.optional(v.number()),
    includedStyleIds: v.optional(v.array(v.id("styles"))),
    excludedStyleIds: v.optional(v.array(v.id("styles"))),
  },
  handler: async (ctx, args) => {
    await requireAuth(ctx);
    const tenantId = await getTenantId(ctx);
    const trimmedName = args.name.trim();
    if (trimmedName.length === 0)
      throw new Error("Segment name cannot be empty");
    if (args.priceModifier !== undefined &&
        (args.priceModifier <= 0 || !isFinite(args.priceModifier)))
      throw new Error("Price modifier must be a positive finite number");
    // ...
  },
});

The priceModifier validation catches a subtle bug — 0 as a falsy value was initially treated as "no modifier." The fix uses an explicit check: priceModifier <= 0 || !isFinite(priceModifier). Delete cascades are handled by nullifying segmentId on all affected share links when a segment is removed.

Multi-Currency Display

Share links can now display prices in any currency supported by Intl.NumberFormat. The implementation is presentation-only — all prices are stored in the base currency (USD), and conversion happens at display time using a manually configured exchange rate:

// src/lib/currency.ts
export function formatPrice(
  price: number,
  currencyCode?: string,
  exchangeRate?: number
): string {
  const code = currencyCode || "USD";
  const rate = exchangeRate != null && exchangeRate > 0
    && isFinite(exchangeRate) ? exchangeRate : 1;
  const converted = price * rate;
 
  try {
    return new Intl.NumberFormat("en-US", {
      style: "currency",
      currency: code,
    }).format(converted);
  } catch {
    return new Intl.NumberFormat("en-US", {
      style: "currency",
      currency: "USD",
    }).format(converted);
  }
}

The try/catch wrapping Intl.NumberFormat handles invalid currency codes — if someone types "XYZ" as a currency, it falls back to USD formatting instead of crashing the share page. The exchange rate also guards against zero, negative, and Infinity values.

Season Archives

Season archives capture a frozen snapshot of a linesheet's complete state — styles, colors, images, overrides, brand settings, pricing — into a single JSON document. This lets sellers preserve the exact state of a Fall 2026 linesheet before modifying it for Spring 2027.

The archive mutation resolves all style data inline (including price resolution through the full tier/discount/override chain) and serializes it to a JSON string stored as a single Convex document:

// convex/linesheetArchives.ts — size guard
const snapshot = JSON.stringify({ name, template, styles: resolvedStyles, /* ... */ });
 
if (snapshot.length > 900_000) {
  throw new Error(
    `Linesheet too large to archive (${Math.round(snapshot.length / 1024)}KB). ` +
    `Try removing unused styles first.`
  );
}

The 900KB guard exists because Convex has a 1MB document size limit. A linesheet with hundreds of styles and full color/image data can approach that boundary. The archive stores everything needed to render the linesheet independently — a viewer page at /linesheets/[id]/archives/[archiveId] deserializes and displays the snapshot without any live database queries.

Purchase Order PDF Generation

PO PDFs use react-pdf with dynamic imports ({ ssr: false }) to avoid server-side rendering issues. The template renders a professional document with brand header, buyer info, line items table, and summary totals:

// src/components/pdf/PurchaseOrderTemplate.tsx
export function PurchaseOrderTemplate({ order }: { order: PurchaseOrderData }) {
  const totalItems = order.lineItems.reduce((sum, li) => sum + li.quantity, 0);
  const hasAnyPrice = order.lineItems.some((li) => li.price !== undefined);
  const subtotal = order.lineItems.reduce(
    (sum, li) => sum + li.quantity * (li.price ?? 0), 0
  );
  // Renders: Header (brand + PO#) → Bill To → Line Items Table → Summary → Notes → Footer
}

The template conditionally shows price columns — if no line items have a price, the Unit Price and Total columns are omitted entirely. The download flow uses usePoPdfDownload, a custom hook that renders the PDF to a blob and triggers a browser download. Error feedback was added to surface rendering failures to the user instead of silently failing.

Order Confirmation Emails

When a buyer submits an order through a share link, an automated confirmation email is sent via Convex actions and a Next.js API route. The email pipeline uses Resend as the provider with HTML escaping to prevent injection:

// convex/emails.ts
function escapeHtml(unsafe: string): string {
  return unsafe
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#039;");
}

Rate limiting is enforced at 30 emails per 10 minutes per tenant. Email validation supports comma-separated recipient lists with per-address regex checking.

Ships-Now Filter and Sustainability Metadata

The ships-now filter is a client-side toggle on share link pages that filters styles to only those with totalInventory > 0. Buyers browsing a linesheet can flip the toggle to see only immediately available product.

Sustainability metadata adds three new fields to the style schema: originCountry, certifications (an array of strings), and factoryInfo. These fields propagate through all 5 output paths — web preview, PDF export, email, share link, and CSV export. The share page displays origin and certifications as inline badges when populated.

Enhanced Analytics

The engagement analytics system gained three new metrics: time-on-page tracking (via visibility API), device breakdown (desktop/mobile/tablet from user agent), and per-style interaction tracking. Conversion rate — orders divided by unique sessions — is computed in the analytics detail query. The updateEngagement mutation includes share link validation to prevent tracking calls against invalid or expired links. Engagement tracking calls use fire-and-forget .catch() patterns to avoid blocking the buyer's browsing experience.

Quality Review Fixes

The commit includes 11 quality fixes discovered during code review: fixing the resolvePriceForStyle call signature in archives (was 3 args, needed 5), adding cascade delete on segment removal, correcting a stale closure in useCallback dependency arrays, handling the falsy 0 in price modifiers, and more. The archive size guard and currency formatting fallback were both added during this review pass — they address edge cases that wouldn't surface until production data hit the boundaries.

Further Reading

Sources

About the Author

Vache Sarkissian

Building research infrastructure and products at the intersection of knowledge systems and machine learning. Creator of Linesheet Pro, vault-search, and the vachsark learning engine.

View Full Bio →
© 2026 Vache Sarkissian·Built with Claude Code
vachsark.com