> ## Documentation Index
> Fetch the complete documentation index at: https://docs.gorules.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Dynamic pricing rules

> Build pricing rules that adjust based on customer segments, order values, and promotions.

export const DecisionGraphViz = ({nodes = [], edges = [], highlighted = null, title = null}) => {
  const nodeColors = {
    input: {
      bg: '#f8fafc',
      border: '#94a3b8',
      text: '#475569',
      icon: '→'
    },
    output: {
      bg: '#f8fafc',
      border: '#94a3b8',
      text: '#475569',
      icon: '←'
    },
    table: {
      bg: '#fefce8',
      border: '#d9c860',
      text: '#7c6f2a',
      icon: '▤'
    },
    expression: {
      bg: '#f5f3ff',
      border: '#a78bda',
      text: '#5b4a8c',
      icon: 'ƒ'
    },
    function: {
      bg: '#fdf2f8',
      border: '#e293b7',
      text: '#8c4a6d',
      icon: '{ }'
    },
    switch: {
      bg: '#f0fdfa',
      border: '#6bc4b8',
      text: '#3d7a72',
      icon: '◇'
    }
  };
  const nodeWidth = 140;
  const nodeHeight = 64;
  const horizontalGap = 80;
  const verticalGap = 24;
  const inDegree = {};
  const outEdges = {};
  nodes.forEach(n => {
    inDegree[n.id] = 0;
    outEdges[n.id] = [];
  });
  edges.forEach(e => {
    inDegree[e.to] = (inDegree[e.to] || 0) + 1;
    outEdges[e.from] = outEdges[e.from] || [];
    outEdges[e.from].push(e.to);
  });
  const queue = nodes.filter(n => inDegree[n.id] === 0).map(n => n.id);
  const nodeLevel = {};
  queue.forEach(id => nodeLevel[id] = 0);
  const tempInDegree = {
    ...inDegree
  };
  while (queue.length > 0) {
    const current = queue.shift();
    const currentLevel = nodeLevel[current];
    (outEdges[current] || []).forEach(next => {
      nodeLevel[next] = Math.max(nodeLevel[next] || 0, currentLevel + 1);
      tempInDegree[next]--;
      if (tempInDegree[next] === 0) queue.push(next);
    });
  }
  const levels = {};
  nodes.forEach(n => {
    const level = nodeLevel[n.id] || 0;
    levels[level] = levels[level] || [];
    levels[level].push(n);
  });
  const nodePositions = {};
  const numLevels = Object.keys(levels).length;
  const maxNodesInLevel = Math.max(...Object.values(levels).map(l => l.length), 1);
  const canvasHeight = Math.max(maxNodesInLevel * (nodeHeight + verticalGap) + 60, 180);
  const canvasWidth = numLevels * (nodeWidth + horizontalGap) + 80;
  Object.entries(levels).forEach(([level, levelNodes]) => {
    const levelNum = parseInt(level);
    const x = 40 + levelNum * (nodeWidth + horizontalGap);
    const totalHeight = levelNodes.length * nodeHeight + (levelNodes.length - 1) * verticalGap;
    const startY = (canvasHeight - totalHeight) / 2;
    levelNodes.forEach((node, index) => {
      nodePositions[node.id] = {
        x,
        y: startY + index * (nodeHeight + verticalGap)
      };
    });
  });
  const edgePaths = edges.map((edge, i) => {
    const from = nodePositions[edge.from];
    const to = nodePositions[edge.to];
    if (!from || !to) return null;
    const startX = from.x + nodeWidth;
    const startY = from.y + nodeHeight / 2;
    const endX = to.x - 6;
    const endY = to.y + nodeHeight / 2;
    const dx = endX - startX;
    const controlOffset = Math.min(dx * 0.4, 40);
    return {
      key: `edge-${i}`,
      path: `M ${startX} ${startY} C ${startX + controlOffset} ${startY}, ${endX - controlOffset} ${endY}, ${endX} ${endY}`
    };
  }).filter(Boolean);
  const uniqueId = `graph-${Math.random().toString(36).substring(2, 11)}`;
  const containerRef = useRef(null);
  const [scale, setScale] = useState(1);
  const [position, setPosition] = useState({
    x: 0,
    y: 0
  });
  const [isDragging, setIsDragging] = useState(false);
  const [dragStart, setDragStart] = useState({
    x: 0,
    y: 0
  });
  const handleWheel = e => {
    e.preventDefault();
    const delta = e.deltaY > 0 ? -0.1 : 0.1;
    setScale(s => Math.max(0.4, Math.min(2, s + delta)));
  };
  const handleMouseDown = e => {
    if (e.target.closest('button')) return;
    setIsDragging(true);
    setDragStart({
      x: e.clientX - position.x,
      y: e.clientY - position.y
    });
  };
  const handleMouseMove = e => {
    if (!isDragging) return;
    setPosition({
      x: e.clientX - dragStart.x,
      y: e.clientY - dragStart.y
    });
  };
  const handleMouseUp = () => setIsDragging(false);
  const handleFit = () => {
    const container = containerRef.current;
    if (container) {
      const containerWidth = container.clientWidth - 40;
      const containerHeight = container.clientHeight - 80;
      const scaleX = containerWidth / canvasWidth;
      const scaleY = containerHeight / canvasHeight;
      const fitScale = Math.min(scaleX, scaleY, 1);
      setScale(Math.max(0.4, fitScale));
    } else {
      setScale(1);
    }
    setPosition({
      x: 0,
      y: 0
    });
  };
  const handleZoomIn = () => setScale(s => Math.min(s + 0.15, 2));
  const handleZoomOut = () => setScale(s => Math.max(s - 0.15, 0.4));
  useEffect(() => {
    const container = containerRef.current;
    if (container) {
      container.addEventListener('wheel', handleWheel, {
        passive: false
      });
      return () => container.removeEventListener('wheel', handleWheel);
    }
  }, []);
  useEffect(() => {
    const timer = setTimeout(() => {
      const container = containerRef.current;
      if (container) {
        const containerWidth = container.clientWidth - 40;
        const containerHeight = container.clientHeight - 80;
        const scaleX = containerWidth / canvasWidth;
        const scaleY = containerHeight / canvasHeight;
        const fitScale = Math.min(scaleX, scaleY, 1);
        setScale(Math.max(0.4, fitScale));
      }
    }, 0);
    return () => clearTimeout(timer);
  }, [canvasWidth, canvasHeight]);
  const containerHeight = Math.max(300, Math.min(canvasHeight + 80, 420));
  return <div style={{
    marginTop: '16px',
    marginBottom: '24px'
  }}>
      {title && <div style={{
    fontSize: '13px',
    fontWeight: '500',
    color: '#64748b',
    marginBottom: '8px',
    fontFamily: 'system-ui, -apple-system, sans-serif'
  }}>
          {title}
        </div>}
      <div ref={containerRef} onMouseDown={handleMouseDown} onMouseMove={handleMouseMove} onMouseUp={handleMouseUp} onMouseLeave={handleMouseUp} style={{
    position: 'relative',
    border: '1px solid #e5e7eb',
    borderRadius: '12px',
    backgroundColor: '#f9fafb',
    overflow: 'hidden',
    height: containerHeight,
    cursor: isDragging ? 'grabbing' : 'grab',
    userSelect: 'none'
  }}>
        {}
        <div style={{
    position: 'absolute',
    top: '12px',
    right: '12px',
    display: 'flex',
    gap: '6px',
    zIndex: 10
  }}>
          <button onClick={handleZoomIn} style={{
    width: '32px',
    height: '32px',
    border: '1px solid #d1d5db',
    borderRadius: '8px',
    backgroundColor: 'white',
    cursor: 'pointer',
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'center',
    fontSize: '18px',
    color: '#6b7280',
    fontWeight: '400',
    boxShadow: '0 1px 2px rgba(0,0,0,0.05)',
    transition: 'background-color 0.15s'
  }} onMouseOver={e => e.target.style.backgroundColor = '#f3f4f6'} onMouseOut={e => e.target.style.backgroundColor = 'white'}>+</button>
          <button onClick={handleZoomOut} style={{
    width: '32px',
    height: '32px',
    border: '1px solid #d1d5db',
    borderRadius: '8px',
    backgroundColor: 'white',
    cursor: 'pointer',
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'center',
    fontSize: '18px',
    color: '#6b7280',
    fontWeight: '400',
    boxShadow: '0 1px 2px rgba(0,0,0,0.05)',
    transition: 'background-color 0.15s'
  }} onMouseOver={e => e.target.style.backgroundColor = '#f3f4f6'} onMouseOut={e => e.target.style.backgroundColor = 'white'}>−</button>
          <button onClick={handleFit} style={{
    height: '32px',
    padding: '0 12px',
    border: '1px solid #d1d5db',
    borderRadius: '8px',
    backgroundColor: 'white',
    cursor: 'pointer',
    fontSize: '12px',
    fontWeight: '500',
    color: '#6b7280',
    fontFamily: 'system-ui, -apple-system, sans-serif',
    boxShadow: '0 1px 2px rgba(0,0,0,0.05)',
    transition: 'background-color 0.15s'
  }} onMouseOver={e => e.target.style.backgroundColor = '#f3f4f6'} onMouseOut={e => e.target.style.backgroundColor = 'white'}>Reset</button>
        </div>

        {}
        <div style={{
    transform: `translate(calc(-50% + ${position.x}px), calc(-50% + ${position.y}px)) scale(${scale})`,
    transformOrigin: 'center center',
    width: canvasWidth,
    height: canvasHeight,
    position: 'absolute',
    left: '50%',
    top: '50%'
  }}>
          {}
          <svg style={{
    position: 'absolute',
    top: 0,
    left: 0,
    width: canvasWidth,
    height: canvasHeight,
    pointerEvents: 'none',
    overflow: 'visible'
  }}>
            <defs>
              <marker id={`${uniqueId}-arrow`} markerWidth="8" markerHeight="8" refX="6" refY="4" orient="auto" markerUnits="strokeWidth">
                <path d="M0,1 L6,4 L0,7 L2,4 Z" fill="#9ca3af" />
              </marker>
            </defs>
            {edgePaths.map(edge => <path key={edge.key} d={edge.path} fill="none" stroke="#d1d5db" strokeWidth="2" strokeLinecap="round" markerEnd={`url(#${uniqueId}-arrow)`} />)}
          </svg>

          {}
          {nodes.map(node => {
    const pos = nodePositions[node.id];
    if (!pos) return null;
    const colors = nodeColors[node.type] || nodeColors.expression;
    const isHighlighted = highlighted === node.id;
    return <div key={node.id} style={{
      position: 'absolute',
      left: pos.x,
      top: pos.y,
      width: nodeWidth,
      height: nodeHeight,
      backgroundColor: isHighlighted ? '#fef2f2' : colors.bg,
      border: `1.5px solid ${isHighlighted ? '#f87171' : colors.border}`,
      borderRadius: '10px',
      display: 'flex',
      flexDirection: 'column',
      alignItems: 'center',
      justifyContent: 'center',
      boxShadow: isHighlighted ? '0 0 0 2px rgba(248, 113, 113, 0.25)' : '0 1px 2px rgba(0,0,0,0.04)',
      cursor: 'default'
    }}>
                <div style={{
      fontSize: '14px',
      marginBottom: '3px',
      color: isHighlighted ? '#ef4444' : colors.text,
      opacity: 0.85
    }}>
                  {colors.icon}
                </div>
                <div style={{
      fontSize: '11px',
      fontWeight: '500',
      color: isHighlighted ? '#dc2626' : colors.text,
      textAlign: 'center',
      padding: '0 8px',
      fontFamily: 'system-ui, -apple-system, sans-serif',
      lineHeight: '1.35',
      maxWidth: '100%'
    }}>
                  {node.label}
                </div>
              </div>;
  })}
        </div>

        {}
        <div style={{
    position: 'absolute',
    bottom: '12px',
    left: '12px',
    display: 'flex',
    gap: '14px',
    padding: '8px 12px',
    fontSize: '11px',
    color: '#6b7280',
    fontFamily: 'system-ui, -apple-system, sans-serif',
    backgroundColor: 'white',
    borderRadius: '8px',
    border: '1px solid #e5e7eb',
    boxShadow: '0 1px 2px rgba(0,0,0,0.05)'
  }}>
          {Object.entries(nodeColors).map(([type, colors]) => <div key={type} style={{
    display: 'flex',
    alignItems: 'center',
    gap: '5px'
  }}>
              <div style={{
    width: '16px',
    height: '16px',
    backgroundColor: colors.bg,
    border: `1px solid ${colors.border}`,
    borderRadius: '4px',
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'center',
    fontSize: '9px',
    color: colors.text
  }}>
                {colors.icon}
              </div>
              <span style={{
    textTransform: 'capitalize'
  }}>{type}</span>
            </div>)}
        </div>
      </div>
    </div>;
};

