diff --git a/kb/pricing/oci-sku-catalog.yaml b/kb/pricing/oci-sku-catalog.yaml index b5fac67..235fc61 100644 --- a/kb/pricing/oci-sku-catalog.yaml +++ b/kb/pricing/oci-sku-catalog.yaml @@ -1262,6 +1262,60 @@ categories: list_price_usd: 0.0003 default_hours_units: 1 billing_type: per_unit + data_services: + display_name: Oracle Cloud Infrastructure - Data & Analytics Services + skus: + - sku: B91128 + product: Oracle Big Data Service - Compute - Standard + metric: OCPU Per Hour + list_price_usd: 0.1344 + default_hours_units: 730 + billing_type: hourly + - sku: B91129 + product: Oracle Big Data Service - Compute - Dense I/O + metric: OCPU Per Hour + list_price_usd: 0.214 + default_hours_units: 730 + billing_type: hourly + - sku: B91130 + product: Oracle Big Data Service - Compute - HPC + metric: OCPU Per Hour + list_price_usd: 0.1536 + default_hours_units: 730 + billing_type: hourly + - sku: B93555 + product: Oracle Big Data Service - Core + metric: OCPU Per Hour + list_price_usd: 0.015 + default_hours_units: 730 + billing_type: hourly + # OCI Data Science and Data Flow do not have dedicated SKUs in the Oracle + # public pricing API — they are billed via the underlying Compute + Block/Object + # Storage consumed by notebook sessions, model-deployment endpoints, and Spark + # runs. The entries below are placeholder estimates (VM.Standard3.Flex OCPU rate) + # so BOMs can include them as line items; always confirm against the customer's + # actual shape + usage before quoting. + - sku: EST-DS-NOTEBOOK + product: '[ESTIMATE] OCI Data Science - Notebook Session (billed via underlying VM.Standard compute OCPU)' + metric: OCPU Per Hour + list_price_usd: 0.0255 + default_hours_units: 730 + billing_type: hourly + estimate: true + - sku: EST-DS-MODEL + product: '[ESTIMATE] OCI Data Science - Model Deployment endpoint (billed via underlying VM.Standard compute OCPU)' + metric: OCPU Per Hour + list_price_usd: 0.0255 + default_hours_units: 730 + billing_type: hourly + estimate: true + - sku: EST-DF-SPARK + product: '[ESTIMATE] OCI Data Flow - Managed Spark runtime (billed via underlying VM.Standard compute OCPU)' + metric: OCPU Per Hour + list_price_usd: 0.0255 + default_hours_units: 730 + billing_type: hourly + estimate: true roving_edge: display_name: Oracle Roving Edge Infrastructure skus: diff --git a/tools/oci_bizcase_gen.py b/tools/oci_bizcase_gen.py index 3e4ac14..602c45c 100644 --- a/tools/oci_bizcase_gen.py +++ b/tools/oci_bizcase_gen.py @@ -637,23 +637,56 @@ class BusinessCaseDeckGenerator(OraclePresBase): # Slide 3: Business Drivers drivers_data = bc.get("drivers", {}) - if drivers_data: + # Accept primary_driver at the top level as well (common user shape) + top_primary = pick(bc, "primary_driver", "main_driver") + # Accept a plain string or list for drivers + if isinstance(drivers_data, str): + drivers_data = {"primary": drivers_data} + elif isinstance(drivers_data, list): + drivers_data = {"items": drivers_data} + elif not isinstance(drivers_data, dict): + drivers_data = {} + + if drivers_data or top_primary: driver_statements = [] - primary = pick(drivers_data, "primary") - urgency = pick(drivers_data, "urgency") - coi = drivers_data.get("cost_of_inaction", {}) + primary = pick(drivers_data, "primary", "primary_driver", "main_driver") or top_primary + urgency = pick(drivers_data, "urgency", "why_now") + coi = drivers_data.get("cost_of_inaction", {}) or {} if primary: - driver_statements.append(f"Primary Driver: {primary.replace('_', ' ').title()}\n{urgency}" if urgency else primary.replace('_', ' ').title()) - financial = pick(coi, "financial") - operational = pick(coi, "operational") - strategic = pick(coi, "strategic") - if financial: - driver_statements.append(f"Financial Impact of Inaction\n{financial}") - if operational: - driver_statements.append(f"Operational Impact\n{operational}") - elif strategic: - driver_statements.append(f"Strategic Risk\n{strategic}") + primary_text = primary if isinstance(primary, str) else str(primary) + # Only rewrite snake_case enum tokens ("compliance_driven"); leave natural + # language intact so values like "Soberanía de datos + compliance regulatorio" + # render verbatim. + if "_" in primary_text and " " not in primary_text and len(primary_text) <= 40: + headline = f"Primary Driver: {primary_text.replace('_', ' ').title()}" + else: + headline = primary_text + driver_statements.append(f"{headline}\n{urgency}" if urgency else headline) + + # Spec-provided additional cards take precedence over the inaction fallback + extra_items = pick_list(drivers_data, "items", "additional", "secondary", "cards") + for item in extra_items: + if isinstance(item, str) and item.strip(): + driver_statements.append(item.strip()) + elif isinstance(item, dict): + label = pick(item, "label", "headline", "title", "name") + desc = pick(item, "detail", "description", "text", "body") + if label and desc: + driver_statements.append(f"{label}\n{desc}") + elif label or desc: + driver_statements.append(label or desc) + + if not extra_items: + financial = pick(coi, "financial") + operational = pick(coi, "operational") + strategic = pick(coi, "strategic") + if financial: + driver_statements.append(f"Financial Impact of Inaction\n{financial}") + if operational: + driver_statements.append(f"Operational Impact\n{operational}") + elif strategic: + driver_statements.append(f"Strategic Risk\n{strategic}") if driver_statements: gen.add_business_drivers_slide(driver_statements) @@ -700,10 +733,6 @@ class BusinessCaseDeckGenerator(OraclePresBase): next_steps=pick_list(rec, "next_steps"), ) - # Closing slides - prepared_by = pick(bc, "prepared_by", "author") - gen.add_closing_slide(name=prepared_by) - return gen diff --git a/tools/oci_bom_gen.py b/tools/oci_bom_gen.py index 782aca6..6160f8a 100644 --- a/tools/oci_bom_gen.py +++ b/tools/oci_bom_gen.py @@ -163,6 +163,16 @@ class OCIBomGenerator: sku = str(sku) cat_entry = self.catalog.get(sku, {}) + note = custom_note + if not cat_entry: + warn = f"[WARN] SKU {sku!r} not in catalog — included as line item with $0 price and warning note" + print(warn, file=sys.stderr) + flag = "⚠ SKU not in catalog — estimated $0, must be confirmed before quoting" + note = f"{note}. {flag}" if note else flag + elif cat_entry.get("estimate"): + flag = "⚠ Estimate only — billed via underlying compute/storage; confirm actual shape + hours" + note = f"{note}. {flag}" if note else flag + item = { "sku": sku, "product": cat_entry.get("product", f"Unknown SKU ({sku})"), @@ -176,7 +186,7 @@ class OCIBomGenerator: "discountable": cat_entry.get("discountable", True), "billing_type": cat_entry.get("billing_type", "hourly"), "custom_label": custom_label, - "custom_note": custom_note, + "custom_note": note, } self.line_items.append(item) @@ -308,12 +318,22 @@ class OCIBomGenerator: data_start_row = row cat_subtotal_rows = [] - for cat_key in self.category_order: + # Ensure categories present in items but missing from catalog order + # (e.g., "other" from unknown SKUs) still render at the end. + render_order = list(self.category_order) + for cat_key in items_by_cat: + if cat_key not in render_order: + render_order.append(cat_key) + + for cat_key in render_order: if cat_key not in items_by_cat: continue cat_items = items_by_cat[cat_key] - display_name = self.category_names.get(cat_key, cat_key) + display_name = self.category_names.get( + cat_key, + "Uncategorized — confirm SKUs" if cat_key == "other" else cat_key, + ) # Category header row ws.cell(row=row, column=COL_PRODUCT, value=display_name).font = cat_font @@ -579,11 +599,19 @@ class OCIBomGenerator: cat = item["category"] items_by_cat.setdefault(cat, []).append(item) - for cat_key in self.category_order: + render_order = list(self.category_order) + for cat_key in items_by_cat: + if cat_key not in render_order: + render_order.append(cat_key) + + for cat_key in render_order: if cat_key not in items_by_cat: continue - display_name = self.category_names.get(cat_key, cat_key) + display_name = self.category_names.get( + cat_key, + "Uncategorized — confirm SKUs" if cat_key == "other" else cat_key, + ) # Category header ws_bom.cell(row=row, column=4, value=display_name).font = cat_font diff --git a/tools/oci_deck_gen.py b/tools/oci_deck_gen.py index 326d583..6adc411 100644 --- a/tools/oci_deck_gen.py +++ b/tools/oci_deck_gen.py @@ -1440,12 +1440,6 @@ class OCIDeckGenerator(OraclePresBase): contact_info=pick(ns, "contact_info"), ) - # Closing slides - gen.add_closing_slide( - name=meta.get("architect", ""), - title=meta.get("firm", ""), - ) - return gen