Hanzo
CommerceRecipes

B2B Commerce

Enterprise commerce with customer groups, bulk ordering, and purchase orders

This recipe covers setting up B2B (business-to-business) commerce with Hanzo Commerce -- customer groups for tiered pricing, bulk ordering, purchase orders with net terms, company accounts, and quote requests.

What You Will Build

  • Customer groups with tier-based pricing
  • Bulk ordering with quantity discounts
  • Purchase order and net-terms payment flow
  • Company accounts with multiple buyers
  • Quote request and approval workflow

Customer Groups

Customer groups let you segment business customers by tier and assign group-specific pricing.

import { Commerce } from '@hanzo/commerce'

const commerce = new Commerce({
  apiKey: process.env.HANZO_API_KEY!,
  environment: 'production',
})

const wholesale = await commerce.customerGroups.create({
  name: 'Wholesale',
  metadata: { tier: 'wholesale', discount: 30 },
})

const distributor = await commerce.customerGroups.create({
  name: 'Distributor',
  metadata: { tier: 'distributor', discount: 45 },
})

const enterprise = await commerce.customerGroups.create({
  name: 'Enterprise',
  metadata: { tier: 'enterprise', discount: 'custom' },
})

Assign a customer to a group:

await commerce.customers.update(customerId, {
  groupId: wholesale.id,
})

Tiered Price Lists

Create price lists scoped to customer groups. These override the default retail price.

// Retail price: $100.00
const product = await commerce.products.create({
  name: 'Industrial Widget',
  slug: 'industrial-widget',
  price: 10000,
  currency: 'USD',
})

// Wholesale price: $70.00 (30% off)
await commerce.priceLists.create({
  name: 'Wholesale Pricing',
  customerGroupId: wholesale.id,
  prices: [
    { productId: product.id, amount: 7000 },
  ],
})

// Distributor price: $55.00 (45% off)
await commerce.priceLists.create({
  name: 'Distributor Pricing',
  customerGroupId: distributor.id,
  prices: [
    { productId: product.id, amount: 5500 },
  ],
})

When a customer in the Wholesale group views products, they see $70.00 automatically.

Quantity Discounts

Add volume-based pricing tiers to encourage larger orders:

await commerce.priceLists.create({
  name: 'Wholesale Volume Pricing',
  customerGroupId: wholesale.id,
  prices: [
    {
      productId: product.id,
      tiers: [
        { minQuantity: 1,    amount: 7000 },  // 1-49:   $70.00 each
        { minQuantity: 50,   amount: 6000 },  // 50-99:  $60.00 each
        { minQuantity: 100,  amount: 5000 },  // 100-499: $50.00 each
        { minQuantity: 500,  amount: 4000 },  // 500+:   $40.00 each
      ],
    },
  ],
})

The cart automatically applies the correct tier based on quantity:

const cart = await commerce.carts.create({
  customerId: wholesaleCustomer.id,
  items: [
    { productId: product.id, quantity: 200 },
    // Price: 200 x $50.00 = $10,000.00
  ],
})

Company Accounts

B2B customers often have multiple buyers under one company. Company accounts group individual users under a shared billing entity.

const company = await commerce.companies.create({
  name: 'Acme Corp',
  email: '[email protected]',
  groupId: wholesale.id,
  billingAddress: {
    line1: '100 Industrial Blvd',
    city: 'Chicago',
    state: 'IL',
    postalCode: '60601',
    country: 'US',
  },
  settings: {
    paymentMethods: ['credit_card', 'purchase_order'],
    purchaseOrderLimit: 5000000, // $50,000.00
    netTerms: 30, // Net 30 days
  },
})

// Add buyers to the company
await commerce.companies.addMember(company.id, {
  customerId: buyerOne.id,
  role: 'admin', // can manage company settings
})

await commerce.companies.addMember(company.id, {
  customerId: buyerTwo.id,
  role: 'buyer', // can place orders
})

Company Roles