export const ExpressionViz = ({entries = []}) => {
  const allCells = entries.flatMap((entry, i) => [<div key={`key-${i}`} style={{
    backgroundColor: '#dbeafe',
    color: '#1e40af',
    padding: '12px 16px',
    borderRadius: '8px',
    fontFamily: 'ui-monospace, monospace',
    fontSize: '14px',
    fontWeight: '500'
  }}>
      {entry.key}
    </div>, <div key={`val-${i}`} style={{
    backgroundColor: '#ffffff',
    color: '#334155',
    padding: '12px 16px',
    borderRadius: '8px',
    fontFamily: 'ui-monospace, monospace',
    fontSize: '14px',
    border: '1px solid #e2e8f0'
  }}>
      {entry.value}
    </div>]);
  return <div style={{
    backgroundColor: '#f1f5f9',
    borderRadius: '12px',
    padding: '20px',
    margin: '16px 0'
  }}>
      <div style={{
    display: 'grid',
    gridTemplateColumns: 'auto 1fr',
    gap: '8px'
  }}>
        {allCells}
      </div>
    </div>;
};

export const DecisionTableViz = ({inputs = [], outputs = [], rows = []}) => {
  const colCount = inputs.length + outputs.length;
  const getFieldDisplay = field => !field || field === '-' ? '-' : field;
  const allCells = rows.flatMap((row, ri) => [...inputs.map((input, i) => <div key={`c-${ri}-in-${i}`} style={{
    backgroundColor: '#ffffff',
    padding: '12px 16px',
    borderRadius: '8px',
    fontFamily: 'ui-monospace, monospace',
    fontSize: '14px',
    border: '1px solid #e2e8f0'
  }}>
        {row[input.field] ?? ''}
      </div>), ...outputs.map((output, i) => <div key={`c-${ri}-out-${i}`} style={{
    backgroundColor: '#ffffff',
    padding: '12px 16px',
    borderRadius: '8px',
    fontFamily: 'ui-monospace, monospace',
    fontSize: '14px',
    border: '1px solid #e2e8f0'
  }}>
        {row[output.field] ?? ''}
      </div>)]);
  return <div style={{
    backgroundColor: '#f1f5f9',
    borderRadius: '12px',
    padding: '20px',
    overflowX: 'auto',
    margin: '16px 0'
  }}>
      <div style={{
    display: 'grid',
    gridTemplateColumns: `repeat(${colCount}, 1fr)`,
    gap: '8px'
  }}>
        {}
        <div key="group-inputs" style={{
    gridColumn: `span ${inputs.length}`,
    padding: '8px 16px',
    fontWeight: '600',
    fontSize: '13px',
    color: '#374151',
    borderBottom: '2px solid #e2e8f0'
  }}>
          Inputs
        </div>
        <div key="group-outputs" style={{
    gridColumn: `span ${outputs.length}`,
    padding: '8px 16px',
    fontWeight: '600',
    fontSize: '13px',
    color: '#374151',
    borderBottom: '2px solid #e2e8f0'
  }}>
          Outputs
        </div>

        {}
        {inputs.map((input, i) => <div key={`h-in-${i}`} style={{
    backgroundColor: '#f0abfc',
    color: '#581c87',
    padding: '12px 16px',
    borderRadius: '8px',
    fontWeight: '500'
  }}>
            <div>{input.name}</div>
            <div style={{
    display: 'inline-block',
    marginTop: '6px',
    padding: '2px 8px',
    fontSize: '12px',
    fontWeight: '400',
    backgroundColor: 'rgba(88, 28, 135, 0.15)',
    borderRadius: '4px',
    fontFamily: 'ui-monospace, monospace'
  }}>
              {getFieldDisplay(input.field)}
            </div>
          </div>)}
        {outputs.map((output, i) => <div key={`h-out-${i}`} style={{
    backgroundColor: '#a5b4fc',
    color: '#312e81',
    padding: '12px 16px',
    borderRadius: '8px',
    fontWeight: '500'
  }}>
            <div>{output.name}</div>
            <div style={{
    display: 'inline-block',
    marginTop: '6px',
    padding: '2px 8px',
    fontSize: '12px',
    fontWeight: '400',
    backgroundColor: 'rgba(49, 46, 129, 0.15)',
    borderRadius: '4px',
    fontFamily: 'ui-monospace, monospace'
  }}>
              {getFieldDisplay(output.field)}
            </div>
          </div>)}

        {}
        {allCells}
      </div>
    </div>;
};

