# Vendor Packet

A packet of three documents — the purchase order composed live, a filled W-9 painted from its PDF layer, and the vendor's certificate of insurance as an annex, whose bytes it ships.

Canonical URL: https://docs.asterdoc.com/components/blocks/vendor-packet/

`vendor-packet` is the one block here that is not a single document: it is
three, one of each kind a packet can hold — a live composition, a filled
registry PDF form, and an uploaded annex — bundled so a buyer can send a new
supplier everything they need to sign and certify in one scroll.

## Preview


```tsx
import { useEffect, useState } from "react";

/**
 * Block-preview wrapper for the `/components/blocks/vendor-packet` docs
 * page.
 *
 * `VendorPacketDocument` already self-paginates — it wraps its purchase-order
 * part in its own `Pages` (see `vendor-packet-document.tsx`) — so this
 * wrapper exists for a different reason: `taxpayerPdf` is a required prop the
 * composition takes as already-rendered bytes, and producing those bytes is
 * an async step every consumer does themselves. `insurancePdf` needs no such
 * step — the certificate ships as bytes the block installs — so only the
 * taxpayer PDF is loaded here.
 */

import { w9 } from "@asterdoc/essentials";
import { vendorPacketData } from "@/artifacts/asterdoc/vendor-packet.data";
import { VendorPacketDocument } from "@/components/asterdoc/vendor-packet";

function errorMessage(error: unknown): string {
  return error instanceof Error ? error.message : String(error);
}

export function VendorPacketBlockPreview() {
  const [taxpayerPdf, setTaxpayerPdf] = useState<Uint8Array | null>(null);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    let cancelled = false;
    const parsed = w9.safeParseData(vendorPacketData.taxpayer);
    if (!parsed.success) {
      const details = parsed.errors.map((issue) => `${issue.field}: ${issue.message}`).join("; ");
      setError(
        `The taxpayer's sample data failed validation against the W-9 artifact. ${details}`
      );
      return;
    }
    w9.fill(parsed.data)
      .render({ layer: "pdf" })
      .then((output) => {
        if (cancelled) return;
        if (output instanceof Uint8Array) setTaxpayerPdf(output);
        else setError("Rendering the W-9's PDF layer did not return PDF bytes.");
      })
      .catch((cause: unknown) => {
        if (!cancelled) setError(`Rendering the W-9's PDF layer failed. ${errorMessage(cause)}`);
      });
    return () => {
      cancelled = true;
    };
  }, []);

  if (error) {
    return <p className="p-6 text-sm text-red-600">{error}</p>;
  }

  if (!taxpayerPdf) {
    return <p className="p-6 text-sm text-neutral-500">Rendering the filled W-9&hellip;</p>;
  }

  return (
    <VendorPacketDocument taxpayerPdf={taxpayerPdf} insurancePdf={vendorPacketData.insurance} />
  );
}
```


## Installation


```bash
npx shadcn@4 add @asterdoc/vendor-packet
```


```bash
asterdoc add vendor-packet
```


Installs to artifacts/asterdoc/vendor-packet.artifact.ts, artifacts/asterdoc/vendor-packet.annex.ts, artifacts/asterdoc/vendor-packet.data.ts, components/asterdoc/vendor-packet.tsx.


## Usage


**Composition**

```tsx
/** @jsxRuntime classic */
import React from "react";
import { Bundle } from "@/components/asterdoc/bundle";
import { markDocumentRoot, type PagePlan } from "@asterdoc/react";
import { PdfPages, type PdfPaintReport } from "@/components/asterdoc/pdf-pages";
import { Pages } from "@/components/asterdoc/pages";
import { Part } from "@/components/asterdoc/part";
import {
  PurchaseOrderDocument,
  purchaseOrderFurniture,
} from "@/components/asterdoc/purchase-order";
import {
  purchaseOrderData as defaultPurchaseOrderData,
  type PurchaseOrderData,
} from "@/artifacts/asterdoc/purchase-order.data";
import {
  VENDOR_PACKET_ANNEX_FILENAME,
  VENDOR_PACKET_KEYS,
} from "../../artifacts/asterdoc/vendor-packet.artifact";

