Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 18 additions & 10 deletions internal/purchase/execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
func mapSavingsPlansSlug(service string) (common.ServiceType, bool) {
slugs := map[string]common.ServiceType{
"savings-plans": common.ServiceSavingsPlans,
Expand Down
17 changes: 9 additions & 8 deletions pkg/common/service_details_codec.go
Original file line number Diff line number Diff line change
Expand Up @@ -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-
Expand All @@ -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
}
Expand Down Expand Up @@ -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),
Expand All @@ -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
Expand Down
Loading