This guide walks through building a dynamic pricing system that calculates discounts, applies promotions, and determines final prices based on customer and order data.

## What you'll build

A pricing decision that:

* Applies tiered discounts based on customer loyalty level
* Adds volume discounts for large orders
* Stacks promotional codes
* Calculates final price with all adjustments

<Note>
  Want to skip ahead? <a href="/downloads/dynamic-pricing.json" download>Download the completed decision</a> and import it directly.
</Note>

### Decision flow

<DecisionGraphViz
  title="Dynamic Pricing"
  nodes={[
{ id: "input", type: "input", label: "Order Data" },
{ id: "tierDiscount", type: "table", label: "Tier Discount" },
{ id: "volumeDiscount", type: "table", label: "Volume Discount" },
{ id: "promoCode", type: "table", label: "Promo Code" },
{ id: "calculate", type: "expression", label: "Calculate Final" }
]}
  edges={[
{ from: "input", to: "tierDiscount" },
{ from: "input", to: "volumeDiscount" },
{ from: "input", to: "promoCode" },
{ from: "tierDiscount", to: "calculate" },
{ from: "volumeDiscount", to: "calculate" },
{ from: "promoCode", to: "calculate" }
]}
/>

## Example input

```json theme={null}
{
  "customer": {
    "tier": "gold",
    "yearsActive": 3
  },
  "order": {
    "subtotal": 450,
    "itemCount": 12
  },
  "promoCode": "SUMMER20"
}
```