/**
 * The vendor packet on screen.
 *
 * **Sample material, not a stable API**, for the reason `./examples` gives.
 *
 * Three documents in one scroll, one of each kind, each numbering its own
 * pages: the purchase order paginated live from its tree, the filled W-9
 * painted from the PDF its official layer produced, and the certificate of
 * insurance painted from the bytes the vendor supplied. This is the whole of
 * what a packet preview is; everything specific to it is the labels.
 *
 * The PDFs arrive as bytes rather than being produced here, because both are
 * produced outside the browser: the W-9 by filling its layer, the certificate
 * by whoever issued it. Passing the seal's `parts` in as well labels each
 * document with the packet pages it occupies, so what a reader sees on screen
 * and what the signature map says are visibly the same pages.
 */

/** Where one part sits in the packet, as core's bundle seal reports it. */
export interface PacketPlacement {
  /** The bundle content key. */
  key: string;
  /** 1-based packet page the part's first page is. Zero for an attached part. */
  firstPage: number;
  /** Pages the part contributes. Zero for an attached part. */
  pageCount: number;
  /** True when the part is carried beside the packet rather than merged into it. */
  attached?: boolean;
}

export interface VendorPacketDocumentProps {
  /** The purchase order's data. Defaults to the sample's own. */
  purchaseOrderData?: PurchaseOrderData;
  /** The filled W-9, as its PDF layer rendered it. */
  taxpayerPdf: Uint8Array;
  /** The certificate of insurance, as the vendor supplied it. */
  insurancePdf: Uint8Array;
  /** Where each part sits in the packet, when the caller has sealed it. */
  placements?: readonly PacketPlacement[];
  /**
   * The `packetHash` the placements were computed for, and the packet on screen.
   *
   * They are one value here because the packet on screen is the packet that was
   * sealed. A host that lets a document change under a stale seal passes the
   * two separately to `Part`, and each part says its pages are pending until
   * the reseal lands.
   */
  packetHash?: string;
  /** Where pdf.js loads its worker from. See `paintPdfPages`. */
  workerSrc?: string;
  /** Where pdf.js loads the standard fourteen fonts from. See `paintPdfPages`. */
  standardFontDataUrl?: string;
  /** Where pdf.js loads its CMaps from. See `paintPdfPages`. */
  cMapUrl?: string;
  /** How long each PDF part may take to paint before it becomes an attachment. */
  timeoutMs?: number;
  /** Called as each PDF part settles, painted or attached. */
  onPaint?: (key: string, report: PdfPaintReport) => void;
  /** Called with the purchase order's page plan. */
  onPaginate?: (plan: PagePlan) => void;
}

/** The packet, on screen. */
export function VendorPacketDocument({
  purchaseOrderData = defaultPurchaseOrderData,
  taxpayerPdf,
  insurancePdf,
  placements,
  packetHash,
  workerSrc,
  standardFontDataUrl,
  cMapUrl,
  timeoutMs,
  onPaint,
  onPaginate,
}: VendorPacketDocumentProps) {
  const placed = (key: string) => placements?.find((placement) => placement.key === key);
  /** Everything a part needs to say where it sits, or nothing when unsealed. */
  const placement = (key: string) => ({
    firstPage: placed(key)?.firstPage,
    pageCount: placed(key)?.pageCount,
    attached: placed(key)?.attached,
    placedFor: packetHash,
    packetHash,
  });

  return (
    <Bundle id="vendor-packet" className="flex flex-col gap-10">
      <Part
        id={VENDOR_PACKET_KEYS.purchaseOrder}
        kind="composition"
        label="Purchase order · composed live"
        {...placement(VENDOR_PACKET_KEYS.purchaseOrder)}
      >
        <Pages furniture={purchaseOrderFurniture} onPaginate={onPaginate}>
          <PurchaseOrderDocument data={purchaseOrderData} />
        </Pages>
      </Part>

      <Part
        id={VENDOR_PACKET_KEYS.taxpayer}
        kind="form"
        label="Form W-9 · filled through its PDF layer"
        {...placement(VENDOR_PACKET_KEYS.taxpayer)}
      >
        <PdfPages
          bytes={taxpayerPdf}
          filename="w-9.pdf"
          workerSrc={workerSrc}
          standardFontDataUrl={standardFontDataUrl}
          cMapUrl={cMapUrl}
          timeoutMs={timeoutMs}
          onPaint={(report) => onPaint?.(VENDOR_PACKET_KEYS.taxpayer, report)}
        />
      </Part>

      <Part
        id={VENDOR_PACKET_KEYS.insurance}
        kind="annex"
        label="Certificate of insurance · annex"
        {...placement(VENDOR_PACKET_KEYS.insurance)}
      >
        <PdfPages
          bytes={insurancePdf}
          filename={VENDOR_PACKET_ANNEX_FILENAME}
          workerSrc={workerSrc}
          standardFontDataUrl={standardFontDataUrl}
          cMapUrl={cMapUrl}
          timeoutMs={timeoutMs}
          onPaint={(report) => onPaint?.(VENDOR_PACKET_KEYS.insurance, report)}
        />
      </Part>
    </Bundle>
  );
}

