Vibrometer

Olympiad

Solar Billing Disputes

Ticket BILLOPS-1187, from Renata Solis, NEM Billing Supervisor, Pacific Valley Power

We moved our customer billing from Atlas to Meridian on October 20, and since then our net-metered solar customers have been disputing their bills. I don't trust what Meridian computed, so I need the dispute batch reworked from the raw data: for every bill in the batch a corrected bill, a production statement where we have the customer's inverter log, a line on the rebill worksheet, and the list of meter readings that had to be replaced. Please write it as a function so that we can run it on the next batches too.

We worked the first three disputes after the cutover by hand. That pilot is in data/reference_case/ with approved_batch.json, which I reviewed line by line. Rework this batch the same way.

I'm on the phone with customers most of the day. Ask me what you need, but I can answer a few questions, not dozens.

Deliverable

Write solve(files) in solution.mjs. files maps each path under data/ to its text contents, including the files in data/reference_case/. The function returns the output described under Output.

The graders run solve on other dispute batches with the same layout, generated with different accounts, billing periods, rates, meters and data problems. Some of those batches have no data/reference_case/ folder and some have a different one, so solve must ignore it. Don't copy numbers from this batch into code.

Working on this challenge

  • Questions. Renata decides what goes into the batch and how the desk works, and much of that isn't written down in the folder. Ask her with vibrometer ask "your question". You have 8 questions, she answers only what you ask, and a vague question gets a question back.
  • Follow-up. Run vibrometer update from time to time. Anything new from Renata arrives in your folder with a CHANGE.md. The graders always test the latest requirements.

Input files

  • data/disputes/dispute_log.csv: the dispute tickets.
  • data/cis/: Meridian's account master and bill history extracts, with the extract layout.
  • data/meter/: the meters' Green Button downloads. There can be several for one service agreement, and they overlap.
  • data/tariff/: the tariff sheets for E-TOU-X and E-TOU-D. data/calendar/: Attachment H, the holiday calendar.
  • data/inverter/: inverter logs that installers sent, from SunStack inverters (one folder per site) and Solvane gateways (one JSON file per month).
  • Some files in data/ have nothing to do with the batch.

Standards

  • Meter data: NAESB REQ.21 Energy Services Provider Interface (ESPI), the format behind Green Button "Download My Data", including its reading quality codes (QualityOfReading).
  • Holiday calendar: iCalendar, RFC 5545.
  • Time: the IANA time zone in the account master, with United States daylight saving time. ISO 8601 date-times with a UTC offset.
  • CSV files: RFC 4180.
  • Transformer-rated meters: instrument transformer ratios as in IEEE C57.13.

Output

{
  bills: [{
    account: "4410-883-217", billId: "MB10000001", ratePlan: "E-TOU-X",
    period: { from: "2025-10-15", to: "2025-11-13", days: 30 },
    energy: [{ ratesEffective: "2025-06-01", season: "winter", period: "peak",
               importKwh: 80.125, exportKwh: 20.5, netKwh: 59.625,
               rate: 0.51234, exportRate: 0.27001, charge: 30.55, credit: 0.52 } /* , ... */],
    fixed: [{ ratesEffective: "2025-06-01", days: 17, rate: 0.39001, charge: 6.63 } /* , ... */],
    baseline: { allowanceKwh: 210.5, creditedKwh: 210.5, rate: 0.08512, credit: 17.92 },
    minimumBill: { amount: 30.02, adjustment: 0 },
    totals: { importKwh: 700.5, exportKwh: 201.5, netKwh: 499,
              energyCharges: 270.1, nemCredit: 8.4, baselineCredit: 17.92, fixedCharges: 11.7,
              minimumBillAdjustment: 0, total: 255.48 }
  } /* , ... */],
  production: [{ account: "4410-883-217", billId: "MB10000001", productionKwh: 512.25, exportKwh: 201.5, selfConsumedKwh: 310.75 }],
  rebills: [{ account: "4410-883-217", billId: "MB10000001", issued: 281.4, corrected: 255.48, difference: -25.92, action: "REFUND", adjustment: 25.92 }],
  meterExceptions: [{ account: "4410-883-217", date: "2025-10-21", flow: "delivered", intervals: 2, kwh: 1.375 }]
}

The numbers above only show the format. approved_batch.json has the same shape.

  • bills has one entry for each bill in the batch. account is the account number and billId is the bill's ID as it appears in the bill history. ratePlan is the account's rate schedule in the account master, and the corrected bill is computed on that schedule. period.from and period.to are the first and last days of service, and period.days is the number of days of service.
  • energy has one line for each combination of rate table, season and time-of-use period that occurs in the billing period. ratesEffective is the rate table's effective date as YYYY-MM-DD. season is "summer" or "winter". period is "peak", "offPeak" or "superOffPeak". rate and exportRate are in $/kWh. charge is the line's energy charge and credit is its net energy metering credit.
  • fixed has one line for each rate table that applies to at least one day of service, with the base services charge in $/day.
  • totals.energyCharges and totals.nemCredit are the sums of the energy lines' charge and credit. totals.fixedCharges is the sum of the fixed charges. Credits and the minimum bill adjustment are positive numbers or zero.
  • production has one entry for each bill in bills whose account has an inverter on file. productionKwh is the solar energy that the inverter log shows for the bill's days of service, and exportKwh is the bill's totals.exportKwh. selfConsumedKwh is the part of the production that the home used itself: productionKwh minus exportKwh, or 0 when exportKwh is larger than productionKwh. That happens when a battery that charges from the grid exports more than the panels produced.
  • rebills has one entry for each bill in bills. issued is the amount of the bill as it stands now in the bill history, corrected is the bill's totals.total, and difference is corrected minus issued. action is what the dispute desk does about the difference: REFUND, BACKBILL or NONE. adjustment is the amount credited to the customer (positive) or charged to the customer (negative), and 0 when the action is NONE.
  • meterExceptions has one entry for each account, day of service and direction where interval readings had to be replaced because they were missing or because, according to their ESPI quality code, they aren't valid actual readings. How they're replaced is the meter data desk's rule. flow is "delivered" (energy delivered to the customer) or "received" (energy received from the customer). intervals is the number of intervals that were replaced and kwh is the energy that was put in their place, after the meter's multiplier.
  • Sort bills by account, then period.from, and list production and rebills in the same order as their bills. Sort each bill's energy by ratesEffective, then season (summer before winter), then period in the order peak, offPeak, superOffPeak, and sort fixed by ratesEffective. Sort meterExceptions by account, then date, then flow.

Rounding

Round half up. kWh values have at most 3 decimals and money values have at most 2. Each money amount on a line is rounded to cents on its own, and totals are sums of the rounded line amounts. Use exact arithmetic, for example integer watt-hours and rates as integers in units of $0.00001. A binary floating-point product such as 12.345 × 0.43215 can round the wrong way.

The solution is a single file and can only import node: built-ins. It has no file system or network access. Intl.DateTimeFormat with a timeZone option is available.

Start this challenge

First, add Vibrometer to Claude Code

You only do this once. In Claude Code, type these two commands:

/plugin marketplace add usesieve/vibrometer-plugin
/plugin install vibrometer@vibrometer

Then read the ticket and start the clock. In Claude Code, from an empty folder, get the ticket (the clock doesn’t start):

/vibrometer:challenge tou-solar-bill

When you’re ready to lead the work:

/vibrometer:start

When you’re done:

/vibrometer:submit

Fastest

AI alone, one prompt: 11/25 tests in 8 min.

No entries yet.