## Step 1: Customer tier discounts

Create a decision table for base discounts by customer tier:

<DecisionTableViz
  inputs={[
{ name: "Customer Tier", field: "customer.tier" },
{ name: "Years Active", field: "customer.yearsActive" }
]}
  outputs={[
{ name: "Base Discount", field: "discount" }
]}
  rows={[
{ "customer.tier": '"platinum"', "customer.yearsActive": "", discount: "0.20" },
{ "customer.tier": '"gold"', "customer.yearsActive": ">= 3", discount: "0.15" },
{ "customer.tier": '"gold"', "customer.yearsActive": "< 3", discount: "0.12" },
{ "customer.tier": '"silver"', "customer.yearsActive": ">= 2", discount: "0.10" },
{ "customer.tier": '"silver"', "customer.yearsActive": "< 2", discount: "0.08" },
{ "customer.tier": "", "customer.yearsActive": "", discount: "0.05" }
]}
/>

**Logic:**

* Platinum customers always get 20%
* Gold customers get 15% after 3 years, 12% before
* Silver customers get 10% after 2 years, 8% before
* Everyone else gets 5%

## Step 2: Volume discounts

Add another decision table for quantity-based discounts:

<DecisionTableViz
  inputs={[
{ name: "Item Count", field: "order.itemCount" }
]}
  outputs={[
{ name: "Volume Discount", field: "volumeDiscount" }
]}
  rows={[
{ "order.itemCount": ">= 50", volumeDiscount: "0.10" },
{ "order.itemCount": ">= 25", volumeDiscount: "0.07" },
{ "order.itemCount": ">= 10", volumeDiscount: "0.05" },
{ "order.itemCount": "", volumeDiscount: "0" }
]}
/>

