```ts
/**
 * Spend charges against named quota buckets, refusing what will not fit.
 */
function whyRefused(cap: [number, number] | undefined, left: number, amount: number): string {
  if (cap === undefined) {
    return "unknown";
  }
  if (amount > cap[1]) {
    return "single";
  }
  return amount > left ? "cap" : "";
}

export function applyCharges(caps: Record<string, [number, number]>, charges: [string, string, number][]): { left: Record<string, number>; refused: [string, string][] } {
  const left: Record<string, number> = Object.fromEntries(Object.keys(caps).map((name) => [name, caps[name][0]]));
  const refused: [string, string][] = [];
  // Iterate over each charge and apply it to the corresponding bucket
  for (const [chargeId, bucket, amount] of charges) {
    if (typeof amount !== 'number' || !Number.isInteger(amount) || amount <= 0) {
      throw new Error("Charge amount must be a positive whole number");
    }
    const bucketCap = caps[bucket];
    const refusalReason = whyRefused(bucketCap, left[bucket], amount);
    if (refusalReason === "") {
      left[bucket] -= amount;
    } else {
      refused.push([chargeId, refusalReason]);
    }
  }
  return { left, refused };
}
```