forked from diegoecab/oci-deal-accelerator
Enhance ADB-S to ADB-D business cases
This commit is contained in:
@@ -100,3 +100,43 @@ def test_from_spec_enriches_sparse_business_case_from_executive_summary():
|
||||
assert "Implementation Roadmap" in text
|
||||
assert "Our Recommendation" in text
|
||||
assert "12 weeks" in text
|
||||
|
||||
|
||||
def test_from_spec_enriches_adbs_to_adbd_business_case_without_meli_hardcoding():
|
||||
spec = {
|
||||
"customer_name": "Synthetic Retail",
|
||||
"cover_subtitle": "ADB-S to ADB-D Migration Business Case",
|
||||
"prepared_by": "Diego Cabrera",
|
||||
"prepared_by_role": "Solutions Architect",
|
||||
"executive_summary": "Move a fraud analytics workload from ADB-S to ADB-D for stability.",
|
||||
"adbs_to_adbd": {
|
||||
"license_model": "BYOL",
|
||||
"discount_pct": 0.11,
|
||||
"ecpu_per_db_node": 760,
|
||||
"current": {"label": "ADB-S As-Is", "workload_ecpu": 1000, "storage_tb": 80},
|
||||
"target": {"label": "ADB-D Dedicated", "workload_ecpu": 1000, "storage_tb": 80, "db_nodes": 2, "storage_servers": 3},
|
||||
"forecasts": [
|
||||
{
|
||||
"period": "24M",
|
||||
"display_label": "Year 2",
|
||||
"as_is": {"workload_ecpu": 1300, "storage_tb": 120},
|
||||
"to_be": {"workload_ecpu": 1300, "storage_tb": 120, "db_nodes": 3, "storage_servers": 3},
|
||||
}
|
||||
],
|
||||
"goldengate": {"mode": "migration_bridge_only", "ocpus": 2, "bridge_months": 3},
|
||||
},
|
||||
}
|
||||
|
||||
gen = BusinessCaseDeckGenerator.from_spec(spec)
|
||||
text = _collect_slide_text(gen)
|
||||
|
||||
assert "Prepared by: Diego Cabrera, Solutions Architect" in text
|
||||
assert "ADB-S As-Is" in text
|
||||
assert "ADB-D Dedicated" in text
|
||||
assert "BOM + Operations Cost Breakdown" in text
|
||||
assert "TCO Crossover" in text
|
||||
assert "Business Value Model" in text
|
||||
assert "Workload ECPU demand" in text
|
||||
assert "ECPU capacity" in text
|
||||
assert "No invented revenue" in text
|
||||
assert "MELI" not in text
|
||||
|
||||
123
tests/test_oci_business_case_model.py
Normal file
123
tests/test_oci_business_case_model.py
Normal file
@@ -0,0 +1,123 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||||
TOOLS_DIR = PROJECT_ROOT / "tools"
|
||||
|
||||
if str(TOOLS_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(TOOLS_DIR))
|
||||
|
||||
from oci_business_case_model import ( # noqa: E402
|
||||
SKU_ADBD_DB_SERVER,
|
||||
SKU_ADBD_STORAGE_SERVER,
|
||||
SKU_ADBS_STORAGE_ATP,
|
||||
build_bom_specs,
|
||||
build_crossover_chart,
|
||||
build_storage_economics,
|
||||
build_tco_projection,
|
||||
capacity_model,
|
||||
first_crossover_period,
|
||||
storage_break_even_tb,
|
||||
)
|
||||
|
||||
|
||||
def _scenario():
|
||||
return {
|
||||
"customer_name": "Synthetic Retail",
|
||||
"prepared_by": "Diego Cabrera",
|
||||
"license_model": "BYOL",
|
||||
"discount_pct": 0.10,
|
||||
"ecpu_per_db_node": 760,
|
||||
"current": {
|
||||
"label": "ADB-S As-Is",
|
||||
"workload_ecpu": 1000,
|
||||
"storage_tb": 80,
|
||||
},
|
||||
"target": {
|
||||
"label": "ADB-D Dedicated",
|
||||
"workload_ecpu": 1000,
|
||||
"storage_tb": 80,
|
||||
"db_nodes": 2,
|
||||
"storage_servers": 3,
|
||||
},
|
||||
"forecasts": [
|
||||
{
|
||||
"period": "24M",
|
||||
"display_label": "Year 2",
|
||||
"as_is": {"workload_ecpu": 1300, "storage_tb": 120},
|
||||
"to_be": {"workload_ecpu": 1300, "storage_tb": 120, "db_nodes": 3, "storage_servers": 3},
|
||||
},
|
||||
{
|
||||
"period": "36M",
|
||||
"display_label": "Year 3",
|
||||
"as_is": {"workload_ecpu": 1600, "storage_tb": 160},
|
||||
"to_be": {"workload_ecpu": 1600, "storage_tb": 160, "db_nodes": 4, "storage_servers": 3},
|
||||
},
|
||||
],
|
||||
"goldengate": {"mode": "migration_bridge_only", "ocpus": 2, "bridge_months": 3},
|
||||
}
|
||||
|
||||
|
||||
def test_capacity_model_separates_workload_demand_from_physical_capacity():
|
||||
cap = capacity_model({"workload_ecpu": 3705, "db_nodes": 7}, 760)
|
||||
|
||||
assert cap.physical_capacity_ecpu == 5320
|
||||
assert round(cap.utilization_pct) == 70
|
||||
assert "3,705 ECPU used over 5,320 ECPU capacity" in cap.label
|
||||
|
||||
|
||||
def test_storage_break_even_calculation_applies_uniform_discount():
|
||||
tb = storage_break_even_tb(14500, 0.1156, 0.10)
|
||||
|
||||
assert round(tb) == round(14500 / (0.1156 * 0.90) / 1024)
|
||||
|
||||
|
||||
def test_build_bom_specs_generates_current_and_projected_run_rate_boms():
|
||||
specs = build_bom_specs(_scenario())
|
||||
|
||||
assert "current_as_is" in specs
|
||||
assert "to_be_steady_state" in specs
|
||||
assert "as_is_projected_24m" in specs
|
||||
assert "to_be_projected_36m" in specs
|
||||
assert "migration_bridge_year1" in specs
|
||||
|
||||
current_skus = {item["sku"] for item in specs["current_as_is"]["bom"]["line_items"]}
|
||||
target_skus = {item["sku"] for item in specs["to_be_steady_state"]["bom"]["line_items"]}
|
||||
future_target_skus = {item["sku"] for item in specs["to_be_projected_36m"]["bom"]["line_items"]}
|
||||
bridge_skus = {item["sku"] for item in specs["migration_bridge_year1"]["bom"]["line_items"]}
|
||||
|
||||
assert SKU_ADBS_STORAGE_ATP in current_skus
|
||||
assert SKU_ADBD_DB_SERVER in target_skus
|
||||
assert SKU_ADBD_STORAGE_SERVER in target_skus
|
||||
assert future_target_skus == target_skus
|
||||
assert "B92993" in bridge_skus
|
||||
assert "B92993" not in future_target_skus
|
||||
assert any("annual run-rate snapshots" in note for note in specs["to_be_projected_36m"]["bom"]["notes"])
|
||||
|
||||
|
||||
def test_forecast_projection_and_crossover_chart_are_native_shape_ready():
|
||||
projection = build_tco_projection(_scenario())
|
||||
chart = build_crossover_chart(projection, _scenario())
|
||||
|
||||
assert projection[0]["label"] == "Near-term"
|
||||
assert chart["categories"] == ["Near-term", "Year 2", "Year 3"]
|
||||
assert chart["note"].startswith("Bars are native PowerPoint shapes")
|
||||
assert chart["bullets"][0].startswith("Near-term: ADB-D")
|
||||
|
||||
|
||||
def test_storage_economics_exposes_break_even_and_offset():
|
||||
economics = build_storage_economics(_scenario())
|
||||
|
||||
assert economics["break_even_tb"] > 0
|
||||
assert economics["storage_offset_monthly"] > 0
|
||||
assert any(card["label"] == "Storage offset" for card in economics["cards"])
|
||||
|
||||
|
||||
def test_first_crossover_period_detects_first_cheaper_target():
|
||||
rows = [
|
||||
{"label": "Near-term", "as_is_annual_tco": 100, "to_be_annual_tco": 110},
|
||||
{"label": "Year 1", "as_is_annual_tco": 120, "to_be_annual_tco": 115},
|
||||
]
|
||||
|
||||
assert first_crossover_period(rows) == "Year 1"
|
||||
60
tests/test_pptx_validator.py
Normal file
60
tests/test_pptx_validator.py
Normal file
@@ -0,0 +1,60 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from pptx import Presentation
|
||||
from pptx.util import Inches
|
||||
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||||
TOOLS_DIR = PROJECT_ROOT / "tools"
|
||||
|
||||
if str(TOOLS_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(TOOLS_DIR))
|
||||
|
||||
from pptx_validator import validate_pptx # noqa: E402
|
||||
|
||||
|
||||
def test_validate_pptx_reads_text_boxes_tables_and_flags_forbidden(tmp_path):
|
||||
pptx = tmp_path / "validation-fixture.pptx"
|
||||
prs = Presentation()
|
||||
slide = prs.slides.add_slide(prs.slide_layouts[6])
|
||||
slide.shapes.add_textbox(Inches(0.5), Inches(0.5), Inches(4), Inches(0.4)).text = "TCO Crossover"
|
||||
table = slide.shapes.add_table(1, 1, Inches(0.5), Inches(1.0), Inches(4), Inches(0.5)).table
|
||||
table.cell(0, 0).text = "Storage break-even"
|
||||
slide.shapes.add_textbox(Inches(0.5), Inches(1.7), Inches(4), Inches(0.4)).text = "OCI Annual"
|
||||
last = prs.slides.add_slide(prs.slide_layouts[6])
|
||||
last.shapes.add_textbox(Inches(0.5), Inches(0.5), Inches(4), Inches(0.4)).text = "Commercial Disclaimer"
|
||||
prs.save(pptx)
|
||||
|
||||
report = validate_pptx(
|
||||
pptx,
|
||||
expected_titles=["TCO Crossover"],
|
||||
required_phrases=["Storage break-even"],
|
||||
forbidden_phrases=["OCI Annual"],
|
||||
disclaimer_last=True,
|
||||
)
|
||||
|
||||
assert report["status"] == "fail"
|
||||
assert any(issue["code"] == "FORBIDDEN_PHRASE" for issue in report["issues"])
|
||||
|
||||
|
||||
def test_validate_pptx_passes_required_and_disclaimer_last(tmp_path):
|
||||
pptx = tmp_path / "validation-pass.pptx"
|
||||
prs = Presentation()
|
||||
slide = prs.slides.add_slide(prs.slide_layouts[6])
|
||||
slide.shapes.add_textbox(Inches(0.5), Inches(0.5), Inches(7), Inches(0.4)).text = (
|
||||
"BYOL/PAYG model Discount GoldenGate bridge duration "
|
||||
"Workload ECPU demand ECPU capacity Storage break-even Crossover"
|
||||
)
|
||||
last = prs.slides.add_slide(prs.slide_layouts[6])
|
||||
last.shapes.add_textbox(Inches(0.5), Inches(0.5), Inches(4), Inches(0.4)).text = "Commercial Disclaimer"
|
||||
prs.save(pptx)
|
||||
|
||||
report = validate_pptx(
|
||||
pptx,
|
||||
required_phrases=["BYOL/PAYG model", "Storage break-even"],
|
||||
forbidden_phrases=["FTE-year"],
|
||||
disclaimer_last=True,
|
||||
)
|
||||
|
||||
assert report["status"] == "pass"
|
||||
Reference in New Issue
Block a user