## Step 3: Promotional codes

Handle promo codes with another table:

<DecisionTableViz
  inputs={[
{ name: "Promo Code", field: "promoCode" },
{ name: "Subtotal", field: "order.subtotal" }
]}
  outputs={[
{ name: "Promo Discount", field: "promoDiscount" },
{ name: "Promo Type", field: "promoType" }
]}
  rows={[
{ promoCode: '"SUMMER20"', "order.subtotal": ">= 100", promoDiscount: "0.20", promoType: '"percentage"' },
{ promoCode: '"FLAT50"', "order.subtotal": ">= 200", promoDiscount: "50", promoType: '"fixed"' },
{ promoCode: '"NEWUSER"', "order.subtotal": "", promoDiscount: "0.15", promoType: '"percentage"' },
{ promoCode: "", "order.subtotal": "", promoDiscount: "0", promoType: '"none"' }
]}
/>

## Step 4: Calculate final price

Use an expression node to combine all discounts:

<ExpressionViz
  entries={[
{ key: "baseAmount", value: "order.subtotal" },
{ key: "tierSavings", value: "baseAmount * TierDiscount.discount" },
{ key: "volumeSavings", value: "baseAmount * VolumeDiscount.volumeDiscount" },
{ key: "promoSavings", value: "PromoCode.promoType == 'percentage' ? baseAmount * PromoCode.promoDiscount : PromoCode.promoDiscount" },
{ key: "totalDiscount", value: "min([tierSavings + volumeSavings + promoSavings, baseAmount * 0.40])" },
{ key: "finalPrice", value: "round(baseAmount - totalDiscount, 2)" }
]}
/>