markDocumentRoot(VendorPacketDocument);
export default VendorPacketDocument;
```

**Sample data**

```ts
import {
  purchaseOrderData,
  type PurchaseOrderData,
} from "@/artifacts/asterdoc/purchase-order.data";
import { vendorPacketAnnexBytes } from "./vendor-packet.annex";

/**
 * Sample data for the vendor packet.
 *
 * **Sample material, not a stable API**, for the reason `./examples` gives.
 *
 * A packet is three documents and its sample is three things, one per part: the
 * purchase order's own data, the taxpayer's values for the W-9, and the annex
 * bytes. The three are gathered here so the block installs a packet a consumer
 * can render without inventing any of them.
 *
 * The W-9 itself is not here, and that is deliberate: this data module carries
 * values only. The block preview imports `@asterdoc/essentials`; other consumers
 * may resolve the packet's registry part through their own application wiring.
 * So this carries the taxpayer's values and nothing else. A consumer holds them
 * against the artifact:
 *
 * ```ts
 * import { w9 } from "@asterdoc/essentials";
 *
 * const parsed = w9.safeParseData(vendorPacketTaxpayerData);
 * const draft = w9
 *   .fill(parsed.data)
 *   .addSigner("taxpayer-signer", { person: { name: "Dana Whitfield" } })
 *   .addSignatory("taxpayer", "taxpayer-0", { signerId: "taxpayer-signer" });
 *
 * const pdf = await draft.render({ layer: "pdf" });
 * ```
 *
 * No resolver is passed: `@asterdoc/essentials` binds each artifact's bundled
 * resolver when it constructs the form, and every instance derived from it —
 * including the one every mutator above returns — carries it.
 */

/**
 * The taxpayer's values for the packet's W-9.
 *
 * Untyped against the artifact on purpose: typing it would mean importing the
 * artifact, and this module does not. The shape is the payload `safeParseData`
 * takes, and the values are the supplier the purchase order names, so the two
 * parts of the packet describe one company.
 *
 * The party carries `firstName`/`lastName` alongside `name`: the format
 * package infers a party's identity (person vs. organization) from which of
 * those disambiguating keys are present on the record itself, and a bare
 * `name` alone answers neither, so rendering the taxpayer's identity — filled
 * PDF included — would otherwise fail as ambiguous.
 */
export const vendorPacketTaxpayerData = {
  parties: {
    taxpayer: { id: "taxpayer-0", name: "Dana Whitfield", firstName: "Dana", lastName: "Whitfield" },
  },
  fields: {
    taxClassification: "partnership",
    businessName: "Northgate Systems, LLC",
    ein: "47-2938471",
    mailingAddress: {
      line1: "1400 Rio Grande Street",
      line2: "Suite 220",
      locality: "Austin",
      region: "TX",
      postalCode: "78701",
      country: "US",
    },
  },
} as const;

/** The person who signs the W-9 for the taxpayer party. */
export const VENDOR_PACKET_TAXPAYER_SIGNER = {
  signerId: "taxpayer-signer",
  partyId: "taxpayer-0",
  person: { name: "Dana Whitfield" },
} as const;

/** The whole packet's sample, one entry per part. */
export interface VendorPacketData {
  /** The purchase order's data, as its own block ships it. */
  purchaseOrder: PurchaseOrderData;
  /** The taxpayer's values for the W-9. */
  taxpayer: typeof vendorPacketTaxpayerData;
  /** The certificate of insurance, as bytes the packet carries. */
  insurance: Uint8Array;
}

/** The vendor packet's sample. */
export const vendorPacketData: VendorPacketData = {
  purchaseOrder: purchaseOrderData,
  taxpayer: vendorPacketTaxpayerData,
  insurance: vendorPacketAnnexBytes,
};

