diff --git a/.agents/skills/oci-deal-accelerator/SKILL.md b/.agents/skills/oci-deal-accelerator/SKILL.md index a022ce0..bc033d8 100644 --- a/.agents/skills/oci-deal-accelerator/SKILL.md +++ b/.agents/skills/oci-deal-accelerator/SKILL.md @@ -59,6 +59,11 @@ Present these options as a compact numbered list. Each option has a bold title f ───────────────── 12. 📊 ECAL readiness score — *60-artefact gap analysis* + SA TOOLS + ───────────────── + 13. 📦 BOM generator — *services + quantities → .xlsx Bill of Materials* + 14. 📤 BOM for AppCA — *BOM → .xlsx ready to import into AppCA* + ━━━━━━━━━━━━━━━━━━━━━━━ Pick a number, or just describe what you need. ``` @@ -247,6 +252,29 @@ Pick a number, or just describe what you need. → [D] Re-score after updates ``` +- If the user picks **13**, ask: "What services does the customer need? (e.g., 'ExaCS X11M BYOL 2 DB servers + 4 storage + 128 ECPUs + ADB-S 8 ECPU + 2TB Block Storage + FastConnect 1Gbps'). I'll generate the BOM with only those SKUs." + Then follow the BOM generation flow: + 1. Parse the customer request to identify needed OCI services and quantities + 2. Match services against `kb/pricing/oci-sku-catalog.yaml` to select exact SKUs + 3. Ask for discount % and contract duration if not specified (default: 0%, 12 months) + 4. Ask if currency conversion is needed (e.g., USD→BRL with exchange rate and tax) + 5. Generate the BOM spec YAML and save to the output folder + 6. Run `tools/oci_bom_gen.py` to produce the .xlsx + 7. Present a summary table in the terminal showing key totals (monthly, ARR) + 8. List the files generated + + **BOM Output Rules:** + - NEVER include "Confidential: Internal ONLY" or any confidentiality marking + - ALWAYS include the Oracle Cost Estimator disclaimer at the bottom + - Only include SKUs the customer actually requested — never dump the full catalog + - Show cost proportions so the customer can see where their spend concentrates + - Use Excel formulas (not static values) so the customer can adjust quantities + +- If the user picks **14**, follow the same flow as option 13 (BOM generator) but generate the output in AppCA import format. AppCA is Oracle's internal deal approval tool. The generated .xlsx has two sheets: + - **"Export to AppCA"**: Flat table with columns SKU, QTY, HOURS, MONTHS, DISCOUNT, BURSTABLE — ready to paste/import into AppCA + - **"BOM.C1"**: Full BOM detail with product names, metrics, prices, and formulas for cost calculations + Run with: `python tools/oci_bom_gen.py --spec .yaml --output .xlsx --appca` + - If the user sends discovery notes directly (without picking a number), detect this and go straight to option 1 (full proposal flow). - If the user asks a specific question (e.g., "does ADB-S support vector search?"), detect this and go straight to the relevant capability without showing the menu. - Only show the welcome menu on the FIRST message if it's a greeting or empty context. Don't re-show it on every turn. diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 5229293..8602955 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -60,7 +60,21 @@ "Bash(PYTHONPATH=/tmp/pptx_ok python3 -c \"from pptx import Presentation; print\\(''''OK''''\\)\")", "Bash(PYTHONPATH=/tmp/pptx_ok python3 /tmp/migrate_to_oracle.py)", "Bash(PYTHONPATH=/tmp/pptx_ok python3 -c \"\nfrom pptx import Presentation\nprs = Presentation\\('/mnt/c/Users/Diego/Documents/oci-deal-accelerator/oci-deal-accelerator/templates/Oracle_PPT-template_FY26.pptx'\\)\nprint\\('Template has {} existing slides'.format\\(len\\(prs.slides\\)\\)\\)\n\")", - "Bash(grep:*)" + "Bash(grep:*)", + "Bash(ls \"/mnt/c/Users/Diego/Documents/oci-deal-accelerator-oracle/oci-deal-accelerator/templates/Oracle_PPT\"*)", + "Bash(ls \"/mnt/c/Users/Diego/Documents/oci-deal-accelerator-oracle/oci-deal-accelerator/templates/\"*.pptx)", + "Bash(ls \"/mnt/c/Users/Diego/Documents/oci-deal-accelerator-oracle/oci-deal-accelerator/templates/\"*.potx)", + "Bash(python3.12 -c \"import drawpyo\")", + "Bash(python3.12 tools/oci_diagram_gen.py --spec examples/output-meli-kvs/diagram-spec.yaml --output examples/output-meli-kvs/architecture.drawio)", + "Bash(python3.12 tools/oci_pdf_gen.py --spec examples/output-meli-kvs/proposal-spec.yaml --output examples/output-meli-kvs/customer-proposal.pdf)", + "Bash(python3.12 -c \":*)", + "Bash(python3.12 tools/oci_deck_gen.py --spec examples/output-meli-kvs/proposal-spec.yaml --output examples/output-meli-kvs/architecture-proposal.pptx)", + "Bash(python3.12 tools/oci_bizcase_gen.py --spec examples/output-meli-kvs/business-case.yaml --output examples/output-meli-kvs/business-case.pptx)", + "Bash(python3 -c \":*)", + "WebFetch(domain:github.com)", + "Bash(curl -s --max-time 15 -H \"X-Oracle-Accept-CurrencyCode: USD\" \"https://itra.oraclecloud.com/itas/.anon/myservices/api/v1/products?limit=2&partNumber=B110627\")", + "Bash(curl -v --max-time 10 -H \"X-Oracle-Accept-CurrencyCode: USD\" \"https://itra.oraclecloud.com/itas/.anon/myservices/api/v1/products?limit=1\")", + "Bash(curl -s --max-time 15 \"https://apexapps.oracle.com/pls/apex/cetools/api/v1/products/?partNumber=B110627¤cyCode=USD\")" ] } } diff --git a/CLAUDE.md b/CLAUDE.md index ebcdb46..93996b7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -38,6 +38,7 @@ AI skill aligned with Oracle ECAL framework (Define → Design → Deliver) that │ │ └── (dirs: database-ha/, database-dr/, networking-hub-spoke/, etc.) │ ├── sizing/ # CPU conversion ratios, IOPS, scaling rules │ ├── pricing/ # Simplified pricing for estimation +│ │ └── oci-sku-catalog.yaml # ~160 OCI SKUs for BOM generation │ ├── competitive/ # AWS/Azure/GCP service mapping │ ├── well-architected/ # 5-pillar WA Framework checklists │ ├── compatibility/ # Feature matrices (ADB, etc.) @@ -47,7 +48,9 @@ AI skill aligned with Oracle ECAL framework (Define → Design → Deliver) that │ ├── oci_deck_gen.py # .pptx slide deck generator (DEFAULT output) │ ├── oci_pdf_gen.py # .pdf customer-facing document (branded, no internal refs) │ ├── oci_diagram_gen.py # .drawio diagram generator +│ ├── oci_bom_gen.py # .xlsx Bill of Materials generator (SA tool) │ ├── oci_output.py # Output orchestrator +│ ├── refresh_sku_catalog.py # SKU catalog refresh from Oracle pricing API │ └── refresh_arch_catalog.py # Architecture Center catalog refresh tool ├── scripts/ # Validation and utilities │ └── validate-architecture.py # WA validation engine @@ -72,7 +75,8 @@ AI skill aligned with Oracle ECAL framework (Define → Design → Deliver) that │ ├── handover-document.yaml # DELIVER: Implementation handover │ ├── go-live-checklist.yaml # DELIVER: Pre-cutover verification │ ├── success-criteria.yaml # DELIVER: Post go-live metrics -│ └── lessons-learned.yaml # DELIVER: Engagement retrospective +│ ├── lessons-learned.yaml # DELIVER: Engagement retrospective +│ └── bom-spec.yaml # SA TOOL: BOM input spec template ├── codex/ # Codex setup guide (README only) └── examples/ # Example specs and generated outputs ``` @@ -115,6 +119,9 @@ python tools/oci_deck_gen.py --spec examples/proposal-spec.yaml --output proposa python tools/oci_pdf_gen.py --spec examples/proposal-spec.yaml --output proposal.pdf python tools/oci_pdf_gen.py --spec examples/proposal-spec.yaml --output proposal.pdf --diagram arch.png +# Generate BOM (.xlsx Bill of Materials) +python tools/oci_bom_gen.py --spec examples/bom-spec.yaml --output customer-bom.xlsx + # Generate diagram python tools/oci_diagram_gen.py --spec examples/diagram-spec.yaml --output arch.drawio @@ -127,6 +134,12 @@ python scripts/validate-architecture.py \ # Output orchestrator (multiple formats at once) python tools/oci_output.py --spec examples/proposal-spec.yaml --format full --output-dir output/ +# Refresh SKU catalog from Oracle pricing API +python tools/refresh_sku_catalog.py --refresh # update all prices +python tools/refresh_sku_catalog.py --refresh --diff # update + show changes +python tools/refresh_sku_catalog.py --validate # check for stale prices +python tools/refresh_sku_catalog.py --sku B110627 # inspect single SKU + # Refresh Architecture Center catalog python tools/refresh_arch_catalog.py --whats-new # crawl What's New pages python tools/refresh_arch_catalog.py --url # add single entry diff --git a/SKILL.md b/SKILL.md index ddc0323..b7020b8 100644 --- a/SKILL.md +++ b/SKILL.md @@ -59,6 +59,11 @@ Present these options as a compact numbered list. Each option has a bold title f ───────────────── 12. 📊 ECAL readiness score — *60-artefact gap analysis* + SA TOOLS + ───────────────── + 13. 📦 BOM generator — *services + quantities → .xlsx Bill of Materials* + 14. 📤 BOM for AppCA — *BOM → .xlsx ready to import into AppCA* + ━━━━━━━━━━━━━━━━━━━━━━━ Pick a number, or just describe what you need. ``` @@ -264,6 +269,29 @@ Pick a number, or just describe what you need. → [D] Re-score after updates ``` +- If the user picks **13**, ask: "What services does the customer need? (e.g., 'ExaCS X11M BYOL 2 DB servers + 4 storage + 128 ECPUs + ADB-S 8 ECPU + 2TB Block Storage + FastConnect 1Gbps'). I'll generate the BOM with only those SKUs." + Then follow the BOM generation flow: + 1. Parse the customer request to identify needed OCI services and quantities + 2. Match services against `kb/pricing/oci-sku-catalog.yaml` to select exact SKUs + 3. Ask for discount % and contract duration if not specified (default: 0%, 12 months) + 4. Ask if currency conversion is needed (e.g., USD→BRL with exchange rate and tax) + 5. Generate the BOM spec YAML and save to the output folder + 6. Run `tools/oci_bom_gen.py` to produce the .xlsx + 7. Present a summary table in the terminal showing key totals (monthly, ARR) + 8. List the files generated + + **BOM Output Rules:** + - NEVER include "Confidential: Internal ONLY" or any confidentiality marking + - ALWAYS include the Oracle Cost Estimator disclaimer at the bottom + - Only include SKUs the customer actually requested — never dump the full catalog + - Show cost proportions so the customer can see where their spend concentrates + - Use Excel formulas (not static values) so the customer can adjust quantities + +- If the user picks **14**, follow the same flow as option 13 (BOM generator) but generate the output in AppCA import format. AppCA is Oracle's internal deal approval tool. The generated .xlsx has two sheets: + - **"Export to AppCA"**: Flat table with columns SKU, QTY, HOURS, MONTHS, DISCOUNT, BURSTABLE — ready to paste/import into AppCA + - **"BOM.C1"**: Full BOM detail with product names, metrics, prices, and formulas for cost calculations + Run with: `python tools/oci_bom_gen.py --spec .yaml --output .xlsx --appca` + - If the user sends discovery notes directly (without picking a number), detect this and go straight to option 1 (full proposal flow). - If the user asks a specific question (e.g., "does ADB-S support vector search?"), detect this and go straight to the relevant capability without showing the menu. - Only show the welcome menu on the FIRST message if it's a greeting or empty context. Don't re-show it on every turn. diff --git a/examples/exacs-bom-appca-v2.xlsx b/examples/exacs-bom-appca-v2.xlsx new file mode 100644 index 0000000..1b3cad3 Binary files /dev/null and b/examples/exacs-bom-appca-v2.xlsx differ diff --git a/examples/exacs-bom-appca-v3.xlsx b/examples/exacs-bom-appca-v3.xlsx new file mode 100644 index 0000000..128f90c Binary files /dev/null and b/examples/exacs-bom-appca-v3.xlsx differ diff --git a/examples/exacs-bom-appca-v4.xlsx b/examples/exacs-bom-appca-v4.xlsx new file mode 100644 index 0000000..d3a3b9a Binary files /dev/null and b/examples/exacs-bom-appca-v4.xlsx differ diff --git a/examples/exacs-bom-appca.xlsx b/examples/exacs-bom-appca.xlsx new file mode 100644 index 0000000..0b0f308 Binary files /dev/null and b/examples/exacs-bom-appca.xlsx differ diff --git a/examples/exacs-bom-v2.xlsx b/examples/exacs-bom-v2.xlsx new file mode 100644 index 0000000..172dd80 Binary files /dev/null and b/examples/exacs-bom-v2.xlsx differ diff --git a/examples/exacs-bom.xlsx b/examples/exacs-bom.xlsx new file mode 100644 index 0000000..4935b10 Binary files /dev/null and b/examples/exacs-bom.xlsx differ diff --git a/examples/exacs-x11m-bom-appca.xlsx b/examples/exacs-x11m-bom-appca.xlsx new file mode 100644 index 0000000..c6f7b0f Binary files /dev/null and b/examples/exacs-x11m-bom-appca.xlsx differ diff --git a/examples/exacs-x11m-bom.xlsx b/examples/exacs-x11m-bom.xlsx new file mode 100644 index 0000000..0c165e8 Binary files /dev/null and b/examples/exacs-x11m-bom.xlsx differ diff --git a/examples/~$exacs-bom.xlsx b/examples/~$exacs-bom.xlsx new file mode 100644 index 0000000..c03d7f3 Binary files /dev/null and b/examples/~$exacs-bom.xlsx differ diff --git a/kb/pricing/oci-sku-catalog.yaml b/kb/pricing/oci-sku-catalog.yaml new file mode 100644 index 0000000..b5fac67 --- /dev/null +++ b/kb/pricing/oci-sku-catalog.yaml @@ -0,0 +1,1327 @@ +last_verified: '2026-03-27' +source: Oracle OCI Price List + internal BOM reference v1.73 +description: 'OCI SKU catalog for BOM generation. ~160 SKUs across 14+ categories. Prices are approximate and for estimation + only. Always verify against https://www.oracle.com/cloud/pricing/ + + ' +currency: USD +disclaimer: 'This sample quote is provided solely for evaluation purposes and is intended to further discussions between you + and Oracle. This sample quote is not eligible for acceptance by you and is not a binding contract between you and Oracle + for the services specified. If you would like to purchase the services specified in this sample quote, please request that + Oracle issue you a formal quote (which may include an OMA or a CSA if you do not already have an appropriate agreement in + place with Oracle) for your acceptance and execution. Your formal quote will be effective only upon Oracle''s acceptance + of the formal quote (and the OMA or CSA, if required). + + ' +categories: + iaas: + display_name: Oracle Cloud Infrastructure - IaaS + skus: + - sku: B97384 + product: Compute - Standard - E5 (OCPU) + metric: OCPU Per Hour + list_price_usd: 0.03 + default_hours_units: 730 + billing_type: hourly + - sku: B97385 + product: Compute - Standard - E5 (Memory) + metric: Gigabyte Per Hour + list_price_usd: 0.002 + default_hours_units: 730 + billing_type: hourly + - sku: B111129 + product: Compute - Standard - E6 (OCPU) + metric: OCPU Per Hour + list_price_usd: 0.03 + default_hours_units: 730 + billing_type: hourly + - sku: B111130 + product: Compute - Standard - E6 (Memory) + metric: Gigabyte Per Hour + list_price_usd: 0.002 + default_hours_units: 730 + billing_type: hourly + - sku: B94176 + product: Compute - Standard - X9 (OCPU) + metric: OCPU Per Hour + list_price_usd: 0.04 + default_hours_units: 730 + billing_type: hourly + - sku: B94177 + product: Compute - Standard - X9 (Memory) + metric: Gigabyte Per Hour + list_price_usd: 0.0015 + default_hours_units: 730 + billing_type: hourly + - sku: B93311 + product: Compute - Optimized - X9 (OCPU) + metric: OCPU Per Hour + list_price_usd: 0.054 + default_hours_units: 730 + billing_type: hourly + - sku: B93312 + product: Compute - Optimized - X9 (Memory) + metric: Gigabyte Per Hour + list_price_usd: 0.0015 + default_hours_units: 730 + billing_type: hourly + - sku: B93297 + product: 'Compute - Standard - A1 (OCPU) (Free Tier: 4 OCPUs)' + metric: OCPU Per Hour + list_price_usd: 0.01 + default_hours_units: 730 + billing_type: hourly + - sku: B93298 + product: 'Compute - Standard - A1 (Memory) (Free Tier: 24 GB RAM)' + metric: Gigabyte Per Hour + list_price_usd: 0.0015 + default_hours_units: 730 + billing_type: hourly + - sku: B109529 + product: Compute - Standard - A2 (OCPU) + metric: OCPU Per Hour + list_price_usd: 0.014 + default_hours_units: 730 + billing_type: hourly + - sku: B109530 + product: Compute - Standard - A2 (Memory) + metric: Gigabyte Per Hour + list_price_usd: 0.002 + default_hours_units: 730 + billing_type: hourly + gpu: + display_name: Oracle Cloud Infrastructure - GPUs + skus: + - sku: B95909 + product: Compute - GPU - A10 (1, 2 or 4 GPUs NVIDIA Ampere A10 / 24 GB per GPU) + metric: GPU Per Hour + list_price_usd: 2.0 + default_hours_units: 730 + billing_type: hourly + - sku: B109479 + product: Compute - GPU - BM.GPU.L40S.4 (4 GPUs NVIDIA Hopper L40S / 192 GB Total) + metric: GPU Per Hour + list_price_usd: 3.5 + default_hours_units: 730 + billing_type: hourly + - sku: B92740 + product: Compute - GPU - BM.GPU4.8 (8 GPUs NVIDIA Ampere A100 v1 / 320 GB Total) + metric: GPU Per Hour + list_price_usd: 3.05 + default_hours_units: 730 + billing_type: hourly + - sku: B95907 + product: Compute - GPU - BM.GPU.A100-v2.8 (8 GPUs NVIDIA Ampere A100 / 640 GB Total) + metric: GPU Per Hour + list_price_usd: 4.0 + default_hours_units: 730 + billing_type: hourly + - sku: B109485 + product: Compute - GPU - BM.GPU.MI300X.8 (8 GPUs AMD MI300X / 1536 GB Total) + metric: GPU Per Hour + list_price_usd: 6.0 + default_hours_units: 730 + billing_type: hourly + - sku: B98415 + product: Compute - GPU - BM.GPU.H100.8 (8 GPUs NVIDIA Hopper H100 / 640 GB Total) + metric: GPU Per Hour + list_price_usd: 10.0 + default_hours_units: 730 + billing_type: hourly + - sku: B110519 + product: Compute - GPU - BM.GPU.H200.8 (8 GPUs NVIDIA Hopper H200 / 1128 GB Total) + metric: GPU Per Hour + list_price_usd: 10.0 + default_hours_units: 730 + billing_type: hourly + - sku: B110978 + product: Compute - GPU - BM.GPU.B200.8 (8 GPUs NVIDIA Blackwell B200 / 1440 GB Total) + metric: GPU Per Hour + list_price_usd: 14.0 + default_hours_units: 730 + billing_type: hourly + - sku: B110979 + product: Compute - GPU - BM.GPU.GB200.4 (4 GPUs NVIDIA Blackwell GB200 NVL72 / 768 GB Total) + metric: GPU Per Hour + list_price_usd: 16.0 + default_hours_units: 730 + billing_type: hourly + vmware: + display_name: Oracle Cloud Infrastructure - VMware + skus: + - sku: B95178 + product: Oracle Cloud VMware Solution - BM.DenseIO.E4.64 - Hourly Commit (minimum 192 OCPUs) + metric: OCPU Per Hour + list_price_usd: 0.2437 + default_hours_units: 730 + billing_type: hourly + discountable: false + - sku: B95179 + product: Oracle Cloud VMware Solution - BM.DenseIO.E4.64 - Monthly Commit (minimum 192 OCPUs) + metric: OCPU Per Hour + list_price_usd: 0.2031 + default_hours_units: 730 + billing_type: hourly + discountable: false + - sku: B95180 + product: Oracle Cloud VMware Solution - BM.DenseIO.E4.64 - 1 Year Commit (minimum 192 OCPUs) + metric: OCPU Per Hour + list_price_usd: 0.1625 + default_hours_units: 730 + billing_type: hourly + discountable: false + - sku: B95181 + product: Oracle Cloud VMware Solution - BM.DenseIO.E4.64 - 3 Year Commit (minimum 192 OCPUs) + metric: OCPU Per Hour + list_price_usd: 0.132 + default_hours_units: 730 + billing_type: hourly + discountable: false + - sku: B95415 + product: Oracle Cloud VMware Solution - BM.DenseIO.E4.128 - Hourly Commit (minimum 384 OCPUs) + metric: OCPU Per Hour + list_price_usd: 0.195 + default_hours_units: 730 + billing_type: hourly + discountable: false + - sku: B95416 + product: Oracle Cloud VMware Solution - BM.DenseIO.E4.128 - Monthly Commit (minimum 384 OCPUs) + metric: OCPU Per Hour + list_price_usd: 0.1625 + default_hours_units: 730 + billing_type: hourly + discountable: false + - sku: B95417 + product: Oracle Cloud VMware Solution - BM.DenseIO.E4.128 - 1 Year Commit (minimum 384 OCPUs) + metric: OCPU Per Hour + list_price_usd: 0.13 + default_hours_units: 730 + billing_type: hourly + discountable: false + - sku: B95418 + product: Oracle Cloud VMware Solution - BM.DenseIO.E4.128 - 3 Year Commit (minimum 384 OCPUs) + metric: OCPU Per Hour + list_price_usd: 0.1056 + default_hours_units: 730 + billing_type: hourly + discountable: false + - sku: B108809 + product: Oracle Cloud VMware Solution - BM.Standard.E5.48 - Hourly Commit (minimum 144 OCPUs) - 8 hours min + metric: Node Per Hour (48 OCPUs each, min x3) + list_price_usd: 16.6617 + default_hours_units: 730 + billing_type: hourly + discountable: false + - sku: B108810 + product: Oracle Cloud VMware Solution - BM.Standard.E5.48 - 1 Year Commit (minimum 144 OCPUs) + metric: Node Per Hour (48 OCPUs each, min x3) + list_price_usd: 13.1604 + default_hours_units: 730 + billing_type: hourly + discountable: false + - sku: B108811 + product: Oracle Cloud VMware Solution - BM.Standard.E5.48 - 3 Year Commit (minimum 144 OCPUs) + metric: Node Per Hour (48 OCPUs each, min x3) + list_price_usd: 10.6119 + default_hours_units: 730 + billing_type: hourly + discountable: false + - sku: B110680 + product: Oracle Cloud VMware Solution - BM.DenseIO.E5.32 - Hourly Commit (minimum 96 OCPUs) - 8 hours min + metric: Node Per Hour (32 OCPUs each, min x3) + list_price_usd: 10.2379 + default_hours_units: 730 + billing_type: hourly + discountable: false + - sku: B110681 + product: Oracle Cloud VMware Solution - BM.DenseIO.E5.32 - Monthly Commit (minimum 96 OCPUs) + metric: Node Per Hour (32 OCPUs each, min x3) + list_price_usd: 8.531 + default_hours_units: 730 + billing_type: hourly + discountable: false + - sku: B110682 + product: Oracle Cloud VMware Solution - BM.DenseIO.E5.32 - 1 Year Commit (minimum 96 OCPUs) + metric: Node Per Hour (32 OCPUs each, min x3) + list_price_usd: 6.8242 + default_hours_units: 730 + billing_type: hourly + discountable: false + - sku: B110683 + product: Oracle Cloud VMware Solution - BM.DenseIO.E5.32 - 3 Year Commit (minimum 96 OCPUs) + metric: Node Per Hour (32 OCPUs each, min x3) + list_price_usd: 6.072 + default_hours_units: 730 + billing_type: hourly + discountable: false + - sku: B97105 + product: Oracle Cloud VMware Solution - BM.Standard3.16 - Hourly Commit (minimum 48 OCPUs) - 8 hours min + metric: Node Per Hour (16 OCPUs each, min x3) + list_price_usd: 5.554 + default_hours_units: 730 + billing_type: hourly + discountable: false + - sku: B97106 + product: Oracle Cloud VMware Solution - BM.Standard3.16 - 1 Year Commit (minimum 48 OCPUs) + metric: Node Per Hour (16 OCPUs each, min x3) + list_price_usd: 4.2943 + default_hours_units: 730 + billing_type: hourly + discountable: false + - sku: B97107 + product: Oracle Cloud VMware Solution - BM.Standard3.16 - 3 Year Commit (minimum 48 OCPUs) + metric: Node Per Hour (16 OCPUs each, min x3) + list_price_usd: 3.3522 + default_hours_units: 730 + billing_type: hourly + discountable: false + - sku: B97111 + product: Oracle Cloud VMware Solution - Expansion - Hourly Commit (Add. OCPUs for BM.E5 / X9) + metric: OCPU Per Hour + list_price_usd: 0.1605 + default_hours_units: 730 + billing_type: hourly + discountable: false + - sku: B110684 + product: Oracle Cloud VMware Solution - Expansion - Monthly Commit (Add. OCPUs for BM.E5 / X9) + metric: OCPU Per Hour + list_price_usd: 0.1338 + default_hours_units: 730 + billing_type: hourly + discountable: false + - sku: B97112 + product: Oracle Cloud VMware Solution - Expansion - 1 Year Commit (Add. OCPUs for BM.E5 / X9) + metric: OCPU Per Hour + list_price_usd: 0.1092 + default_hours_units: 730 + billing_type: hourly + discountable: false + - sku: B97113 + product: Oracle Cloud VMware Solution - Expansion - 3 Year Commit (Add. OCPUs for BM.E5 / X9) + metric: OCPU Per Hour + list_price_usd: 0.0739 + default_hours_units: 730 + billing_type: hourly + discountable: false + - sku: B93421 + product: Oracle Cloud VMware Solution - HCX Enterprise - Monthly + metric: OCPU Per Hour + list_price_usd: 0.0126 + default_hours_units: 730 + billing_type: hourly + discountable: false + software: + display_name: Oracle Cloud Infrastructure - Software + skus: + - sku: B88318 + product: OCI - Compute - Windows OS (license only) + metric: OCPU Per Hour + list_price_usd: 0.092 + default_hours_units: 730 + billing_type: hourly + discountable: false + - sku: B91373 + product: OCI - Compute - Microsoft SQL Standard (LICENSE ONLY, no IaaS, X9.Std3 only) + metric: OCPU Per Hour (minimum 2 OCPUs) + list_price_usd: 0.37 + default_hours_units: 730 + billing_type: hourly + - sku: B91372 + product: OCI - Compute - Microsoft SQL Enterprise (LICENSE ONLY, no IaaS, X7 only) + metric: OCPU Per Hour (minimum 2 OCPUs) + list_price_usd: 1.47 + default_hours_units: 730 + billing_type: hourly + storage: + display_name: Oracle Cloud Infrastructure - Storage + skus: + - sku: B91961 + product: OCI - Block Volume Storage + metric: Gigabyte Storage Capacity Per Month + list_price_usd: 0.0255 + default_hours_units: 1 + billing_type: monthly + - sku: B91962 + product: OCI - Block Volume Performance (VPU) + metric: Performance Units Per Gigabyte Per Month + list_price_usd: 0.0017 + default_hours_units: 1 + billing_type: monthly + - sku: B91628 + product: 'OCI - Object Storage - Storage (Free Tier: 10 GB)' + metric: Gigabyte Storage Capacity Per Month + list_price_usd: 0.0255 + default_hours_units: 1 + billing_type: monthly + - sku: B91627 + product: 'OCI - Object Storage - Requests (Free Tier: 50000 Requests)' + metric: 10,000 (10K) Requests Per Month + list_price_usd: 0.0034 + default_hours_units: 1 + billing_type: monthly + - sku: B93000 + product: 'OCI - Infrequent Access - Storage (Free Tier: 10 GB)' + metric: Gigabyte Storage Capacity Per Month + list_price_usd: 0.01 + default_hours_units: 1 + billing_type: monthly + - sku: B93001 + product: 'OCI - Infrequent Access - Data Retrieval (Free Tier: 10 GB)' + metric: Gigabyte Storage Capacity Per Month + list_price_usd: 0.01 + default_hours_units: 1 + billing_type: monthly + - sku: B91633 + product: 'OCI - Archive Storage (Free Tier: 10 GB)' + metric: Gigabyte Storage Capacity Per Month + list_price_usd: 0.0026 + default_hours_units: 1 + billing_type: monthly + - sku: B89057 + product: OCI - File Storage Service + metric: Gigabyte Storage Capacity Per Month + list_price_usd: 0.3 + default_hours_units: 1 + billing_type: monthly + - sku: B109546 + product: 'OCI - File Storage Service - High Performance Mount Target (Options: 20, 40 or 80 TB)' + metric: Gigabyte Storage Capacity Per Month + list_price_usd: 0.3 + default_hours_units: 1 + billing_type: monthly + base_database: + display_name: Oracle Cloud Infrastructure - Base Database (DBCS) + skus: + - sku: B90573 + product: Oracle Base Database Service - BYOL + metric: OCPU Per Hour + list_price_usd: 0.1935 + default_hours_units: 730 + billing_type: hourly + - sku: B90569 + product: Oracle Base Database Service - Standard Edition + metric: OCPU Per Hour + list_price_usd: 0.215 + default_hours_units: 730 + billing_type: hourly + - sku: B90570 + product: Oracle Base Database Service - Enterprise Edition + metric: OCPU Per Hour + list_price_usd: 0.4301 + default_hours_units: 730 + billing_type: hourly + - sku: B90571 + product: Oracle Base Database Service - Enterprise High Performance Edition + metric: OCPU Per Hour + list_price_usd: 0.8871 + default_hours_units: 730 + billing_type: hourly + - sku: B90572 + product: Oracle Base Database Service - Enterprise Extreme Performance Edition + metric: OCPU Per Hour + list_price_usd: 1.3441 + default_hours_units: 730 + billing_type: hourly + - sku: B97200 + product: Oracle Base Database Service on ARM - BYOL + metric: OCPU (1 vCPU, single thread) Per Hour + list_price_usd: 0.0968 + default_hours_units: 730 + billing_type: hourly + - sku: B97197 + product: Oracle Base Database Service on ARM - Enterprise Edition + metric: OCPU (1 vCPU, single thread) Per Hour + list_price_usd: 0.2151 + default_hours_units: 730 + billing_type: hourly + - sku: B97198 + product: Oracle Base Database Service on ARM - Enterprise High Performance Edition + metric: OCPU (1 vCPU, single thread) Per Hour + list_price_usd: 0.4436 + default_hours_units: 730 + billing_type: hourly + - sku: B97199 + product: Oracle Base Database Service on ARM - Enterprise Extreme Performance Edition + metric: OCPU (1 vCPU, single thread) Per Hour + list_price_usd: 0.6721 + default_hours_units: 730 + billing_type: hourly + - sku: B90230 + product: Database Backup Cloud - Object Storage (add SKU) + metric: Gigabyte Storage Capacity Per Month + list_price_usd: 0.0051 + default_hours_units: 1 + billing_type: monthly + - sku: B90231 + product: Database Backup Cloud - Archive Storage (add SKU) + metric: Gigabyte Storage Capacity Per Month + list_price_usd: 0.0005 + default_hours_units: 1 + billing_type: monthly + autonomous_exadata: + display_name: Oracle Cloud Infrastructure - Autonomous Database & Exadata + skus: + - sku: B95701 + product: Oracle Autonomous Database (ATP or ADW) - ECPU (2 ECPU minimum) + metric: ECPU Per Hour + list_price_usd: 0.336 + default_hours_units: 730 + billing_type: hourly + - sku: B95703 + product: Oracle Autonomous Database (ATP or ADW) - BYOL ECPU (2 ECPU minimum) + metric: ECPU Per Hour + list_price_usd: 0.0807 + default_hours_units: 730 + billing_type: hourly + - sku: B95706 + product: Oracle Autonomous Database Storage for Transaction Processing (ATP), using ECPU + metric: Gigabyte Storage Capacity Per Month + list_price_usd: 0.1156 + default_hours_units: 1 + billing_type: monthly + - sku: B95754 + product: Oracle Autonomous Database Storage for Data Warehouse (ADW), using ECPU + metric: Gigabyte Storage Capacity Per Month + list_price_usd: 0.0244 + default_hours_units: 1 + billing_type: monthly + - sku: B95754a + product: Oracle Autonomous Database Backup Storage (ATP or ADW), using ECPU + metric: Gigabyte Storage Capacity Per Month + list_price_usd: 0.0244 + default_hours_units: 1 + billing_type: monthly + - sku: B92992 + product: OCI - GoldenGate + metric: OCPU Per Hour + list_price_usd: 1.3441 + default_hours_units: 730 + billing_type: hourly + - sku: B92993 + product: OCI - GoldenGate - BYOL + metric: OCPU Per Hour + list_price_usd: 0.3226 + default_hours_units: 730 + billing_type: hourly + exadata_infra: + display_name: Oracle Cloud Infrastructure - Database Exadata Infrastructure + skus: + - sku: B90777 + product: OCI - Database Exadata Infrastructure - Base System + metric: Hosted Environment Per Hour + list_price_usd: 10.7527 + default_hours_units: 730 + billing_type: hourly + - sku: B92380 + product: OCI - Database Exadata Infrastructure - Quarter Rack - X8M + metric: Hosted Environment Per Hour + list_price_usd: 14.5162 + default_hours_units: 730 + billing_type: hourly + - sku: B93380 + product: OCI - Database Exadata Infrastructure - Quarter Rack - X9M + metric: Hosted Environment Per Hour + list_price_usd: 14.5162 + default_hours_units: 730 + billing_type: hourly + - sku: B88592 + product: OCI - Database Exadata OCPU + metric: OCPU Per Hour + list_price_usd: 1.3441 + default_hours_units: 730 + billing_type: hourly + - sku: B88847 + product: OCI - Database Exadata OCPU - BYOL + metric: OCPU Per Hour + list_price_usd: 0.3226 + default_hours_units: 730 + billing_type: hourly + - sku: B110627 + product: OCI - Exadata Cloud Infrastructure - Database Server - X11M + metric: Hosted Environment Per Hour + list_price_usd: 2.9032 + default_hours_units: 730 + billing_type: hourly + - sku: B110629 + product: OCI - Exadata Cloud Infrastructure - Storage Server - X11M + metric: Hosted Environment Per Hour + list_price_usd: 2.9032 + default_hours_units: 730 + billing_type: hourly + - sku: B110631 + product: OCI - Exadata Database ECPU - Dedicated Infrastructure (X11M) + metric: ECPU Per Hour + list_price_usd: 0.336 + default_hours_units: 730 + billing_type: hourly + - sku: B110632 + product: OCI - Exadata Database ECPU - Dedicated Infrastructure - BYOL (X11M) + metric: ECPU Per Hour + list_price_usd: 0.0807 + default_hours_units: 730 + billing_type: hourly + exascale: + display_name: Oracle Cloud Infrastructure - Database Exadata Exascale Infrastructure + skus: + - sku: B109356 + product: Oracle Exadata Exascale Database - ECPU + metric: ECPU Per Hour + list_price_usd: 0.336 + default_hours_units: 730 + billing_type: hourly + - sku: B109357 + product: Oracle Exadata Exascale Database - ECPU BYOL + metric: ECPU Per Hour + list_price_usd: 0.0807 + default_hours_units: 730 + billing_type: hourly + - sku: B109355 + product: Oracle Exadata Exascale RDMA Compute Infrastructure + metric: ECPU Per Hour + list_price_usd: 0.025 + default_hours_units: 730 + billing_type: hourly + - sku: B107951 + product: Oracle Exadata Exascale VM Filesystem Storage + metric: Gigabyte Storage Capacity Per Month + list_price_usd: 0.0425 + default_hours_units: 1 + billing_type: monthly + - sku: B107952 + product: Oracle Exadata Exascale Smart Database Storage + metric: Gigabyte Storage Capacity Per Month + list_price_usd: 0.1156 + default_hours_units: 1 + billing_type: monthly + - sku: B109375 + product: Oracle Exadata Exascale Additional Flash Cache + metric: Gigabyte Per Hour + list_price_usd: 0.0005 + default_hours_units: 730 + billing_type: hourly + - sku: B110989 + product: Oracle Globally Distributed Exadata Exascale Database ECPU + metric: ECPU Per Hour + list_price_usd: 0.3864 + default_hours_units: 730 + billing_type: hourly + - sku: B110990 + product: Oracle Globally Distributed Exadata Exascale Database ECPU - BYOL + metric: ECPU Per Hour + list_price_usd: 0.0928 + default_hours_units: 730 + billing_type: hourly + postgresql: + display_name: Oracle Cloud Infrastructure - Database with PostgreSQL + skus: + - sku: B99060 + product: 'OCI - PostgreSQL: OCPU Database with PostgreSQL (additional cost to IaaS)' + metric: OCPU Per Hour + list_price_usd: 0.098 + default_hours_units: 730 + billing_type: hourly + - sku: B99062 + product: 'OCI - PostgreSQL: Database Optimized Storage' + metric: Gigabyte Storage Capacity Per Month + list_price_usd: 0.072 + default_hours_units: 1 + billing_type: monthly + mysql: + display_name: Oracle Cloud Infrastructure - MySQL Database Service (MDS) + skus: + - sku: B108030 + product: MySQL Database - ECPU (includes GB RAM, according to shapes) + metric: ECPU Per Hour + list_price_usd: 0.0366 + default_hours_units: 730 + billing_type: hourly + - sku: B92426 + product: MySQL Database - Storage + metric: Gigabyte Storage Capacity Per Month + list_price_usd: 0.04 + default_hours_units: 1 + billing_type: monthly + - sku: B92483 + product: MySQL Database - Backup Storage + metric: Gigabyte Storage Capacity Per Month + list_price_usd: 0.04 + default_hours_units: 1 + billing_type: monthly + - sku: B96626 + product: 'OCI - HeatWave Node (Options: 32 GB RAM or 512 GB RAM each)' + metric: Node per Hour (bundle 16 GB RAM) + list_price_usd: 0.011 + default_hours_units: 730 + billing_type: hourly + - sku: B96625 + product: OCI - HeatWave - Storage + metric: Gigabyte Storage Capacity Per Month + list_price_usd: 0.02 + default_hours_units: 1 + billing_type: monthly + - sku: B109169 + product: MySQL Database - Outbound Data Transfer - Inter OCI Region + metric: Gigabyte Outbound Data Transfer Per Month + list_price_usd: 0.04 + default_hours_units: 1 + billing_type: monthly + networking: + display_name: Oracle Cloud Infrastructure - Networking + skus: + - sku: B88327 + product: 'OCI - Outbound Data Transfer - Originating in North America, Europe, and UK (Free Tier: 10240 GB)' + metric: Gigabyte Outbound Data Transfer Per Month + list_price_usd: 0.0085 + default_hours_units: 1 + billing_type: monthly + - sku: B93455 + product: 'OCI - Outbound Data Transfer - Originating in APAC, Japan, and South America (Free Tier: 10240 GB)' + metric: Gigabyte Outbound Data Transfer Per Month + list_price_usd: 0.025 + default_hours_units: 1 + billing_type: monthly + - sku: B93456 + product: 'OCI - Outbound Data Transfer - Originating in Middle East and Africa (Free Tier: 10240 GB)' + metric: Gigabyte Outbound Data Transfer Per Month + list_price_usd: 0.05 + default_hours_units: 1 + billing_type: monthly + - sku: B93030 + product: 'OCI - Load Balancer Base (Free Tier: 1 Load Balancer Instance)' + metric: Load Balancer Hour + list_price_usd: 0.0113 + default_hours_units: 730 + billing_type: hourly + - sku: B93031 + product: 'OCI - Load Balancer Bandwidth (Free Tier: 10 Mbps Per Hour)' + metric: Mbps Per Hour + list_price_usd: 0.0001 + default_hours_units: 730 + billing_type: hourly + - sku: B88325 + product: OCI - FastConnect 1 Gbps + metric: Port Hour + list_price_usd: 0.2125 + default_hours_units: 730 + billing_type: hourly + - sku: B88326 + product: OCI - FastConnect 10 Gbps + metric: Port Hour + list_price_usd: 1.275 + default_hours_units: 730 + billing_type: hourly + - sku: B93126 + product: OCI - FastConnect 100 Gbps + metric: Port Hour + list_price_usd: 10.75 + default_hours_units: 730 + billing_type: hourly + - sku: B88525 + product: OCI - DNS + metric: 1 Million Queries + list_price_usd: 0.85 + default_hours_units: 1 + billing_type: per_unit + - sku: B90327 + product: OCI - DNS Traffic Management + metric: 1 Million Queries + list_price_usd: 4.0 + default_hours_units: 1 + billing_type: per_unit + - sku: B88523 + product: 'OCI - Email Delivery (Free Tier: 3000 Emails Sent)' + metric: 1,000 (1K) Emails Sent + list_price_usd: 0.085 + default_hours_units: 1 + billing_type: per_unit + security: + display_name: Oracle Cloud Infrastructure - Security + skus: + - sku: B94579 + product: 'OCI - Web Application Firewall - Instance (Free Tier: 1 WAF Instance)' + metric: WAF Instance Per Month + list_price_usd: 5.0 + default_hours_units: 1 + billing_type: monthly + - sku: B94277 + product: 'OCI - Web Application Firewall - Requests (Free Tier: 10 Million Incoming Requests)' + metric: 1 Million Incoming Requests Per Month + list_price_usd: 0.6 + default_hours_units: 1 + billing_type: monthly + - sku: B92092 + product: 'OCI - KMS Vault - Key Versions (Free Tier: 20 Key Versions)' + metric: Key Version Per Month + list_price_usd: 0.5334 + default_hours_units: 1 + billing_type: monthly + - sku: B95403 + product: OCI - Network Firewall Instance + metric: Instance Per Hour + list_price_usd: 2.75 + default_hours_units: 730 + billing_type: hourly + discountable: false + - sku: B95404 + product: 'OCI - Network Firewall Data Processing (Free Tier: 10240 GB)' + metric: Gigabyte of Data Processed + list_price_usd: 0.01 + default_hours_units: 730 + billing_type: hourly + discountable: false + observability: + display_name: Oracle Cloud Infrastructure - Observability and Management + skus: + - sku: B90940 + product: 'OCI - Notifications - HTTPS Delivery (Free Tier: 1 Million Delivery Operations)' + metric: 1 Million Delivery Operations + list_price_usd: 0.6 + default_hours_units: 1 + billing_type: per_unit + - sku: B90941 + product: 'OCI - Notifications - Email Delivery (Free Tier: 1000 Emails Sent)' + metric: 1,000 (1K) Emails Sent + list_price_usd: 0.02 + default_hours_units: 1 + billing_type: per_unit + - sku: B93006 + product: 'OCI - Notifications - SMS Outbound to Country Zone 3 [Brazil] (Free Tier: 100 SMS Messages Sent)' + metric: SMS Message Sent + list_price_usd: 0.086 + default_hours_units: 1 + billing_type: per_unit + - sku: B92593 + product: 'OCI - Logging - Storage (Free Tier: 10 GB)' + metric: Gigabyte Log Storage Per Month + list_price_usd: 0.05 + default_hours_units: 1 + billing_type: monthly + - sku: B92941 + product: 'Application Performance Monitoring Service - Tracing Data (Free Tier: 1000 Events Per Hour)' + metric: 100,000 (100K) Events Per Hour + list_price_usd: 0.65 + default_hours_units: 1 + billing_type: per_unit + - sku: B92942 + product: Application Performance Monitoring Service - Synthetic Usage + metric: 10 Monitor Runs Per Hour + list_price_usd: 0.02 + default_hours_units: 1 + billing_type: per_unit + - sku: B95264 + product: Application Performance Monitoring Service - Stack Monitoring - 10 Monitored Resources Per Hour + metric: 10 Monitored Resources Per Hour + list_price_usd: 0.075 + default_hours_units: 1 + billing_type: per_unit + - sku: B92682 + product: Oracle Analytics - Professional (min 10 users) + metric: User Per Month + list_price_usd: 16.0 + default_hours_units: 1 + billing_type: monthly + - sku: B92683 + product: Oracle Analytics - Enterprise (min 10 users) + metric: User Per Month + list_price_usd: 80.0 + default_hours_units: 1 + billing_type: monthly + - sku: B89630 + product: Oracle Analytics - Professional (per OCPU) + metric: OCPU Per Hour + list_price_usd: 1.0753 + default_hours_units: 730 + billing_type: hourly + - sku: B89631 + product: Oracle Analytics - Enterprise (per OCPU) + metric: OCPU Per Hour + list_price_usd: 2.1506 + default_hours_units: 730 + billing_type: hourly + - sku: B95485 + product: OCI - Full Stack Disaster Recovery Service (per OCPU) + metric: OCPUs Per Month + list_price_usd: 0.0128 + default_hours_units: 730 + billing_type: hourly + - sku: B110274 + product: OCI - Full Stack Disaster Recovery Service (per ECPU) + metric: ECPUs Per Month + list_price_usd: 0.0032 + default_hours_units: 730 + billing_type: hourly + - sku: B95240 + product: Oracle Database Zero Data Loss Recovery Service + metric: Virtualized Gigabyte Per Month + list_price_usd: 0.0306 + default_hours_units: 1 + billing_type: monthly + - sku: B95241 + product: Oracle Database Zero Data Loss Autonomous Recovery Service (Real-Time Protection) + metric: Virtualized Gigabyte Per Month + list_price_usd: 0.04 + default_hours_units: 1 + billing_type: monthly + - sku: B90323 + product: OCI - Health Checks - Basic + metric: Endpoints Per Month + list_price_usd: 0.3 + default_hours_units: 730 + billing_type: hourly + - sku: B90925 + product: 'OCI - Monitoring - Ingestion (Free Tier: 500 Million Datapoints)' + metric: 1 Million Datapoints + list_price_usd: 0.0025 + default_hours_units: 730 + billing_type: hourly + - sku: B90926 + product: 'OCI - Monitoring - Retrieval (Free Tier: 1 Billion Datapoints)' + metric: 1 Billion Data Points + list_price_usd: 0.0015 + default_hours_units: 1 + billing_type: per_unit + - sku: B110475 + product: 'OCI - Fleet Application Management Service (Free Tier: 25 Managed Resources)' + metric: Managed Resource Per Month + list_price_usd: 2.604 + default_hours_units: 1 + billing_type: monthly + devops: + display_name: Oracle Cloud Infrastructure - DevOps + skus: + - sku: B92072 + product: OCI - API Gateway - 1,000,000 API Calls + metric: 1 Million API Calls Per Month + list_price_usd: 3.0 + default_hours_units: 1 + billing_type: per_unit + - sku: B90938 + product: OCI - Streaming - PUT or GET + metric: Gigabytes of Data Transferred + list_price_usd: 0.025 + default_hours_units: 1 + billing_type: per_unit + - sku: B90939 + product: OCI - Streaming - Storage + metric: Gigabyte Per Hour + list_price_usd: 0.0002 + default_hours_units: 730 + billing_type: hourly + - sku: B110500 + product: OCI - Streaming With Apache Kafka + metric: OCPU Per Hour + list_price_usd: 0.1 + default_hours_units: 730 + billing_type: hourly + - sku: B90617 + product: 'Oracle Functions - Execution Time (Free Tier: 40 Blocks of 10000 Gigabyte Memory-Seconds)' + metric: 10,000 (10K) Gigabyte Memory-Seconds + list_price_usd: 0.1417 + default_hours_units: 1 + billing_type: per_unit + - sku: B90618 + product: 'Oracle Functions - Invocations (Free Tier: 2 Blocks of 1M Function Invocations)' + metric: 1 Million Function Invocations + list_price_usd: 0.2 + default_hours_units: 1 + billing_type: per_unit + - sku: B89737 + product: 'Oracle NoSQL Database Cloud - Write (Free Tier: 50 Units)' + metric: Write Unit Per Month (2.6M Writes Month) + list_price_usd: 0.1254 + default_hours_units: 1 + billing_type: monthly + - sku: B89738 + product: 'Oracle NoSQL Database Cloud - Read (Free Tier: 50 Units)' + metric: Read Unit Per Month (2.6M Reads Month) + list_price_usd: 0.0064 + default_hours_units: 1 + billing_type: monthly + - sku: B89739 + product: 'Oracle NoSQL Database Cloud - Storage (Free Tier: 25 GB per table and a maximum of 3 tables)' + metric: Gigabyte Storage Capacity Per Month + list_price_usd: 0.066 + default_hours_units: 1 + billing_type: monthly + - sku: B93709 + product: Oracle Cloud Infrastructure Search Service with Open Search HA (Over 2 Data Nodes) + metric: Node Per Hour + list_price_usd: 0.25 + default_hours_units: 730 + billing_type: hourly + - sku: B95697 + product: 'OCI Queue - Requests (Free Tier: 1 Million Requests)' + metric: 1 Million Requests per Month + list_price_usd: 0.22 + default_hours_units: 1 + billing_type: monthly + - sku: B96109 + product: Oracle Container Engine for Kubernetes - Virtual Node (Virtual Node Per Hour) + metric: Virtual Node Per Hour + list_price_usd: 0.015 + default_hours_units: 730 + billing_type: hourly + - sku: B96545 + product: Oracle Container Engine for Kubernetes - Enhanced Cluster (Cluster Per Hour) + metric: Cluster Per Hour + list_price_usd: 0.1 + default_hours_units: 730 + billing_type: hourly + - sku: B95634 + product: 'OCI Logging Analytics - Active Storage (Free Tier: 10 GB) - Tiered Pricing' + metric: Logging Analytics Storage Unit Per Month + list_price_usd: 372.0 + default_hours_units: 1 + billing_type: monthly + - sku: B91346 + product: Oracle WebLogic Server Enterprise Edition for Oracle Cloud Infrastructure + metric: OCPU Per Hour + list_price_usd: 0.2581 + default_hours_units: 730 + billing_type: hourly + - sku: B91347 + product: Oracle WebLogic Suite for Oracle Cloud Infrastructure + metric: OCPU Per Hour + list_price_usd: 0.4646 + default_hours_units: 730 + billing_type: hourly + - sku: B92599 + product: OCI Data Integration (OCI DI) - Data Processed + metric: Gigabyte of Data Processed Per Hour + list_price_usd: 0.04 + default_hours_units: 730 + billing_type: hourly + - sku: B92598 + product: OCI Data Integration (OCI DI) - Workspace Usage + metric: Workspace Usage Per Hour + list_price_usd: 0.16 + default_hours_units: 730 + billing_type: hourly + - sku: B93306 + product: 'OCI Data Integration (OCI DI) - Pipeline Operator Execution (Free Tier: 30 Execution Hours)' + metric: Execution Hour + list_price_usd: 0.3 + default_hours_units: 730 + billing_type: hourly + - sku: B98217 + product: OCI Cache with Redis - Low Memory (up to 10 GB per node, min 2 GB) + metric: Redis Memory Gigabyte Per Hour + list_price_usd: 0.0194 + default_hours_units: 730 + billing_type: hourly + - sku: B99591 + product: OCI Cache with Redis - High Memory (above 10 GB per node, min 2 GB) + metric: Redis Memory Gigabyte Per Hour + list_price_usd: 0.0136 + default_hours_units: 730 + billing_type: hourly + - sku: B89639 + product: Oracle Integration Cloud Service - Standard + metric: Messages Pack (5,000 Messages Per Hour) + list_price_usd: 0.6452 + default_hours_units: 730 + billing_type: hourly + - sku: B89640 + product: Oracle Integration Cloud Service - Enterprise + metric: Messages Pack (5,000 Messages Per Hour) + list_price_usd: 1.2903 + default_hours_units: 730 + billing_type: hourly + - sku: B109559 + product: Oracle Integration Cloud Service - Healthcare + metric: Messages Pack (5,000 Messages Per Hour) + list_price_usd: 1.9355 + default_hours_units: 730 + billing_type: hourly + ai: + display_name: Oracle Cloud Infrastructure - Artificial Intelligence (AI) + skus: + - sku: B90260 + product: OCI - Oracle Digital Assistant Cloud Service + metric: Requests + list_price_usd: 0.0232 + default_hours_units: 730 + billing_type: hourly + - sku: B94973 + product: 'OCI - Vision: Image Analysis (Free Tier: 5,000 Transactions)' + metric: 1,000 (1K) Transactions + list_price_usd: 0.25 + default_hours_units: 1 + billing_type: per_unit + - sku: B94974 + product: 'OCI - Vision: OCR (Free Tier: 5,000 Transactions)' + metric: 1,000 (1K) Transactions + list_price_usd: 1.0 + default_hours_units: 1 + billing_type: per_unit + - sku: B110617 + product: 'OCI - Vision: Stored Video Analysis (Free Tier: 1,000 Minutes)' + metric: Processed Video Minute + list_price_usd: 0.1 + default_hours_units: 1 + billing_type: per_unit + - sku: B94977 + product: 'OCI - Vision: Custom Training (Free Tier: 15 Training Hours)' + metric: Training Hour + list_price_usd: 1.5 + default_hours_units: 1 + billing_type: per_unit + - sku: B94896 + product: 'OCI - Speech (Free Tier: 5 Transcription Hours)' + metric: Transcription Hour + list_price_usd: 0.35 + default_hours_units: 1 + billing_type: per_unit + - sku: B95917 + product: 'OCI - Language: Custom Inferencing' + metric: 1,000 (1K) Transactions + list_price_usd: 3.5 + default_hours_units: 1 + billing_type: per_unit + - sku: B95919 + product: 'OCI - Language: Custom Training (Free Tier: 15 Training Hours)' + metric: Training Hour + list_price_usd: 1.5 + default_hours_units: 1 + billing_type: per_unit + - sku: B95918 + product: 'OCI - Language: Custom Inferencing - Dedicated (Free Tier: 15 Inferencing Unit Hours)' + metric: Inferencing Unit Hour + list_price_usd: 1.5 + default_hours_units: 1 + billing_type: per_unit + - sku: B108711 + product: 'OCI - Language: Dedicated Inferencing - Healthcare' + metric: Inferencing Unit Hour + list_price_usd: 20.0 + default_hours_units: 1 + billing_type: per_unit + - sku: B93423 + product: 'OCI - Language: Pre-trained Inferencing (Free Tier: 5,000 Transactions)' + metric: 1,000 (1K) Transactions + list_price_usd: 0.25 + default_hours_units: 1 + billing_type: per_unit + - sku: B95920 + product: 'OCI - Language: Text Translating (Free Tier: 1,000 Transactions)' + metric: 1,000 (1K) Transactions + list_price_usd: 10.0 + default_hours_units: 1 + billing_type: per_unit + - sku: B96113 + product: 'OCI - Document Understanding: Custom Training (Free Tier: 15 Training Hours)' + metric: Training Hour + list_price_usd: 1.5 + default_hours_units: 1 + billing_type: per_unit + - sku: B96112 + product: 'OCI - Document Understanding: Document Extraction (Free Tier: 5,000 Transactions)' + metric: 1,000 (1K) Transactions + list_price_usd: 10.0 + default_hours_units: 1 + billing_type: per_unit + - sku: B97194 + product: 'OCI - Document Understanding: Custom Document Extraction (Free Tier: 5,000 Transactions)' + metric: 1,000 (1K) Transactions + list_price_usd: 30.0 + default_hours_units: 1 + billing_type: per_unit + - sku: B96111 + product: 'OCI - Document Understanding: Document Properties (Free Tier: 5,000 Transactions)' + metric: 1,000 (1K) Transactions + list_price_usd: 0.25 + default_hours_units: 1 + billing_type: per_unit + - sku: B97193 + product: 'OCI - Document Understanding: Custom Document Properties (Free Tier: 5,000 Transactions)' + metric: 1,000 (1K) Transactions + list_price_usd: 1.5 + default_hours_units: 1 + billing_type: per_unit + - sku: B96110 + product: 'OCI - Document Understanding: OCR (Free Tier: 5,000 Transactions)' + metric: 1,000 (1K) Transactions + list_price_usd: 1.0 + default_hours_units: 1 + billing_type: per_unit + - sku: B94282 + product: 'OCI - Data Labeling (Free Tier: 1,000 Annotated Data Records)' + metric: Annotated Data Record + list_price_usd: 0.0002 + default_hours_units: 1 + billing_type: per_unit + genai_cohere: + display_name: Oracle Cloud Infrastructure - Generative AI - Cohere + skus: + - sku: B108077 + product: Oracle Cloud Infrastructure Generative AI - Large Cohere + metric: 10,000 (10K) Transactions + list_price_usd: 0.0156 + default_hours_units: 1 + billing_type: per_unit + discountable: false + - sku: B108078 + product: Oracle Cloud Infrastructure Generative AI - Small Cohere + metric: 10,000 (10K) Transactions + list_price_usd: 0.0009 + default_hours_units: 1 + billing_type: per_unit + discountable: false + - sku: B108079 + product: Oracle Cloud Infrastructure Generative AI - Embed Cohere + metric: 10,000 (10K) Transactions + list_price_usd: 0.001 + default_hours_units: 1 + billing_type: per_unit + discountable: false + - sku: B108082 + product: Oracle Cloud Infrastructure Generative AI - Large Cohere - Dedicated + metric: AI Unit Per Hour + list_price_usd: 24.0 + default_hours_units: 730 + billing_type: hourly + discountable: false + - sku: B108083 + product: Oracle Cloud Infrastructure Generative AI - Small Cohere - Dedicated + metric: AI Unit Per Hour + list_price_usd: 6.5 + default_hours_units: 730 + billing_type: hourly + discountable: false + - sku: B108084 + product: Oracle Cloud Infrastructure Generative AI - Embed Cohere - Dedicated + metric: AI Unit Per Hour + list_price_usd: 10.9 + default_hours_units: 730 + billing_type: hourly + discountable: false + - sku: B111015 + product: Oracle Cloud Infrastructure Generative AI - Cohere Rerank - Dedicated + metric: Cluster Hour + list_price_usd: 10.0 + default_hours_units: 730 + billing_type: hourly + discountable: false + genai_llama: + display_name: Oracle Cloud Infrastructure - Generative AI - Llama + skus: + - sku: B108080 + product: Oracle Cloud Infrastructure Generative AI - Large Meta 70B + metric: 10,000 (10K) Transactions + list_price_usd: 0.0018 + default_hours_units: 1 + billing_type: per_unit + - sku: B110517 + product: Oracle Cloud Infrastructure Generative AI - Meta Llama 3.1 405B + metric: 10,000 (10K) Transactions + list_price_usd: 0.0267 + default_hours_units: 1 + billing_type: per_unit + - sku: B110679 + product: Oracle Cloud Infrastructure Generative AI - Meta Llama 3.2 90B Vision + metric: 10,000 (10K) Transactions + list_price_usd: 0.005 + default_hours_units: 1 + billing_type: per_unit + - sku: B111035 + product: Oracle Cloud Infrastructure Generative AI - Meta Llama 4 Scout + metric: 10,000 (10K) Transactions + list_price_usd: 0.0018 + default_hours_units: 1 + billing_type: per_unit + - sku: B111036 + product: Oracle Cloud Infrastructure Generative AI - Meta Llama 4 Maverick + metric: 10,000 (10K) Transactions + list_price_usd: 0.0018 + default_hours_units: 1 + billing_type: per_unit + - sku: B108085 + product: Oracle Cloud Infrastructure Generative AI - Large Meta - Dedicated + metric: AI Unit Per Hour + list_price_usd: 12.0 + default_hours_units: 730 + billing_type: hourly + genai_agents: + display_name: Oracle Cloud Infrastructure - Generative AI Agents - RAG + skus: + - sku: B110461 + product: Oracle Cloud Infrastructure Generative AI Agents - Retrieval-Augmented Generation (RAG) + metric: 10,000 (10K) Transactions + list_price_usd: 0.003 + default_hours_units: 1 + billing_type: per_unit + - sku: B110462 + product: Oracle Cloud Infrastructure Generative AI Agents - Knowledge Base Storage + metric: Gigabyte Storage Capacity Per HOUR + list_price_usd: 0.0084 + default_hours_units: 730 + billing_type: hourly + - sku: B110463 + product: Oracle Cloud Infrastructure Generative AI Agents - Data Ingestion + metric: 10,000 (10K) Transactions + list_price_usd: 0.0003 + default_hours_units: 1 + billing_type: per_unit + roving_edge: + display_name: Oracle Roving Edge Infrastructure + skus: + - sku: B92615 + product: Roving Edge Device - Compute Optimized - Ruggedized + metric: Resource Possession Per Day + list_price_usd: 160.0 + default_hours_units: 31 + billing_type: daily + - sku: B93039 + product: Roving Edge Device - Compute Optimized - Standard + metric: Resource Possession Per Day + list_price_usd: 160.0 + default_hours_units: 31 + billing_type: daily + - sku: B95228 + product: Roving Edge Ultra + metric: Resource Possession Per Day + list_price_usd: 45.0 + default_hours_units: 31 + billing_type: daily + - sku: B109492 + product: Roving Edge Device - RED.2 Compute + metric: Resource Possession Per Day + list_price_usd: 80.0 + default_hours_units: 31 + billing_type: daily + - sku: B109493 + product: Roving Edge Device - RED.2 GPU + metric: Resource Possession Per Day + list_price_usd: 100.0 + default_hours_units: 31 + billing_type: daily + - sku: B109494 + product: Roving Edge Device - RED.2.STG Storage + metric: Resource Possession Per Day + list_price_usd: 95.0 + default_hours_units: 31 + billing_type: daily + - sku: B109496 + product: Roving Edge Device - RED.2 Ruggedized Case (add-on) + metric: Resource Possession Per Day + list_price_usd: 10.0 + default_hours_units: 31 + billing_type: daily + - sku: B109495 + product: Roving Edge Device - RED.2 Non-Return Fee + metric: Device + list_price_usd: 25000.0 + default_hours_units: 1 + billing_type: one_time + - sku: B93040 + product: Roving Edge Device - Compute Optimized - Unreturnable/Loss Fee + metric: Device + list_price_usd: 45000.0 + default_hours_units: 1 + billing_type: one_time + - sku: B95229 + product: Roving Edge Ultra - Non-Return or Loss Fee + metric: Device + list_price_usd: 35000.0 + default_hours_units: 1 + billing_type: one_time diff --git a/templates/bom-spec.yaml b/templates/bom-spec.yaml new file mode 100644 index 0000000..195f7ea --- /dev/null +++ b/templates/bom-spec.yaml @@ -0,0 +1,50 @@ +# ────────────────────────────────────────────────────────────────── +# OCI Bill of Materials (BOM) — Spec Template +# ────────────────────────────────────────────────────────────────── +# The AI generates this YAML based on the customer's request, +# selecting only the relevant SKUs from kb/pricing/oci-sku-catalog.yaml. +# The BOM generator tool (tools/oci_bom_gen.py) renders it to .xlsx. +# +# Usage: +# python tools/oci_bom_gen.py --spec this-file.yaml --output bom.xlsx + +bom: + # ── Header ────────────────────────────────────────────────────── + customer_name: "" + project_name: "" + prepared_by: "" + date: "" # ISO format: 2026-03-27 + reference_label: "" # e.g., "ACME-BOM-001" + currency: USD # Primary currency + # realm: "OC1 - Commercial" # Optional: OC1 / OC2 / OC3 / OC9 + # service_type: "Pay as You Go" # Optional: PAYG / UCM / Annual Flex + + # ── Optional currency conversion ──────────────────────────────── + # Enable to add converted columns (e.g., USD → BRL with tax) + conversion: + enabled: false + target_currency: BRL + exchange_rate: 5.75 # 1 USD = X target currency + tax_rate: 0.0 # e.g., 0.1385 for 13.85% ISS+PIS+COFINS + + # ── Line items (only requested services) ──────────────────────── + # Each item must reference a SKU from oci-sku-catalog.yaml. + # Only include services the customer actually needs. + line_items: + - sku: "B97384" # Must match oci-sku-catalog.yaml + qty: 4 # Number of instances/units + # hours_units: 730 # Override catalog default if needed + months: 12 # Contract duration + discount: 0.0 # 0.0 to 1.0 (e.g., 0.30 = 30%) + # custom_label: "" # Optional label + # custom_note: "" # Optional note + + - sku: "B97385" + qty: 64 # 64 GB memory + months: 12 + discount: 0.0 + + # ── Notes (optional) ──────────────────────────────────────────── + notes: + - "Pricing based on Ashburn region (OC1)" + - "Discounts subject to Oracle approval" diff --git a/tools/oci_bom_gen.py b/tools/oci_bom_gen.py new file mode 100644 index 0000000..2eee985 --- /dev/null +++ b/tools/oci_bom_gen.py @@ -0,0 +1,730 @@ +#!/usr/bin/env python3 +""" +OCI Deal Accelerator — Bill of Materials (BOM) Generator + +Generates a professional .xlsx BOM from a YAML spec, resolving SKUs +against the OCI SKU catalog. Only requested services appear in the output. + +Usage: + python oci_bom_gen.py --spec bom-spec.yaml --output customer-bom.xlsx + python oci_bom_gen.py --spec bom-spec.yaml --output bom.xlsx --catalog kb/pricing/oci-sku-catalog.yaml +""" + +import argparse +import os +import sys +from datetime import date +from pathlib import Path +from typing import Dict, List, Optional, Tuple + +import yaml +from openpyxl import Workbook +from openpyxl.styles import ( + Alignment, + Border, + Font, + NamedStyle, + PatternFill, + Side, + numbers, +) +from openpyxl.utils import get_column_letter + + +# ── Oracle Redwood palette ──────────────────────────────────────── +class Colors: + TEAL = "2C5967" + TEAL_LIGHT = "E8F0F2" + DARK_BG = "312D2A" + COPPER = "AA643B" + WHITE = "FFFFFF" + GRAY_100 = "F5F4F2" + GRAY_300 = "D4D1CC" + GRAY_500 = "6F6964" + GRAY_700 = "3A3631" + BLACK = "000000" + + +# ── Column definitions ──────────────────────────────────────────── +# Base columns (always present) +BASE_COLS = [ + ("Part (SKU)", 14), + ("Product / Description", 55), + ("Metric", 32), + ("Unit Price (USD)", 16), + ("Hours/Units", 12), + ("Qty", 10), + ("Months", 10), + ("Discount", 10), + ("Monthly USD\n(w/o discount)", 18), + ("Monthly USD\n(w/ discount)", 18), +] + +# Conversion columns (added when conversion is enabled) +# Placeholders — header text is set dynamically with target currency +CONV_COLS = [ + ("Monthly {cur}\n(w/o tax)", 18), + ("Monthly {cur}\n(w/ tax)", 18), +] + +# Always-last column +TAIL_COLS = [ + ("Cost %", 10), +] + + +class OCIBomGenerator: + """Generate OCI Bill of Materials (.xlsx) with Oracle Redwood styling.""" + + def __init__( + self, + customer: str = "", + project: str = "", + prepared_by: str = "", + bom_date: str = "", + reference_label: str = "", + currency: str = "USD", + realm: str = "", + service_type: str = "", + conversion: Optional[dict] = None, + notes: Optional[list] = None, + ): + self.customer = customer + self.project = project + self.prepared_by = prepared_by + self.bom_date = bom_date or date.today().isoformat() + self.reference_label = reference_label + self.currency = currency + self.realm = realm + self.service_type = service_type + self.conversion = conversion or {"enabled": False} + self.notes = notes or [] + + self.catalog: dict = {} # sku_code -> {product, metric, ...} + self.catalog_meta: dict = {} # top-level catalog metadata + self.category_order: list = [] # ordered category keys + self.category_names: dict = {} # cat_key -> display_name + self.line_items: list = [] # resolved line items + self.wb = Workbook() + + # ── Catalog loading ─────────────────────────────────────────── + + def load_catalog(self, catalog_path: Optional[str] = None): + """Load SKU catalog from YAML. Auto-discovers path if not given.""" + if not catalog_path: + # Auto-discover relative to this script + tools_dir = Path(__file__).resolve().parent + catalog_path = str(tools_dir.parent / "kb" / "pricing" / "oci-sku-catalog.yaml") + + with open(catalog_path, "r") as f: + raw = yaml.safe_load(f) + + self.catalog_meta = { + k: v for k, v in raw.items() if k != "categories" + } + + categories = raw.get("categories", {}) + for cat_key, cat_data in categories.items(): + self.category_order.append(cat_key) + self.category_names[cat_key] = cat_data.get("display_name", cat_key) + for sku_entry in cat_data.get("skus", []): + sku_code = str(sku_entry["sku"]) + self.catalog[sku_code] = { + **sku_entry, + "sku": sku_code, + "category": cat_key, + } + + # ── Line items ──────────────────────────────────────────────── + + def add_line_item( + self, + sku: str, + qty: float = 0, + hours_units: Optional[float] = None, + months: int = 12, + discount: float = 0.0, + custom_label: str = "", + custom_note: str = "", + ): + """Add a line item, resolving SKU details from catalog.""" + sku = str(sku) + cat_entry = self.catalog.get(sku, {}) + + item = { + "sku": sku, + "product": cat_entry.get("product", f"Unknown SKU ({sku})"), + "metric": cat_entry.get("metric", ""), + "list_price_usd": cat_entry.get("list_price_usd", 0), + "hours_units": hours_units if hours_units is not None else cat_entry.get("default_hours_units", 730), + "qty": qty, + "months": months, + "discount": discount, + "category": cat_entry.get("category", "other"), + "discountable": cat_entry.get("discountable", True), + "billing_type": cat_entry.get("billing_type", "hourly"), + "custom_label": custom_label, + "custom_note": custom_note, + } + self.line_items.append(item) + + # ── Excel generation ────────────────────────────────────────── + + def _col_defs(self) -> List[Tuple[str, int]]: + """Return full column definitions based on conversion setting.""" + cols = list(BASE_COLS) + if self.conversion.get("enabled"): + cur = self.conversion.get("target_currency", "BRL") + cols += [(h.format(cur=cur), w) for h, w in CONV_COLS] + cols += list(TAIL_COLS) + return cols + + def _col_count(self) -> int: + return len(self._col_defs()) + + def _build_sheet(self, ws): + """Build the complete BOM sheet.""" + ws.title = "BOM" + col_defs = self._col_defs() + num_cols = len(col_defs) + conv = self.conversion.get("enabled", False) + + # ── Styles ──────────────────────────────────────────────── + thin_border = Border( + left=Side(style="thin", color=Colors.GRAY_300), + right=Side(style="thin", color=Colors.GRAY_300), + top=Side(style="thin", color=Colors.GRAY_300), + bottom=Side(style="thin", color=Colors.GRAY_300), + ) + header_font = Font(name="Segoe UI", size=11, bold=True, color=Colors.WHITE) + header_fill = PatternFill(start_color=Colors.TEAL, end_color=Colors.TEAL, fill_type="solid") + header_align = Alignment(horizontal="center", vertical="center", wrap_text=True) + + cat_font = Font(name="Segoe UI", size=10, bold=True, color=Colors.TEAL) + cat_fill = PatternFill(start_color=Colors.TEAL_LIGHT, end_color=Colors.TEAL_LIGHT, fill_type="solid") + + data_font = Font(name="Segoe UI", size=10, color=Colors.GRAY_700) + alt_fill = PatternFill(start_color=Colors.GRAY_100, end_color=Colors.GRAY_100, fill_type="solid") + + total_font = Font(name="Segoe UI", size=11, bold=True, color=Colors.DARK_BG) + total_fill = PatternFill(start_color=Colors.GRAY_300, end_color=Colors.GRAY_300, fill_type="solid") + + subtotal_font = Font(name="Segoe UI", size=10, bold=True, color=Colors.GRAY_700) + + # ── Header area ─────────────────────────────────────────── + row = 1 + title_font = Font(name="Segoe UI", size=16, bold=True, color=Colors.TEAL) + ws.cell(row=row, column=1, value="Oracle Investment Proposal").font = title_font + ws.merge_cells(start_row=row, start_column=1, end_row=row, end_column=num_cols) + row += 1 + + subtitle_font = Font(name="Segoe UI", size=10, color=Colors.GRAY_500) + ws.cell(row=row, column=1, value=f"as of {self.bom_date}").font = subtitle_font + row += 1 + + meta_font = Font(name="Segoe UI", size=10, color=Colors.GRAY_700) + meta_items = [ + ("Customer", self.customer), + ("Project", self.project), + ("Prepared by", self.prepared_by), + ("Reference", self.reference_label), + ("Currency", self.currency), + ("Realm", self.realm), + ("Service Type", self.service_type), + ] + for label, value in meta_items: + if value: + ws.cell(row=row, column=1, value=f"{label}:").font = Font( + name="Segoe UI", size=10, bold=True, color=Colors.GRAY_700 + ) + ws.cell(row=row, column=2, value=value).font = meta_font + row += 1 + + if conv: + xr = self.conversion.get("exchange_rate", 1) + tc = self.conversion.get("target_currency", "BRL") + tr = self.conversion.get("tax_rate", 0) + ws.cell(row=row, column=1, value="Exchange Rate:").font = Font( + name="Segoe UI", size=10, bold=True, color=Colors.GRAY_700 + ) + ws.cell(row=row, column=2, value=f"1 USD = {xr} {tc}").font = meta_font + row += 1 + if tr > 0: + ws.cell(row=row, column=1, value="Tax Rate:").font = Font( + name="Segoe UI", size=10, bold=True, color=Colors.GRAY_700 + ) + ws.cell(row=row, column=2, value=f"{tr*100:.2f}%").font = meta_font + row += 1 + + row += 1 # blank row before table + + # ── Column headers ──────────────────────────────────────── + header_row = row + for ci, (col_name, col_width) in enumerate(col_defs, 1): + cell = ws.cell(row=row, column=ci, value=col_name) + cell.font = header_font + cell.fill = header_fill + cell.alignment = header_align + cell.border = thin_border + ws.column_dimensions[get_column_letter(ci)].width = col_width + row += 1 + + # ── Group items by category ─────────────────────────────── + items_by_cat: dict[str, list] = {} + for item in self.line_items: + cat = item["category"] + items_by_cat.setdefault(cat, []).append(item) + + # Column index references (1-based) + COL_SKU = 1 + COL_PRODUCT = 2 + COL_METRIC = 3 + COL_PRICE = 4 + COL_HOURS = 5 + COL_QTY = 6 + COL_MONTHS = 7 + COL_DISC = 8 + COL_M_WO = 9 # monthly without discount + COL_M_W = 10 # monthly with discount + if conv: + COL_CONV_WO = 11 # monthly converted without tax + COL_CONV_W = 12 # monthly converted with tax + COL_PCT = 13 # cost proportion + else: + COL_PCT = 11 + + data_start_row = row + cat_subtotal_rows = [] + + for cat_key in self.category_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) + + # Category header row + ws.cell(row=row, column=COL_PRODUCT, value=display_name).font = cat_font + for ci in range(1, num_cols + 1): + ws.cell(row=row, column=ci).fill = cat_fill + ws.cell(row=row, column=ci).border = thin_border + cat_header_row = row + row += 1 + + cat_first_data = row + + for idx, item in enumerate(cat_items): + # Static data + ws.cell(row=row, column=COL_SKU, value=item["sku"]) + ws.cell(row=row, column=COL_PRODUCT, value=item["product"]) + ws.cell(row=row, column=COL_METRIC, value=item["metric"]) + ws.cell(row=row, column=COL_PRICE, value=item["list_price_usd"]) + ws.cell(row=row, column=COL_HOURS, value=item["hours_units"]) + ws.cell(row=row, column=COL_QTY, value=item["qty"]) + ws.cell(row=row, column=COL_MONTHS, value=item["months"]) + ws.cell(row=row, column=COL_DISC, value=item["discount"]) + + # Formula: Monthly w/o discount = Price × Hours × Qty + p = get_column_letter(COL_PRICE) + h = get_column_letter(COL_HOURS) + q = get_column_letter(COL_QTY) + d = get_column_letter(COL_DISC) + ws.cell( + row=row, column=COL_M_WO, + value=f"={p}{row}*{h}{row}*{q}{row}", + ) + + # Formula: Monthly w/ discount = Monthly_wo × (1 - Discount) + mwo = get_column_letter(COL_M_WO) + ws.cell( + row=row, column=COL_M_W, + value=f"={mwo}{row}*(1-{d}{row})", + ) + + # Conversion formulas + if conv: + xr = self.conversion.get("exchange_rate", 1) + tr = self.conversion.get("tax_rate", 0) + mw = get_column_letter(COL_M_W) + # Converted w/o tax + ws.cell( + row=row, column=COL_CONV_WO, + value=f"={mw}{row}*{xr}", + ) + # Converted w/ tax + ws.cell( + row=row, column=COL_CONV_W, + value=f"={get_column_letter(COL_CONV_WO)}{row}*(1+{tr})", + ) + + # Styling per row + is_alt = idx % 2 == 1 + for ci in range(1, num_cols + 1): + cell = ws.cell(row=row, column=ci) + cell.font = data_font + cell.border = thin_border + if is_alt: + cell.fill = alt_fill + cell.alignment = Alignment( + vertical="center", + wrap_text=(ci == COL_PRODUCT), + ) + + # Number formats + ws.cell(row=row, column=COL_PRICE).number_format = '$#,##0.0000' + ws.cell(row=row, column=COL_HOURS).number_format = '#,##0' + ws.cell(row=row, column=COL_QTY).number_format = '#,##0.##' + ws.cell(row=row, column=COL_MONTHS).number_format = '#,##0' + ws.cell(row=row, column=COL_DISC).number_format = '0%' + ws.cell(row=row, column=COL_M_WO).number_format = '$#,##0.00' + ws.cell(row=row, column=COL_M_W).number_format = '$#,##0.00' + if conv: + ws.cell(row=row, column=COL_CONV_WO).number_format = '#,##0.00' + ws.cell(row=row, column=COL_CONV_W).number_format = '#,##0.00' + + row += 1 + + # Category subtotal row + cat_last_data = row - 1 + ws.cell(row=row, column=COL_PRODUCT, value=f"Subtotal — {display_name}") + mwo_letter = get_column_letter(COL_M_WO) + mw_letter = get_column_letter(COL_M_W) + ws.cell(row=row, column=COL_M_WO, value=f"=SUM({mwo_letter}{cat_first_data}:{mwo_letter}{cat_last_data})") + ws.cell(row=row, column=COL_M_W, value=f"=SUM({mw_letter}{cat_first_data}:{mw_letter}{cat_last_data})") + ws.cell(row=row, column=COL_M_WO).number_format = '$#,##0.00' + ws.cell(row=row, column=COL_M_W).number_format = '$#,##0.00' + + if conv: + cwo_letter = get_column_letter(COL_CONV_WO) + cw_letter = get_column_letter(COL_CONV_W) + ws.cell(row=row, column=COL_CONV_WO, value=f"=SUM({cwo_letter}{cat_first_data}:{cwo_letter}{cat_last_data})") + ws.cell(row=row, column=COL_CONV_W, value=f"=SUM({cw_letter}{cat_first_data}:{cw_letter}{cat_last_data})") + ws.cell(row=row, column=COL_CONV_WO).number_format = '#,##0.00' + ws.cell(row=row, column=COL_CONV_W).number_format = '#,##0.00' + + for ci in range(1, num_cols + 1): + cell = ws.cell(row=row, column=ci) + cell.font = subtotal_font + cell.border = thin_border + cell.fill = PatternFill(start_color="EDE9E5", end_color="EDE9E5", fill_type="solid") + + cat_subtotal_rows.append(row) + + row += 1 # blank row between categories + + # ── Grand total ─────────────────────────────────────────── + total_row = row + ws.cell(row=row, column=COL_PRODUCT, value="TOTAL").font = total_font + + # Sum of subtotals + mwo_refs = "+".join(f"{get_column_letter(COL_M_WO)}{r}" for r in cat_subtotal_rows) + mw_refs = "+".join(f"{get_column_letter(COL_M_W)}{r}" for r in cat_subtotal_rows) + ws.cell(row=row, column=COL_M_WO, value=f"={mwo_refs}") + ws.cell(row=row, column=COL_M_W, value=f"={mw_refs}") + ws.cell(row=row, column=COL_M_WO).number_format = '$#,##0.00' + ws.cell(row=row, column=COL_M_W).number_format = '$#,##0.00' + + if conv: + cwo_refs = "+".join(f"{get_column_letter(COL_CONV_WO)}{r}" for r in cat_subtotal_rows) + cw_refs = "+".join(f"{get_column_letter(COL_CONV_W)}{r}" for r in cat_subtotal_rows) + ws.cell(row=row, column=COL_CONV_WO, value=f"={cwo_refs}") + ws.cell(row=row, column=COL_CONV_W, value=f"={cw_refs}") + ws.cell(row=row, column=COL_CONV_WO).number_format = '#,##0.00' + ws.cell(row=row, column=COL_CONV_W).number_format = '#,##0.00' + + for ci in range(1, num_cols + 1): + cell = ws.cell(row=row, column=ci) + cell.font = total_font + cell.fill = total_fill + cell.border = thin_border + row += 1 + + # ── ARR (Annual Run Rate) ──────────────────────────────── + arr_row = row + ws.cell(row=row, column=COL_PRODUCT, value="Annual Run Rate (ARR)").font = Font( + name="Segoe UI", size=11, bold=True, color=Colors.COPPER + ) + mw_total = get_column_letter(COL_M_W) + ws.cell(row=row, column=COL_M_W, value=f"={mw_total}{total_row}*12") + ws.cell(row=row, column=COL_M_W).number_format = '$#,##0.00' + ws.cell(row=row, column=COL_M_W).font = Font( + name="Segoe UI", size=11, bold=True, color=Colors.COPPER + ) + if conv: + cw_total = get_column_letter(COL_CONV_W) + ws.cell(row=row, column=COL_CONV_W, value=f"={cw_total}{total_row}*12") + ws.cell(row=row, column=COL_CONV_W).number_format = '#,##0.00' + ws.cell(row=row, column=COL_CONV_W).font = Font( + name="Segoe UI", size=11, bold=True, color=Colors.COPPER + ) + for ci in range(1, num_cols + 1): + ws.cell(row=row, column=ci).border = thin_border + row += 2 + + # ── Cost proportion formulas (back-fill) ───────────────── + # Cost % = line monthly w/ discount / total monthly w/ discount + for item_row in range(data_start_row, total_row): + cell_val = ws.cell(row=item_row, column=COL_M_W).value + if cell_val and str(cell_val).startswith("=") and "SUM" not in str(cell_val): + mw_l = get_column_letter(COL_M_W) + ws.cell( + row=item_row, column=COL_PCT, + value=f"=IF({mw_l}{total_row}=0,0,{mw_l}{item_row}/{mw_l}{total_row})", + ) + ws.cell(row=item_row, column=COL_PCT).number_format = '0.0%' + ws.cell(row=item_row, column=COL_PCT).font = data_font + ws.cell(row=item_row, column=COL_PCT).border = thin_border + + # ── Notes ───────────────────────────────────────────────── + if self.notes: + ws.cell(row=row, column=1, value="Notes:").font = Font( + name="Segoe UI", size=10, bold=True, color=Colors.GRAY_500 + ) + row += 1 + for note in self.notes: + ws.cell(row=row, column=1, value=f" - {note}").font = Font( + name="Segoe UI", size=9, color=Colors.GRAY_500 + ) + row += 1 + row += 1 + + # ── Disclaimer ──────────────────────────────────────────── + disclaimer = self.catalog_meta.get("disclaimer", "") + if disclaimer: + ws.cell(row=row, column=1, value="Disclaimer:").font = Font( + name="Segoe UI", size=8, bold=True, color=Colors.GRAY_500 + ) + row += 1 + ws.merge_cells( + start_row=row, start_column=1, + end_row=row, end_column=num_cols, + ) + cell = ws.cell(row=row, column=1, value=disclaimer.strip()) + cell.font = Font(name="Segoe UI", size=8, color=Colors.GRAY_500) + cell.alignment = Alignment(wrap_text=True, vertical="top") + ws.row_dimensions[row].height = 60 + + # ── Print setup ─────────────────────────────────────────── + ws.sheet_properties.pageSetUpPr = None + ws.page_setup.orientation = "landscape" + ws.page_setup.fitToWidth = 1 + ws.page_setup.fitToHeight = 0 + ws.print_title_rows = f"{header_row}:{header_row}" + + # ── AppCA Export ────────────────────────────────────────────── + + def _build_appca_sheets(self): + """Build the two sheets needed for AppCA import: 'Export to AppCA' and 'BOM.C1'.""" + + # ── Styles ──────────────────────────────────────────────── + thin_border = Border( + left=Side(style="thin", color=Colors.GRAY_300), + right=Side(style="thin", color=Colors.GRAY_300), + top=Side(style="thin", color=Colors.GRAY_300), + bottom=Side(style="thin", color=Colors.GRAY_300), + ) + header_font = Font(name="Segoe UI", size=11, bold=True, color=Colors.WHITE) + header_fill = PatternFill(start_color=Colors.TEAL, end_color=Colors.TEAL, fill_type="solid") + header_align = Alignment(horizontal="center", vertical="center", wrap_text=True) + data_font = Font(name="Segoe UI", size=10, color=Colors.GRAY_700) + cat_font = Font(name="Segoe UI", size=10, bold=True, color=Colors.TEAL) + cat_fill = PatternFill(start_color=Colors.TEAL_LIGHT, end_color=Colors.TEAL_LIGHT, fill_type="solid") + + # ── Sheet: BOM.C1 ───────────────────────────────────────── + ws_bom = self.wb.create_sheet("BOM.C1") + + # Row 1: global parameters + ws_bom.cell(row=1, column=6, value="Hours: ").font = Font(name="Segoe UI", size=10, bold=True) + ws_bom.cell(row=1, column=7, value=730) + ws_bom.cell(row=1, column=8, value="Months: ").font = Font(name="Segoe UI", size=10, bold=True) + months_val = self.line_items[0]["months"] if self.line_items else 12 + ws_bom.cell(row=1, column=9, value=months_val) + + # Row 4: title + ws_bom.cell(row=4, column=4, value="Oracle Cloud Infrastructure - Bill of Material (BOM)").font = Font( + name="Segoe UI", size=12, bold=True, color=Colors.TEAL + ) + + # Row 5: column headers (matching original BOM.C1 layout: C=SKU, D=Product, etc.) + bom_headers = [ + (3, "SKU", 14), (4, "Product", 50), (5, "Metric", 30), + (6, "Price List US$", 14), (7, "Hours/Units", 12), + (8, "Qty", 10), (9, "Months", 10), (10, "Discount", 10), + (11, "US$\n(w/o discount)", 16), (12, "US$\n(w/ discount)", 16), + ] + for col, name, width in bom_headers: + cell = ws_bom.cell(row=5, column=col, value=name) + cell.font = header_font + cell.fill = header_fill + cell.alignment = header_align + cell.border = thin_border + ws_bom.column_dimensions[get_column_letter(col)].width = width + + # Row 6+: data rows grouped by category + row = 6 + items_by_cat = {} + for item in self.line_items: + cat = item["category"] + items_by_cat.setdefault(cat, []).append(item) + + for cat_key in self.category_order: + if cat_key not in items_by_cat: + continue + + display_name = self.category_names.get(cat_key, cat_key) + + # Category header + ws_bom.cell(row=row, column=4, value=display_name).font = cat_font + for ci in range(3, 13): + ws_bom.cell(row=row, column=ci).fill = cat_fill + ws_bom.cell(row=row, column=ci).border = thin_border + row += 1 + + for item in items_by_cat[cat_key]: + ws_bom.cell(row=row, column=3, value=item["sku"]) + ws_bom.cell(row=row, column=4, value=item["product"]) + ws_bom.cell(row=row, column=5, value=item["metric"]) + ws_bom.cell(row=row, column=6, value=item["list_price_usd"]) + ws_bom.cell(row=row, column=7, value=item["hours_units"]) + ws_bom.cell(row=row, column=8, value=item["qty"]) + ws_bom.cell(row=row, column=9, value=item["months"]) + ws_bom.cell(row=row, column=10, value=item["discount"]) + + # Formulas matching BOM.C1 style + ws_bom.cell(row=row, column=11, + value="=(F{r}*G{r}*H{r})".format(r=row)) + ws_bom.cell(row=row, column=12, + value="=IFERROR(K{r}*(1-J{r}),K{r})".format(r=row)) + + # Formats + ws_bom.cell(row=row, column=6).number_format = '#,##0.0000' + ws_bom.cell(row=row, column=10).number_format = '0.0%' + ws_bom.cell(row=row, column=11).number_format = '$#,##0.00' + ws_bom.cell(row=row, column=12).number_format = '$#,##0.00' + + for ci in range(3, 13): + cell = ws_bom.cell(row=row, column=ci) + cell.font = data_font + cell.border = thin_border + row += 1 + + # ── Sheet: Export to AppCA ───────────────────────────────── + ws_appca = self.wb.create_sheet("Export to AppCA", 0) # first sheet + + # Title + ws_appca.cell(row=1, column=1, value="TEMPLATE BOM - APPCA").font = Font( + name="Segoe UI", size=14, bold=True, color=Colors.TEAL + ) + ws_appca.merge_cells("A1:G1") + + # Headers — AppCA expects: SKU, QTY, HOURS, MONTHS, DISCOUNT, BURSTABLE, STATUS + appca_headers = ["SKU", "QTY", "HOURS", "MONTHS", "DISCOUNT", "BURSTABLE", "STATUS"] + appca_widths = [14, 10, 12, 10, 12, 10, 12] + for ci, (name, width) in enumerate(zip(appca_headers, appca_widths), 1): + cell = ws_appca.cell(row=2, column=ci, value=name) + cell.font = header_font + cell.fill = header_fill + cell.alignment = header_align + cell.border = thin_border + ws_appca.column_dimensions[get_column_letter(ci)].width = width + + # Data rows: only items with qty > 0 + appca_row = 3 + for item in self.line_items: + if item["qty"] > 0: + ws_appca.cell(row=appca_row, column=1, value=item["sku"]) + ws_appca.cell(row=appca_row, column=2, value=item["qty"]) + ws_appca.cell(row=appca_row, column=3, value=item["hours_units"]) + ws_appca.cell(row=appca_row, column=4, value=item["months"]) + ws_appca.cell(row=appca_row, column=5, value=item["discount"]) + ws_appca.cell(row=appca_row, column=6, value="No") # BURSTABLE: No unless specified + ws_appca.cell(row=appca_row, column=7, value="Yes") # STATUS: required by AppCA + + ws_appca.cell(row=appca_row, column=5).number_format = '0.0%' + + for ci in range(1, 8): + cell = ws_appca.cell(row=appca_row, column=ci) + cell.font = data_font + cell.border = thin_border + appca_row += 1 + + # ── Public API ──────────────────────────────────────────────── + + def save(self, filepath: str, appca: bool = False): + """Build and save the workbook to an .xlsx file.""" + if appca: + # Remove default empty sheet, build AppCA sheets + default_sheet = self.wb.active + self._build_appca_sheets() + self.wb.remove(default_sheet) + else: + ws = self.wb.active + self._build_sheet(ws) + self.wb.save(filepath) + print(" BOM saved: {}".format(filepath)) + + @classmethod + def from_spec(cls, spec: dict, catalog_path: Optional[str] = None) -> "OCIBomGenerator": + """Build BOM from a YAML specification dict.""" + bom = spec.get("bom", spec) + + gen = cls( + customer=bom.get("customer_name", ""), + project=bom.get("project_name", ""), + prepared_by=bom.get("prepared_by", ""), + bom_date=bom.get("date", ""), + reference_label=bom.get("reference_label", ""), + currency=bom.get("currency", "USD"), + realm=bom.get("realm", ""), + service_type=bom.get("service_type", ""), + conversion=bom.get("conversion"), + notes=bom.get("notes"), + ) + + gen.load_catalog(catalog_path) + + for item in bom.get("line_items", []): + gen.add_line_item( + sku=str(item["sku"]), + qty=item.get("qty", 0), + hours_units=item.get("hours_units"), + months=item.get("months", 12), + discount=item.get("discount", 0.0), + custom_label=item.get("custom_label", ""), + custom_note=item.get("custom_note", ""), + ) + + return gen + + +def main(): + parser = argparse.ArgumentParser( + description="OCI Deal Accelerator — BOM Generator (.xlsx)" + ) + parser.add_argument( + "--spec", required=True, + help="Path to BOM spec YAML file", + ) + parser.add_argument( + "--output", default="bom.xlsx", + help="Output .xlsx file path (default: bom.xlsx)", + ) + parser.add_argument( + "--catalog", default=None, + help="Override path to SKU catalog YAML (default: auto-discover)", + ) + parser.add_argument( + "--appca", action="store_true", + help="Generate AppCA import format (2 sheets: 'Export to AppCA' + 'BOM.C1')", + ) + args = parser.parse_args() + + with open(args.spec, "r") as f: + spec = yaml.safe_load(f) + + gen = OCIBomGenerator.from_spec(spec, catalog_path=args.catalog) + gen.save(args.output, appca=args.appca) + + +if __name__ == "__main__": + main() diff --git a/tools/oci_output.py b/tools/oci_output.py index a421141..1c6501c 100644 --- a/tools/oci_output.py +++ b/tools/oci_output.py @@ -27,6 +27,7 @@ FORMATS = { "full": ["pptx", "drawio", "docx", "xlsx", "pdf"], "doc": ["docx"], "pdf": ["pdf"], + "bom": ["bom"], } GENERATORS = { @@ -62,6 +63,13 @@ GENERATORS = { "method": "from_spec", "description": "Customer-facing PDF (.pdf)", }, + "bom": { + "module": "oci_bom_gen", + "class": "OCIBomGenerator", + "method": "from_spec", + "description": "Bill of Materials (.xlsx)", + "optional": True, + }, } diff --git a/tools/refresh_sku_catalog.py b/tools/refresh_sku_catalog.py new file mode 100644 index 0000000..6667500 --- /dev/null +++ b/tools/refresh_sku_catalog.py @@ -0,0 +1,335 @@ +#!/usr/bin/env python3 +""" +OCI Deal Accelerator — SKU Catalog Refresh Tool + +Fetches current OCI SKU pricing from Oracle's public pricing API and +updates kb/pricing/oci-sku-catalog.yaml with the latest prices. + +API endpoint (anonymous, no auth required): + https://itra.oraclecloud.com/itas/.anon/myservices/api/v1/products + +Usage: + # Refresh full catalog from API + python tools/refresh_sku_catalog.py --refresh + + # Refresh and show what changed + python tools/refresh_sku_catalog.py --refresh --diff + + # Fetch a specific SKU to inspect + python tools/refresh_sku_catalog.py --sku B110627 + + # Export raw API dump to JSON (for debugging) + python tools/refresh_sku_catalog.py --dump oci-skus-raw.json + + # Validate current catalog against API (report stale prices) + python tools/refresh_sku_catalog.py --validate +""" + +import argparse +import json +import os +import sys +import time +from datetime import date +from pathlib import Path + +try: + import requests +except ImportError: + print("Error: 'requests' package required. Install with: pip install requests") + sys.exit(1) + +import yaml + +# ── API Configuration ───────────────────────────────────────────── +# Public Oracle pricing API (no authentication required) +# Documented: https://docs.oracle.com/en-us/iaas/Content/GSG/Tasks/signingup_topic-Estimating_Costs.htm +API_BASE = "https://apexapps.oracle.com/pls/apex/cetools/api/v1/products/" +DEFAULT_CURRENCY = "USD" + +# ── Paths ───────────────────────────────────────────────────────── +TOOLS_DIR = Path(__file__).resolve().parent +CATALOG_PATH = TOOLS_DIR.parent / "kb" / "pricing" / "oci-sku-catalog.yaml" + + +def fetch_all_products(currency="USD", verbose=False): + """Fetch all OCI products from the APEX pricing API.""" + url = "{}?currencyCode={}".format(API_BASE, currency) + if verbose: + print(" Fetching: {}".format(url)) + + resp = requests.get(url, timeout=60) + resp.raise_for_status() + data = resp.json() + + items = data.get("items", []) + if verbose: + print(" Fetched {} products".format(len(items))) + last_updated = data.get("lastUpdated", "unknown") + print(" API last updated: {}".format(last_updated)) + return items + + +def fetch_single_sku(part_number, currency="USD"): + """Fetch a single SKU by part number.""" + url = "{}?partNumber={}¤cyCode={}".format(API_BASE, part_number, currency) + resp = requests.get(url, timeout=15) + resp.raise_for_status() + data = resp.json() + return data.get("items", []) + + +def extract_payg_price(product, currency="USD"): + """Extract the Pay As You Go price from a product's prices structure. + + APEX API format: + currencyCodeLocalizations: [ + { currencyCode: "USD", prices: [ {model: "PAY_AS_YOU_GO", value: 2.9032} ] } + ] + """ + localizations = product.get("currencyCodeLocalizations", []) + for loc in localizations: + if loc.get("currencyCode") == currency: + for price in loc.get("prices", []): + if price.get("model") == "PAY_AS_YOU_GO": + return price.get("value", 0) + # Fallback: take first price in this currency + prices = loc.get("prices", []) + if prices: + return prices[0].get("value", 0) + + # Legacy format fallback (itra API) + for price in product.get("prices", []): + if price.get("model") == "PAY_AS_YOU_GO": + return price.get("value", 0) + + return 0 + + +def load_current_catalog(): + """Load the current YAML catalog and build a SKU lookup.""" + if not CATALOG_PATH.exists(): + return {}, {} + + with open(str(CATALOG_PATH), "r") as f: + raw = yaml.safe_load(f) + + sku_map = {} # sku -> {product, category, ...} + categories = raw.get("categories", {}) + for cat_key, cat_data in categories.items(): + for entry in cat_data.get("skus", []): + sku_map[str(entry["sku"])] = { + "category": cat_key, + "product": entry.get("product", ""), + "list_price_usd": entry.get("list_price_usd", 0), + } + + return raw, sku_map + + +def refresh_catalog(verbose=False): + """Refresh the catalog YAML with latest API prices.""" + print("Fetching current prices from Oracle API...") + products = fetch_all_products(currency=DEFAULT_CURRENCY, verbose=verbose) + + # Build API lookup: partNumber -> product data + api_map = {} + for p in products: + pn = p.get("partNumber", "") + if pn: + api_map[pn] = p + + print(" {} products from API".format(len(api_map))) + + # Load current catalog + raw, sku_map = load_current_catalog() + if not raw: + print("Error: Could not load catalog at {}".format(CATALOG_PATH)) + return + + updated = 0 + not_found = 0 + unchanged = 0 + + categories = raw.get("categories", {}) + for cat_key, cat_data in categories.items(): + for entry in cat_data.get("skus", []): + sku = str(entry["sku"]) + if sku in api_map: + api_price = extract_payg_price(api_map[sku]) + old_price = entry.get("list_price_usd", 0) + + # Skip $0 API prices when catalog has a real price + # (API returns $0 for tiered/free-tier SKUs where + # the base tier is free but usage above free tier costs) + if api_price == 0 and old_price > 0: + if verbose: + print(" {} SKIP: API=$0 but catalog=${} (tiered/free-tier SKU)".format( + sku, old_price + )) + unchanged += 1 + elif abs(api_price - old_price) > 0.0001: + if verbose: + print(" {} price: {} -> {} ({})".format( + sku, old_price, api_price, entry.get("product", "")[:50] + )) + entry["list_price_usd"] = api_price + updated += 1 + else: + unchanged += 1 + + # Also update product name if different + api_name = api_map[sku].get("displayName", "") + if api_name and api_name != entry.get("product", ""): + # Keep our shorter names, but log the difference + pass + + # Update metric from API + api_metric = api_map[sku].get("metricDisplayName", "") + if api_metric and api_metric != entry.get("metric", ""): + pass # Keep our curated metrics + else: + not_found += 1 + if verbose: + print(" {} NOT in API (may be retired): {}".format( + sku, entry.get("product", "")[:50] + )) + + # Update last_verified date + raw["last_verified"] = date.today().isoformat() + + # Write back + with open(str(CATALOG_PATH), "w") as f: + yaml.dump(raw, f, default_flow_style=False, allow_unicode=True, sort_keys=False, width=120) + + print("\nRefresh complete:") + print(" Updated: {}".format(updated)) + print(" Unchanged: {}".format(unchanged)) + print(" Not in API (possibly retired): {}".format(not_found)) + print(" Catalog saved: {}".format(CATALOG_PATH)) + + +def validate_catalog(verbose=False): + """Compare current catalog prices against API and report differences.""" + print("Validating catalog against Oracle API...") + products = fetch_all_products(currency=DEFAULT_CURRENCY, verbose=verbose) + + api_map = {} + for p in products: + pn = p.get("partNumber", "") + if pn: + api_map[pn] = p + + raw, sku_map = load_current_catalog() + if not raw: + print("Error: Could not load catalog") + return + + diffs = [] + missing = [] + + categories = raw.get("categories", {}) + for cat_key, cat_data in categories.items(): + for entry in cat_data.get("skus", []): + sku = str(entry["sku"]) + if sku in api_map: + api_price = extract_payg_price(api_map[sku]) + cat_price = entry.get("list_price_usd", 0) + # Skip $0 API prices for tiered/free-tier SKUs + if api_price == 0 and cat_price > 0: + continue + if abs(api_price - cat_price) > 0.0001: + diffs.append({ + "sku": sku, + "product": entry.get("product", "")[:60], + "catalog_price": cat_price, + "api_price": api_price, + "diff_pct": ((api_price - cat_price) / cat_price * 100) if cat_price else 0, + }) + else: + missing.append(sku) + + if diffs: + print("\nPrice differences found ({}):\n".format(len(diffs))) + print("{:<10} {:<60} {:>12} {:>12} {:>8}".format( + "SKU", "Product", "Catalog", "API", "Diff %" + )) + print("-" * 102) + for d in diffs: + print("{:<10} {:<60} ${:>10.4f} ${:>10.4f} {:>7.1f}%".format( + d["sku"], d["product"], d["catalog_price"], d["api_price"], d["diff_pct"] + )) + print("\nRun with --refresh to update the catalog.") + else: + print("\nAll prices match the API. Catalog is up to date.") + + if missing: + print("\n{} SKUs not found in API (possibly retired):".format(len(missing))) + for s in missing[:10]: + print(" - {}".format(s)) + if len(missing) > 10: + print(" ... and {} more".format(len(missing) - 10)) + + +def inspect_sku(part_number): + """Fetch and display a single SKU from the API.""" + print("Fetching SKU {} from API...".format(part_number)) + items = fetch_single_sku(part_number) + if not items: + print(" SKU not found in API") + return + + for item in items: + print("\n Part Number: {}".format(item.get("partNumber", ""))) + print(" Display Name: {}".format(item.get("displayName", ""))) + print(" Category: {}".format(item.get("serviceCategory", "N/A"))) + print(" Metric: {}".format(item.get("metricName", "N/A"))) + print(" Prices:") + for loc in item.get("currencyCodeLocalizations", []): + cc = loc.get("currencyCode", "?") + for price in loc.get("prices", []): + print(" - {} {} : {}".format( + cc, price.get("model", "?"), price.get("value", "?") + )) + + +def dump_all(output_path, verbose=False): + """Dump raw API response to JSON file.""" + products = fetch_all_products(currency=DEFAULT_CURRENCY, verbose=verbose) + with open(output_path, "w") as f: + json.dump(products, f, indent=2) + print("Dumped {} products to {}".format(len(products), output_path)) + + +def main(): + parser = argparse.ArgumentParser( + description="OCI Deal Accelerator — SKU Catalog Refresh Tool" + ) + parser.add_argument("--refresh", action="store_true", + help="Refresh catalog prices from Oracle API") + parser.add_argument("--validate", action="store_true", + help="Validate catalog prices against API") + parser.add_argument("--sku", type=str, + help="Inspect a single SKU from the API") + parser.add_argument("--dump", type=str, metavar="FILE", + help="Dump raw API data to JSON file") + parser.add_argument("--diff", action="store_true", + help="Show detailed price changes during refresh") + parser.add_argument("-v", "--verbose", action="store_true", + help="Verbose output") + args = parser.parse_args() + + if args.sku: + inspect_sku(args.sku) + elif args.refresh: + refresh_catalog(verbose=args.verbose or args.diff) + elif args.validate: + validate_catalog(verbose=args.verbose) + elif args.dump: + dump_all(args.dump, verbose=args.verbose) + else: + parser.print_help() + + +if __name__ == "__main__": + main()