**Key logic:**

* Calculate savings from each discount type
* Cap total discount at 40% to protect margins
* Round to 2 decimal places

## Testing scenarios

| Scenario                                    | Expected result                |
| ------------------------------------------- | ------------------------------ |
| Gold customer, 3+ years, 12 items, no promo | 15% tier + 5% volume = 20% off |
| New customer, 5 items, NEWUSER code         | 5% tier + 15% promo = 20% off  |
| Platinum, 50 items, SUMMER20                | Capped at 40% (would be 50%)   |

## Output structure

```json theme={null}
{
  "baseAmount": 450,
  "tierSavings": 67.50,
  "volumeSavings": 22.50,
  "promoSavings": 90,
  "totalDiscount": 180,
  "finalPrice": 270
}
```

## Variations

### Time-based pricing

Add date conditions for seasonal pricing:

<DecisionTableViz
  inputs={[
{ name: "Current Month", field: "month" }
]}
  outputs={[
{ name: "Seasonal Modifier", field: "modifier" }
]}
  rows={[
{ month: "[11..12]", modifier: "1.0" },
{ month: "[1..2]", modifier: "0.85" },
{ month: "", modifier: "1.0" }
]}
/>

### Geographic pricing

Adjust by region:

<DecisionTableViz
  inputs={[
{ name: "Country", field: "country" }
]}
  outputs={[
{ name: "Regional Adjustment", field: "adjustment" }
]}
  rows={[
{ country: '"US", "CA"', adjustment: "1.0" },
{ country: '"GB", "DE", "FR"', adjustment: "1.1" },
{ country: "", adjustment: "1.05" }
]}
/>
