diff --git a/internal/purchase/execution.go b/internal/purchase/execution.go index 0eefefcee..655800e9f 100644 --- a/internal/purchase/execution.go +++ b/internal/purchase/execution.go @@ -1027,7 +1027,7 @@ func (m *Manager) executeSinglePurchase(ctx context.Context, rec config.Recommen // canonical hyphenated slugs (compute, relational-db, cache, search, // data-warehouse) and the legacy AWS-only slugs (ec2, rds, elasticache, // opensearch, redshift, memorydb) are recognized; everything else passes -// through verbatim. Savings Plans slugs are normalised by mapSavingsPlansSlug. +// through verbatim. Savings Plans slugs are normalized by mapSavingsPlansSlug. func (m *Manager) mapServiceType(service string) common.ServiceType { if svc, ok := mapSavingsPlansSlug(service); ok { return svc @@ -1077,20 +1077,28 @@ func mapServiceSlug(service string) (common.ServiceType, bool) { return svc, ok } -// mapSavingsPlansSlug normalises both the canonical hyphenated SP slugs and +// mapSavingsPlansSlug normalizes both the canonical hyphenated SP slugs and // the dash-free spellings the frontend has historically sent into the // matching common.ServiceType. The map covers the legacy umbrella plus the // four per-plan-type slugs in both spellings — pulled out of mapServiceType // to keep that switch under the gocyclo budget. // -// TODO(#85): once purchase_executions JSONB rows persisted before the -// "savingsplans" rename (~6-month retention window) have aged out, the -// "savings-plans"-spelled aliases below can be removed and only the -// dash-free spellings ("savingsplans", "savingsplans-compute", etc.) -// need be matched here. The umbrella rename happened in PR #94; the -// per-plan-type slugs were always dash-form on the wire so their -// "savingsplans-*" aliases are forward-compat for any future -// frontend-canonical normalisation. +// TODO(#95): PR #94 (merged 2026-04-30) renamed "savings-plans" -> +// "savingsplans". purchase_executions rows have a ~6-month retention window, +// so the earliest safe drop date is 2026-10-30. Until then the +// "savings-plans"-spelled aliases below must stay so Lambda-scheduled +// executions persisted before PR #94 still map correctly on retry/approval. +// To drop: verify zero rows with +// +// SELECT id FROM purchase_executions +// WHERE recommendations::text LIKE '%"service":"savings-plans"%' +// LIMIT 1; +// +// then remove every "savings-plans*" key from this map and the matching +// case in pkg/common/service_details_codec.go: newDetailsForService, and +// flip the coverage_extra_test.go case (line ~60) to assert +// ServiceType("savings-plans") (default-arm pass-through). The +// "savingsplans-*" keys are forward-compat and should stay. func mapSavingsPlansSlug(service string) (common.ServiceType, bool) { slugs := map[string]common.ServiceType{ "savings-plans": common.ServiceSavingsPlans, diff --git a/pkg/common/service_details_codec.go b/pkg/common/service_details_codec.go index 75a43f75e..c37268918 100644 --- a/pkg/common/service_details_codec.go +++ b/pkg/common/service_details_codec.go @@ -60,7 +60,7 @@ func MarshalServiceDetails(details ServiceDetails) (json.RawMessage, error) { // "savingsplans", "savings-plans-compute" and the dash-free spellings — // see internal/purchase/execution.go: mapServiceType / mapSavingsPlansSlug). // -// Behaviour: +// Behavior: // - raw is empty AND service maps to a known *Details type → returns a // zero-valued typed pointer (the legacy / pre-#453 fallback). The // downstream service client's buildOfferingFilters tolerates zero- @@ -79,7 +79,7 @@ func DecodeServiceDetailsFor(service string, raw json.RawMessage) (ServiceDetail if !ok { // Service has no *Details type — nothing to decode. Empty raw // payload is fine; a non-empty payload on a service we don't - // recognise is suspicious but tolerated (writers and readers + // recognize is suspicious but tolerated (writers and readers // might be on different versions during a rolling deploy). return nil, nil } @@ -119,15 +119,16 @@ func newDetailsForService(service string) (ServiceDetails, bool) { case string(ServiceElastiCache), string(ServiceCache): return &CacheDetails{}, true - // AWS Savings Plans — all umbrella + per-plan-type slugs use + // AWS Savings Plans -- all umbrella + per-plan-type slugs use // SavingsPlanDetails. The dash-free spellings are the canonical // values of ServiceSavingsPlans / ServiceSavingsPlansCompute etc.; // "savings-plans" (with a dash) is the legacy umbrella alias that // internal/purchase/execution.go: mapSavingsPlansSlug still - // recognises so purchase_executions JSONB rows persisted before - // the rename in PR #94 still resolve. Recognising it here means a - // legacy direct-execute approval still decodes against the right - // type. + // recognizes so purchase_executions JSONB rows persisted before + // the rename in PR #94 (merged 2026-04-30) still resolve. + // TODO(#95): drop "savings-plans" here once the ~6-month retention + // window has passed (earliest 2026-10-30). See execution.go + // mapSavingsPlansSlug for the full removal checklist. case string(ServiceSavingsPlans), string(ServiceSavingsPlansCompute), string(ServiceSavingsPlansEC2Instance), @@ -138,7 +139,7 @@ func newDetailsForService(service string) (ServiceDetails, bool) { // Services that don't currently type-assert Details in // findOfferingID (OpenSearch / Redshift / MemoryDB read rec.ResourceType - // directly). We still recognise OpenSearch and Redshift here so a + // directly). We still recognize OpenSearch and Redshift here so a // forthcoming refactor that starts asserting can flip the table // value without changing call sites. MemoryDB has no dedicated // *Details type yet, so it's intentionally absent — callers see