export { vendorPacketAnnexBytes };
```


## Composition

The packet's root is a [`Bundle`](/components/bundle), and each of its three
documents is a [`Part`](/components/part): the boundary that gives every
document its own page numbers and, once the packet is sealed, says where in
the packet those pages land.

The first part is the [purchase order](/components/blocks/purchase-order)
composed live — the very artifact that block installs, not a copy of it, so
one edit to the order is one edit for both blocks. It wraps in its own
[`Pages`](/components/pages), because a composition's pages come from a plan
this packet computes, unlike the two PDF parts beside it.

The second part is the IRS W-9, filled through its official PDF layer and
painted by [`PdfPages`](/components/pdf-pages). A PDF already has its own
pages at its own paper size, so `PdfPages` paints exactly those pages rather
than planning new ones — the W-9 keeps its own paper even inside a packet
branded to another. Filling it needs `@asterdoc/essentials`, which the block
declares as a dependency even though nothing it ships imports it: the
packet's sample carries the taxpayer's values, not the form, so a consumer
does the filling themselves:

```tsx
import { w9 } from "@asterdoc/essentials";
import { vendorPacketData } from "@/artifacts/asterdoc/vendor-packet.data";

const parsed = w9.safeParseData(vendorPacketData.taxpayer);
if (!parsed.success) throw new Error("Invalid taxpayer data");
const draft = w9.fill(parsed.data);
const taxpayerPdf = await draft.render({ layer: "pdf" });
```

No resolver is passed: `@asterdoc/essentials` binds each artifact's bundled
resolver when it constructs the form, and every instance derived from it,
including `draft`, carries it.

The third part is the certificate of insurance, also painted by `PdfPages`.
Its readable PDF bytes are merged into the packet's pages; an annex is carried
as a true attachment only when its bytes cannot be read as a PDF. It is not
the caller's to supply: the block installs it as `vendor-packet.annex.ts`, a
module of bytes, because a block installed into a browser project has no engine
to draw one with.

## Variants

A block's Variants section shows an existing alternate sample-data scenario,
never a new prop configuration. Both of this block's parts that take a live
sample — the purchase order and the taxpayer's W-9 values — ship exactly one
sample-data scenario today, and the certificate of insurance ships one
annex. There is no second scenario to show, so rather than invent one, this
variant documents that and renders the same real packet again.

### Standard packet

The block's one real sample scenario, shown again for its own subheading.


```tsx
import { useEffect, useState } from "react";

/**
 * Block-preview wrapper for the `/components/blocks/vendor-packet` docs
 * page.
 *
 * `VendorPacketDocument` already self-paginates — it wraps its purchase-order
 * part in its own `Pages` (see `vendor-packet-document.tsx`) — so this
 * wrapper exists for a different reason: `taxpayerPdf` is a required prop the
 * composition takes as already-rendered bytes, and producing those bytes is
 * an async step every consumer does themselves. `insurancePdf` needs no such
 * step — the certificate ships as bytes the block installs — so only the
 * taxpayer PDF is loaded here.
 */

import { w9 } from "@asterdoc/essentials";
import { vendorPacketData } from "@/artifacts/asterdoc/vendor-packet.data";
import { VendorPacketDocument } from "@/components/asterdoc/vendor-packet";

function errorMessage(error: unknown): string {
  return error instanceof Error ? error.message : String(error);
}

export function VendorPacketBlockPreview() {
  const [taxpayerPdf, setTaxpayerPdf] = useState<Uint8Array | null>(null);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    let cancelled = false;
    const parsed = w9.safeParseData(vendorPacketData.taxpayer);
    if (!parsed.success) {
      const details = parsed.errors.map((issue) => `${issue.field}: ${issue.message}`).join("; ");
      setError(
        `The taxpayer's sample data failed validation against the W-9 artifact. ${details}`
      );
      return;
    }
    w9.fill(parsed.data)
      .render({ layer: "pdf" })
      .then((output) => {
        if (cancelled) return;
        if (output instanceof Uint8Array) setTaxpayerPdf(output);
        else setError("Rendering the W-9's PDF layer did not return PDF bytes.");
      })
      .catch((cause: unknown) => {
        if (!cancelled) setError(`Rendering the W-9's PDF layer failed. ${errorMessage(cause)}`);
      });
    return () => {
      cancelled = true;
    };
  }, []);

  if (error) {
    return <p className="p-6 text-sm text-red-600">{error}</p>;
  }

  if (!taxpayerPdf) {
    return <p className="p-6 text-sm text-neutral-500">Rendering the filled W-9&hellip;</p>;
  }

  return (
    <VendorPacketDocument taxpayerPdf={taxpayerPdf} insurancePdf={vendorPacketData.insurance} />
  );
}
```
