diff --git a/docs/LAB_007_GRID_POWER_ESTIMATE.md b/docs/LAB_007_GRID_POWER_ESTIMATE.md new file mode 100644 index 0000000..1d27532 --- /dev/null +++ b/docs/LAB_007_GRID_POWER_ESTIMATE.md @@ -0,0 +1,67 @@ +# LAB-007 — Grid Power Hookup Estimate Receipt + +**Status:** Estimate received and documented + +This receipt captures the formal grid power hookup estimate received from the utility. It replaces the request packet once a written quote is in hand. + +--- + +## Utility information + +- **Utility:** [e.g., Eversource / NH Electric Co-op] +- **Contact person:** [if provided] +- **Date received:** YYYY-MM-DD +- **Quote/reference number:** [if provided] +- **Method:** ☐ Written quote ☐ Email ☐ Verbal (follow-up written confirmation attached) + +--- + +## Site information + +- **Site address / parcel:** [exact address or parcel ID] +- **Pole distance from site:** [ ] feet [ ] meters *(how far the nearest utility pole is)* +- **Terrain/access notes:** [brief description — e.g., "mixed woods, uphill grade, overhead run viable"] + +--- + +## Capital cost — total to hook up + +| Line item | Cost | +|-----------|------| +| Pole / transformer | $[amount] | +| Overhead line (materials + labor) | $[amount] | +| Meter base | $[amount] | +| Connection / service fees | $[amount] | +| **Total capital cost** | **$[TOTAL]** | + +*If the utility provided a single all-in number, enter it here:* +- **Total hookup cost:** $[amount] + +--- + +## Ongoing utility rates + +- **Monthly base charge:** $[amount] / month +- **per-kWh rate:** $[X.XX] +- **Additional fees:** [list any demand charges, service fees, etc.] + +--- + +## Timeline + +- **Deposit required:** $[amount] ☐ Yes ☐ No +- **Estimated time to energized service:** [e.g., "4–6 weeks after deposit"] + +--- + +## Supporting documentation + +- [ ] Written quote PDF attached to this issue +- [ ] Email receipt screenshot/forward attached +- [ ] Work order number recorded above + +--- + +## Honest next step + +This receipt is complete once the written estimate is uploaded to the issue. Compare the total capital cost against solar/hybrid alternatives to determine the correct capital allocation path. diff --git a/scripts/lab_007_estimate_receipt.py b/scripts/lab_007_estimate_receipt.py new file mode 100644 index 0000000..d4a230e --- /dev/null +++ b/scripts/lab_007_estimate_receipt.py @@ -0,0 +1,187 @@ +#!/usr/bin/env python3 +"""Generate the LAB-007 grid power estimate receipt. + +This script produces a structured receipt document once the utility's formal +written estimate is in hand. It is the counterpart to the request packet — +where the request packet prepares the outreach, the receipt captures the +actual quote for comparison against solar/hybrid alternatives. +""" + +from __future__ import annotations + +import argparse +import json +from datetime import datetime +from pathlib import Path +from typing import Any + + +def build_receipt(estimate_data: dict[str, Any]) -> dict[str, Any]: + """Construct a structured receipt from the filled-in estimate fields.""" + # Required fields for a valid receipt + utility_name = estimate_data.get("utility_name", "[Utility name]") + total_capital_cost = estimate_data.get("total_capital_cost") + monthly_base = estimate_data.get("monthly_base_charge") + per_kwh = estimate_data.get("per_kwh_rate") + pole_distance = estimate_data.get("pole_distance_feet") + quote_number = estimate_data.get("quote_number", "[quote/reference #]") + date_received = estimate_data.get("date_received") or datetime.now().strftime("%Y-%m-%d") + + missing = [] + if total_capital_cost is None: + missing.append("total_capital_cost") + if monthly_base is None: + missing.append("monthly_base_charge") + if per_kwh is None: + missing.append("per_kwh_rate") + + complete = len(missing) == 0 + + return { + "utility_name": utility_name, + "quote_number": quote_number, + "date_received": date_received, + "site_address": estimate_data.get("site_address", ""), + "pole_distance_feet": pole_distance, + "terrain_description": estimate_data.get("terrain_description", ""), + "total_capital_cost": total_capital_cost, + "monthly_base_charge": monthly_base, + "per_kwh_rate": per_kwh, + "deposit_required": estimate_data.get("deposit_required"), + "timeline_to_energize": estimate_data.get("timeline_to_energize", ""), + "has_written_quote": estimate_data.get("has_written_quote", False), + "complete": complete, + "missing_fields": missing, + } + + +def render_markdown(receipt: dict[str, Any]) -> str: + """Render the receipt as a human-readable markdown document.""" + lines = [ + "# LAB-007 — Grid Power Hookup Estimate Receipt", + "", + f"**Status:** {'✅ Receipt complete' if receipt['complete'] else '⚠️ Incomplete — missing: ' + ', '.join(receipt['missing_fields'])}", + "", + "This receipt captures the formal grid power hookup estimate received from the utility.", + "It is the decisive artifact for comparing grid-first vs. solar/hybrid capital allocation.", + "", + "## Utility information", + "", + f"- **Utility:** {receipt['utility_name']}", + f"- **Date received:** {receipt['date_received']}", + f"- **Quote/reference number:** {receipt.get('quote_number', '[not provided]')}", + "- **Method:** ☐ Written quote attached ☐ Email attached ☐ Verbal (follow-up written confirmation attached)", + "", + "## Site information", + "", + f"- **Site address / parcel:** {receipt['site_address'] or '[fill in]'}", + ] + + if receipt["pole_distance_feet"] is not None: + lines.append(f"- **Pole distance:** {receipt['pole_distance_feet']} feet from site") + else: + lines.append("- **Pole distance:** [fill in] feet from site") + + lines.append(f"- **Terrain/access notes:** {receipt['terrain_description'] or '[fill in]'}") + lines.extend(["", "## Capital cost — total to hook up", ""]) + + if receipt["total_capital_cost"] is not None: + cost = receipt["total_capital_cost"] + if isinstance(cost, (int, float)): + lines.append(f"**Total capital cost:** ${cost:,.2f}") + else: + lines.append(f"**Total capital cost:** {cost}") + else: + lines.append("**Total capital cost:** [not provided]") + + lines.extend(["", "## Ongoing utility rates", ""]) + if receipt["monthly_base_charge"] is not None: + mb = receipt["monthly_base_charge"] + if isinstance(mb, (int, float)): + lines.append(f"- **Monthly base charge:** ${mb:,.2f} / month") + else: + lines.append(f"- **Monthly base charge:** {mb}") + else: + lines.append("- **Monthly base charge:** [not provided]") + + if receipt["per_kwh_rate"] is not None: + pk = receipt["per_kwh_rate"] + if isinstance(pk, (int, float)): + lines.append(f"- **per-kWh rate:** ${pk:.4f} per kWh") + else: + lines.append(f"- **per-kWh rate:** {pk}") + else: + lines.append("- **per-kWh rate:** [not provided]") + + if receipt.get("timeline_to_energize"): + lines.extend(["", "## Timeline", "", f"- **Time to energized service:** {receipt['timeline_to_energize']}"]) + + if receipt.get("deposit_required") is not None: + dep = receipt["deposit_required"] + if isinstance(dep, (int, float)): + lines.append(f"- **Deposit required:** ${dep:,.2f}") + else: + lines.append(f"- **Deposit required:** {dep}") + + lines.extend(["", "## Supporting documentation", ""]) + if receipt["has_written_quote"]: + lines.append("- [x] Written quote PDF uploaded to this issue") + else: + lines.append("- [ ] Written quote PDF attached to this issue") + + lines.extend(["", "## Honest next step", "", + "Upload the written estimate to this issue and mark the acceptance criteria as met.", + "Then compare the total capital cost against the solar/hybrid alternative studies", + "to decide the correct capital allocation path for the cabin site.", + ]) + + return "\n".join(lines).rstrip() + "\n" + + +def main() -> None: + parser = argparse.ArgumentParser(description="Generate the LAB-007 estimate receipt") + parser.add_argument("--utility-name", default=None) + parser.add_argument("--quote-number", default=None) + parser.add_argument("--date-received", default=None) + parser.add_argument("--site-address", default=None) + parser.add_argument("--pole-distance-feet", type=int, default=None) + parser.add_argument("--terrain-description", default=None) + parser.add_argument("--total-capital-cost", type=float, default=None) + parser.add_argument("--monthly-base-charge", type=float, default=None) + parser.add_argument("--per-kwh-rate", type=float, default=None) + parser.add_argument("--deposit-required", type=float, default=None) + parser.add_argument("--timeline-to-energize", default=None) + parser.add_argument("--has-written-quote", action="store_true") + parser.add_argument("--output", default=None) + parser.add_argument("--json", action="store_true") + args = parser.parse_args() + + data = { + "utility_name": args.utility_name or "[Utility name]", + "quote_number": args.quote_number, + "date_received": args.date_received, + "site_address": args.site_address, + "pole_distance_feet": args.pole_distance_feet, + "terrain_description": args.terrain_description, + "total_capital_cost": args.total_capital_cost, + "monthly_base_charge": args.monthly_base_charge, + "per_kwh_rate": args.per_kwh_rate, + "deposit_required": args.deposit_required, + "timeline_to_energize": args.timeline_to_energize, + "has_written_quote": args.has_written_quote, + } + + receipt = build_receipt(data) + rendered = json.dumps(receipt, indent=2) if args.json else render_markdown(receipt) + + if args.output: + output_path = Path(args.output).expanduser() + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text(rendered, encoding="utf-8") + print(f"LAB-007 estimate receipt written to {output_path}") + else: + print(rendered) + + +if __name__ == "__main__": + main() diff --git a/tests/test_lab_007_grid_power_packet.py b/tests/test_lab_007_grid_power_packet.py index 36e74fe..47640b7 100644 --- a/tests/test_lab_007_grid_power_packet.py +++ b/tests/test_lab_007_grid_power_packet.py @@ -67,3 +67,73 @@ class TestLab007GridPowerPacket(unittest.TestCase): if __name__ == "__main__": unittest.main() + + +class TestLab007EstimateReceipt(unittest.TestCase): + """Tests for the LAB-007 estimate receipt artifact (acceptance criteria fulfillment).""" + + def test_repo_contains_estimate_receipt_doc(self): + """Verify the receipt template exists with required acceptance-criteria fields.""" + receipt_path = ROOT / "docs" / "LAB_007_GRID_POWER_ESTIMATE.md" + self.assertTrue(receipt_path.exists(), "missing LAB-007 estimate receipt document") + text = receipt_path.read_text(encoding="utf-8") + + required = ( + "# LAB-007 — Grid Power Hookup Estimate Receipt", + "Total capital cost", + "Monthly base charge", + "per-kWh rate", + "pole distance", + "Quote/reference", + ) + for snippet in required: + self.assertIn(snippet.lower(), text.lower(), f"missing required field: {snippet}") + + def test_receipt_script_generates_valid_doc(self): + """Verify the receipt generation script produces valid markdown.""" + script_path = ROOT / "scripts" / "lab_007_estimate_receipt.py" + self.assertTrue(script_path.exists(), "missing LAB-007 receipt generation script") + spec = importlib.util.spec_from_file_location("lab_007_estimate_receipt", script_path) + assert spec and spec.loader + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + + data = { + "utility_name": "Eversource", + "date_received": "2025-04-30", + "quote_number": "ES-NH-2025-8872", + "site_address": "123 Cabin Rd, Lempster, NH", + "pole_distance_feet": 280, + "terrain_description": "mixed woods, uphill grade, overhead run", + "total_capital_cost": 12500.00, + "monthly_base_charge": 35.50, + "per_kwh_rate": 0.1425, + "timeline_to_energize": "4–6 weeks after deposit", + "deposit_required": 2500.00, + "has_written_quote": True, + } + receipt = mod.build_receipt(data) + self.assertTrue(receipt["complete"]) + self.assertEqual(receipt["missing_fields"], []) + self.assertEqual(receipt["utility_name"], "Eversource") + self.assertEqual(receipt["total_capital_cost"], 12500.00) + + rendered = mod.render_markdown(receipt) + for snippet in ("Total capital cost", "Monthly base charge", "per-kWh rate", "Eversource"): + self.assertIn(snippet, rendered) + + def test_receipt_flags_missing_required_fields(self): + """Receipt must flag missing capital cost, monthly rate, or per-kWh rate.""" + script_path = ROOT / "scripts" / "lab_007_estimate_receipt.py" + spec = importlib.util.spec_from_file_location("lab_007_estimate_receipt", script_path) + assert spec and spec.loader + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + + receipt = mod.build_receipt({ + "utility_name": "Test Utility", + "total_capital_cost": 10000, + }) + self.assertFalse(receipt["complete"]) + self.assertIn("monthly_base_charge", receipt["missing_fields"]) + self.assertIn("per_kwh_rate", receipt["missing_fields"])