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/withapproved_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 updatefrom time to time. Anything new from Renata arrives in your folder with aCHANGE.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.
billshas one entry for each bill in the batch.accountis the account number andbillIdis the bill's ID as it appears in the bill history.ratePlanis the account's rate schedule in the account master, and the corrected bill is computed on that schedule.period.fromandperiod.toare the first and last days of service, andperiod.daysis the number of days of service.energyhas one line for each combination of rate table, season and time-of-use period that occurs in the billing period.ratesEffectiveis the rate table's effective date asYYYY-MM-DD.seasonis"summer"or"winter".periodis"peak","offPeak"or"superOffPeak".rateandexportRateare in $/kWh.chargeis the line's energy charge andcreditis its net energy metering credit.fixedhas one line for each rate table that applies to at least one day of service, with the base services charge in $/day.totals.energyChargesandtotals.nemCreditare the sums of the energy lines'chargeandcredit.totals.fixedChargesis the sum of thefixedcharges. Credits and the minimum bill adjustment are positive numbers or zero.productionhas one entry for each bill inbillswhose account has an inverter on file.productionKwhis the solar energy that the inverter log shows for the bill's days of service, andexportKwhis the bill'stotals.exportKwh.selfConsumedKwhis the part of the production that the home used itself:productionKwhminusexportKwh, or 0 whenexportKwhis larger thanproductionKwh. That happens when a battery that charges from the grid exports more than the panels produced.rebillshas one entry for each bill inbills.issuedis the amount of the bill as it stands now in the bill history,correctedis the bill'stotals.total, anddifferenceiscorrectedminusissued.actionis what the dispute desk does about the difference:REFUND,BACKBILLorNONE.adjustmentis the amount credited to the customer (positive) or charged to the customer (negative), and 0 when the action isNONE.meterExceptionshas 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.flowis"delivered"(energy delivered to the customer) or"received"(energy received from the customer).intervalsis the number of intervals that were replaced andkwhis the energy that was put in their place, after the meter's multiplier.- Sort
billsbyaccount, thenperiod.from, and listproductionandrebillsin the same order as their bills. Sort each bill'senergybyratesEffective, thenseason(summerbeforewinter), thenperiodin the orderpeak,offPeak,superOffPeak, and sortfixedbyratesEffective. SortmeterExceptionsbyaccount, thendate, thenflow.
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.