RolePermissions
adminManage members, update billing, view all orders, approve POs
buyerPlace orders, view own orders, submit POs for approval
viewerView orders and company info only

Purchase Orders

Enterprise customers often pay by purchase order (PO) with net terms instead of credit card.

Submitting a Purchase Order

const order = await commerce.orders.create({
  companyId: company.id,
  customerId: buyerOne.id,
  items: [
    { productId: product.id, quantity: 500 },
  ],
  paymentMethod: 'purchase_order',
  purchaseOrder: {
    number: 'PO-2026-0042',
    terms: 'net_30',
  },
})

Purchase Order Flow

submitted --> pending_approval --> approved --> invoiced --> paid
                               \-> rejected
  1. Buyer submits an order with a PO number
  2. Company admin receives an approval notification
  3. Admin approves or rejects the PO
  4. On approval, Commerce generates an invoice with net-30 terms
  5. Invoice is sent to the company billing email
  6. Payment is recorded when received

Approving a PO

// Company admin approves
await commerce.orders.approvePurchaseOrder(order.id, {
  approvedBy: adminCustomerId,
})

// Or reject with a reason
await commerce.orders.rejectPurchaseOrder(order.id, {
  rejectedBy: adminCustomerId,
  reason: 'Exceeds quarterly budget',
})

Net Terms and Invoicing

Configure net terms per company:

await commerce.companies.update(company.id, {
  settings: {
    netTerms: 30,       // Net 30 (default)
    // netTerms: 60,    // Net 60
    // netTerms: 0,     // Due on receipt
  },
})

Commerce automatically:

  • Generates invoices on order approval
  • Sends invoice emails to the company billing contact
  • Tracks payment due dates
  • Sends reminders at 7 days and 1 day before due
  • Marks invoices as overdue after the net-terms period
// List outstanding invoices for a company
const invoices = await commerce.invoices.list({
  companyId: company.id,
  status: 'unpaid',
})

Quote Requests

For enterprise deals or custom pricing, support a quote-request workflow:

// Buyer requests a quote
const quote = await commerce.quotes.create({
  companyId: company.id,
  customerId: buyerOne.id,
  items: [
    { productId: product.id, quantity: 10000, note: 'Annual supply contract' },
  ],
  message: 'Requesting volume pricing for annual contract.',
})

Quote Flow

requested --> draft --> sent --> accepted --> converted_to_order
                             \-> declined
                             \-> expired

Admin responds with a quote:

await commerce.quotes.send(quote.id, {
  items: [
    { productId: product.id, quantity: 10000, amount: 3500 }, // $35.00 each
  ],
  validUntil: '2026-03-16T00:00:00Z',
  message: 'Special annual contract pricing. Valid for 30 days.',
})

Customer accepts:

await commerce.quotes.accept(quote.id)
// Automatically creates an order with the quoted prices

B2B Storefront Considerations

When building a B2B storefront, account for these differences from B2C:

import { commerce } from '@/lib/commerce'
import { auth } from '@/lib/auth'

export default async function ProductsPage() {
  const session = await auth()
  const customer = session?.customerId
    ? await commerce.customers.retrieve(session.customerId)
    : null

  // Fetch products with customer-group pricing
  const { products } = await commerce.products.list({
    customerId: customer?.id,     // applies group pricing
    limit: 50,
  })

  // Show quantity input instead of simple "Add to Cart"
  // Show "Request Quote" button for large quantities
  // Show PO payment option at checkout if company allows it
}

Testing

  1. Create customer groups (Wholesale, Distributor)
  2. Create tiered price lists for each group
  3. Assign a customer to a group and verify they see group-specific prices
  4. Test quantity discounts by adding items with different quantities
  5. Create a company account and add members
  6. Submit a purchase order and test the approval flow
  7. Verify invoice generation and net-terms due dates
  8. Submit a quote request and test the accept/reject flow

Next Steps

How is this guide?

Last updated on

On this page