Documentation
¶
Overview ¶
Package config provides configuration management functionality.
Package config provides configuration management for CUDly.
Package config provides configuration management using PostgreSQL.
Index ¶
- Constants
- Variables
- func AccountConfigKey(accountID, provider, service string) string
- func DerivePlanProviders(plan *PurchasePlan) []string
- func GetAllCategories() []string
- func GetDefaultValue(key string) any
- func NormalizePaymentOption(provider, raw string) (string, bool)
- func PaymentCoercionChangesSchedule(raw, canonical string) bool
- func ResolveAccountConfigsForRecs(ctx context.Context, store AccountConfigReader, recs []RecommendationRecord) (map[string]*ServiceConfig, error)
- func RevocationWindowClosesAtFor(provider string, purchaseTime time.Time) *time.Time
- func ValidatePaymentOptionEnv(val string) error
- func ValidateRampScheduleEnv(val string) error
- type AccountConfigReader
- type AccountRegistration
- type AccountRegistrationFilter
- type AccountServiceOverride
- type CloudAccount
- type CloudAccountFilter
- type ConfigSetting
- type ExecutionStatusCounts
- type GlobalConfig
- type LadderConfigDB
- type LadderRunDB
- type LadderTrancheDB
- type PaymentSchedule
- type PostgresStore
- func (s *PostgresStore) CancelAllPendingExchanges(ctx context.Context) (int64, error)
- func (s *PostgresStore) CancelExecutionAtomic(ctx context.Context, tx pgx.Tx, executionID string, cancelledBy *string) (canceled bool, currentStatus string, err error)
- func (s *PostgresStore) CancelPendingExchangesByOrigin(ctx context.Context, origin common.ExchangeOrigin) (int64, error)
- func (s *PostgresStore) CancelScheduledExecutionAtomic(ctx context.Context, tx pgx.Tx, executionID string, cancelledBy *string) (canceled bool, currentStatus string, err error)
- func (s *PostgresStore) ClaimMarketplaceListingSlot(ctx context.Context, purchaseID string) (bool, error)
- func (s *PostgresStore) ClaimRIExchangeIdempotencyKey(ctx context.Context, key string, window time.Duration) (bool, error)
- func (s *PostgresStore) CleanupOldExecutions(ctx context.Context, retentionDays int) (int64, error)
- func (s *PostgresStore) ClearCollectionStarted(ctx context.Context, token string) error
- func (s *PostgresStore) ClearRevocationInFlight(ctx context.Context, purchaseID string) error
- func (s *PostgresStore) CompletePlanStep(ctx context.Context, planID string, stepNumber int) error
- func (s *PostgresStore) CompleteRIExchange(ctx context.Context, id, exchangeID string) error
- func (s *PostgresStore) CompleteRIExchangeWithPayment(ctx context.Context, id, exchangeID, acceptedPaymentDue string) error
- func (s *PostgresStore) CountExecutionsByPlanAndStatus(ctx context.Context, statuses []string, since time.Time) (map[string]ExecutionStatusCounts, error)
- func (s *PostgresStore) CountPendingExecutionsForAccount(ctx context.Context, accountID string) (int, error)
- func (s *PostgresStore) CreateAccountRegistration(ctx context.Context, reg *AccountRegistration) error
- func (s *PostgresStore) CreateCloudAccount(ctx context.Context, account *CloudAccount) error
- func (s *PostgresStore) CreatePurchasePlan(ctx context.Context, plan *PurchasePlan) error
- func (s *PostgresStore) CreateSuppression(ctx context.Context, sup *PurchaseSuppression) error
- func (s *PostgresStore) CreateSuppressionTx(ctx context.Context, tx pgx.Tx, sup *PurchaseSuppression) error
- func (s *PostgresStore) DeleteAccountCredentials(ctx context.Context, accountID string) error
- func (s *PostgresStore) DeleteAccountRegistration(ctx context.Context, id string) error
- func (s *PostgresStore) DeleteAccountServiceOverride(ctx context.Context, accountID, provider, service string) error
- func (s *PostgresStore) DeleteCloudAccount(ctx context.Context, id string) error
- func (s *PostgresStore) DeletePurchasePlan(ctx context.Context, planID string) error
- func (s *PostgresStore) DeleteSuppressionsByExecution(ctx context.Context, executionID string) error
- func (s *PostgresStore) DeleteSuppressionsByExecutionTx(ctx context.Context, tx pgx.Tx, executionID string) error
- func (s *PostgresStore) FailRIExchange(ctx context.Context, id, errorMsg string) error
- func (s *PostgresStore) FlipPurchaseRevocationInFlight(ctx context.Context, purchaseID string) error
- func (s *PostgresStore) GetAccountCredential(ctx context.Context, accountID, credentialType string) (string, error)
- func (s *PostgresStore) GetAccountRegistration(ctx context.Context, id string) (*AccountRegistration, error)
- func (s *PostgresStore) GetAccountRegistrationByToken(ctx context.Context, token string) (*AccountRegistration, error)
- func (s *PostgresStore) GetAccountServiceOverride(ctx context.Context, accountID, provider, service string) (*AccountServiceOverride, error)
- func (s *PostgresStore) GetActivePurchaseHistory(ctx context.Context, asOf time.Time, accountIDs []string, ...) ([]PurchaseHistoryRecord, error)
- func (s *PostgresStore) GetAllPurchaseHistory(ctx context.Context, limit int) ([]PurchaseHistoryRecord, error)
- func (s *PostgresStore) GetCloudAccount(ctx context.Context, id string) (*CloudAccount, error)
- func (s *PostgresStore) GetCloudAccountByExternalID(ctx context.Context, provider, externalID string) (*CloudAccount, error)
- func (s *PostgresStore) GetExecutionByID(ctx context.Context, executionID string) (*PurchaseExecution, error)
- func (s *PostgresStore) GetExecutionByPlanAndDate(ctx context.Context, planID string, scheduledDate time.Time) (*PurchaseExecution, error)
- func (s *PostgresStore) GetExecutionsByStatuses(ctx context.Context, statuses []string, limit int) ([]PurchaseExecution, error)
- func (s *PostgresStore) GetGlobalConfig(ctx context.Context) (*GlobalConfig, error)
- func (s *PostgresStore) GetInFlightLadderCommitUSDHr(ctx context.Context, configID string) (*float64, error)
- func (s *PostgresStore) GetLadderConfig(ctx context.Context, cloudAccountID, provider string) (*LadderConfigDB, error)
- func (s *PostgresStore) GetLadderConfigs(ctx context.Context) ([]LadderConfigDB, error)
- func (s *PostgresStore) GetLadderRun(ctx context.Context, id string) (*LadderRunDB, error)
- func (s *PostgresStore) GetPendingExecutions(ctx context.Context) ([]PurchaseExecution, error)
- func (s *PostgresStore) GetPendingExecutionsTx(ctx context.Context, tx pgx.Tx) ([]PurchaseExecution, error)
- func (s *PostgresStore) GetPlanAccounts(ctx context.Context, planID string) ([]CloudAccount, error)
- func (s *PostgresStore) GetPlannedExecutions(ctx context.Context, statuses []string, limit int) ([]PurchaseExecution, error)
- func (s *PostgresStore) GetPurchaseHistory(ctx context.Context, accountID string, limit int) ([]PurchaseHistoryRecord, error)
- func (s *PostgresStore) GetPurchaseHistoryByPurchaseID(ctx context.Context, purchaseID string) (*PurchaseHistoryRecord, error)
- func (s *PostgresStore) GetPurchaseHistoryFiltered(ctx context.Context, filter PurchaseHistoryFilter) ([]PurchaseHistoryRecord, error)
- func (s *PostgresStore) GetPurchaseHistoryInFlight(ctx context.Context) ([]*PurchaseHistoryRecord, error)
- func (s *PostgresStore) GetPurchasePlan(ctx context.Context, planID string) (*PurchasePlan, error)
- func (s *PostgresStore) GetRIExchangeDailySpend(ctx context.Context, date time.Time) (string, error)
- func (s *PostgresStore) GetRIExchangeHistory(ctx context.Context, since time.Time, limit int) ([]RIExchangeRecord, error)
- func (s *PostgresStore) GetRIExchangeRecord(ctx context.Context, id string) (*RIExchangeRecord, error)
- func (s *PostgresStore) GetRIExchangeRecordByToken(ctx context.Context, token string) (*RIExchangeRecord, error)
- func (s *PostgresStore) GetRIUtilizationCache(ctx context.Context, region string, lookbackDays int) (*RIUtilizationCacheEntry, error)
- func (s *PostgresStore) GetRecommendationsFreshness(ctx context.Context) (*RecommendationsFreshness, error)
- func (s *PostgresStore) GetScheduledExecutionsDue(ctx context.Context) ([]PurchaseExecution, error)
- func (s *PostgresStore) GetServiceConfig(ctx context.Context, provider, service string) (*ServiceConfig, error)
- func (s *PostgresStore) GetStaleApprovedExecutions(ctx context.Context, olderThan time.Duration) ([]PurchaseExecution, error)
- func (s *PostgresStore) GetStaleProcessingExchanges(ctx context.Context, olderThan time.Duration) ([]RIExchangeRecord, error)
- func (s *PostgresStore) GetStuckRampSteps(ctx context.Context) (map[string]RampStepBlock, error)
- func (s *PostgresStore) GetUserEmailByID(ctx context.Context, userID string) (string, error)
- func (s *PostgresStore) HasAccountCredentials(ctx context.Context, accountID string) (bool, error)
- func (s *PostgresStore) IsNotificationMuted(ctx context.Context, recipientEmail, scope string) (bool, error)
- func (s *PostgresStore) LatestLadderRunStartedAt(ctx context.Context, configID string) (*time.Time, error)
- func (s *PostgresStore) ListAccountRegistrations(ctx context.Context, filter AccountRegistrationFilter) ([]AccountRegistration, error)
- func (s *PostgresStore) ListAccountServiceOverrides(ctx context.Context, accountID string) ([]AccountServiceOverride, error)
- func (s *PostgresStore) ListActiveSuppressions(ctx context.Context) ([]PurchaseSuppression, error)
- func (s *PostgresStore) ListCloudAccounts(ctx context.Context, filter CloudAccountFilter) ([]CloudAccount, error)
- func (s *PostgresStore) ListPendingExecutionIDsForAccount(ctx context.Context, accountID string) ([]string, error)
- func (s *PostgresStore) ListPurchasePlans(ctx context.Context, filter PurchasePlanFilter) ([]PurchasePlan, error)
- func (s *PostgresStore) ListServiceConfigs(ctx context.Context) ([]ServiceConfig, error)
- func (s *PostgresStore) ListStoredRecommendations(ctx context.Context, filter RecommendationFilter) ([]RecommendationRecord, error)
- func (s *PostgresStore) ListStuckExecutions(ctx context.Context, statuses []string, olderThan time.Duration) ([]PurchaseExecution, error)
- func (s *PostgresStore) LockPurchasePlanTx(ctx context.Context, tx pgx.Tx, planID string) (*PurchasePlan, error)
- func (s *PostgresStore) MarkCollectionStarted(ctx context.Context) (token string, ok bool, err error)
- func (s *PostgresStore) MarkPurchaseRevoked(ctx context.Context, purchaseID string, revokedAt time.Time, ...) error
- func (s *PostgresStore) OccupiedRampStepsInRangeTx(ctx context.Context, tx pgx.Tx, planID string, from, to int) ([]int, error)
- func (s *PostgresStore) ReplaceRecommendations(ctx context.Context, collectedAt time.Time, recs []RecommendationRecord) error
- func (s *PostgresStore) SaveAccountCredential(ctx context.Context, accountID, credentialType, encryptedBlob string) error
- func (s *PostgresStore) SaveAccountServiceOverride(ctx context.Context, o *AccountServiceOverride) error
- func (s *PostgresStore) SaveGlobalConfig(ctx context.Context, config *GlobalConfig) error
- func (s *PostgresStore) SaveLadderRun(ctx context.Context, run *LadderRunDB) (*LadderRunDB, error)
- func (s *PostgresStore) SaveLadderRunWithTranches(ctx context.Context, run *LadderRunDB, tranches []LadderTrancheDB) (*LadderRunDB, error)
- func (s *PostgresStore) SaveLadderTranches(ctx context.Context, tranches []LadderTrancheDB) error
- func (s *PostgresStore) SavePurchaseExecution(ctx context.Context, execution *PurchaseExecution) error
- func (s *PostgresStore) SavePurchaseExecutionTx(ctx context.Context, tx pgx.Tx, execution *PurchaseExecution) error
- func (s *PostgresStore) SavePurchaseHistory(ctx context.Context, record *PurchaseHistoryRecord) error
- func (s *PostgresStore) SaveRIExchangeRecord(ctx context.Context, record *RIExchangeRecord) error
- func (s *PostgresStore) SaveServiceConfig(ctx context.Context, config *ServiceConfig) error
- func (s *PostgresStore) SetCancelledBy(ctx context.Context, executionID, cancelledBy string) error
- func (s *PostgresStore) SetPlanAccounts(ctx context.Context, planID string, accountIDs []string) error
- func (s *PostgresStore) SetRecommendationsCollectionError(ctx context.Context, errMsg string) error
- func (s *PostgresStore) StampOfferingClass(ctx context.Context, purchaseID, offeringClass string) error
- func (s *PostgresStore) StampRIExchangeApprovedBy(ctx context.Context, id, approverEmail string) error
- func (s *PostgresStore) TransitionExecutionStatus(ctx context.Context, executionID string, fromStatuses []string, ...) (*PurchaseExecution, error)
- func (s *PostgresStore) TransitionLadderRunStatus(ctx context.Context, id string, fromStatuses []ladder.RunStatus, ...) (*LadderRunDB, error)
- func (s *PostgresStore) TransitionRIExchangeStatus(ctx context.Context, id, fromStatus, toStatus string, actor *string) (*RIExchangeRecord, error)
- func (s *PostgresStore) TransitionRegistrationStatus(ctx context.Context, reg *AccountRegistration, fromStatus string, ...) error
- func (s *PostgresStore) UpdateAccountRegistration(ctx context.Context, reg *AccountRegistration) error
- func (s *PostgresStore) UpdateCloudAccount(ctx context.Context, account *CloudAccount) error
- func (s *PostgresStore) UpdateGlobalConfigAtomic(ctx context.Context, apply func(*GlobalConfig) error) (*GlobalConfig, error)
- func (s *PostgresStore) UpdatePurchaseHistoryListing(ctx context.Context, purchaseID, listingID, listingState string) error
- func (s *PostgresStore) UpdatePurchasePlan(ctx context.Context, plan *PurchasePlan) error
- func (s *PostgresStore) UpdatePurchasePlanTx(ctx context.Context, tx pgx.Tx, plan *PurchasePlan) error
- func (s *PostgresStore) UpsertLadderConfig(ctx context.Context, cfg *LadderConfigDB) (*LadderConfigDB, error)
- func (s *PostgresStore) UpsertNotificationMute(ctx context.Context, recipientEmail, scope, unmuteToken string) error
- func (s *PostgresStore) UpsertRIUtilizationCache(ctx context.Context, region string, lookbackDays int, payload []byte, ...) error
- func (s *PostgresStore) UpsertRecommendations(ctx context.Context, collectedAt time.Time, recs []RecommendationRecord, ...) error
- func (s *PostgresStore) WithTx(ctx context.Context, fn func(tx pgx.Tx) error) error
- type PurchaseExecution
- type PurchaseHistoryFilter
- type PurchaseHistoryRecord
- type PurchasePlan
- type PurchasePlanFilter
- type PurchaseSuppression
- type RIExchangeRecord
- type RIUtilizationCacheEntry
- type RampSchedule
- type RampStepBlock
- type RecommendationFilter
- type RecommendationRecord
- type RecommendationsFreshness
- type ServiceConfig
- type StoreInterface
- type SuccessfulCollect
Constants ¶
const ( // DefaultListLimit is the default number of items returned in list operations. DefaultListLimit = 100 // MaxListLimit is the maximum number of items allowed in a single list request. MaxListLimit = 1000 // DefaultExecutionTTLDays is how long execution records are kept. DefaultExecutionTTLDays = 30 // DefaultMaxRecommendationsInEmail is the max recommendations shown in email notifications. DefaultMaxRecommendationsInEmail = 10 // DefaultPasswordResetExpiry is how long password reset tokens are valid. DefaultPasswordResetExpiry = 1 * time.Hour )
Default configuration values.
const ( // MaxCoverage is the maximum allowed coverage percentage. MaxCoverage = 100 // MinCoverage is the minimum allowed coverage percentage. MinCoverage = 0 // MaxPlanNameLength is the maximum length for plan names. MaxPlanNameLength = 100 // MaxNotificationDaysBefore is the maximum days before purchase to send notification. MaxNotificationDaysBefore = 30 // MaxStepIntervalDays is the maximum interval between ramp steps. MaxStepIntervalDays = 365 // MaxTotalSteps is the maximum number of ramp steps. MaxTotalSteps = 100 // MaxServiceMinCount caps the per-service min-count recommendation // filter. Mirrors the CLI's MaxReasonableInstances ceiling so a typo // (e.g. a stray trailing zero) can't silently suppress every // recommendation. 0 disables the filter; values above this are rejected // at validation time. MaxServiceMinCount = 10000 )
Validation constants.
const ( // DefaultCoveragePercent is the default coverage percentage for new configs. DefaultCoveragePercent = 80 // DefaultNotifyDaysBefore is the default days before purchase to send notification. DefaultNotifyDaysBefore = 7 )
Default values for new configurations.
const ( // RampImmediate means all at once. RampImmediate = "immediate" // RampWeekly25Pct means 25% per week for 4 weeks. RampWeekly25Pct = "weekly-25pct" // #nosec G101 -- schedule constant; gosec misidentifies "pct" suffix as a credential pattern // RampMonthly10Pct means 10% per month for 10 months. RampMonthly10Pct = "monthly-10pct" // Weekly step interval in days. WeeklyStepIntervalDays = 7 // Monthly step interval in days. MonthlyStepIntervalDays = 30 )
Ramp schedule presets.
const ( // HoursPerDay is the number of hours in a day. HoursPerDay = 24 // MinHoursBetweenNotifications is the minimum hours between notification emails. MinHoursBetweenNotifications = 24 )
Time constants.
const ( // TokenByteLength is the length of generated tokens in bytes. TokenByteLength = 32 // MFATimeStep is the TOTP time step in seconds. MFATimeStep = 30 // MFADigits is the number of digits in MFA codes. MFADigits = 6 )
Token constants.
const ( StatusCompleted = "completed" StatusPartiallyCompleted = "partially_completed" )
StatusCompleted / StatusPartiallyCompleted are the two terminal statuses that mean commitment was actually bought. A partially-completed row bought some of its recommendations and failed others (issue #642); it is a real purchase, which is why re-approving one would double-buy.
const ( ListingStateActive = "active" ListingStatePending = "pending" ListingStateCancelled = "cancelled" //nolint:misspell // AWS ListingStatus enum literal, not prose ListingStateClosed = "closed" )
AWS EC2 ReservedInstancesListing status values, mirroring the ec2types.ListingStatus enum. Stored verbatim in purchase_history.listing_state so the strings must match AWS exactly. ListingStatePending is additionally written by the marketplace-list handler as a transient claim that guards against concurrent listing creation (issue #292); the other three come straight from AWS.
const ApprovalTokenTTL = 7 * 24 * time.Hour
ApprovalTokenTTL is the lifetime of a purchase approval token (issue #397). Tokens older than this are rejected by ApproveExecution and loadCancelableExecution. 7 days gives approvers a full business week to act without the window being infinite. Mirror the RI exchange model which uses a 6-hour TTL; purchase approvals are higher-stakes so a longer window is appropriate but must still be bounded.
const AzureRevocationWindowDays = 7
AzureRevocationWindowDays is the length of the Azure reservation free-cancel window: a reservation can be returned for a full refund within this many days of purchase (issue #290). It is the single source of truth for the window, referenced both at purchase-write time (to stamp PurchaseHistoryRecord.RevocationWindowClosesAt) and by the revoke endpoint's window check, so the two never drift.
const DefaultGracePeriodDays = 7
DefaultGracePeriodDays is the fallback window used when a provider has no entry in GlobalConfig.GracePeriodDays. A week gives cloud providers enough time to reflect a fresh commitment in their utilization metrics before we'd re-propose the same capacity.
const DefaultLadderBaselinePercentile = ladder.DefaultBaselinePercentile
DefaultLadderBaselinePercentile is the default usage percentile used to anchor the base commitment layer.
const DefaultLadderBufferFraction = ladder.DefaultBufferFraction
DefaultLadderBufferFraction is the default fraction of the base allocation reserved in short-term / convertible buffer commitments.
const DefaultLadderBufferUtilThreshold = ladder.DefaultBufferUtilizationThresholdPct
DefaultLadderBufferUtilThreshold is the default buffer-layer utilization % below which the engine emits a reshape recommendation.
const DefaultLadderLookbackDays = ladder.DefaultLookbackDays
DefaultLadderLookbackDays is the default historical window (days) used to compute the usage baseline.
const DefaultLadderMaxActionsPerRun = 10
DefaultLadderMaxActionsPerRun is the default cap on the number of PlannedActions the engine may execute per run.
const DefaultLadderTargetCoverage = ladder.DefaultTargetCoveragePct
DefaultLadderTargetCoverage is the default commitment coverage target (%). Aliases ladder.DefaultTargetCoveragePct so only one source of truth exists.
const DefaultPurchaseDelayHours = 48
DefaultPurchaseDelayHours is the default Gmail-style pre-fire delay. 48 hours gives most users a working-day window to spot and cancel an approval they didn't intend.
const DefaultRecommendationsCacheStaleHours = 24
DefaultRecommendationsCacheStaleHours is the default age (hours) after which the recommendations cache triggers a background refresh.
const DefaultRecommendationsLookbackDays = 7
DefaultRecommendationsLookbackDays is the default AWS Cost Explorer lookback window when no explicit value is configured.
const LegacyStatusCanceled = "cancel" + "led"
LegacyStatusCanceled is the British-spelling status value old code writes during the expand-contract rename (migration 000089). It is constructed by concatenation rather than a single literal so the US-locale misspell linter does not flag it -- this lets the dual-spelling read paths reference the legacy value without a //nolint:misspell directive. The CONTRACT migration (#1278) normalizes all rows to StatusCanceled once old code is gone, after which every reference to this constant can be deleted.
const MaxGracePeriodDays = 90
MaxGracePeriodDays is the ceiling enforced at read time as a safety net. The UI clamps input to [0, 30]; the DB isn't constrained, so a rogue write through psql shouldn't be able to suppress recs for years.
const MaxLadderActionsPerRun = 50
MaxLadderActionsPerRun is the ceiling enforced at validation time. A value above this is almost certainly a misconfiguration and should fail loud rather than fan out unbounded actions.
const MaxPurchaseDelayHours = 168
MaxPurchaseDelayHours is the ceiling for PurchaseDelayHours. One week is long enough for any reasonable review cycle; longer delays make the UX confusing and the scheduler overhead non-trivial.
const MaxRecommendationsCacheStaleHours = 8760
MaxRecommendationsCacheStaleHours is the maximum configurable stale threshold: one year. Values above this are rejected at validation time.
const RevocationWindow = 24 * time.Hour
RevocationWindow is the time window after a purchase completes during which the buyer may request revocation (issue #291). 24 hours matches the AWS RI/SP support-case window advertised in the post-execution email. It is the single source of truth for both the fresh revocation token's expiry (minted in purchase.mintRevocationToken) and the enforcement check in api.validateRevokeToken: the two MUST stay equal or a token could expire before the window closes and silently block a valid revoke.
const StatusCanceled = "canceled"
StatusCanceled is the canonical US-spelling status value new code writes.
const StatusExpired = "expired"
StatusExpired is written when an approval lapsed before anyone acted on it.
const StatusFailed = "failed"
StatusFailed is the terminal status written when an execution's purchase attempt errors out, and by the stuck-purchase reaper (internal/purchase/ reaper.go) for rows left in approved/running past its threshold.
Variables ¶
var ( RampStepSucceededStatuses = []string{StatusCompleted, StatusPartiallyCompleted} RampStepSettledStatuses = []string{StatusCompleted, StatusPartiallyCompleted, StatusCanceled, LegacyStatusCanceled} RampStepStuckStatuses = []string{StatusFailed, StatusExpired} )
The three ramp-step status classes below drive the advance gate in CompletePlanStep and the stuck-ramp report GetStuckRampSteps (issue #1861). They are separate exported lists rather than one, because the gate asks two different questions of the same rows and the health report asks a third:
- RampStepSucceededStatuses answers "did this fan-out unit buy?". A step no unit bought must not advance the ramp at all.
- RampStepSettledStatuses answers "is this fan-out unit done holding the step open?". It is deliberately an ALLOWLIST: a status added to the schema later holds the step open until someone classifies it, which is the fail-closed direction on a money path.
- RampStepStuckStatuses answers "is this unit stuck rather than in flight?". Only these produce the plan-health ramp_blocked factor, so a retry still working its way through pending/running reads as recovery rather than as a frozen ramp.
Canceled counts as settled but neither succeeded nor stuck: to cancel a step's row is an operator deciding that unit will not buy, and that decision is what releases a ramp an unrecoverable account would otherwise freeze. Both spellings are listed for the duration of the expand-contract rename (migration 000089, contract in #1278), same as HealthScoredExecutionStatuses.
RampStepSucceededStatuses is the same predicate migration 000098 spells out inline as status IN ('completed','partially_completed') when it decides which rows a sibling has already bought for. The migration is frozen SQL and cannot import this, so the two are kept identical by hand: changing one without the other would make the backfill and the advance gate disagree about what "bought" means.
var DefaultSettings = []ConfigSetting{ { Key: "purchase_defaults.term", Value: 3, Type: "int", Category: "purchase_defaults", Description: "Default commitment term in years (1 or 3)", UpdatedAt: time.Time{}, }, { Key: "purchase_defaults.payment_option", Value: "no-upfront", Type: "string", Category: "purchase_defaults", Description: "Default payment option: no-upfront, partial-upfront, all-upfront", UpdatedAt: time.Time{}, }, { Key: "purchase_defaults.coverage", Value: 80.0, Type: "float", Category: "purchase_defaults", Description: "Default coverage percentage (0-100)", UpdatedAt: time.Time{}, }, { Key: "purchase_defaults.ramp_schedule", Value: "immediate", Type: "string", Category: "purchase_defaults", Description: "Default ramp schedule: immediate, weekly-25pct, monthly-10pct", UpdatedAt: time.Time{}, }, { Key: "notification.days_before", Value: 3, Type: "int", Category: "notification", Description: "Days before purchase to send notification", UpdatedAt: time.Time{}, }, { Key: "notification.email_enabled", Value: true, Type: "bool", Category: "notification", Description: "Enable email notifications for purchases", UpdatedAt: time.Time{}, }, { Key: "notification.approval_required", Value: true, Type: "bool", Category: "notification", Description: "Require approval before executing purchases", UpdatedAt: time.Time{}, }, { Key: "notification.email_from", Value: "noreply@cudly.io", Type: "string", Category: "notification", Description: "Email sender address for notifications", UpdatedAt: time.Time{}, }, { Key: "providers.aws_enabled", Value: true, Type: "bool", Category: "providers", Description: "Enable AWS provider for recommendations and purchases", UpdatedAt: time.Time{}, }, { Key: "providers.azure_enabled", Value: false, Type: "bool", Category: "providers", Description: "Enable Azure provider for recommendations and purchases", UpdatedAt: time.Time{}, }, { Key: "providers.gcp_enabled", Value: false, Type: "bool", Category: "providers", Description: "Enable GCP provider for recommendations and purchases", UpdatedAt: time.Time{}, }, { Key: "security.session_duration_hours", Value: 24, Type: "int", Category: "security", Description: "Session duration in hours before re-authentication required", UpdatedAt: time.Time{}, }, { Key: "security.lockout_attempts", Value: 5, Type: "int", Category: "security", Description: "Failed login attempts before account lockout", UpdatedAt: time.Time{}, }, { Key: "security.lockout_duration_minutes", Value: 15, Type: "int", Category: "security", Description: "Account lockout duration in minutes", UpdatedAt: time.Time{}, }, { Key: "security.password_min_length", Value: 12, Type: "int", Category: "security", Description: "Minimum password length requirement", UpdatedAt: time.Time{}, }, { Key: "security.password_require_special", Value: true, Type: "bool", Category: "security", Description: "Require special characters in passwords", UpdatedAt: time.Time{}, }, { Key: "security.password_require_number", Value: true, Type: "bool", Category: "security", Description: "Require numbers in passwords", UpdatedAt: time.Time{}, }, { Key: "security.password_require_uppercase", Value: true, Type: "bool", Category: "security", Description: "Require uppercase letters in passwords", UpdatedAt: time.Time{}, }, { Key: "scheduling.auto_collect", Value: false, Type: "bool", Category: "scheduling", Description: "Automatically collect recommendations on schedule", UpdatedAt: time.Time{}, }, { Key: "scheduling.collect_schedule", Value: "rate(1 day)", Type: "string", Category: "scheduling", Description: "Schedule for automatic recommendation collection (EventBridge format)", UpdatedAt: time.Time{}, }, { Key: "scheduling.auto_purchase", Value: false, Type: "bool", Category: "scheduling", Description: "Automatically execute approved purchase plans", UpdatedAt: time.Time{}, }, { Key: "scheduling.purchase_schedule", Value: "rate(1 day)", Type: "string", Category: "scheduling", Description: "Schedule for checking and executing purchase plans (EventBridge format)", UpdatedAt: time.Time{}, }, { Key: "aws.rds.min_utilization_percent", Value: 50.0, Type: "float", Category: "aws", Description: "Minimum RDS instance utilization for RI recommendations", UpdatedAt: time.Time{}, }, { Key: "aws.elasticache.min_utilization_percent", Value: 50.0, Type: "float", Category: "aws", Description: "Minimum ElastiCache node utilization for RI recommendations", UpdatedAt: time.Time{}, }, { Key: "aws.opensearch.min_utilization_percent", Value: 50.0, Type: "float", Category: "aws", Description: "Minimum OpenSearch instance utilization for RI recommendations", UpdatedAt: time.Time{}, }, { Key: "aws.ec2.include_convertible", Value: true, Type: "bool", Category: "aws", Description: "Include convertible EC2 Reserved Instances in recommendations", UpdatedAt: time.Time{}, }, { Key: "aws.savings_plans.compute_enabled", Value: true, Type: "bool", Category: "aws", Description: "Include Compute Savings Plans in recommendations", UpdatedAt: time.Time{}, }, { Key: "aws.savings_plans.ec2_enabled", Value: true, Type: "bool", Category: "aws", Description: "Include EC2 Instance Savings Plans in recommendations", UpdatedAt: time.Time{}, }, { Key: "aws.savings_plans.sagemaker_enabled", Value: true, Type: "bool", Category: "aws", Description: "Include SageMaker Savings Plans in recommendations", UpdatedAt: time.Time{}, }, { Key: "thresholds.min_monthly_savings", Value: 10.0, Type: "float", Category: "thresholds", Description: "Minimum monthly savings ($) to include recommendation", UpdatedAt: time.Time{}, }, { Key: "thresholds.min_savings_percentage", Value: 5.0, Type: "float", Category: "thresholds", Description: "Minimum savings percentage to include recommendation", UpdatedAt: time.Time{}, }, { Key: "thresholds.max_upfront_cost", Value: 0.0, Type: "float", Category: "thresholds", Description: "Maximum upfront cost ($) per purchase (0 = no limit)", UpdatedAt: time.Time{}, }, { Key: "retention.purchase_history_days", Value: 1095, Type: "int", Category: "retention", Description: "Days to retain purchase history (3 years default)", UpdatedAt: time.Time{}, }, { Key: "retention.execution_history_days", Value: 90, Type: "int", Category: "retention", Description: "Days to retain execution history records", UpdatedAt: time.Time{}, }, { Key: "retention.recommendation_cache_hours", Value: 24, Type: "int", Category: "retention", Description: "Hours to cache recommendation data", UpdatedAt: time.Time{}, }, { Key: "ri_exchange.auto_exchange_enabled", Value: false, Type: "bool", Category: "ri_exchange", Description: "Master toggle for automated RI exchange", UpdatedAt: time.Time{}, }, { Key: "ri_exchange.mode", Value: "manual", Type: "string", Category: "ri_exchange", Description: "Exchange mode: manual (email approval) or auto (fully automated)", UpdatedAt: time.Time{}, }, { Key: "ri_exchange.utilization_threshold", Value: 95.0, Type: "float", Category: "ri_exchange", Description: "Utilization percentage below which an RI triggers exchange consideration", UpdatedAt: time.Time{}, }, { Key: "ri_exchange.max_payment_per_exchange_usd", Value: 0.0, Type: "float", Category: "ri_exchange", Description: "Maximum payment per single exchange in USD (0 = refuse any payment)", UpdatedAt: time.Time{}, }, { Key: "ri_exchange.max_payment_daily_usd", Value: 0.0, Type: "float", Category: "ri_exchange", Description: "Maximum total daily exchange spend in USD (0 = refuse any payment)", UpdatedAt: time.Time{}, }, { Key: "ri_exchange.lookback_days", Value: 30, Type: "int", Category: "ri_exchange", Description: "Days of utilization data to consider for exchange recommendations", UpdatedAt: time.Time{}, }, { Key: "api.rate_limit_requests_per_minute", Value: 100, Type: "int", Category: "api", Description: "Maximum API requests per minute per user", UpdatedAt: time.Time{}, }, { Key: "api.rate_limit_enabled", Value: true, Type: "bool", Category: "api", Description: "Enable API rate limiting", UpdatedAt: time.Time{}, }, { Key: "api.timeout_seconds", Value: 30, Type: "int", Category: "api", Description: "Default API request timeout in seconds", UpdatedAt: time.Time{}, }, }
DefaultSettings defines the default configuration values for CUDly. UpdatedAt is the zero time.Time{} for every entry: these are static compile-time defaults and have never been "updated" by a user.
var ErrAuditLoss = errors.New("audit loss: execution persistence failed after purchase")
ErrAuditLoss is returned (wrapped) by executeAndFinalize when the purchase run itself completed but the subsequent SavePurchaseExecution call failed. The execution is already "running" (per the CAS in claimAndRedrive) but its final state was never persisted -- the row is stranded in "running" until the next recovery sweep. Callers that silence all drive errors (e.g. claimAndRedrive) must propagate this sentinel so the sweep surfaces the persistence failure rather than silently dropping the stranded row.
var ErrExecutionNotInExpectedStatus = errors.New("execution not in expected status")
ErrExecutionNotInExpectedStatus is returned by TransitionExecutionStatus when the target execution exists but its current status is not in the allowed `fromStatuses` set -- i.e. the atomic CAS rejected because some other writer transitioned the row first (e.g. the real executor finished between the reaper's SELECT and CAS). Callers can use errors.Is to distinguish this legitimate race-loss from a hard DB error.
var ErrNotFound = errors.New("not found")
ErrNotFound is returned when a requested config-store row does not exist.
var ErrRampStepAlreadyCounted = errors.New("ramp step already counted")
ErrRampStepAlreadyCounted is returned (wrapped) by CompletePlanStep when the plan has moved PAST the completing step. Unlike ErrRampStepCountedBySibling this is an anomaly worth an audit note on the execution row: the purchase bought commitment for a step the ramp counted earlier and will never count again, which is how a step_number stamped by pre-#1669 code (or during that deploy overlap) surfaces.
var ErrRampStepCountedBySibling = errors.New("ramp step already counted by a sibling execution")
ErrRampStepCountedBySibling is returned (wrapped) by CompletePlanStep when the plan is sitting exactly on the completing step: a sibling execution of the same step won the race to advance it, moments earlier. Routine on a multi-account step whose last two accounts finish together, and benign -- the step WAS counted, just not by this caller -- so it must not be stamped on the execution row. A note there would flip a cleanly-completed purchase into History's audit-gap rendering, which keys on a non-empty error.
var ErrRampStepIncomplete = errors.New("ramp step incomplete")
ErrRampStepIncomplete is returned (wrapped) by CompletePlanStep when the ramp step the completing execution belongs to has not been bought in full: another cloud account on that step is still outstanding, or nothing on the step bought at all. Counting it would report commitment the plan has not made (issue #1861). Transient by construction -- it clears when the outstanding account buys, when its row is canceled, or when the step is abandoned -- so callers must treat it as "not yet", not as a failure of the purchase that just completed.
The sentinel text stays generic because both cases wrap it and each supplies its own specifics; naming one of them here would mis-describe the other.
var ErrRegistrationConflict = errors.New("registration status conflict: already processed")
ErrRegistrationConflict is returned when a registration status transition fails because another request already changed the status (concurrent modification).
var HealthScoredExecutionStatuses = []string{ StatusFailed, StatusCanceled, LegacyStatusCanceled, }
HealthScoredExecutionStatuses are the execution statuses the plan health score counts (internal/api/plan_health.go). It lives here, in the package that owns both the status constants and the retention sweep, because two unrelated-looking pieces of code have to agree on it exactly:
- CountExecutionsByPlanAndStatus is asked for these statuses and windows them on updated_at.
- CleanupOldExecutions must not delete a row in one of these statuses while that window still covers it. Retaining on any other clock lets the sweep purge a row the score is still counting, which shows up as a plan's health score jumping overnight with no operator action.
Keeping one exported slice rather than a literal list on each side is what makes that agreement structural: adding a status to the score automatically extends the sweep's exclusion, so the two cannot drift.
Both spellings of canceled are present for the duration of the expand-contract rename (migration 000089, contract in #1278).
var PresetRampSchedules = map[string]RampSchedule{
"immediate": {
Type: "immediate",
PercentPerStep: 100,
TotalSteps: 1,
},
"weekly-25pct": {
Type: "weekly",
PercentPerStep: 25,
StepIntervalDays: 7,
TotalSteps: 4,
},
"monthly-10pct": {
Type: "monthly",
PercentPerStep: 10,
StepIntervalDays: 30,
TotalSteps: 10,
},
}
PresetRampSchedules provides common ramp-up configurations.
var ValidCollectionSchedules = []string{"", "hourly", "daily", "weekly"}
ValidCollectionSchedules lists all valid collection schedule values.
var ValidOfferingClasses = []string{"convertible", "standard"}
ValidOfferingClasses lists the accepted EC2 RI offering class values for GlobalConfig. The empty string is also accepted (maps to "convertible" at purchase time to preserve pre-694 behavior).
var ValidPaymentOptions = []string{"no-upfront", "partial-upfront", "all-upfront"}
ValidPaymentOptions lists the AWS-canonical payment options. Kept for backwards compatibility; prefer ValidPaymentOptionsByProvider for provider-aware validation.
var ValidPaymentOptionsByProvider = map[string][]string{
"aws": {"no-upfront", "partial-upfront", "all-upfront"},
"azure": {"upfront", "monthly"},
"gcp": {"monthly"},
}
ValidPaymentOptionsByProvider maps each provider to the payment option tokens it accepts. Each provider's set is the canonical set verified against the provider's service-client switch statements:
aws: {no-upfront, partial-upfront, all-upfront}: the three RI/SP billing tiers exposed by AWS APIs.
azure: {upfront, monthly}: verified against the 7 Azure service-client switches in providers/azure/services/{compute,cache,cosmosdb,database, search,synapse,managedredis}/client.go. (The savingsplans client mirrors AWS's three-tier set, but Azure savings-plan recs are not currently emitted; GetRecommendations returns []. The canonical set follows the only path that emits today.)
gcp: {monthly}: GCP CUDs are billed monthly across the commitment term; there is no upfront billing tier. See providers/gcp/services/computeengine/client.go:buildCommitmentRequests which takes only a Plan (TWELVE_MONTH/THIRTY_SIX_MONTH) and never reads PaymentOption.
Cross-provider tokens are rejected loudly by the validator. Legacy AWS-style tokens emitted by older code paths are canonicalized via NormalizePaymentOption at the rec-emission boundary BEFORE reaching this validator (see internal/scheduler/scheduler.go:convertRecommendations).
var ValidProviders = []string{"aws", "azure", "gcp"}
ValidProviders lists all supported cloud providers.
var ValidRampScheduleTypes = []string{"immediate", "weekly", "monthly", "custom"}
ValidRampScheduleTypes lists all supported ramp schedule types.
var ValidRecommendationsLookbackDays = []int{7, 30, 60}
ValidRecommendationsLookbackDays lists the AWS Cost Explorer LookbackPeriodInDays enum values. Other values are rejected.
Functions ¶
func AccountConfigKey ¶
AccountConfigKey returns the map key used by ResolveAccountConfigsForRecs. Exposed so callers (scheduler filter, dashboard aggregator) can look up the resolved config for a given rec without re-implementing the format.
func DerivePlanProviders ¶
func DerivePlanProviders(plan *PurchasePlan) []string
DerivePlanProviders extracts the distinct set of providers a plan targets by parsing the keys of plan.Services. Keys are expected to use the "provider/service" format produced by buildServiceConfig.
func GetAllCategories ¶
func GetAllCategories() []string
GetAllCategories returns a list of all configuration categories.
func GetDefaultValue ¶
GetDefaultValue returns the default value for a given key.
func NormalizePaymentOption ¶
NormalizePaymentOption maps a raw payment-option token onto the canonical token the given provider semantically models (see ValidPaymentOptionsByProvider). It exists so the recommendation-emission boundary (see internal/scheduler/scheduler.go:convertRecommendations) can defensively canonicalize any AWS-style token that a code path or a globally-default payment-option setting might stamp onto a non-AWS rec, before the rec is persisted and later validated against the provider-canonical set.
Returns (canonical, true) when raw is already canonical for the provider or has an unambiguous canonical mapping. Returns ("", false) for unknown providers and (raw, false) for tokens that have no canonical mapping on the given provider (e.g. an Azure/GCP-style "upfront" on AWS) — callers can use ok=false to surface the unmapped token at the next validator boundary. Per-provider mapping:
- AWS : passthrough (AWS already speaks the three-tier set).
- Azure: all-upfront → upfront, no-upfront → monthly, partial-upfront → monthly (no semantic equivalent — coerce to the no-upfront tier, CUDly's default billing schedule for Azure, rather than drop the rec; caller may log). Coercing to upfront here would silently bill an all-upfront schedule the caller never chose: the canonical token is persisted onto the execution and copied into common.Recommendation.PaymentOption (internal/purchase/execution.go), and the reservation-purchase billingPlan wiring being added in #1495/#1502 maps only upfront/monthly tokens and hard-errors on partial-upfront, so landing on monthly keeps the rec on the no-upfront schedule instead of an irreversible upfront charge — Azure does not allow changing a reservation's billing frequency after purchase.
- GCP : all-upfront → monthly, no-upfront → monthly, partial-upfront → monthly, upfront → monthly (GCP CUDs are inherently monthly-billed — every non-monthly token collapses to the one billing plan GCP actually models). The collapse from "upfront" to "monthly" is what makes the existing providers/gcp/services/computeengine/client.go:804 stamp safe: the scheduler.convertRecommendations boundary coerces it once before persistence so the rec carries the canonical token downstream.
The partial-upfront coercion is deliberate on both Azure and GCP: dropping the rec would be a silent data loss for the user, while coercing to the closest billing model the provider offers preserves the rec without changing the total the caller committed to. Microsoft documents that choice as cost-neutral: "The total cost of up-front and monthly reservations is the same and you don't pay any extra fees when you choose to pay monthly" (https://learn.microsoft.com/en-us/azure/cost-management-billing/reservations/prepare-buy-reservation). Caveat from the same page: monthly payments are NOT offered for SUSE Linux reservations, Red Hat plans, Azure Red Hat OpenShift licenses, or pre-purchase plans, so for those products the coerced token can make Azure reject the purchase. That is the intended failure mode — a loud purchase- time rejection is preferable to silently committing the caller to an upfront charge they never chose and cannot undo.
The caller is expected to log a warning when raw != canonical so an operator notices the upstream input bug.
The frontend mirrors this mapping in frontend/src/commitmentOptions.ts:normalizePaymentValue (it decides which option the plan/purchase dropdowns pre-select); the two must stay in lockstep.
Empty raw passes through as ("", true) — callers that distinguish "unset" from "invalid" can check the returned bool only when raw is non-empty.
func PaymentCoercionChangesSchedule ¶
PaymentCoercionChangesSchedule reports whether rewriting raw to canonical actually changes what the customer pays and when, as opposed to merely renaming the same schedule into the target provider's vocabulary.
Azure "all-upfront" -> "upfront" and Azure/GCP "no-upfront" -> "monthly" are renames: identical cash flow, different spelling. Azure "partial-upfront" -> "monthly" and GCP "upfront" -> "monthly" are real changes: the customer is billed on a schedule they did not ask for.
Only real changes are worth putting in front of a user (#1503). The fan-out purchase modal submits the AWS-style "all-upfront" for Azure buckets by construction (frontend/src/lib/purchase-compatibility.ts: paymentOptionsFor), so treating every rewrite as a change would fire a "billing schedule adjusted" warning on the ordinary Azure upfront purchase and train users to dismiss the one notice that matters.
func ResolveAccountConfigsForRecs ¶
func ResolveAccountConfigsForRecs( ctx context.Context, store AccountConfigReader, recs []RecommendationRecord, ) (map[string]*ServiceConfig, error)
ResolveAccountConfigsForRecs walks the recs once, collects the unique (cloud_account_id, provider, service) triples, and resolves each via ResolveServiceConfig(provider, service, global, override). Returns a map keyed by AccountConfigKey -> resolved *ServiceConfig.
Triples are skipped (not present in the map) when:
- rec.CloudAccountID is nil — no per-account override possible (e.g. AWS ambient-credentials path).
- Neither a global ServiceConfig nor a per-account override exists for the (provider, service) pair — no configuration to apply, so callers treat the triple as "no filter applies".
When a per-account override exists but no global ServiceConfig does, the override is applied against a synthesized default baseline (Enabled: true) so the operator's intent is honored even when a global row has not been created yet.
Errors from either lookup are returned alongside the partial map so the caller decides whether to fail the whole operation or pass through. The scheduler / dashboard read paths choose pass-through (over-show vs. under-show), matching the precedent set by applySuppressions.
func RevocationWindowClosesAtFor ¶
RevocationWindowClosesAtFor returns the timestamp at which the in-app revoke button should stop being offered for a purchase of the given provider made at purchaseTime, or nil when the provider has no in-app free-cancel window.
Only Azure has a direct-API free-cancel window in Phase 1. AWS EC2 RIs have a 24h window but no direct cancel API (revocation goes through an AWS Support case, out of Phase-1 scope), and GCP commitments have no free-cancel window at all, so both return nil and the History UI hides the button.
func ValidatePaymentOptionEnv ¶
ValidatePaymentOptionEnv validates a payment-option value read from an environment variable (e.g. DEFAULT_PAYMENT_OPTION). Empty string is always valid ("use the purchase manager's built-in default"). Non-empty values must be in the union of all provider payment option sets. Called by the server startup boundary so misconfiguration is caught at boot time rather than silently propagated into purchases (issue #1026).
func ValidateRampScheduleEnv ¶
ValidateRampScheduleEnv validates a ramp-schedule value read from an environment variable (e.g. DEFAULT_RAMP_SCHEDULE). Empty string is always valid ("use the purchase manager's built-in default"). Non-empty values must be in ValidRampScheduleTypes. Called by the server startup boundary so misconfiguration is caught at boot time rather than silently propagated into purchases (issue #1026).
Types ¶
type AccountConfigReader ¶
type AccountConfigReader interface {
GetServiceConfig(ctx context.Context, provider, service string) (*ServiceConfig, error)
GetAccountServiceOverride(ctx context.Context, accountID, provider, service string) (*AccountServiceOverride, error)
}
AccountConfigReader is the minimal store surface needed by ResolveAccountConfigsForRecs. Both PostgresStore (production) and the scheduler's MockConfigStore (tests) already satisfy it via the broader StoreInterface — we narrow here so the helper can be unit-tested with a tiny ad-hoc fake.
type AccountRegistration ¶
type AccountRegistration struct {
ID string `json:"id"`
ReferenceToken string `json:"reference_token"`
Status string `json:"status"` // pending, approved, rejected
Provider string `json:"provider"`
ExternalID string `json:"external_id"`
AccountName string `json:"account_name"`
ContactEmail string `json:"contact_email"`
Description string `json:"description,omitempty"`
SourceProvider string `json:"source_provider,omitempty"`
AWSRoleARN string `json:"aws_role_arn,omitempty"`
AWSAuthMode string `json:"aws_auth_mode,omitempty"`
AWSExternalID string `json:"aws_external_id,omitempty"`
AzureSubscriptionID string `json:"azure_subscription_id,omitempty"`
AzureTenantID string `json:"azure_tenant_id,omitempty"`
AzureClientID string `json:"azure_client_id,omitempty"`
AzureAuthMode string `json:"azure_auth_mode,omitempty"`
GCPProjectID string `json:"gcp_project_id,omitempty"`
GCPClientEmail string `json:"gcp_client_email,omitempty"`
GCPAuthMode string `json:"gcp_auth_mode,omitempty"`
GCPWIFAudience string `json:"gcp_wif_audience,omitempty"` // Full WIF provider resource; only set for federated path.
RegCredentialType string `json:"reg_credential_type,omitempty"`
RegCredentialPayload string `json:"-"` // never returned in API responses (encrypted at rest)
HasCredentials bool `json:"has_credentials,omitempty"` // derived: true when reg_credential_type is set
RejectionReason string `json:"rejection_reason,omitempty"`
CloudAccountID *string `json:"cloud_account_id,omitempty"`
ReviewedBy *string `json:"reviewed_by,omitempty"`
ReviewedAt *time.Time `json:"reviewed_at,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
AccountRegistration represents a self-service registration request from a target account owner. Submitted via POST /api/register during Terraform apply of the federation IaC, then approved or rejected by a CUDly admin.
type AccountRegistrationFilter ¶
AccountRegistrationFilter for ListAccountRegistrations queries.
type AccountServiceOverride ¶
type AccountServiceOverride struct {
ID string `json:"id"`
AccountID string `json:"account_id"`
Provider string `json:"provider"`
Service string `json:"service"`
Enabled *bool `json:"enabled,omitempty"`
Term *int `json:"term,omitempty"`
Payment *string `json:"payment,omitempty"`
Coverage *float64 `json:"coverage,omitempty"`
RampSchedule *string `json:"ramp_schedule,omitempty"`
IncludeEngines []string `json:"include_engines,omitempty"`
ExcludeEngines []string `json:"exclude_engines,omitempty"`
IncludeRegions []string `json:"include_regions,omitempty"`
ExcludeRegions []string `json:"exclude_regions,omitempty"`
IncludeTypes []string `json:"include_types,omitempty"`
ExcludeTypes []string `json:"exclude_types,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
AccountServiceOverride is a sparse per-account override on top of the global ServiceConfig. Nil pointer fields inherit the global value.
type CloudAccount ¶
type CloudAccount struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
ContactEmail string `json:"contact_email,omitempty"`
Enabled bool `json:"enabled"`
Provider string `json:"provider"`
ExternalID string `json:"external_id"`
// AWS-specific
AWSAuthMode string `json:"aws_auth_mode,omitempty"`
AWSRoleARN string `json:"aws_role_arn,omitempty"`
AWSExternalID string `json:"aws_external_id,omitempty"`
AWSBastionID string `json:"aws_bastion_id,omitempty"`
AWSWebIdentityTokenFile string `json:"aws_web_identity_token_file,omitempty"`
AWSIsOrgRoot bool `json:"aws_is_org_root,omitempty"`
// Azure-specific
AzureSubscriptionID string `json:"azure_subscription_id,omitempty"`
AzureTenantID string `json:"azure_tenant_id,omitempty"`
AzureClientID string `json:"azure_client_id,omitempty"`
AzureAuthMode string `json:"azure_auth_mode,omitempty"`
// GCP-specific
GCPProjectID string `json:"gcp_project_id,omitempty"`
GCPClientEmail string `json:"gcp_client_email,omitempty"`
GCPAuthMode string `json:"gcp_auth_mode,omitempty"`
// GCPWIFAudience is the full Workload Identity Pool provider
// resource used as the STS audience when exchanging a CUDly
// KMS-signed JWT for a GCP access token. Only set for accounts
// using the secret-free workload_identity_federation path.
// Shape: //iam.googleapis.com/projects/<number>/locations/global/workloadIdentityPools/<pool>/providers/<provider>
GCPWIFAudience string `json:"gcp_wif_audience,omitempty"`
// Derived (not stored in DB)
CredentialsConfigured bool `json:"credentials_configured"`
BastionAccountName string `json:"bastion_account_name,omitempty"`
IsSelf bool `json:"is_self,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
CreatedBy string `json:"created_by,omitempty"`
}
CloudAccount represents a single managed cloud account/subscription/project.
type CloudAccountFilter ¶
type CloudAccountFilter struct {
Provider *string
Enabled *bool
Search string // substring match on name or external_id
BastionID *string // return accounts whose aws_bastion_id = *BastionID
}
CloudAccountFilter for ListCloudAccounts queries.
type ConfigSetting ¶
type ConfigSetting struct {
Key string `json:"key"`
Value any `json:"value"`
Type string `json:"type"` // int, float, bool, string, json
Category string `json:"category"`
Description string `json:"description"`
UpdatedAt time.Time `json:"updated_at"`
}
ConfigSetting represents a configuration setting for the defaults system.
func GetDefaultSetting ¶
func GetDefaultSetting(key string) *ConfigSetting
GetDefaultSetting returns the complete default setting for a given key.
func GetDefaultsByCategory ¶
func GetDefaultsByCategory(category string) []ConfigSetting
GetDefaultsByCategory returns all default settings for a given category.
type ExecutionStatusCounts ¶
ExecutionStatusCounts maps an execution status value to the number of executions in that status, for a single plan. Returned by CountExecutionsByPlanAndStatus; a status with no rows is simply absent from the map, so a plain lookup yields the correct zero.
type GlobalConfig ¶
type GlobalConfig struct {
EnabledProviders []string `json:"enabled_providers" dynamodbav:"enabled_providers"`
NotificationEmail *string `json:"notification_email,omitempty" dynamodbav:"notification_email,omitempty"`
AutoCollect bool `json:"auto_collect"`
CollectionSchedule string `json:"collection_schedule"`
NotificationDaysBefore int `json:"notification_days_before"`
ApprovalRequired bool `json:"approval_required" dynamodbav:"approval_required"`
DefaultTerm int `json:"default_term" dynamodbav:"default_term"`
DefaultPayment string `json:"default_payment" dynamodbav:"default_payment"`
DefaultCoverage float64 `json:"default_coverage" dynamodbav:"default_coverage"`
DefaultRampSchedule string `json:"default_ramp_schedule" dynamodbav:"default_ramp_schedule"`
// GracePeriodDays is a per-provider window (in days) during which
// just-purchased capacity is suppressed from the recommendations
// list so users don't re-buy the same capacity while the cloud
// provider's utilization metrics catch up. Keys are provider slugs
// ("aws", "azure", "gcp"). Missing keys default to DefaultGracePeriodDays
// (7). An explicit 0 disables suppression for that provider. Use
// GracePeriodFor to read a specific provider's effective value (it
// applies the default + safety clamp).
GracePeriodDays map[string]int `json:"grace_period_days,omitempty" dynamodbav:"grace_period_days,omitempty"`
// RI Exchange automation settings
RIExchangeEnabled bool `json:"ri_exchange_enabled" dynamodbav:"ri_exchange_enabled"`
RIExchangeMode string `json:"ri_exchange_mode" dynamodbav:"ri_exchange_mode"`
RIExchangeUtilizationThreshold float64 `json:"ri_exchange_utilization_threshold" dynamodbav:"ri_exchange_utilization_threshold"`
RIExchangeMaxPerExchangeUSD float64 `json:"ri_exchange_max_per_exchange_usd" dynamodbav:"ri_exchange_max_per_exchange_usd"`
RIExchangeMaxDailyUSD float64 `json:"ri_exchange_max_daily_usd" dynamodbav:"ri_exchange_max_daily_usd"`
RIExchangeLookbackDays int `json:"ri_exchange_lookback_days" dynamodbav:"ri_exchange_lookback_days"`
// RecommendationsCacheStaleHours is the age (hours) at which the
// recommendations cache is considered stale and a background refresh
// fires automatically (stale-while-revalidate). 0 disables automatic
// background refresh; the cron scheduler and the manual Refresh button
// still work regardless. Valid range: 0–8760 (up to one year).
// Default: 24.
RecommendationsCacheStaleHours int `json:"recommendations_cache_stale_hours" db:"recommendations_cache_stale_hours"`
// RecommendationsLookbackDays is the AWS Cost Explorer lookback window
// (days) used when fetching fresh recommendations. Must be one of 7,
// 30, or 60 -- the AWS Cost Explorer LookbackPeriodInDays enum.
// GCP CUD Recommender has no equivalent lookback parameter (fixed
// internally); this setting applies to AWS only.
// Default: 7.
RecommendationsLookbackDays int `json:"recommendations_lookback_days" db:"recommendations_lookback_days"`
// PurchaseDelayHours is the Gmail-style pre-fire delay (issue #291 wave-2).
// When > 0, approving a purchase defers the actual cloud SDK call by this
// many hours. The user receives a "scheduled, revoke before X" email
// immediately after approval and may cancel at $0 until the window closes.
// 0 means immediate-execute (backward compat). Valid range: [0, 168].
// Default: 48.
PurchaseDelayHours int `json:"purchase_delay_hours" db:"purchase_delay_hours"`
// LadderingEnabled is the global kill-switch for the commitment-laddering
// feature (issue #1333 phase 3). When false (the default), no laddering
// engine runs fire regardless of per-account LadderConfig.Enabled settings.
// Set to true to allow per-account configs to activate individually.
LadderingEnabled bool `json:"laddering_enabled" db:"laddering_enabled"`
// LadderExecutionEnabled gates the write side of the ladder capability
// (migration 000083). BOTH LadderingEnabled AND LadderExecutionEnabled
// must be true for PurchaseLayer / ReshapeBuffer to be wired with real
// AWS SDK clients. Default false: existing deployments that enable
// laddering produce plans but never call AWS purchase APIs until an
// operator explicitly opts in. Fail-loud: wireLadderWriteSide returns
// a typed ErrLadderExecutionDisabled when this is false.
LadderExecutionEnabled bool `json:"ladder_execution_enabled" db:"ladder_execution_enabled"`
// OfferingClass controls the EC2 Reserved Instance offering class used
// during purchase. Accepted values: "convertible" (default) and
// "standard". Convertible RIs can be exchanged for a different
// instance family/size/region/OS; Standard RIs are locked to the exact
// instance type for the full term but are ~5% cheaper.
// Unknown values are rejected at purchase time with an explicit error.
OfferingClass string `json:"offering_class,omitempty" dynamodbav:"offering_class,omitempty"`
// RequireDifferentApprover enables 4-eyes approval mode (issue #1005).
// When true, the user who created a purchase execution cannot approve it
// themselves; a different person with approval rights must do so. This is
// a standard SOX / SOC2 segregation-of-duties control. Default: false.
// Admins who created an execution and need to approve it must disable this
// mode first (the admin wildcard is NOT exempt from the restriction).
RequireDifferentApprover bool `json:"require_different_approver" dynamodbav:"require_different_approver"`
}
GlobalConfig represents the global CUDly configuration.
func (*GlobalConfig) GetPurchaseDelay ¶
func (g *GlobalConfig) GetPurchaseDelay() time.Duration
GetPurchaseDelay returns the pre-fire delay as a time.Duration. Nil receiver returns the default. Values outside [0, MaxPurchaseDelayHours] are clamped so a rogue DB write cannot break the scheduler.
func (*GlobalConfig) GracePeriodFor ¶
func (g *GlobalConfig) GracePeriodFor(provider string) int
GracePeriodFor returns the effective grace-period window (in days) for the given provider slug ("aws", "azure", "gcp"). Returns the default when the provider has no explicit entry. Preserves an explicit 0 (which disables the feature for that provider). Clamps the result to [0, MaxGracePeriodDays] so a misconfigured DB row can't suppress recs indefinitely.
func (*GlobalConfig) Validate ¶
func (c *GlobalConfig) Validate() error
Validate validates the GlobalConfig.
type LadderConfigDB ¶
type LadderConfigDB struct {
UpdatedAt time.Time `json:"updated_at"`
CreatedAt time.Time `json:"created_at"`
// MaxHourlyCommitPerRun caps the total hourly commitment delta a single run
// may purchase. nil means no cap.
MaxHourlyCommitPerRun *float64 `json:"max_hourly_commit_per_run,omitempty"`
CloudAccountID string `json:"cloud_account_id"`
Provider string `json:"provider"`
Mode string `json:"mode"` // ladder.ModeEmailApproval | ladder.ModeAutoApprove
Cadence string `json:"cadence"` // ladder.CadenceDaily | ladder.CadenceWeekly
ID string `json:"id"`
RampSchedule json.RawMessage `json:"ramp_schedule"`
BufferUtilizationThreshold float64 `json:"buffer_utilization_threshold"`
LookbackDays int `json:"lookback_days"`
MaxActionsPerRun int `json:"max_actions_per_run"`
// BaselinePercentile is the statistical percentile used to anchor the base
// commitment layer. Must be in (0, 50].
BaselinePercentile float64 `json:"baseline_percentile"`
BufferFraction float64 `json:"buffer_fraction"`
TargetCoverage float64 `json:"target_coverage"`
Enabled bool `json:"enabled"`
}
LadderConfigDB is the DB-persistence mirror of pkg/ladder.LadderConfig. It stores one per-account, per-provider ladder configuration row and is used by the store layer (GetLadderConfig / UpsertLadderConfig) and the API handler. Mode and Cadence are plain strings whose valid values are defined by pkg/ladder (ModeEmailApproval, ModeAutoApprove, CadenceDaily, CadenceWeekly). Validation calls pkg/ladder's Parse* functions so the internal/config package never redefines those constants.
MaxHourlyCommitPerRun is a pointer because nil means "no cap" (distinct from 0, which would cap all spending). All numeric money fields follow the project rule: absent = nil/pointer, never 0. Field order is optimized for govet fieldalignment (pointer-containing fields grouped first to shrink the GC pointer-scan range, then scalars, bool last). It intentionally does not follow the logical/SQL column order; see the scanLadderConfig / Upsert SQL for the wire order.
func (*LadderConfigDB) Validate ¶
func (c *LadderConfigDB) Validate() error
Validate validates a LadderConfigDB before persist. It delegates mode and cadence checks to pkg/ladder's Parse* functions (single source of truth for those enums) and validates numeric bounds using the same invariants that pkg/ladder.LadderConfig.Validate() enforces.
Returns a specific, descriptive error on any violation. Callers must never silently default away a validation failure.
type LadderRunDB ¶
type LadderRunDB struct {
// Nullable monetary snapshot: nil means "not computed", never $0.
BaselineUSDHr *float64 `json:"baseline_usd_hr,omitempty"`
TargetUSDHr *float64 `json:"target_usd_hr,omitempty"`
ExistingUSDHr *float64 `json:"existing_usd_hr,omitempty"`
GapUSDHr *float64 `json:"gap_usd_hr,omitempty"`
// Nullable FK and optional text fields (all pointer types).
ConfigID *string `json:"config_id,omitempty"`
CompletedAt *time.Time `json:"completed_at,omitempty"`
ApprovalTokenHash *string `json:"approval_token_hash,omitempty"`
ApprovalTokenExpiresAt *time.Time `json:"approval_token_expires_at,omitempty"`
ApprovedBy *string `json:"approved_by,omitempty"`
CancelledBy *string `json:"cancelled_by,omitempty"`
FireAt *time.Time `json:"fire_at,omitempty"`
// Mode and Cadence are nullable in the DB (populated from LadderConfigDB
// at run creation time; nil only for legacy / partially-failed rows).
Mode *string `json:"mode,omitempty"`
Cadence *string `json:"cadence,omitempty"`
// Plan JSON blob (JSONB, NOT NULL DEFAULT '{}').
Plan json.RawMessage `json:"plan"`
// Non-nullable accumulator totals (initialised to 0, not measurements;
// zero is a meaningful value for these counters unlike the monetary snapshot).
TotalHourlyCommit float64 `json:"total_hourly_commit"`
TotalUpfrontCost float64 `json:"total_upfront_cost"`
EstimatedSavings float64 `json:"estimated_savings"`
// Required string / enum fields.
ID string `json:"id"`
Status ladder.RunStatus `json:"status"`
// Required timestamps.
StartedAt time.Time `json:"started_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
LadderRunDB mirrors the ladder_runs table (migration 000080). Monetary snapshot columns are *float64 (nullable, NEVER 0-coerced: NULL means "not computed", not "$0"). Field order minimizes GC pointer-scan range: explicit pointer fields come before scalars.
type LadderTrancheDB ¶
type LadderTrancheDB struct {
// Nullable FK pointers.
ConfigID *string `json:"config_id,omitempty"`
RunID *string `json:"run_id,omitempty"`
ExecutionID *string `json:"execution_id,omitempty"` // references purchase_executions.execution_id
// Required fields.
ID string `json:"id"`
LayerType ladder.LayerType `json:"layer_type"`
Term ladder.Term `json:"term"`
PaymentOption ladder.PaymentOption `json:"payment_option"`
Status ladder.TrancheStatus `json:"status"`
// Monetary and timing.
AmountUSDHr float64 `json:"amount_usd_hr"`
ScheduledDate time.Time `json:"scheduled_date"`
CreatedAt time.Time `json:"created_at"`
}
LadderTrancheDB mirrors the ladder_tranches table (migration 000081). One row per ramp step per allocation; persisted as status=scheduled audit rows only in PR-2 (no firing sweep wired yet). Field order minimizes GC pointer-scan range.
type PaymentSchedule ¶
type PaymentSchedule string
PaymentSchedule is the cash-flow shape a payment-option token implies, stripped of the provider-specific spelling of that token. Two tokens that map to the same PaymentSchedule bill the customer identically; only the word differs (AWS spells all-upfront what Azure spells upfront, and AWS spells no-upfront what Azure and GCP spell monthly).
It exists so callers can tell a rename apart from a real change when NormalizePaymentOption rewrites a token: a rename is bookkeeping, a real change moves the customer's money and has to be disclosed (#1503).
const ( // PaymentScheduleUpfront: the whole commitment is charged once, at purchase. PaymentScheduleUpfront PaymentSchedule = "upfront" // PaymentSchedulePartialUpfront: part is charged at purchase, the rest recurs. PaymentSchedulePartialUpfront PaymentSchedule = "partial-upfront" // PaymentScheduleRecurring: nothing is charged at purchase; the commitment // is billed per period across the term. PaymentScheduleRecurring PaymentSchedule = "recurring" // PaymentScheduleUnknown: the token is not one of the modeled schedules. // Two unrecognized tokens compare equal under this classification, so // callers that must distinguish them have to compare the raw tokens too. // In practice unmapped tokens never reach a coercion comparison: // NormalizePaymentOption returns ok=false for them and the validator // rejects them at the next boundary. PaymentScheduleUnknown PaymentSchedule = "unknown" )
func PaymentScheduleFor ¶
func PaymentScheduleFor(token string) PaymentSchedule
PaymentScheduleFor classifies a payment-option token by the billing schedule it implies. The token must already be lowercased and trimmed (validatePurchaseRecommendation and NormalizePaymentOption both work on such tokens).
type PostgresStore ¶
type PostgresStore struct {
// contains filtered or unexported fields
}
PostgresStore implements StoreInterface using PostgreSQL.
func NewPostgresStore ¶
func NewPostgresStore(db *database.Connection) *PostgresStore
NewPostgresStore creates a new PostgreSQL-backed config store.
func (*PostgresStore) CancelAllPendingExchanges ¶
func (s *PostgresStore) CancelAllPendingExchanges(ctx context.Context) (int64, error)
CancelAllPendingExchanges cancels all pending RI exchange records regardless of origin. Kept for interface compatibility; new callers should prefer CancelPendingExchangesByOrigin to avoid cross-origin contamination.
func (*PostgresStore) CancelExecutionAtomic ¶
func (s *PostgresStore) CancelExecutionAtomic(ctx context.Context, tx pgx.Tx, executionID string, cancelledBy *string) (canceled bool, currentStatus string, err error)
CancelExecutionAtomic atomically transitions an execution from pending or notified to 'canceled' (canonical US spelling), setting canceled_by to the supplied actor (NULL when actor is nil). The UPDATE is conditional on status IN ('pending','notified') so a concurrent approve that has already transitioned the row to 'approved' causes zero rows to be affected and the method returns (false, currentStatus, nil) with the live status fetched via a follow-up SELECT. Returns (true, "canceled", nil) on success and (false, "", err) on a real DB error.
The 'scheduled' status is intentionally NOT accepted here -- the Gmail-style pre-fire delay revoke flow has its own CancelScheduledExecutionAtomic so the two paths surface distinct CAS race outcomes (cancel returns 409 on already-approved; scheduled-revoke returns 410 "window closed" on already-fired).
Callers must run the suppression cleanup in the same transaction; use the WithTx + DeleteSuppressionsByExecutionTx pairing at the call site exactly as the old SavePurchaseExecutionTx path did, except now the status guard is inside the UPDATE rather than checked optimistically before entering the tx.
func (*PostgresStore) CancelPendingExchangesByOrigin ¶
func (s *PostgresStore) CancelPendingExchangesByOrigin(ctx context.Context, origin common.ExchangeOrigin) (int64, error)
CancelPendingExchangesByOrigin cancels only pending records that match the given origin (gap G10 / issue #1348):
- common.ExchangeOriginStandalone: cancels WHERE ladder_run_id IS NULL
- common.ExchangeOriginLadder: cancels WHERE ladder_run_id IS NOT NULL
The origin is validated at this boundary; an unknown value fails loud rather than silently canceling the wrong partition on a money path.
DELIBERATE COARSE PARTITION: the ExchangeOriginLadder branch cancels EVERY ladder-linked pending record (ladder_run_id IS NOT NULL) across ALL ladder runs and configs, not just the current run's. This is acceptable today because the ladder never creates pending exchange records: buildRIExchangeConfig forces Mode=auto, which completes or fails immediately without leaving a pending row. Per-run / per-config scoping needs an additional selection key (e.g. the specific ladder_run_id or config_id) and is tracked in TODO(#1367).
func (*PostgresStore) CancelScheduledExecutionAtomic ¶
func (s *PostgresStore) CancelScheduledExecutionAtomic(ctx context.Context, tx pgx.Tx, executionID string, cancelledBy *string) (canceled bool, currentStatus string, err error)
CancelScheduledExecutionAtomic atomically transitions an execution from 'scheduled' to 'canceled' (canonical US spelling), setting canceled_by to the supplied actor (NULL when actor is nil). Used by the Gmail-style pre-fire delay revoke path (issue #290 / #291 wave-2): an approved-but-not-yet-fired execution can be revoked at $0 by flipping it to canceled before the scheduler fires the cloud SDK call.
The 'scheduled' status is the only accepted source. A concurrent scheduler tick that already transitioned the row to 'approved' or 'running' causes zero rows to be affected and the method returns (false, currentStatus, nil) -- the caller maps that to a 410 ("revocation window has closed") so the frontend can fall through to the post-execution Azure direct-cancel API path.
Returns (true, StatusCanceled, nil) on success and (false, "", err) on a real DB error. Must be called inside a WithTx block so the suppression cleanup commits atomically with the status flip.
func (*PostgresStore) ClaimMarketplaceListingSlot ¶
func (s *PostgresStore) ClaimMarketplaceListingSlot(ctx context.Context, purchaseID string) (bool, error)
ClaimMarketplaceListingSlot atomically reserves the marketplace-listing slot for a purchase_history row so two concurrent marketplace-list requests cannot both proceed to create a duplicate AWS listing (issue #292). The single conditional UPDATE transitions listing_state to ListingStatePending only when the row is not already active or pending, so exactly one racing request wins. It returns (true, nil) when this call reserved the slot and (false, nil) when the row is already active/pending (or absent); the caller maps false to a 409.
func (*PostgresStore) ClaimRIExchangeIdempotencyKey ¶
func (s *PostgresStore) ClaimRIExchangeIdempotencyKey(ctx context.Context, key string, window time.Duration) (bool, error)
ClaimRIExchangeIdempotencyKey atomically claims an RI exchange submit fingerprint (issue #1642). See the interface doc for the contract.
The whole decision is one statement, so two concurrent submits of the same fingerprint cannot both win: the INSERT and the conditional takeover of an expired row are the same atomic row operation, with no read-then-write window between them. RowsAffected() is 1 only when this call inserted the row or took over an expired one; the ON CONFLICT ... WHERE predicate evaluating false leaves it at 0.
Both the write and the expiry comparison use now(), the DATABASE clock, so clock skew between concurrent application instances cannot distort the window.
func (*PostgresStore) CleanupOldExecutions ¶
CleanupOldExecutions deletes purchase executions older than retentionDays.
Three independent cleanup branches, each with its own retention window so that a row far in one dimension doesn't block cleanup in the other:
Completed-state cleanup: `status = 'completed' AND scheduled_date < NOW() - retention`. Keeps recent completions visible in the UI for at least `retention` days before purging. `scheduled_date` is the right clock here because a completed row executed on (or very near) the day it was scheduled for.
Canceled-state cleanup: `status IN ('cancelled', 'canceled') AND updated_at < NOW() - retention`. Both spellings are included to cover legacy rows ('cancelled') and new code rows ('canceled', canonical US spelling per StatusCanceled / migration 000089).
This branch retains on `updated_at`, NOT `scheduled_date`, and must stay that way: CountExecutionsByPlanAndStatus windows the plan health score's canceled count on `updated_at` over exactly this retention period (see internal/api/plan_health.go planHealthLookbackDays), and CancelExecutionAtomic stamps `updated_at` while leaving `scheduled_date` untouched. Retaining on `scheduled_date` therefore purged rows the score was still counting: cancel a purchase whose scheduled_date is already past the horizon (a plan's rows are created up front, and parseCreatePurchasesRequest accepts a past start date) and the next sweep deleted it the same day, silently raising that plan's health score by up to 20 points overnight with no operator action. Nothing accumulates indefinitely: a genuinely old canceled row still has an old `updated_at`.
This cuts the other way too, and the change is user-visible: a canceled row with a FUTURE `scheduled_date` used to be retained forever, because `scheduled_date < NOW() - retention` was never true for it. It now leaves History `retention` days after the cancellation rather than `retention` days after the date it was scheduled for, so a purchase canceled today for a 2-year-out date disappears in a month instead of in two years. That closes an unbounded-retention leak and is the behavior the health window already assumed.
Expired-row cleanup: `expires_at IS NOT NULL AND expires_at < NOW() - retention`, for any status the health score does not count. This is the row's own TTL column (SavePurchaseExecution writes it from PurchaseExecution.TTL), NOT the approval-token deadline -- that is a separate column, `approval_token_expires_at`, added by migration
A row whose TTL lapsed longer than `retention` ago is dead, so without this branch it would accumulate indefinitely.
Every status in HealthScoredExecutionStatuses is excluded here, which is the whole reason that slice is exported rather than spelled out twice. `expires_at` is written once at insert and never rewritten when a row later reaches a terminal status, so it can be arbitrarily older than the terminal transition. Without the exclusion, a row that failed or was canceled TODAY but carries a lapsed `expires_at` is purged by this branch the same day, while the score is still counting it -- reintroducing through this branch the exact overnight score jump branch 2 exists to prevent, and at up to -40 for `failed`, twice canceled's worst case. Their own terminal-status branches govern them.
`failed` deliberately has no cleanup branch of its own: the health score's lookback comment (internal/api/plan_health.go planHealthLookbackDays) documents that this sweep "never deletes failed ones", and the score's window depends on that being true.
Note on reachability: `expires_at` currently has exactly one writer, SavePurchaseExecution via timeFromTTL(execution.TTL), and no production path assigns PurchaseExecution.TTL, so rows created by current code leave it NULL and this branch is inert for them. It can still hold non-NULL values on legacy imported rows, which is why the exclusion is written defensively rather than omitted.
The branches are OR'd: a row that qualifies under ANY is deleted, regardless of the other columns. An earlier revision of this function incorrectly AND'd the `scheduled_date` gate with both branches, which meant pending rows with a far-future `scheduled_date` but a long-past `expires_at` never got cleaned up (a 2-year-out purchase whose row TTL lapsed would leave a dead row accumulating for 1.9 years). Keep each branch fully parenthesized so a future edit can't change the predicate's meaning through AND/OR precedence.
NULL `expires_at` is excluded from branch 3 so rows that never had a TTL are safe from expiry-based cleanup -- which, per the reachability note above, is every row current code writes.
func (*PostgresStore) ClearCollectionStarted ¶
func (s *PostgresStore) ClearCollectionStarted(ctx context.Context, token string) error
ClearCollectionStarted clears last_collection_started_at (and the owner token) so the frontend knows an async collection has finished. Called by the scheduler on both success and failure paths. On the success path, last_collected_at and last_collection_error are updated by UpsertRecommendations/ReplaceRecommendations, so this method only touches started_at and the owner column.
The clear is scoped to rows where last_collection_owner_id still matches token (issue #261): a caller whose token no longer matches (another run has since started and won the race) has nothing left to clear and this is a documented silent no-op, not an error. An empty token is a boundary error: only a caller that actually won MarkCollectionStarted should ever call Clear; callers with no marker to own (cron, cold-start) must skip the call entirely rather than pass an empty token.
func (*PostgresStore) ClearRevocationInFlight ¶
func (s *PostgresStore) ClearRevocationInFlight(ctx context.Context, purchaseID string) error
ClearRevocationInFlight sets revocation_in_flight=false on a purchase_history row. Called when the Azure Return call fails transiently (before Azure actually issued a refund) so the row is not left stuck in the in-flight state, which would mislead the finalize_revocations sweep into thinking Azure succeeded (issue #290, second-wave CR Finding D). No-op when already false.
func (*PostgresStore) CompletePlanStep ¶
CompletePlanStep records that ramp step stepNumber of planID completed and advances the schedule to it, inside a transaction. The row is locked with SELECT FOR UPDATE so concurrent callers (overlapping Lambda invocations, multi-tick cron) cannot both read the same CurrentStep value and both write CurrentStep+1, skipping a step (issue #1071).
The operation is idempotent in stepNumber rather than a blind increment: a multi-account plan produces one execution per account per ramp step, and retrying two separately-failed accounts of the same step used to advance the ramp twice, so the plan reported itself a step further along than the commitment it had actually bought (issue #1669). Completing a step at or below CurrentStep therefore writes nothing and reports ErrRampStepAlreadyCounted.
Completing a step more than one beyond CurrentStep is refused for the same reason in the other direction: the steps in between never completed, and advancing over them would silently overstate how much the plan has bought.
Granularity: a step is complete when EVERY cloud account it fanned out to has bought, not when any one of them has (issue #1861) -- see requireRampStepBought. Until then the advance reports ErrRampStepIncomplete and the plan row is left untouched.
Returns nil when the plan no longer exists (deleted between execution and progress update) so the caller is not penalized for a race it cannot control.
func (*PostgresStore) CompleteRIExchange ¶
func (s *PostgresStore) CompleteRIExchange(ctx context.Context, id, exchangeID string) error
CompleteRIExchange marks an RI exchange as completed.
func (*PostgresStore) CompleteRIExchangeWithPayment ¶
func (s *PostgresStore) CompleteRIExchangeWithPayment(ctx context.Context, id, exchangeID, acceptedPaymentDue string) error
CompleteRIExchangeWithPayment marks an RI exchange as completed and updates payment_due to the amount AWS actually accepted. Use this on the manual-approval path instead of CompleteRIExchange so the daily-spend ledger (GetRIExchangeDailySpend) reflects the accepted amount, not the stale pre-execution quote (H3 fix).
func (*PostgresStore) CountExecutionsByPlanAndStatus ¶
func (s *PostgresStore) CountExecutionsByPlanAndStatus(ctx context.Context, statuses []string, since time.Time) (map[string]ExecutionStatusCounts, error)
CountExecutionsByPlanAndStatus returns, keyed by plan ID, the exact number of executions in each of the supplied statuses that last changed state at or after `since`.
Aggregated with GROUP BY rather than counted from a GetExecutionsByStatuses page: that method's DESC + LIMIT truncation would silently understate any plan whose executions fall outside the newest `limit` rows, which for the plan health score means an unhealthy plan quietly renders as healthy once newer rows from other plans push its failures out of the window. The result set here is bounded by plans x statuses, not by execution volume, so it needs no limit of its own.
The window is on updated_at, NOT scheduled_date, because for the terminal statuses this method exists to count those are different dates pointing in opposite directions. A plan's executions are created up front for the whole ramp, so a pending row carries a scheduled_date months in the FUTURE; canceling it (CancelExecutionAtomic) leaves that future date untouched. Windowing on scheduled_date would therefore count a purchase cancelled today under a date next year, and answer "how many rows are scheduled recently-or-later" instead of "how many changed state recently". updated_at is stamped by CancelExecutionAtomic and TransitionExecutionStatus, and backstopped by the update_purchase_executions_updated_at trigger, so it is when the row actually entered the status being counted.
plan_id has been nullable since migration 000033 (direct-execute purchases from the Recommendations page have no originating plan, and deleting a plan SET NULLs its executions). Those rows belong to no plan and are excluded in SQL; the scan still goes through sql.NullString so a NULL can never turn a per-plan count into a scan error that blanks the score for every plan.
func (*PostgresStore) CountPendingExecutionsForAccount ¶
func (s *PostgresStore) CountPendingExecutionsForAccount(ctx context.Context, accountID string) (int, error)
CountPendingExecutionsForAccount returns the number of pending/notified purchase executions still referencing this cloud account. The deleteAccount handler calls this before issuing DELETE FROM cloud_accounts so it can short-circuit with a 409 instead of letting migration 000053's ON DELETE RESTRICT bubble up as an opaque FK-violation error. See issue #606.
func (*PostgresStore) CreateAccountRegistration ¶
func (s *PostgresStore) CreateAccountRegistration(ctx context.Context, reg *AccountRegistration) error
CreateAccountRegistration inserts a new registration request. Returns an error wrapping "duplicate" when the partial unique index rejects a second pending registration for the same provider+external_id.
func (*PostgresStore) CreateCloudAccount ¶
func (s *PostgresStore) CreateCloudAccount(ctx context.Context, account *CloudAccount) error
CreateCloudAccount inserts a new cloud account record.
func (*PostgresStore) CreatePurchasePlan ¶
func (s *PostgresStore) CreatePurchasePlan(ctx context.Context, plan *PurchasePlan) error
CreatePurchasePlan creates a new purchase plan.
func (*PostgresStore) CreateSuppression ¶
func (s *PostgresStore) CreateSuppression(ctx context.Context, sup *PurchaseSuppression) error
CreateSuppression inserts a suppression row using a one-call transaction. Callers that need to bundle this write with other statements (e.g. the matching execution insert) should use CreateSuppressionTx inside a WithTx block instead.
func (*PostgresStore) CreateSuppressionTx ¶
func (s *PostgresStore) CreateSuppressionTx(ctx context.Context, tx pgx.Tx, sup *PurchaseSuppression) error
CreateSuppressionTx inserts a suppression row inside a caller-owned transaction. sup.ID is populated from the Postgres-generated UUID if the caller left it blank.
func (*PostgresStore) DeleteAccountCredentials ¶
func (s *PostgresStore) DeleteAccountCredentials(ctx context.Context, accountID string) error
DeleteAccountCredentials removes all credential records for an account.
func (*PostgresStore) DeleteAccountRegistration ¶
func (s *PostgresStore) DeleteAccountRegistration(ctx context.Context, id string) error
DeleteAccountRegistration removes a registration record by ID.
func (*PostgresStore) DeleteAccountServiceOverride ¶
func (s *PostgresStore) DeleteAccountServiceOverride(ctx context.Context, accountID, provider, service string) error
DeleteAccountServiceOverride removes an override, reverting to global defaults.
func (*PostgresStore) DeleteCloudAccount ¶
func (s *PostgresStore) DeleteCloudAccount(ctx context.Context, id string) error
DeleteCloudAccount deletes a cloud account. Cascades to credentials and overrides. If an approved account_registrations row points at this account, it is reset to 'pending' in the same transaction so the admin can re-approve through the normal flow instead of being left with a dead-end "Approved (account pending link)" row.
func (*PostgresStore) DeletePurchasePlan ¶
func (s *PostgresStore) DeletePurchasePlan(ctx context.Context, planID string) error
DeletePurchasePlan deletes a purchase plan.
func (*PostgresStore) DeleteSuppressionsByExecution ¶
func (s *PostgresStore) DeleteSuppressionsByExecution(ctx context.Context, executionID string) error
DeleteSuppressionsByExecution deletes all suppression rows for the given execution using a one-call transaction. Used by the cancel path when the whole execution's capacity should be un-suppressed.
func (*PostgresStore) DeleteSuppressionsByExecutionTx ¶
func (s *PostgresStore) DeleteSuppressionsByExecutionTx(ctx context.Context, tx pgx.Tx, executionID string) error
DeleteSuppressionsByExecutionTx deletes all suppression rows for the given execution inside a caller-owned transaction. Safe to call on an execution with no suppression rows (e.g. grace_period_days=0 at insert time) — returns nil, no error.
func (*PostgresStore) FailRIExchange ¶
func (s *PostgresStore) FailRIExchange(ctx context.Context, id, errorMsg string) error
FailRIExchange marks an RI exchange as failed.
func (*PostgresStore) FlipPurchaseRevocationInFlight ¶
func (s *PostgresStore) FlipPurchaseRevocationInFlight(ctx context.Context, purchaseID string) error
FlipPurchaseRevocationInFlight sets revocation_in_flight=true on the purchase_history row for purchaseID. Called immediately before the Azure Return API call to enable partial-success reconciliation (issue #290 Finding #6). Idempotent: already-true rows are not modified. Returns a not-found error when no row matches.
func (*PostgresStore) GetAccountCredential ¶
func (s *PostgresStore) GetAccountCredential(ctx context.Context, accountID, credentialType string) (string, error)
GetAccountCredential returns the encrypted blob for an account credential.
func (*PostgresStore) GetAccountRegistration ¶
func (s *PostgresStore) GetAccountRegistration(ctx context.Context, id string) (*AccountRegistration, error)
GetAccountRegistration returns a single registration by UUID.
func (*PostgresStore) GetAccountRegistrationByToken ¶
func (s *PostgresStore) GetAccountRegistrationByToken(ctx context.Context, token string) (*AccountRegistration, error)
GetAccountRegistrationByToken returns a registration by its reference_token.
func (*PostgresStore) GetAccountServiceOverride ¶
func (s *PostgresStore) GetAccountServiceOverride(ctx context.Context, accountID, provider, service string) (*AccountServiceOverride, error)
GetAccountServiceOverride returns a single override, or nil if none exists.
func (*PostgresStore) GetActivePurchaseHistory ¶
func (s *PostgresStore) GetActivePurchaseHistory(ctx context.Context, asOf time.Time, accountIDs []string, externalIDsByProvider map[string][]string) ([]PurchaseHistoryRecord, error)
GetActivePurchaseHistory retrieves every purchase_history row still within its commitment term at asOf, optionally scoped to a set of accounts via the same dual-column predicate as GetPurchaseHistoryFiltered (appendAccountPredicate): both accountIDs (cloud_accounts UUIDs matched on cloud_account_id) and externalIDsByProvider (provider-scoped external numbers matched on account_id) are applied with OR semantics so rows carrying only one identifier are still returned (issues #701/#498/#866). Both empty means all accounts. The active filter is pushed into SQL so the result is bounded by the number of live commitments (not by all history ever recorded), which is what the analytics collector, dashboard KPIs, and inventory endpoints need: it cannot silently truncate older-but-still-active 1y/3y commitments the way a newest-first LIMIT page does (issue #1140). term*8760 hours matches the collector's HoursPerYear and the API layer's commitmentExpiry (both 365*24) so the SQL and Go term windows agree. The expiry comparison is inclusive (expiry >= asOf): a commitment expiring exactly at asOf is still active, matching the API layer's isActiveCommitment (!now.After(expiry)) so the SQL result set and the Go-side active checks share one boundary definition.
func (*PostgresStore) GetAllPurchaseHistory ¶
func (s *PostgresStore) GetAllPurchaseHistory(ctx context.Context, limit int) ([]PurchaseHistoryRecord, error)
GetAllPurchaseHistory retrieves all purchase history.
func (*PostgresStore) GetCloudAccount ¶
func (s *PostgresStore) GetCloudAccount(ctx context.Context, id string) (*CloudAccount, error)
GetCloudAccount returns a single cloud account by ID with credentials_configured derived.
func (*PostgresStore) GetCloudAccountByExternalID ¶
func (s *PostgresStore) GetCloudAccountByExternalID(ctx context.Context, provider, externalID string) (*CloudAccount, error)
GetCloudAccountByExternalID returns a single cloud account matched by (provider, external_id). Used by the scheduler ambient-path tagging fix for issue #604 — when the Lambda's STS identity matches a registered account (regardless of enabled state), rec rows are stamped with that account's UUID so the approve modal shows the account name instead of `(ambient)`. Returns (nil, nil) when no row matches.
The cloud_accounts table declares `UNIQUE(provider, external_id)` (migration 000011), which guarantees the lookup hits an index.
func (*PostgresStore) GetExecutionByID ¶
func (s *PostgresStore) GetExecutionByID(ctx context.Context, executionID string) (*PurchaseExecution, error)
GetExecutionByID retrieves a purchase execution by execution ID. Returns an error wrapping ErrNotFound when no row matches executionID so callers can cleanly distinguish "not found" (errors.Is(err, ErrNotFound)) from a real DB failure (any other non-nil error). A nil error guarantees a non-nil execution (fail-loud contract; issues #976, #1339).
func (*PostgresStore) GetExecutionByPlanAndDate ¶
func (s *PostgresStore) GetExecutionByPlanAndDate(ctx context.Context, planID string, scheduledDate time.Time) (*PurchaseExecution, error)
GetExecutionByPlanAndDate retrieves execution for a specific plan and date.
func (*PostgresStore) GetExecutionsByStatuses ¶
func (s *PostgresStore) GetExecutionsByStatuses(ctx context.Context, statuses []string, limit int) ([]PurchaseExecution, error)
GetExecutionsByStatuses returns executions whose Status is any of the supplied values, newest-first, capped at `limit`. Used by the History handler to merge pending/failed/expired rows alongside completed purchases without changing the narrower GetPendingExecutions contract the scheduler depends on.
func (*PostgresStore) GetGlobalConfig ¶
func (s *PostgresStore) GetGlobalConfig(ctx context.Context) (*GlobalConfig, error)
GetGlobalConfig retrieves the global configuration.
func (*PostgresStore) GetInFlightLadderCommitUSDHr ¶
func (s *PostgresStore) GetInFlightLadderCommitUSDHr(ctx context.Context, configID string) (*float64, error)
GetInFlightLadderCommitUSDHr returns the total hourly USD commitment already in flight for the given config: the SUM of amount_usd_hr for ladder_tranches rows where config_id=$1 and status = 'scheduled'.
SCHEDULED ONLY: fired/completed tranches are executed purchases already counted in the engine's ExistingUSDPerHour (the provider adapters fold payment-pending and active commitments into E). Summing them here as well would double-subtract them from the gap and cause under-purchasing. Only not-yet-fired (scheduled) tranches are genuinely "in flight" and absent from E, so they alone must be netted out of the gap.
Returns a non-nil *float64 (zero when no scheduled tranches exist); never returns nil without a non-nil error, so callers can pass it directly to AllocationInput.InFlightUSDPerHour without an extra nil-guard.
func (*PostgresStore) GetLadderConfig ¶
func (s *PostgresStore) GetLadderConfig(ctx context.Context, cloudAccountID, provider string) (*LadderConfigDB, error)
GetLadderConfig returns the ladder_config row for the given (cloud_account_id, provider) pair. Returns (nil, nil) when no row exists.
func (*PostgresStore) GetLadderConfigs ¶
func (s *PostgresStore) GetLadderConfigs(ctx context.Context) ([]LadderConfigDB, error)
GetLadderConfigs returns all ladder_configs rows, newest first. Returns an empty slice (not nil) when no rows exist.
func (*PostgresStore) GetLadderRun ¶
func (s *PostgresStore) GetLadderRun(ctx context.Context, id string) (*LadderRunDB, error)
GetLadderRun returns the ladder_runs row for the given id, or (nil, nil) when no row exists.
func (*PostgresStore) GetPendingExecutions ¶
func (s *PostgresStore) GetPendingExecutions(ctx context.Context) ([]PurchaseExecution, error)
GetPendingExecutions retrieves all pending purchase executions.
func (*PostgresStore) GetPendingExecutionsTx ¶
func (s *PostgresStore) GetPendingExecutionsTx(ctx context.Context, tx pgx.Tx) ([]PurchaseExecution, error)
GetPendingExecutionsTx is the tx-accepting variant of GetPendingExecutions. Running the read inside the same transaction as the subsequent insert makes duplicate-detection and execution creation atomic, closing the TOCTOU race in executePurchase (issue #643).
func (*PostgresStore) GetPlanAccounts ¶
func (s *PostgresStore) GetPlanAccounts(ctx context.Context, planID string) ([]CloudAccount, error)
GetPlanAccounts returns all cloud accounts associated with a plan.
func (*PostgresStore) GetPlannedExecutions ¶
func (s *PostgresStore) GetPlannedExecutions(ctx context.Context, statuses []string, limit int) ([]PurchaseExecution, error)
GetPlannedExecutions returns executions whose Status is any of the supplied values, ordered by scheduled_date ASC (soonest first), capped at `limit`. Used by the Planned Purchases handler where the user expects to act on imminent rows first.
Distinct from GetExecutionsByStatuses (DESC + LIMIT for History's newest-first semantics): when total rows exceed `limit`, a DESC truncation drops the soonest rows, exactly the ones this list must surface. Sorting the already-truncated subset in-memory cannot recover them.
Secondary sort by id ASC keeps ordering stable when multiple rows share a scheduled_date. NULLS LAST is defensive: the schema makes scheduled_date NOT NULL today, but the clause guards against a future relaxation silently hiding rows at the top of the list.
func (*PostgresStore) GetPurchaseHistory ¶
func (s *PostgresStore) GetPurchaseHistory(ctx context.Context, accountID string, limit int) ([]PurchaseHistoryRecord, error)
GetPurchaseHistory retrieves purchase history for an account.
func (*PostgresStore) GetPurchaseHistoryByPurchaseID ¶
func (s *PostgresStore) GetPurchaseHistoryByPurchaseID(ctx context.Context, purchaseID string) (*PurchaseHistoryRecord, error)
GetPurchaseHistoryByPurchaseID returns the single purchase_history row whose purchase_id matches purchaseID. Returns (nil, nil) when the row does not exist. The revocation-window columns (revocation_window_closes_at, revoked_at, revoked_via, support_case_id) are read alongside the base columns so the revoke endpoint can check idempotency without a second round trip (issue #290). The marketplace columns (offering_class, listing_id, listing_state) are read so the marketplace-list handler can validate offering_class and look up the cloud account (issue #292).
func (*PostgresStore) GetPurchaseHistoryFiltered ¶
func (s *PostgresStore) GetPurchaseHistoryFiltered( ctx context.Context, filter PurchaseHistoryFilter, ) ([]PurchaseHistoryRecord, error)
GetPurchaseHistoryFiltered reads purchase_history rows matching the supplied filter set, newest-first, capped at filter.Limit. See the StoreInterface docstring and PurchaseHistoryFilter for the per-field semantics. Each WHERE clause is appended only when its field is populated, so an empty filter gets the same plan-shape as GetAllPurchaseHistory. Implementation mirrors buildRecommendationFilter (store_postgres_recommendations.go).
The account predicate matches BOTH identifier columns:
(cloud_account_id = ANY($uuids) OR (provider = $p AND account_id = ANY($extsForP)) OR ...)
purchase_history carries two account identifiers and either may be the only one populated on a given row: cloud_account_id (the cloud_accounts UUID FK, added in migration 000011 with no backfill, so NULL on every direct-execute / ambient / pre-000011 row) and account_id (the cloud-provider external number, e.g. an AWS account number, always populated). The top-bar Account chip emits the UUID, so a UUID-only predicate silently dropped every NULL-cloud_account_id row (issue #701/#498) while an external-only predicate dropped every row that only has the UUID (issue #866). Matching both columns includes rows written by either path. The caller resolves the requested UUIDs to their (provider, external_id) pairs scoped to the user's accessible accounts and groups the external ids by provider, so the external-id half stays provider-scoped and a reused external number (aws/123 vs azure/123) cannot leak the wrong rows.
func (*PostgresStore) GetPurchaseHistoryInFlight ¶
func (s *PostgresStore) GetPurchaseHistoryInFlight(ctx context.Context) ([]*PurchaseHistoryRecord, error)
GetPurchaseHistoryInFlight returns all purchase_history rows with revocation_in_flight=true and revoked_at IS NULL. Used by the finalize_revocations scheduled sweep to retry MarkPurchaseRevoked for rows where the Azure Return succeeded but the subsequent DB write failed (issue #290 Finding #6).
func (*PostgresStore) GetPurchasePlan ¶
func (s *PostgresStore) GetPurchasePlan(ctx context.Context, planID string) (*PurchasePlan, error)
GetPurchasePlan retrieves a purchase plan by ID.
func (*PostgresStore) GetRIExchangeDailySpend ¶
func (s *PostgresStore) GetRIExchangeDailySpend(ctx context.Context, date time.Time) (string, error)
GetRIExchangeDailySpend returns total payment_due for completed and in-flight (processing) exchanges on a given date (UTC).
M5 fix: including 'processing' rows prevents a TOCTOU race where two concurrent approvals both read the same daily-spend total (before either exchange's ledger row is committed) and together exceed the daily cap. For 'completed' rows the time anchor is completed_at; for 'processing' rows it is updated_at (the moment the record transitioned to processing, i.e. when it was approved).
func (*PostgresStore) GetRIExchangeHistory ¶
func (s *PostgresStore) GetRIExchangeHistory(ctx context.Context, since time.Time, limit int) ([]RIExchangeRecord, error)
GetRIExchangeHistory retrieves RI exchange history records.
func (*PostgresStore) GetRIExchangeRecord ¶
func (s *PostgresStore) GetRIExchangeRecord(ctx context.Context, id string) (*RIExchangeRecord, error)
GetRIExchangeRecord retrieves an RI exchange record by ID.
func (*PostgresStore) GetRIExchangeRecordByToken ¶
func (s *PostgresStore) GetRIExchangeRecordByToken(ctx context.Context, token string) (*RIExchangeRecord, error)
GetRIExchangeRecordByToken retrieves an RI exchange record by approval token.
func (*PostgresStore) GetRIUtilizationCache ¶
func (s *PostgresStore) GetRIUtilizationCache(ctx context.Context, region string, lookbackDays int) (*RIUtilizationCacheEntry, error)
GetRIUtilizationCache returns the cached Cost Explorer utilization result for (region, lookback_days) or nil if no row exists. Staleness is evaluated by the caller — the query doesn't filter on fetched_at so a caller with a longer TTL than expected can still use the row.
func (*PostgresStore) GetRecommendationsFreshness ¶
func (s *PostgresStore) GetRecommendationsFreshness(ctx context.Context) (*RecommendationsFreshness, error)
GetRecommendationsFreshness returns the singleton freshness row. The table is seeded with id=1 by the migration so a row always exists; LastCollectedAt, LastCollectionError, and LastCollectionStartedAt can be NULL.
func (*PostgresStore) GetScheduledExecutionsDue ¶
func (s *PostgresStore) GetScheduledExecutionsDue(ctx context.Context) ([]PurchaseExecution, error)
GetScheduledExecutionsDue returns purchase_executions with status='scheduled' whose scheduled_execution_at has elapsed (scheduled_execution_at <= NOW()). Used by the Gmail-style pre-fire delay scheduler tick (issue #291 wave-2). Results are ordered oldest-due-first so the scheduler fires them in FIFO order. Capped at MaxListLimit per sweep to bound the per-tick blast radius.
func (*PostgresStore) GetServiceConfig ¶
func (s *PostgresStore) GetServiceConfig(ctx context.Context, provider, service string) (*ServiceConfig, error)
GetServiceConfig retrieves configuration for a specific service.
func (*PostgresStore) GetStaleApprovedExecutions ¶
func (s *PostgresStore) GetStaleApprovedExecutions(ctx context.Context, olderThan time.Duration) ([]PurchaseExecution, error)
GetStaleApprovedExecutions returns executions stuck in the "approved" status whose last update is older than olderThan. These are executions that were flipped to "approved" by ApproveAndExecute but whose synchronous purchase run never finalized (Lambda timeout, cold-start eviction, panic) — issue #632. updated_at is stamped to NOW() at the moment of the approved transition (see TransitionExecutionStatus) and is not touched again unless the run finalizes, so it is the age of the strand. Mirrors GetStaleProcessingExchanges.
func (*PostgresStore) GetStaleProcessingExchanges ¶
func (s *PostgresStore) GetStaleProcessingExchanges(ctx context.Context, olderThan time.Duration) ([]RIExchangeRecord, error)
GetStaleProcessingExchanges returns processing exchanges older than the given duration.
func (*PostgresStore) GetStuckRampSteps ¶
func (s *PostgresStore) GetStuckRampSteps(ctx context.Context) (map[string]RampStepBlock, error)
GetStuckRampSteps returns, keyed by plan ID, the plan's next ramp step and how many of that step's units are stuck on it -- never bought, latest attempt terminal and unsuccessful, no retry in flight.
Derived on every read rather than stamped on a row when the advance was refused, which is what makes it safe to score a plan's health on: a stamped refusal is true only at the instant it is written and nothing clears it, so a plan that recovered would stay marked unhealthy until an operator noticed. Recomputing from the same rows the advance gate reads means the report disappears exactly when the ramp unfreezes, and cannot drift from the gate.
Plans with nothing stuck are absent from the map rather than present with a zero, so a caller cannot confuse "healthy" with "not reported".
func (*PostgresStore) GetUserEmailByID ¶
GetUserEmailByID resolves the email address of the auth user identified by userID. The `users` table belongs to internal/auth's schema, but internal/ auth already imports internal/config (service_mfa.go), so internal/config cannot import internal/auth back without a cycle. This method queries the shared database directly by table/column name instead, returning a plain string so no auth.User type crosses the package boundary. Returns ("", nil) when userID does not resolve to a row -- callers must treat that as "identity unresolved," not as a legitimately blank email.
func (*PostgresStore) HasAccountCredentials ¶
HasAccountCredentials returns true if at least one credential exists for the account.
func (*PostgresStore) IsNotificationMuted ¶
func (s *PostgresStore) IsNotificationMuted(ctx context.Context, recipientEmail, scope string) (bool, error)
IsNotificationMuted returns true when (email, scope) has a matching row in muted_recipients. The email lookup is case-insensitive (LOWER on insert plus LOWER($1) here).
func (*PostgresStore) LatestLadderRunStartedAt ¶
func (s *PostgresStore) LatestLadderRunStartedAt(ctx context.Context, configID string) (*time.Time, error)
LatestLadderRunStartedAt returns the maximum started_at for the given config_id, or nil when no run has been recorded yet. Drives the per-cadence self-gate in handleLadderRun (Q6).
func (*PostgresStore) ListAccountRegistrations ¶
func (s *PostgresStore) ListAccountRegistrations(ctx context.Context, filter AccountRegistrationFilter) ([]AccountRegistration, error)
ListAccountRegistrations returns registrations matching the filter.
func (*PostgresStore) ListAccountServiceOverrides ¶
func (s *PostgresStore) ListAccountServiceOverrides(ctx context.Context, accountID string) ([]AccountServiceOverride, error)
ListAccountServiceOverrides returns all overrides for an account.
func (*PostgresStore) ListActiveSuppressions ¶
func (s *PostgresStore) ListActiveSuppressions(ctx context.Context) ([]PurchaseSuppression, error)
ListActiveSuppressions returns every suppression row whose expiry hasn't passed. The scheduler groups these by the 6-tuple key (account, provider, service, region, resource_type, engine) to subtract cumulative suppressed counts from its rec-list output.
func (*PostgresStore) ListCloudAccounts ¶
func (s *PostgresStore) ListCloudAccounts(ctx context.Context, filter CloudAccountFilter) ([]CloudAccount, error)
ListCloudAccounts returns accounts matching the filter, with credentials_configured derived.
func (*PostgresStore) ListPendingExecutionIDsForAccount ¶
func (s *PostgresStore) ListPendingExecutionIDsForAccount(ctx context.Context, accountID string) ([]string, error)
ListPendingExecutionIDsForAccount returns the execution IDs that the frontend's Cancel-All-Then-Delete flow needs to POST cancel for. Capped at 1000 rows — a single account with more pending executions than that is an unusual operator-cleanup task rather than a button-click flow.
func (*PostgresStore) ListPurchasePlans ¶
func (s *PostgresStore) ListPurchasePlans(ctx context.Context, filter PurchasePlanFilter) ([]PurchasePlan, error)
ListPurchasePlans lists purchase plans, optionally filtered by account IDs. When filter.AccountIDs is non-empty the result includes both plans that reference at least one of the given accounts AND legacy plans with zero plan_accounts rows (flagged with Unassigned=true). Plans that have at least one account row are returned with Unassigned=false. The no-filter case returns all plans with Unassigned=false.
func (*PostgresStore) ListServiceConfigs ¶
func (s *PostgresStore) ListServiceConfigs(ctx context.Context) ([]ServiceConfig, error)
ListServiceConfigs lists all service configurations.
LIMIT 1000 caps the result set at three orders of magnitude above the realistic upper bound (each cloud has a bounded set of services, so the total is roughly (providers × service-types × per-service-variants), which stays under ~150 even with generous provider growth). The cap is defense-in-depth against a compromised admin inserting millions of rows and matches the sibling GetPendingExecutions limit.
func (*PostgresStore) ListStoredRecommendations ¶
func (s *PostgresStore) ListStoredRecommendations(ctx context.Context, filter RecommendationFilter) ([]RecommendationRecord, error)
ListStoredRecommendations reads recommendations matching the filter. SQL-pushed conditions (Provider, Service, Region, AccountIDs, MinSavingsUSD) are applied in SQL so Postgres prunes the rows; the MinSavingsPct filter is applied in-process because the on-demand baseline lives inside the JSONB payload (not a native column).
func (*PostgresStore) ListStuckExecutions ¶
func (s *PostgresStore) ListStuckExecutions(ctx context.Context, statuses []string, olderThan time.Duration) ([]PurchaseExecution, error)
ListStuckExecutions returns purchase executions whose Status is any of the supplied values and whose updated_at is older than the given duration. Used by the reaper sweep (issue #678) to find executions stuck in approved/running long enough that the synchronous executor has clearly failed without flipping the row to a terminal state.
Returns rows oldest-first (ORDER BY updated_at ASC) so the longest-stuck rows are processed first within a single sweep, capped at MaxListLimit so an unbounded backlog doesn't blow up the Lambda's memory budget. The reaper invokes the sweep periodically; a backlog larger than MaxListLimit just gets drained across successive invocations.
olderThan must be > 0; a zero/negative value would invert the WHERE clause into "updated_at < NOW() + |olderThan|" and reap fresh rows. Defense-in- depth: the caller (ParseReapAfterFromEnv) also rejects non-positive env values.
olderThan is passed as a Postgres interval (seconds) so the comparison happens server-side against NOW() — keeping the cutoff in the DB clock avoids any drift between the API process and the database.
func (*PostgresStore) LockPurchasePlanTx ¶
func (s *PostgresStore) LockPurchasePlanTx(ctx context.Context, tx pgx.Tx, planID string) (*PurchasePlan, error)
LockPurchasePlanTx reads a purchase plan under a row lock held for the rest of tx. It is the single definition of "the per-plan ramp lock": every path that reads a plan's ramp position in order to write against it calls THIS method, so completions and creations serialize against each other rather than each racing on its own read. CompletePlanStep below is one caller; the create-planned-purchases handler is the other.
Returns (nil, nil) when the plan does not exist, which callers must handle explicitly: CompletePlanStep treats it as a benign race it cannot control, the create path turns it into a 404.
func (*PostgresStore) MarkCollectionStarted ¶
func (s *PostgresStore) MarkCollectionStarted(ctx context.Context) (token string, ok bool, err error)
MarkCollectionStarted atomically sets last_collection_started_at = NOW() only when no in-flight collection is currently running. The WHERE clause treats a started_at older than 5 minutes as stale (the scheduler Lambda must have crashed) so a new collection can proceed rather than being permanently blocked. A fresh owner token is stamped into last_collection_owner_id alongside the timestamp so the caller that wins the race is the only one that can later clear the marker (issue #261 compare-and-clear guard; see ClearCollectionStarted).
Returns the token and true when this caller won the race (rowsAffected == 1) and should proceed with the async invoke, threading the token through so it can be passed to ClearCollectionStarted later. Returns ("", false, nil) when another collection is already in flight (rowsAffected == 0), signaling the handler to return 409 Conflict.
func (*PostgresStore) MarkPurchaseRevoked ¶
func (s *PostgresStore) MarkPurchaseRevoked(ctx context.Context, purchaseID string, revokedAt time.Time, revokedVia, supportCaseID string, calcRefundAmount *float64, calcRefundCurrency string) error
MarkPurchaseRevoked stamps revoked_at / revoked_via / support_case_id and the refund-quote audit columns (calc_refund_amount, calc_refund_currency) on the purchase_history row identified by purchaseID. The UPDATE is a no-op when revoked_at is already non-null (idempotency guard). Returns a not-found error when zero rows are affected and revoked_at was previously NULL.
func (*PostgresStore) OccupiedRampStepsInRangeTx ¶
func (s *PostgresStore) OccupiedRampStepsInRangeTx(ctx context.Context, tx pgx.Tx, planID string, from, to int) ([]int, error)
OccupiedRampStepsInRangeTx returns the steps between from and to (inclusive) of planID that already have a fan-out unit which bought or is still working, ascending. It runs in the caller's transaction so the answer can be acted on atomically; see the lock requirement below.
It exists so a caller about to mint executions for a range of steps can refuse to target one that is already covered. Both halves of the predicate are load-bearing and neither subsumes the other:
- A step some account BOUGHT must not get a fresh root row. The completeness gate holds CurrentStep still while an account is outstanding, so the plan-scoped create endpoint keeps stamping CurrentStep+1, the same step. Approving that row re-fans-out across every account including the ones that already bought, under a fresh idempotency lineage whose derived tokens miss the provider-side dedupe entirely: a genuine duplicate commitment, not a no-op.
- A step that already has a LIVE unit must not get a second one either. Two concurrent creates both find nothing bought, and each mints its own root row for the same step; approving both double-buys exactly as above. The first create's own pending row is what the second must see, and it is not "bought", so the bought half alone cannot stop it.
A step whose units all settled without buying (canceled) is NOT occupied: the operator abandoned it and rescheduling it is the intended recovery.
CALLER CONTRACT: hold LockPurchasePlanTx on planID for the same transaction before calling this and until the resulting inserts commit. Without that lock the answer is advisory -- two callers read it concurrently, both see the step free and both insert.
func (*PostgresStore) ReplaceRecommendations ¶
func (s *PostgresStore) ReplaceRecommendations(ctx context.Context, collectedAt time.Time, recs []RecommendationRecord) error
ReplaceRecommendations wipes the recommendations table and reinserts the full snapshot inside a single transaction. Used for a force-full-resync path; the steady-state write path (see commit 6) is UpsertRecommendations. Atomic replace means concurrent readers either see the full old snapshot or the full new one — never a partial mid-replace state.
func (*PostgresStore) SaveAccountCredential ¶
func (s *PostgresStore) SaveAccountCredential(ctx context.Context, accountID, credentialType, encryptedBlob string) error
SaveAccountCredential upserts an encrypted credential blob for an account.
func (*PostgresStore) SaveAccountServiceOverride ¶
func (s *PostgresStore) SaveAccountServiceOverride(ctx context.Context, o *AccountServiceOverride) error
SaveAccountServiceOverride upserts an account service override.
func (*PostgresStore) SaveGlobalConfig ¶
func (s *PostgresStore) SaveGlobalConfig(ctx context.Context, config *GlobalConfig) error
SaveGlobalConfig saves the global configuration.
func (*PostgresStore) SaveLadderRun ¶
func (s *PostgresStore) SaveLadderRun(ctx context.Context, run *LadderRunDB) (*LadderRunDB, error)
SaveLadderRun inserts a new ladder_runs row. If run.ID is empty a fresh UUID is generated. Returns the persisted row with all DB-stamped fields populated.
func (*PostgresStore) SaveLadderRunWithTranches ¶
func (s *PostgresStore) SaveLadderRunWithTranches(ctx context.Context, run *LadderRunDB, tranches []LadderTrancheDB) (*LadderRunDB, error)
SaveLadderRunWithTranches inserts the ladder_runs row AND its ladder_tranches rows in ONE transaction. If any tranche insert fails, the whole transaction (including the run row) is rolled back, so a run is never persisted without its tranches. This prevents a status=planned run with zero tranches, which the cadence self-gate (keyed on any run's started_at) would otherwise use to suppress the retry for the full cadence window. If run.ID is empty a fresh UUID is generated. Returns the persisted run row.
func (*PostgresStore) SaveLadderTranches ¶
func (s *PostgresStore) SaveLadderTranches(ctx context.Context, tranches []LadderTrancheDB) error
SaveLadderTranches inserts a batch of ladder_tranches rows within a single transaction. Each tranche must carry a non-empty ID; duplicate IDs are rejected at the DB UNIQUE PRIMARY KEY constraint. An empty slice is a no-op.
func (*PostgresStore) SavePurchaseExecution ¶
func (s *PostgresStore) SavePurchaseExecution(ctx context.Context, execution *PurchaseExecution) error
SavePurchaseExecution saves a purchase execution record.
func (*PostgresStore) SavePurchaseExecutionTx ¶
func (s *PostgresStore) SavePurchaseExecutionTx(ctx context.Context, tx pgx.Tx, execution *PurchaseExecution) error
SavePurchaseExecutionTx is the tx-accepting variant of SavePurchaseExecution. Used from handlers that need to bundle the execution insert with other writes (e.g. purchase_suppressions rows) in a single atomic transaction.
func (*PostgresStore) SavePurchaseHistory ¶
func (s *PostgresStore) SavePurchaseHistory(ctx context.Context, record *PurchaseHistoryRecord) error
SavePurchaseHistory saves a purchase history record.
func (*PostgresStore) SaveRIExchangeRecord ¶
func (s *PostgresStore) SaveRIExchangeRecord(ctx context.Context, record *RIExchangeRecord) error
SaveRIExchangeRecord saves an RI exchange record.
func (*PostgresStore) SaveServiceConfig ¶
func (s *PostgresStore) SaveServiceConfig(ctx context.Context, config *ServiceConfig) error
SaveServiceConfig saves configuration for a service.
func (*PostgresStore) SetCancelledBy ¶
func (s *PostgresStore) SetCancelledBy(ctx context.Context, executionID, cancelledBy string) error
SetCancelledBy stamps both the canceled_by and legacy cancelled_by columns for a single execution without touching any other column. This avoids the full-row overwrite that a SavePurchaseExecution follow-up would perform, eliminating the lost-update window between TransitionExecutionStatus and the attribution write (Finding #5). Writing both columns keeps this path symmetric with CancelExecutionAtomic/CancelScheduledExecutionAtomic (which write canceled_by only) so revoke-attribution keeps working unchanged if a future contract migration (#1278) drops the legacy column.
func (*PostgresStore) SetPlanAccounts ¶
func (s *PostgresStore) SetPlanAccounts(ctx context.Context, planID string, accountIDs []string) error
SetPlanAccounts replaces the full account list for a plan atomically.
func (*PostgresStore) SetRecommendationsCollectionError ¶
func (s *PostgresStore) SetRecommendationsCollectionError(ctx context.Context, errMsg string) error
SetRecommendationsCollectionError records the most recent collection's error message without touching last_collected_at or last_collection_started_at. Used by the scheduler when a collect fails partially or fully so the frontend banner surfaces the issue while existing cached rows stay visible.
This method must NOT clear last_collection_started_at (issue #261): it is called mid-run from persistCollection on every CollectRecommendations invocation that hits a provider error, including tokenless cron/cold-start/ background runs. Clearing the marker here, unconditionally and with no owner check, previously let a tokenless run's routine provider error wipe a concurrent owner run's in-flight marker, reopening the exact race the compare-and-clear guard exists to close. Only the deferred, token-guarded clearCollectionStartedBestEffort (which runs on both the success and failure exit paths of CollectRecommendations) may clear started_at.
func (*PostgresStore) StampOfferingClass ¶
func (s *PostgresStore) StampOfferingClass(ctx context.Context, purchaseID, offeringClass string) error
StampOfferingClass writes the offering_class value onto a purchase_history row identified by purchase_id. Used by the marketplace-list handler to lazily persist offering_class fetched from AWS DescribeReservedInstances for rows created before migration 000087 or for externally-created Standard RIs. A no-match (row not found) is treated as a non-fatal warning by callers.
func (*PostgresStore) StampRIExchangeApprovedBy ¶
func (s *PostgresStore) StampRIExchangeApprovedBy(ctx context.Context, id, approverEmail string) error
StampRIExchangeApprovedBy sets the approved_by column on an RI exchange row (issue #300). Called after CompleteRIExchangeWithPayment when approval came from a session-authed user. The stamping is best-effort (log + continue on failure so the exchange itself isn't rolled back just because the audit stamp failed).
func (*PostgresStore) TransitionExecutionStatus ¶
func (s *PostgresStore) TransitionExecutionStatus(ctx context.Context, executionID string, fromStatuses []string, toStatus string, actor *string) (*PurchaseExecution, error)
TransitionExecutionStatus atomically transitions an execution from one of the allowed statuses to a new status. Returns the updated record, or an error if the execution was not found or not in an allowed status. actor is the UUID of the user performing the transition (nil for system-initiated paths); it is stamped onto transitioned_by and transitioned_at is always set to NOW().
func (*PostgresStore) TransitionLadderRunStatus ¶
func (s *PostgresStore) TransitionLadderRunStatus(ctx context.Context, id string, fromStatuses []ladder.RunStatus, toStatus ladder.RunStatus) (*LadderRunDB, error)
TransitionLadderRunStatus atomically transitions a ladder_runs row from one of fromStatuses to toStatus via a CAS UPDATE. Returns the updated row on success, or (nil, nil) when zero rows are affected (race lost or unexpected current status). A hard DB error is returned as a non-nil error.
func (*PostgresStore) TransitionRIExchangeStatus ¶
func (s *PostgresStore) TransitionRIExchangeStatus(ctx context.Context, id, fromStatus, toStatus string, actor *string) (*RIExchangeRecord, error)
TransitionRIExchangeStatus atomically transitions an RI exchange record status. Uses a single UPDATE...WHERE...RETURNING for atomicity, then diagnoses failure only if zero rows are returned. actor is the UUID of the user performing the transition (nil for system-initiated paths).
func (*PostgresStore) TransitionRegistrationStatus ¶
func (s *PostgresStore) TransitionRegistrationStatus(ctx context.Context, reg *AccountRegistration, fromStatus string, actor *string) error
TransitionRegistrationStatus atomically updates a registration's workflow fields only if the current status matches fromStatus. Returns ErrRegistrationConflict when 0 rows are affected (another request already changed the status). actor is the UUID of the reviewer (nil for system-initiated transitions).
func (*PostgresStore) UpdateAccountRegistration ¶
func (s *PostgresStore) UpdateAccountRegistration(ctx context.Context, reg *AccountRegistration) error
UpdateAccountRegistration updates the mutable workflow fields of a registration.
func (*PostgresStore) UpdateCloudAccount ¶
func (s *PostgresStore) UpdateCloudAccount(ctx context.Context, account *CloudAccount) error
UpdateCloudAccount updates mutable fields of a cloud account.
func (*PostgresStore) UpdateGlobalConfigAtomic ¶
func (s *PostgresStore) UpdateGlobalConfigAtomic(ctx context.Context, apply func(*GlobalConfig) error) (*GlobalConfig, error)
UpdateGlobalConfigAtomic performs a serialized read-modify-write of the global_config singleton. It opens a transaction, takes a transaction-scoped advisory lock (which serializes even the first insert, since a row-level FOR UPDATE cannot lock a not-yet-existing singleton row), reads the current config, applies the caller's in-place mutation, and upserts the result in the SAME transaction. This eliminates the lost-update race where two concurrent partial PUTs each read the same stale base and the later save silently drops the earlier change.
apply mutates the loaded config in place (e.g. json.Unmarshal the request body over it and validate); an error it returns aborts the transaction and is propagated unchanged (so callers can surface a 400 from validation while transport/DB errors surface as 500).
func (*PostgresStore) UpdatePurchaseHistoryListing ¶
func (s *PostgresStore) UpdatePurchaseHistoryListing(ctx context.Context, purchaseID, listingID, listingState string) error
UpdatePurchaseHistoryListing stamps the marketplace listing fields onto a purchase_history row identified by its purchase_id (RI reservation ID). Called by the marketplace-list handler after CreateReservedInstancesListing succeeds (listing_id + listing_state="active") and by the status-poll path on the later closed/canceled transitions.
func (*PostgresStore) UpdatePurchasePlan ¶
func (s *PostgresStore) UpdatePurchasePlan(ctx context.Context, plan *PurchasePlan) error
UpdatePurchasePlan updates an existing purchase plan. Delegates to UpdatePurchasePlanTx inside a single-call WithTx — keeps the public surface unchanged for callers that don't need to bundle this with other writes, while sharing the SQL with the Tx variant. UpdatedAt is stamped here (before the WithTx call) so existing tests that inspect plan.UpdatedAt without exercising the DB still see it set — see TestPostgresStore_UpdatePurchasePlan_NilDB.
func (*PostgresStore) UpdatePurchasePlanTx ¶
func (s *PostgresStore) UpdatePurchasePlanTx(ctx context.Context, tx pgx.Tx, plan *PurchasePlan) error
UpdatePurchasePlanTx is the tx-accepting variant of UpdatePurchasePlan. Used by createPlannedPurchases so the per-row execution inserts and the plan's next_execution_date bump commit atomically — see the interface doc for the partial-failure rationale. Callers that need the auto-stamp of UpdatedAt either use UpdatePurchasePlan (which stamps before WithTx) or stamp it themselves before calling Tx.
func (*PostgresStore) UpsertLadderConfig ¶
func (s *PostgresStore) UpsertLadderConfig(ctx context.Context, cfg *LadderConfigDB) (*LadderConfigDB, error)
UpsertLadderConfig inserts or updates the per-account ladder configuration. The upsert key is (cloud_account_id, provider). If ID is empty a new UUID is generated; existing rows retain their original id and created_at.
Validate() must be called by the API handler before this method; the store does not re-validate to avoid duplicating error messages.
func (*PostgresStore) UpsertNotificationMute ¶
func (s *PostgresStore) UpsertNotificationMute(ctx context.Context, recipientEmail, scope, unmuteToken string) error
UpsertNotificationMute inserts or replaces the mute row for (recipientEmail, scope). ON CONFLICT updates muted_at and unmute_token so a repeated one-click opt-out resets the audit timestamp without error.
func (*PostgresStore) UpsertRIUtilizationCache ¶
func (s *PostgresStore) UpsertRIUtilizationCache(ctx context.Context, region string, lookbackDays int, payload []byte, fetchedAt time.Time) error
UpsertRIUtilizationCache writes or overwrites the cached utilization payload for (region, lookback_days). fetchedAt should be "now" at the time of the underlying Cost Explorer call — readers compare this against their TTL to decide freshness.
func (*PostgresStore) UpsertRecommendations ¶
func (s *PostgresStore) UpsertRecommendations(ctx context.Context, collectedAt time.Time, recs []RecommendationRecord, successfulCollects []SuccessfulCollect) error
UpsertRecommendations is the incremental write path: it upserts each row by natural key (cloud_account_id, provider, service, region, resource_type, term, payment_option) and then evicts stale rows for the set of (provider, account) pairs that successfully collected in this run. Pairs whose collection failed are NOT in successfulCollects and their stale rows stay — callers see older data with a banner rather than a blank section.
Migration 000032 broadened the natural key to include term + payment_option, so per-rec ON CONFLICT no longer collides on SQLSTATE 21000.
The eviction predicate uses (provider, account_key) IN (unnest($2, $3)) where account_key matches the generated column on the table — nil CloudAccountID maps to uuid.Nil at the Go boundary, matching the COALESCE(cloud_account_id, '00000000-...') rule the table applies on insert. This collapses ambient and registered identities consistently on both sides of the join.
type PurchaseExecution ¶
type PurchaseExecution struct {
PlanID string `json:"plan_id" dynamodbav:"plan_id"`
ExecutionID string `json:"execution_id" dynamodbav:"execution_id"`
Status string `json:"status" dynamodbav:"status"` // pending, notified, approved, canceled, completed, failed
StepNumber int `json:"step_number" dynamodbav:"step_number"`
ScheduledDate time.Time `json:"scheduled_date" dynamodbav:"scheduled_date"`
NotificationSent *time.Time `json:"notification_sent,omitempty" dynamodbav:"notification_sent,omitempty"`
ApprovalToken string `json:"approval_token,omitempty" dynamodbav:"approval_token,omitempty"`
Recommendations []RecommendationRecord `json:"recommendations" dynamodbav:"recommendations"`
TotalUpfrontCost float64 `json:"total_upfront_cost" dynamodbav:"total_upfront_cost"`
EstimatedSavings float64 `json:"estimated_savings" dynamodbav:"estimated_savings"`
CompletedAt *time.Time `json:"completed_at,omitempty" dynamodbav:"completed_at,omitempty"`
Error string `json:"error,omitempty" dynamodbav:"error,omitempty"`
TTL int64 `json:"ttl,omitempty" dynamodbav:"ttl,omitempty"`
CloudAccountID *string `json:"cloud_account_id,omitempty" dynamodbav:"cloud_account_id,omitempty"`
// Source identifies the CUDly surface that triggered this execution
// ("cudly-cli" or "cudly-web"). Propagated into PurchaseOptions and
// stamped as a tag/label onto every commitment this execution buys.
Source string `json:"source,omitempty" dynamodbav:"source,omitempty"`
// ApprovedBy / CancelledBy carry the email of the session-authenticated
// user who acted on this execution via the auth-gated deep-link flow
// (frontend /purchases/{action}/:id → login-if-needed → session-authed
// endpoint). Nil on legacy token-only approve/cancel paths — the
// handler / History UI falls back to the notification email as the
// accountable party in that case. Nullable TEXT in Postgres.
ApprovedBy *string `json:"approved_by,omitempty" dynamodbav:"approved_by,omitempty"`
CancelledBy *string `json:"cancelled_by,omitempty" dynamodbav:"cancelled_by,omitempty"`
// CreatedByUserID is the UUID of the session-authenticated user who
// triggered this execution (e.g. clicked Execute on the Recommendations
// page or submitted the bulk-purchase modal). NULL on rows created
// before the column was introduced (migration 000041) and on
// scheduler-driven executions where there is no human creator. Used
// by the session-authed cancel handler to enforce cancel:own_executions
// — a non-admin may cancel only executions they themselves created.
// NULL is treated as "not the current user".
CreatedByUserID *string `json:"created_by_user_id,omitempty" dynamodbav:"created_by_user_id,omitempty"`
// RetryExecutionID points from a *failed* execution to the new
// execution created when the user clicked Retry (issue #47). Set
// only on the original failed row; NULL on every other row including
// the retry itself (the retry's own RetryAttemptN > 0 is the
// "this is a retry" marker). Forms a forward-pointing chain:
// failed_v1.retry_execution_id = failed_v2.execution_id, etc.
// Migration 000042 self-FKs the column ON DELETE SET NULL so a
// cleanup of a successor doesn't cascade-delete its predecessor.
RetryExecutionID *string `json:"retry_execution_id,omitempty" dynamodbav:"retry_execution_id,omitempty"`
// RetryAttemptN is the position of this execution in a retry chain.
// 0 (default) on every fresh execution; 1 on the first retry of any
// failed row; n+1 on the n+1-th retry. The handler reads the
// predecessor's count and stamps n+1 atomically with the new
// INSERT inside the retry transaction. The History UI uses this to
// soft-block retries past a threshold so an obviously-stuck
// configuration doesn't accumulate dozens of dead retry rows.
// Migration 000042 added the column with default 0 so legacy rows
// look exactly like fresh first-retry candidates.
RetryAttemptN int `json:"retry_attempt_n,omitempty" dynamodbav:"retry_attempt_n,omitempty"`
// CapacityPercent records what fraction of the originally-recommended
// counts the user chose when the bulk Purchase flow submitted this
// execution (1..100). Audit-only: the Recommendations slice already
// carries the scaled counts, so backend math is unaffected by this
// field. Defaults to 100 for legacy and scheduler-driven executions.
CapacityPercent int `json:"capacity_percent,omitempty" dynamodbav:"capacity_percent,omitempty"`
// ApprovalTokenExpiresAt is the UTC deadline after which the
// ApprovalToken must be rejected by ApproveExecution and
// loadCancelableExecution (issue #397). Set at execution creation to
// ScheduledDate + ApprovalTokenTTL. NULL on rows created before
// migration 000051 — legacy rows are treated as not-yet-expired
// (backward-compatible: the TTL-checking gate only fires when the
// field is non-nil). Migration 000051 adds the column; new rows
// always carry a non-nil value.
ApprovalTokenExpiresAt *time.Time `json:"approval_token_expires_at,omitempty" dynamodbav:"approval_token_expires_at,omitempty"`
// ExecutedByUserID is the UUID of the session user who triggered a
// direct-execute (issue #289, execute-any/execute-own). NULL on rows
// that went through the normal approval flow. Non-null signals the
// approval step was intentionally skipped by an authorized operator.
// Migration 000058 adds the column.
ExecutedByUserID *string `json:"executed_by_user_id,omitempty" dynamodbav:"executed_by_user_id,omitempty"`
// ExecutedAt is the UTC timestamp when the direct-execute path fired.
// NULL for rows on the normal approval flow. Migration 000058.
ExecutedAt *time.Time `json:"executed_at,omitempty" dynamodbav:"executed_at,omitempty"`
// PreApprovalSkipReason is a human-readable token describing why the
// approval step was skipped. For direct-execute rows it is the literal
// string "direct-execute permission". NULL on every normal-flow row.
// Migration 000058.
PreApprovalSkipReason *string `json:"pre_approval_skip_reason,omitempty" dynamodbav:"pre_approval_skip_reason,omitempty"`
// IdempotencyKey is the stable lineage anchor the per-rec provider
// idempotency token is derived from (issue #1012). Unlike ExecutionID
// it is NOT regenerated on Retry or multi-account fan-out: it is
// generated once at first creation, copied verbatim onto every Retry
// successor, and combined with the account ID to seed each per-account
// fan-out row. This makes DeriveIdempotencyToken reproduce the same
// token across a strand-and-re-drive so the provider dedupes and the
// commitment is never bought twice. Empty on rows created before
// migration 000066 — the derivation falls back to ExecutionID for those
// (identical to the pre-fix behavior for a single un-retried execution).
IdempotencyKey string `json:"idempotency_key,omitempty" dynamodbav:"idempotency_key,omitempty"`
// ScheduledExecutionAt is set by the Gmail-style pre-fire delay path
// (issue #291 wave-2) when an approve defers the cloud SDK call. The
// scheduler fires the actual SDK call when this timestamp is in the past.
// NULL on every immediate-execute row. Migration 000065.
ScheduledExecutionAt *time.Time `json:"scheduled_execution_at,omitempty" dynamodbav:"scheduled_execution_at,omitempty"`
}
PurchaseExecution represents a single execution of a purchase plan.
func (*PurchaseExecution) IsCancelable ¶
func (e *PurchaseExecution) IsCancelable() bool
IsCancelable reports whether an execution may still be canceled. Only the pre-purchase states ("pending"/"notified"/"scheduled") qualify: once a row reaches "approved" or "running" the AWS commitment is being or has been created, so canceling would leave the DB and the cloud out of sync; "canceled", "completed", "failed", "expired", and "paused" are likewise non-cancelable. The "scheduled" state is cancellable because the cloud SDK has not been called yet (issue #291 wave-2). Both cancel paths (purchase.Manager.CancelExecution on the email-token flow and the session-authed cancelPurchaseViaSession) call this single predicate so the policy can never drift between them (issue #645).
type PurchaseHistoryFilter ¶
type PurchaseHistoryFilter struct {
// Provider matches purchase_history.provider exactly. Empty skips the clause.
Provider string
// AccountIDs matches purchase_history.cloud_account_id (the cloud_accounts
// UUID FK) with ANY($). Empty/nil skips this half of the account predicate.
AccountIDs []string
// ExternalIDsByProvider matches purchase_history.account_id (the
// cloud-provider external account number) scoped per provider. The caller
// resolves AccountIDs to their (provider, external_id) pairs and groups the
// external ids by provider, so the predicate matches each external id only
// against rows of its own provider:
//
// (provider = $p AND account_id = ANY($extsForP))
//
// This keeps the (provider, external_id) pairing intact so a filter for
// aws/123 never pulls azure/123 rows that reuse the same external number.
// The "" provider key means "provider unknown" (legacy raw external number)
// and matches account_id without a provider gate. Empty/nil skips this half
// of the account predicate.
ExternalIDsByProvider map[string][]string
// Start/End bound purchase_history.timestamp. nil for both skips the clause;
// nil for either leaves that side open (caller owns any range cap, see
// api.MaxHistoryDateRangeDays).
Start *time.Time
End *time.Time
// Limit caps the row count; clamped to [1, MaxListLimit] with a
// DefaultListLimit fallback when <= 0.
Limit int
}
PurchaseHistoryFilter is the filter set consumed by StoreInterface.GetPurchaseHistoryFiltered. Each field is optional; a zero-valued filter selects all rows (same plan-shape as GetAllPurchaseHistory). See the implementation docstring for the per-field semantics and the dual-column account predicate.
type PurchaseHistoryRecord ¶
type PurchaseHistoryRecord struct {
AccountID string `json:"account_id" dynamodbav:"account_id"`
PurchaseID string `json:"purchase_id" dynamodbav:"purchase_id"`
Timestamp time.Time `json:"timestamp" dynamodbav:"timestamp"`
Provider string `json:"provider" dynamodbav:"provider"`
Service string `json:"service" dynamodbav:"service"`
Region string `json:"region" dynamodbav:"region"`
ResourceType string `json:"resource_type" dynamodbav:"resource_type"`
Count int `json:"count" dynamodbav:"count"`
Term int `json:"term" dynamodbav:"term"`
Payment string `json:"payment" dynamodbav:"payment"`
UpfrontCost float64 `json:"upfront_cost" dynamodbav:"upfront_cost"`
// MonthlyCost is nil when the provider API did not return a monthly
// recurring breakdown for this commitment (e.g. Azure all-upfront where
// no recurring charge exists at the commitment layer). GCP commitments
// are monthly-billed in this repo, so they always populate MonthlyCost.
// The frontend renders "—" for nil, "$X.XX" when populated. Aggregations
// must skip nil entries rather than treating them as $0 to avoid
// distorting totals. Migration 000063 dropped the NOT NULL constraint so
// new rows can carry NULL; existing rows with 0.0 are preserved as-is
// (those are real zeros from AWS all-upfront commitments).
MonthlyCost *float64 `json:"monthly_cost" dynamodbav:"monthly_cost"`
EstimatedSavings float64 `json:"estimated_savings" dynamodbav:"estimated_savings"`
PlanID string `json:"plan_id,omitempty" dynamodbav:"plan_id,omitempty"`
PlanName string `json:"plan_name,omitempty" dynamodbav:"plan_name,omitempty"`
RampStep int `json:"ramp_step,omitempty" dynamodbav:"ramp_step,omitempty"`
CloudAccountID *string `json:"cloud_account_id,omitempty" dynamodbav:"cloud_account_id,omitempty"`
Status string `json:"status,omitempty" dynamodbav:"-"`
// Approver holds the email address the approval request was sent to (or
// would have been, if SES failed). Set only on pending rows, so the
// History UI can show "awaiting approval from <addr>" and the user knows
// exactly whose inbox to check. Excluded from DB persistence.
Approver string `json:"approver,omitempty" dynamodbav:"-"`
Source string `json:"source,omitempty" dynamodbav:"source,omitempty"`
// StatusDescription carries a short human-readable explanation for non-
// completed rows. For "failed", this is the stored Error message (e.g.
// "send failed: Missing domain"). For "expired", a canned reminder that
// the 7-day approval window elapsed. Empty on completed/pending rows —
// those speak for themselves via Status alone.
StatusDescription string `json:"status_description,omitempty" dynamodbav:"-"`
// CreatedByUserID propagates the originating execution's
// created_by_user_id so the History UI can decide whether to render
// the inline Cancel button (issue #46): a non-admin user only sees
// the button on their own pending rows. Set only for synthesized
// pending/notified rows (executions); empty on completed history
// rows (where the action has already completed). Excluded from DB
// persistence.
CreatedByUserID string `json:"created_by_user_id,omitempty" dynamodbav:"-"`
// RetryExecutionID propagates the originating execution's pointer to
// its successor when the user retried it (issue #47). Set only on
// the *original* failed row that has been retried; the History UI
// renders an inline "Retried as #abc" link to the successor row when
// this is non-empty. Excluded from DB persistence (synthesized from
// purchase_executions).
RetryExecutionID string `json:"retry_execution_id,omitempty" dynamodbav:"-"`
// RetryAttemptN propagates the originating execution's retry-chain
// position so the History UI can render "↻ Retry of #xyz" inline
// links on retry rows (n > 0) and gate the Retry button against the
// soft-block threshold (n >= 5). Excluded from DB persistence.
RetryAttemptN int `json:"retry_attempt_n,omitempty" dynamodbav:"-"`
// OpsHint is a short operator-actionable message rendered inline in
// place of the Retry button when the failure reason on the row
// matches a known-persistent-misconfiguration pattern (e.g.
// "FROM_EMAIL not configured" → "Set FROM_EMAIL tfvar then retry").
// Set only on `failed` rows whose Error matches the persistent map;
// empty otherwise. Excluded from DB persistence (computed at read
// time so updates to the persistent-failure map land instantly).
OpsHint string `json:"ops_hint,omitempty" dynamodbav:"-"`
// IsAuditGap marks a synthesized "completed" row whose purchase_history
// write failed after a successful purchase (issue #621). Such a row is
// reconstructed from the execution so the purchase stays visible, but its
// execution-level dollars are excluded from the committed totals: a
// partially-saved multi-rec execution can have BOTH some real
// purchase_history rows AND this synthesized row, so adding the full
// execution total would double-count the recs that did save. The dollars
// are surfaced via the individual purchase_history rows that succeeded;
// this row is the audit flag, not a money source. Real purchase_history
// rows loaded from the DB always leave this false. Excluded from DB
// persistence (set only at read time on synthesized rows).
IsAuditGap bool `json:"is_audit_gap,omitempty" dynamodbav:"-"`
// CreatedByUserEmail is the email address of the user who created the
// underlying execution, resolved from CreatedByUserID via the auth
// service. Populated only on synthesized execution rows (pending,
// notified, failed, expired, canceled) when a valid user ID is
// present; empty for scheduler-driven executions, legacy NULL-creator
// rows, and completed purchase_history rows. Excluded from DB
// persistence (resolved at read time). The UI renders this in the
// Approval Queue "Created by" column instead of the raw UUID.
CreatedByUserEmail string `json:"created_by_user_email,omitempty" dynamodbav:"-"`
// --- Revocation window fields (issue #290) ---
//
// RevocationWindowClosesAt is set when the purchase_history row is
// written: Timestamp + the provider-specific free-cancel window
// (Azure: 7 days). NULL for AWS EC2 (no direct cancel API) and GCP
// (no free-cancel window). Persisted in purchase_history.
RevocationWindowClosesAt *time.Time `json:"revocation_window_closes_at,omitempty" dynamodbav:"revocation_window_closes_at,omitempty"`
// RevokedAt is set by the revoke endpoint when the provider API
// confirmed the cancellation / refund. Persisted.
RevokedAt *time.Time `json:"revoked_at,omitempty" dynamodbav:"revoked_at,omitempty"`
// RevokedVia identifies how the revocation was completed: "direct-api"
// (provider returned 2xx) or "support-case" (AWS Support case filed).
// Persisted.
RevokedVia string `json:"revoked_via,omitempty" dynamodbav:"revoked_via,omitempty"`
// SupportCaseID is non-empty when RevokedVia == "support-case".
// Persisted.
SupportCaseID string `json:"support_case_id,omitempty" dynamodbav:"support_case_id,omitempty"`
// --- Refund-quote audit fields (issue #290 Finding #4, migration 000071) ---
//
// CalcRefundAmount is the amount Azure quoted at CalculateRefund time, captured
// for audit and TOCTOU-divergence detection in the two-step revoke confirm flow.
// NULL for revocations that predate this feature or where Azure returned no amount.
CalcRefundAmount *float64 `json:"calc_refund_amount,omitempty" dynamodbav:"calc_refund_amount,omitempty"`
// CalcRefundCurrency is the ISO-4217 currency code from the CalculateRefund quote
// (e.g. "USD"). NULL when CalcRefundAmount is NULL.
CalcRefundCurrency string `json:"calc_refund_currency,omitempty" dynamodbav:"calc_refund_currency,omitempty"`
// --- Partial-success reconciliation (issue #290 Finding #6, migration 000072) ---
//
// RevocationInFlight is set to true immediately before the Azure Return API call
// and cleared (set to false) by a successful MarkPurchaseRevoked. When all DB
// retries fail, the flag stays true so the finalize_revocations scheduled sweep
// can detect and retry the MarkPurchaseRevoked write without re-calling Azure
// (preventing a duplicate-refund error).
RevocationInFlight bool `json:"revocation_in_flight,omitempty" dynamodbav:"revocation_in_flight,omitempty"`
// OfferingClass records whether this commitment is a 'standard' or
// 'convertible' RI. NULL on pre-migration rows. The Sell-on-Marketplace
// button renders only when this equals "standard" (issue #292).
// Persisted in purchase_history via migration 000087.
OfferingClass string `json:"offering_class,omitempty" dynamodbav:"offering_class,omitempty"`
// ListingID is the AWS ReservedInstancesListingId returned by
// CreateReservedInstancesListing. Empty when the RI has not been
// listed. Persisted in purchase_history via migration 000087.
ListingID string `json:"listing_id,omitempty" dynamodbav:"listing_id,omitempty"`
// ListingState mirrors the AWS marketplace listing state (see the
// ListingState* constants). Empty when not listed. Persisted in
// purchase_history via migration 000087.
ListingState string `json:"listing_state,omitempty" dynamodbav:"listing_state,omitempty"`
}
PurchaseHistoryRecord is the response-layer representation for rows on the /api/history page. DB-backed rows always describe *completed* purchases; the handler additionally synthesizes rows for pending executions so users can see (and cancel) in-flight approvals. Status is the discriminator — the DB layer never writes it (tag `dynamodbav:"-"` keeps it out of persistence), and the API layer populates it as "completed" or "pending" before returning.
type PurchasePlan ¶
type PurchasePlan struct {
ID string `json:"id" dynamodbav:"id"`
Name string `json:"name" dynamodbav:"name"`
Enabled bool `json:"enabled" dynamodbav:"enabled"`
AutoPurchase bool `json:"auto_purchase" dynamodbav:"auto_purchase"`
NotificationDaysBefore int `json:"notification_days_before" dynamodbav:"notification_days_before"`
Services map[string]ServiceConfig `json:"services" dynamodbav:"services"`
RampSchedule RampSchedule `json:"ramp_schedule" dynamodbav:"ramp_schedule"`
CreatedAt time.Time `json:"created_at" dynamodbav:"created_at"`
UpdatedAt time.Time `json:"updated_at" dynamodbav:"updated_at"`
NextExecutionDate *time.Time `json:"next_execution_date,omitempty" dynamodbav:"next_execution_date,omitempty"`
LastExecutionDate *time.Time `json:"last_execution_date,omitempty" dynamodbav:"last_execution_date,omitempty"`
LastNotificationSent *time.Time `json:"last_notification_sent,omitempty" dynamodbav:"last_notification_sent,omitempty"`
// Unassigned is true when the plan has zero rows in plan_accounts.
// This can happen for legacy plans created before target_accounts was
// required (issue #743). Such plans are invisible when an account filter
// is active because the normal JOIN excludes them; ListPurchasePlans
// surfaces them alongside filtered results so operators can find and
// re-scope them. The field is omitted (false) in the no-filter case
// where all plans are returned unconditionally.
Unassigned bool `json:"unassigned,omitempty" dynamodbav:"unassigned,omitempty"`
}
PurchasePlan represents a saved purchase plan for automated execution.
func (*PurchasePlan) Validate ¶
func (p *PurchasePlan) Validate() error
Validate validates the PurchasePlan.
type PurchasePlanFilter ¶
type PurchasePlanFilter struct {
AccountIDs []string // nil/empty = all plans
}
PurchasePlanFilter parameterises ListPurchasePlans. Zero-value means "no filter" (all plans are returned). Non-empty AccountIDs restricts the result to plans that reference at least one of the given account IDs via the plan_accounts join table.
type PurchaseSuppression ¶
type PurchaseSuppression struct {
ID string `json:"id"`
ExecutionID string `json:"execution_id"`
AccountID string `json:"account_id"`
Provider string `json:"provider"`
Service string `json:"service"`
Region string `json:"region"`
ResourceType string `json:"resource_type"`
Engine string `json:"engine"`
SuppressedCount int `json:"suppressed_count"`
ExpiresAt time.Time `json:"expires_at"`
CreatedAt time.Time `json:"created_at"`
}
PurchaseSuppression records the per-tuple grace window after a bulk purchase. See migration 000037 for the full SQL shape + lifecycle documentation. Written inside the same transaction as the execution insert; deleted inside the same transaction as a cancel/expire status transition.
type RIExchangeRecord ¶
type RIExchangeRecord struct {
ID string `json:"id"`
AccountID string `json:"account_id"`
ExchangeID string `json:"exchange_id"`
Region string `json:"region"`
SourceRIIDs []string `json:"source_ri_ids"`
SourceInstanceType string `json:"source_instance_type"`
SourceCount int `json:"source_count"`
TargetOfferingID string `json:"target_offering_id"`
TargetInstanceType string `json:"target_instance_type"`
TargetCount int `json:"target_count"`
PaymentDue string `json:"payment_due"`
Status string `json:"status"`
ApprovalToken string `json:"approval_token,omitempty"`
Error string `json:"error,omitempty"`
Mode string `json:"mode"`
// CreatedByUserID is the UUID of the session user who submitted the exchange
// (populated for dashboard-initiated exchanges; nil for automated or legacy
// email-link-initiated ones). Exposed to the frontend so the Approve button
// can apply the approve-own ownership check client-side.
CreatedByUserID *string `json:"created_by_user_id,omitempty"`
// ApprovedBy carries the email of the session user who approved the exchange
// via the dashboard Approve button (issue #300). Nil for token-authed approvals.
ApprovedBy *string `json:"approved_by,omitempty"`
// LadderRunID links this exchange record to the ladder run that created it
// (cudly-ladder engine). Nil for standalone ri_exchange_reshape task records.
// The database column ri_exchange_history.ladder_run_id was added in migration
// 000080 and is the authoritative source for origin scoping in
// CancelPendingExchangesByOrigin.
//
// KNOWN/ACCEPTABLE: the FK is ON DELETE SET NULL (migration 000080), so
// deleting a ladder_runs row nulls this column and reclassifies the record
// as standalone. A still-pending reshape then becomes standalone-cancellable
// (the standalone-origin sweep would cancel it). This is acceptable: a
// deleted run has no owner to approve its pendings, so canceling them on the
// next standalone sweep is the safe outcome, not a leak.
LadderRunID *string `json:"ladder_run_id,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
CompletedAt *time.Time `json:"completed_at,omitempty"`
ExpiresAt *time.Time `json:"expires_at,omitempty"`
CloudAccountID *string `json:"cloud_account_id,omitempty"`
}
RIExchangeRecord represents a record in the ri_exchange_history table.
type RIUtilizationCacheEntry ¶
type RIUtilizationCacheEntry struct {
Region string
LookbackDays int
Payload []byte
FetchedAt time.Time
}
RIUtilizationCacheEntry is a single cached Cost Explorer GetReservationUtilization result keyed by (region, lookback_days). Payload is the JSON encoding of the caller's utilization slice — kept opaque here so the config package stays free of AWS-provider types. TTL freshness is evaluated in the caller (api-layer cache wrapper) based on FetchedAt vs. a caller-supplied TTL.
type RampSchedule ¶
type RampSchedule struct {
Type string `json:"type" dynamodbav:"type"` // immediate, weekly, monthly, custom
PercentPerStep float64 `json:"percent_per_step" dynamodbav:"percent_per_step"`
StepIntervalDays int `json:"step_interval_days" dynamodbav:"step_interval_days"`
CurrentStep int `json:"current_step" dynamodbav:"current_step"`
TotalSteps int `json:"total_steps" dynamodbav:"total_steps"`
StartDate time.Time `json:"start_date" dynamodbav:"start_date"`
}
RampSchedule defines how purchases are spread over time.
func (*RampSchedule) GetCurrentCoverage ¶
func (r *RampSchedule) GetCurrentCoverage(baseCoverage float64) float64
GetCurrentCoverage calculates the current effective coverage based on ramp progress.
func (*RampSchedule) GetNextPurchaseDate ¶
func (r *RampSchedule) GetNextPurchaseDate() time.Time
GetNextPurchaseDate calculates when the next purchase step should occur.
func (*RampSchedule) IsComplete ¶
func (r *RampSchedule) IsComplete() bool
IsComplete returns true if all ramp steps are done.
func (*RampSchedule) Validate ¶
func (r *RampSchedule) Validate() error
Validate validates the RampSchedule.
type RampStepBlock ¶
RampStepBlock reports a plan's next ramp step and how many of that step's executions are stuck on it. Returned by GetStuckRampSteps and rendered by the plan-health ramp_blocked factor (issue #1861).
StuckExecutions counts executions rather than accounts because that is what the query can prove: a fanned-out step writes one row per cloud account, so the two coincide for the multi-account plans the factor exists for, but a step that never fanned out (an approval that expired before it ran) has one row standing for the whole step. Naming the count for the rows keeps the tooltip from asserting an account total it did not measure.
type RecommendationFilter ¶
type RecommendationFilter struct {
Provider string // "aws" / "azure" / "gcp" / "" (all)
Service string // "" = all services
Region string // "" = all regions
AccountIDs []string // nil/empty = all accounts
MinSavingsUSD float64 // 0 = no floor on monthly savings dollar amount
MinSavingsPct float64 // 0 = no floor on savings percentage (0–100 scale)
ID string // "" = all ids; non-empty = exact match on the id column
}
RecommendationFilter parameterises ListStoredRecommendations and the handler-facing scheduler.ListRecommendations wrapper. Zero-value fields mean "no filter"; non-empty AccountIDs restricts to the given IDs.
MinSavingsUSD is a dollar floor: only recommendations whose monthly savings are >= MinSavingsUSD are returned. 0 means no floor.
MinSavingsPct is a percentage floor (0–100): only recommendations whose effective savings percentage (savings/on-demand*100) meets or exceeds this threshold are returned. 0 means no floor. Applied in-process after the DB query rather than in SQL (avoids a computed column). These two filters are independent and can be combined.
type RecommendationRecord ¶
type RecommendationRecord struct {
ID string `json:"id" dynamodbav:"id"`
Provider string `json:"provider" dynamodbav:"provider"`
Service string `json:"service" dynamodbav:"service"`
Region string `json:"region" dynamodbav:"region"`
ResourceType string `json:"resource_type" dynamodbav:"resource_type"`
Engine string `json:"engine,omitempty" dynamodbav:"engine,omitempty"`
// Details preserves the full common.ServiceDetails payload from the
// source common.Recommendation so the purchase path can reconstruct
// the correct typed *Details pointer at execute time (issue #453).
// Stored as raw JSON because RecommendationRecord lives in the
// config package, which must NOT import pkg/common (the dependency
// graph is config <- common in callers, never the reverse). The
// scheduler populates this at collection time via
// common.MarshalServiceDetails; the purchase manager reads it via
// common.DecodeServiceDetailsFor when it builds the
// common.Recommendation handed to the cloud service client.
//
// Empty for rows persisted before #453 — DecodeServiceDetailsFor
// returns a zero-valued typed pointer in that case so the cloud
// client's findOfferingID type-assertion still succeeds (the
// service-side buildOfferingFilters substitutes default
// Platform / Tenancy / Scope / AZConfig values). New rows always
// carry the full Details, so non-default platforms (Windows EC2,
// Postgres RDS, etc.) round-trip correctly.
Details json.RawMessage `json:"details,omitempty" dynamodbav:"-"`
Count int `json:"count" dynamodbav:"count"`
// RecommendedCount is the pre-scaling count the collector originally
// recommended, before the bulk-purchase Capacity % slider scaled it down.
// The web execute path stamps it so the backend can verify the
// client-supplied capacity_percent against the scaled Count
// (floor(RecommendedCount*pct/100) must equal Count) rather than trusting
// a decorative audit field that could silently disagree (#647). Optional:
// 0 / absent means "not supplied" (legacy callers, scheduler/CLI rows,
// retry replays) and the consistency check is skipped for that rec.
RecommendedCount int `json:"recommended_count,omitempty" dynamodbav:"recommended_count,omitempty"`
Term int `json:"term" dynamodbav:"term"`
Payment string `json:"payment" dynamodbav:"payment"`
UpfrontCost float64 `json:"upfront_cost" dynamodbav:"upfront_cost"`
// MonthlyCost is nil when the provider API did not return a monthly
// recurring breakdown (rendered as "—" in the UI, not "$0").
// Backward-compatible with DynamoDB: existing items with a numeric 0
// attribute unmarshal as a pointer to 0.0; absent attributes unmarshal
// as nil. No migration needed.
MonthlyCost *float64 `json:"monthly_cost" dynamodbav:"monthly_cost"`
Savings float64 `json:"savings" dynamodbav:"savings"`
// OnDemandCost is the canonical on-demand monthly baseline for the
// recommended commitment, sourced directly from the cloud provider
// (Azure `CostWithNoReservedInstances`, AWS Cost Explorer
// `EstimatedMonthlyOnDemandCost`). Persisted via the recommendations
// row's JSONB `payload` column — no DDL needed.
//
// nil means the provider API did not return a baseline; the frontend
// falls back to reconstructing on-demand from `monthly_cost + savings
// + amortized_upfront`. When non-nil, the frontend prefers the raw
// value over reconstruction so anomalies in the reconstructed
// denominator (e.g. Azure all-upfront recs where monthly_cost=$0
// collapses the denominator) don't inflate the displayed effective
// savings %. See #274.
OnDemandCost *float64 `json:"on_demand_cost,omitempty" dynamodbav:"on_demand_cost,omitempty"`
// SavingsPercentage is the provider-authoritative effective savings %
// reported directly by the cloud provider (AWS Cost Explorer
// `EstimatedMonthlySavingsPercentage`, Azure / GCP converters' computed
// SavingsPercentage). It is the same figure the CLI/reporter prints
// verbatim (internal/reporter/reporter.go); persisting it lets the GUI
// show the identical number instead of re-deriving it client-side from
// savings / on-demand. Persisted via the recommendations row's JSONB
// `payload` column; no DDL needed.
//
// nil means the provider did not report a percentage; the frontend then
// falls back to the client-side reconstruction (effectiveSavingsPct).
// When non-nil, the frontend prefers this value so the displayed % cannot
// drift from the provider's authoritative number and AWS recs missing
// on_demand_cost still render a real % rather than an em-dash (see #323).
SavingsPercentage *float64 `json:"savings_percentage" dynamodbav:"savings_percentage,omitempty"`
Selected bool `json:"selected" dynamodbav:"selected"`
Purchased bool `json:"purchased" dynamodbav:"purchased"`
PurchaseID string `json:"purchase_id,omitempty" dynamodbav:"purchase_id,omitempty"`
Error string `json:"error,omitempty" dynamodbav:"error,omitempty"`
CloudAccountID *string `json:"cloud_account_id,omitempty" dynamodbav:"cloud_account_id,omitempty"`
// SuppressedCount is the cumulative count already committed against
// this recommendation's 6-tuple (account, provider, service, region,
// resource_type, engine) within the active grace window. The
// scheduler subtracts this from Count before returning the rec to
// the frontend; a rec where SuppressedCount ≥ original count is
// dropped entirely. Populated by the scheduler — zero on writes.
SuppressedCount int `json:"suppressed_count,omitempty" dynamodbav:"suppressed_count,omitempty"`
// SuppressionExpiresAt is the earliest expiry across all active
// suppression rows contributing to this tuple. The frontend uses it
// to render "Xd remaining" on the recently-purchased badge.
SuppressionExpiresAt *time.Time `json:"suppression_expires_at,omitempty" dynamodbav:"suppression_expires_at,omitempty"`
// PrimarySuppressionExecutionID identifies the execution whose
// suppression contributed the most to this tuple (ties broken by
// newest created_at). The frontend badge deep-links to Purchase
// History filtered to this execution.
PrimarySuppressionExecutionID *string `json:"primary_suppression_execution_id,omitempty" dynamodbav:"primary_suppression_execution_id,omitempty"`
// UsageHistory is a short time-series of daily RI-coverage percentages
// (0-100) for the last N days of the lookback window, ordered from
// oldest to newest. nil means the collector did not populate it (e.g.
// provider not yet wired); an empty non-nil slice means the collector
// ran but returned no daily data. The frontend renders nil as "—" and
// a non-empty slice as a thumbnail sparkline. Stored inside the
// recommendations JSONB payload — no DDL change needed (closes #239
// Part 1 for AWS).
UsageHistory []float64 `json:"usage_history,omitempty" dynamodbav:"usage_history,omitempty"`
// VCPU and MemoryGB surface the compute size of the recommended
// instance type so the frontend's Capacity column can render
// "<vcpu> vCPU / <memory> GB" without parsing the opaque Details blob
// (#219). They are NOT persisted: the canonical source is the typed
// ComputeDetails nested inside Details (config must stay free of
// pkg/common imports). The api layer decodes Details via
// common.DecodeServiceDetailsFor in buildRecommendationsResponse and
// stamps these top-level fields on the way out, so the API JSON carries
// them at the top level where the frontend already reads them.
//
// Pointers (not plain int/float64) so "absent / non-compute / unknown
// size" serializes as omitted rather than a misleading 0: the frontend
// renders absent as "—", and a literal 0 would otherwise look like a
// real "0 vCPU / 0 GB" capacity. dynamodbav:"-" because they are
// derived-on-read, never stored.
VCPU *int `json:"vcpu,omitempty" dynamodbav:"-"`
MemoryGB *float64 `json:"memory_gb,omitempty" dynamodbav:"-"`
}
RecommendationRecord stores a recommendation with purchase status.
type RecommendationsFreshness ¶
type RecommendationsFreshness struct {
LastCollectedAt *time.Time `json:"last_collected_at"`
LastCollectionError *string `json:"last_collection_error"`
LastCollectionStartedAt *time.Time `json:"last_collection_started_at"`
}
RecommendationsFreshness describes the cache staleness state surfaced to the frontend. LastCollectedAt is nil on a cold start. LastCollectionError is non-nil when the most recent collect attempt partially or fully failed. LastCollectionStartedAt is non-nil while an async collect is in flight; the scheduler clears it on completion (success or failure). A value older than 5 minutes means the scheduler crashed mid-run — the refresh handler treats it as stale and allows a new collection.
type ServiceConfig ¶
type ServiceConfig struct {
Provider string `json:"provider" dynamodbav:"provider"`
Service string `json:"service" dynamodbav:"service"`
Enabled bool `json:"enabled" dynamodbav:"enabled"`
Term int `json:"term" dynamodbav:"term"`
Payment string `json:"payment" dynamodbav:"payment"`
Coverage float64 `json:"coverage" dynamodbav:"coverage"`
RampSchedule string `json:"ramp_schedule" dynamodbav:"ramp_schedule"`
IncludeEngines []string `json:"include_engines,omitempty" dynamodbav:"include_engines,omitempty"`
ExcludeEngines []string `json:"exclude_engines,omitempty" dynamodbav:"exclude_engines,omitempty"`
IncludeRegions []string `json:"include_regions,omitempty" dynamodbav:"include_regions,omitempty"`
ExcludeRegions []string `json:"exclude_regions,omitempty" dynamodbav:"exclude_regions,omitempty"`
IncludeTypes []string `json:"include_types,omitempty" dynamodbav:"include_types,omitempty"`
ExcludeTypes []string `json:"exclude_types,omitempty" dynamodbav:"exclude_types,omitempty"`
// MinCount is the GUI/persisted equivalent of the CLI --min-count flag:
// the minimum instance/node count a recommendation must carry to be
// surfaced. Applied at read time by
// scheduler.filterRecsByResolvedConfigs against the persisted
// RecommendationRecord.Count. 0 (the default) disables the filter,
// matching the CLI flag's 0-no-floor semantics.
MinCount int `json:"min_count,omitempty" dynamodbav:"min_count,omitempty"`
}
ServiceConfig represents per-service configuration.
func ResolveServiceConfig ¶
func ResolveServiceConfig(provider, service string, global *ServiceConfig, override *AccountServiceOverride) *ServiceConfig
ResolveServiceConfig merges a sparse per-account override on top of a global ServiceConfig. Any non-nil pointer field in override replaces the global value; nil fields inherit from global. Slice fields (include/exclude lists) are replaced wholesale when non-empty in the override.
If override is nil the global is returned unchanged (no copy is made). If global is nil but override is non-nil, a baseline ServiceConfig is synthesized with safe defaults (Enabled: true, Provider/Service from the override context via the provider and service parameters) and the override is merged into it. This lets a per-account override take effect even when no global ServiceConfig row exists for that (provider, service) pair. If both are nil, nil is returned. When merging, slice fields from the global are copied to avoid callers mutating the global's underlying arrays through the resolved config.
func (*ServiceConfig) Validate ¶
func (c *ServiceConfig) Validate() error
Validate validates the ServiceConfig.
type StoreInterface ¶
type StoreInterface interface {
// Global configuration
GetGlobalConfig(ctx context.Context) (*GlobalConfig, error)
SaveGlobalConfig(ctx context.Context, config *GlobalConfig) error
// UpdateGlobalConfigAtomic serializes a read-modify-write of the
// global_config singleton under an advisory-locked transaction so
// concurrent partial PUTs cannot lose each other's updates. apply mutates
// the loaded config in place; its error aborts the write and is propagated.
UpdateGlobalConfigAtomic(ctx context.Context, apply func(*GlobalConfig) error) (*GlobalConfig, error)
// Service configuration
GetServiceConfig(ctx context.Context, provider, service string) (*ServiceConfig, error)
SaveServiceConfig(ctx context.Context, config *ServiceConfig) error
ListServiceConfigs(ctx context.Context) ([]ServiceConfig, error)
// Purchase plans
CreatePurchasePlan(ctx context.Context, plan *PurchasePlan) error
GetPurchasePlan(ctx context.Context, planID string) (*PurchasePlan, error)
UpdatePurchasePlan(ctx context.Context, plan *PurchasePlan) error
// CompletePlanStep records that ramp step stepNumber finished and advances
// the plan's schedule to it, inside a SELECT FOR UPDATE transaction that
// prevents the concurrent-write lost-update race of issue #1071.
//
// It is idempotent in stepNumber: completing a step the plan has already
// counted writes nothing. Which sentinel says so depends on where the ramp
// sits relative to the completing step, and callers branch on the
// difference (purchase.recordRampAdvanceRefusal does):
//
// - CurrentStep == stepNumber reports ErrRampStepCountedBySibling. This
// is the issue #1669 scenario: an operator retries two separately-failed
// accounts of one ramp step, and the second completion finds the ramp
// already sitting on that step because the first advanced it. The step
// WAS counted, so this is routine and is not recorded against the
// execution.
// - CurrentStep > stepNumber reports ErrRampStepAlreadyCounted. The ramp
// moved past the step earlier, so this purchase bought commitment the
// plan will never count. That is an anomaly and IS recorded.
//
// A step counts as complete only when EVERY cloud account the step fanned
// out to has bought, not when any one of them has (issue #1861). Until
// then it reports ErrRampStepIncomplete, which is a "not yet" rather than a
// failure of the purchase that just completed -- the purchase is real and
// already recorded; only the plan's position is withheld.
CompletePlanStep(ctx context.Context, planID string, stepNumber int) error
// GetStuckRampSteps returns, keyed by plan ID, the plan's next ramp step
// and how many of that step's executions have a terminal, unsuccessful
// latest attempt with no retry in flight -- a ramp CompletePlanStep will
// keep refusing to advance until an operator intervenes. Plans with
// nothing stuck are absent from the map. Feeds the plan-health
// ramp_blocked factor (issue #1861).
GetStuckRampSteps(ctx context.Context) (map[string]RampStepBlock, error)
// LockPurchasePlanTx reads a plan under a row lock held for the rest of tx.
// It is the per-plan ramp lock CompletePlanStep takes, exposed so a caller
// that reads a plan's ramp position and then writes against it does so
// atomically with respect to concurrent completions and creations.
// Returns (nil, nil) when the plan does not exist.
LockPurchasePlanTx(ctx context.Context, tx pgx.Tx, planID string) (*PurchasePlan, error)
// OccupiedRampStepsInRangeTx returns the steps in [from, to] of planID that
// already have a fan-out unit which bought or is still working, ascending.
// Callers about to mint executions for a step range use it to refuse a step
// that is already covered, which would otherwise re-fan-out across accounts
// that already committed under a fresh idempotency lineage (issue #1861).
// Requires the caller to hold LockPurchasePlanTx on planID in the same
// transaction; without it the answer is advisory and two concurrent callers
// both see the step free.
OccupiedRampStepsInRangeTx(ctx context.Context, tx pgx.Tx, planID string, from, to int) ([]int, error)
// UpdatePurchasePlanTx is the tx-accepting variant of UpdatePurchasePlan.
// Used from createPlannedPurchases' WithTx block so the per-row
// SavePurchaseExecutionTx writes and the plan's next_execution_date
// bump commit atomically — a partial failure leaves no orphaned
// rows and no stale plan pointer.
UpdatePurchasePlanTx(ctx context.Context, tx pgx.Tx, plan *PurchasePlan) error
DeletePurchasePlan(ctx context.Context, planID string) error
ListPurchasePlans(ctx context.Context, filter PurchasePlanFilter) ([]PurchasePlan, error)
// Purchase executions
SavePurchaseExecution(ctx context.Context, execution *PurchaseExecution) error
GetPendingExecutions(ctx context.Context) ([]PurchaseExecution, error)
// GetExecutionsByStatuses returns executions in any of the given states,
// newest first, capped at `limit`. Used by the History handler to render
// pending + failed + expired alongside completed purchases; the scheduler
// keeps using GetPendingExecutions (which is narrower and doesn't share
// this method's status filter) to avoid accidental double-processing of
// failed / expired rows.
GetExecutionsByStatuses(ctx context.Context, statuses []string, limit int) ([]PurchaseExecution, error)
// CountExecutionsByPlanAndStatus returns, keyed by plan ID, the exact
// number of executions in each of the supplied statuses that last
// changed state (updated_at) at or after `since` -- not those merely
// scheduled since then, which for a canceled row is a future date. Rows
// with a NULL plan_id (direct-execute purchases, and executions orphaned
// by a deleted plan) belong to no plan and are excluded.
//
// Deliberately NOT derived from a GetExecutionsByStatuses page: that
// method is capped by `limit` across ALL plans, so any caller counting
// per-plan rows out of it silently understates plans whose executions
// fall outside the newest `limit` rows. The plan health score
// (internal/api/plan_health.go) needs exact counts -- an understated
// count renders a confidently-healthy badge for an unhealthy plan --
// so the aggregation happens in SQL and the result is bounded by
// plans x statuses rather than by execution volume.
CountExecutionsByPlanAndStatus(ctx context.Context, statuses []string, since time.Time) (map[string]ExecutionStatusCounts, error)
// GetPlannedExecutions returns executions in any of the given states
// ordered by scheduled_date ASC (soonest first), the order the Planned
// Purchases UI lists rows so the user acts on imminent purchases first.
// Distinct from GetExecutionsByStatuses (which is DESC for History's
// "newest first" semantics): when the result set exceeds `limit`, an
// ORDER-BY-DESC + LIMIT in SQL truncates away the soonest rows, exactly
// the rows this list must surface. Secondary sort by id ASC stabilizes
// ordering when multiple rows share a scheduled_date.
GetPlannedExecutions(ctx context.Context, statuses []string, limit int) ([]PurchaseExecution, error)
// GetStaleApprovedExecutions returns executions stuck in the "approved"
// status with updated_at older than olderThan — strands left behind when a
// synchronous purchase run was interrupted before finalizing (issue #632).
// The recovery sweep in the purchase manager re-drives these into a
// terminal "failed" state so they can never sit permanently approved.
GetStaleApprovedExecutions(ctx context.Context, olderThan time.Duration) ([]PurchaseExecution, error)
// GetExecutionByID retrieves a purchase execution by execution ID.
// Returns an error wrapping ErrNotFound when no execution exists;
// never returns (nil, nil). A nil error guarantees a non-nil execution.
GetExecutionByID(ctx context.Context, executionID string) (*PurchaseExecution, error)
GetExecutionByPlanAndDate(ctx context.Context, planID string, scheduledDate time.Time) (*PurchaseExecution, error)
// GetUserEmailByID resolves the email address of the auth user identified
// by userID (the `users` table, owned by internal/auth). Read-only helper
// used by the 4-eyes approval policy (issue #1005) to compare the acting
// approver's identity against a PurchaseExecution.CreatedByUserID without
// internal/purchase importing internal/auth (which would create an import
// cycle: internal/auth already imports internal/config). Returns ("", nil)
// when no user matches userID — the caller treats an empty result as
// "identity unresolved" and fails closed, never as "email intentionally
// blank".
GetUserEmailByID(ctx context.Context, userID string) (string, error)
// CountPendingExecutionsForAccount returns the number of purchase_executions
// in status 'pending' or 'notified' that reference the given cloud account.
// Used by the deleteAccount handler to preflight DB-level FK violations
// (migration 000053 tightened the FK to ON DELETE RESTRICT) and emit a
// 409 with a count so the frontend can offer Cancel-All-Then-Delete UX
// instead of surfacing a raw constraint error. See issue #606.
CountPendingExecutionsForAccount(ctx context.Context, accountID string) (int, error)
// ListPendingExecutionIDsForAccount returns the execution IDs of all
// pending / notified executions referencing this account, used by the
// frontend's Cancel-All-Then-Delete flow when the operator opts to
// cancel everything in one go after a 409. Capped at 1000 rows; if a
// single account has more pending executions than that, the cleanup
// is a one-off operator task rather than a button click anyway.
ListPendingExecutionIDsForAccount(ctx context.Context, accountID string) ([]string, error)
CleanupOldExecutions(ctx context.Context, retentionDays int) (int64, error)
// TransitionExecutionStatus atomically transitions an execution status.
// actor is the UUID of the user performing the transition (nil for system-initiated paths).
// When non-nil the actor is stamped onto transitioned_by + transitioned_at; when nil,
// transitioned_by is set to NULL and transitioned_at is still set to NOW() for ordering.
TransitionExecutionStatus(ctx context.Context, executionID string, fromStatuses []string, toStatus string, actor *string) (*PurchaseExecution, error)
// SetCancelledBy stamps canceled_by and the legacy cancelled_by column on
// an execution without overwriting any other columns. Used after
// TransitionExecutionStatus to fold the actor attribution into the same
// logical write without the full-row SavePurchaseExecution clobber risk
// (Finding #5 / PR #889).
SetCancelledBy(ctx context.Context, executionID string, cancelledBy string) error
// CancelExecutionAtomic atomically flips status from pending / notified
// to 'canceled' (canonical US spelling), setting canceled_by. The
// 'scheduled' status is NOT accepted here; scheduled rows are revoked via
// CancelScheduledExecutionAtomic (Gmail-style pre-fire delay revoke
// path, issue #291 wave-2) so the two flows surface distinct CAS race
// outcomes. Returns (true, "canceled", nil) on success and (false,
// currentStatus, nil) when zero rows were affected (the execution had
// already been approved or otherwise transitioned). Must be called
// inside a WithTx block so the suppression cleanup and the status flip
// commit atomically.
CancelExecutionAtomic(ctx context.Context, tx pgx.Tx, executionID string, cancelledBy *string) (canceled bool, currentStatus string, err error)
// CancelScheduledExecutionAtomic atomically flips status from 'scheduled' to
// 'canceled' (canonical US spelling), setting canceled_by. Used by the
// Gmail-style pre-fire delay revoke path (issue #291 wave-2) to cancel a
// scheduled execution at $0 before the scheduler fires the SDK call. The
// 'pending'/'notified' set accepted by CancelExecutionAtomic is
// intentionally not extended here so the two revoke flows surface distinct
// CAS race outcomes -- a scheduled row that the scheduler has already
// transitioned to 'approved' / 'running' must surface as a 410 ("window
// closed") rather than a 409 ("not pending"). Returns (true, "canceled",
// nil) on success and (false, currentStatus, nil) when zero rows were
// affected. Must be called inside a WithTx block.
CancelScheduledExecutionAtomic(ctx context.Context, tx pgx.Tx, executionID string, cancelledBy *string) (canceled bool, currentStatus string, err error)
// ListStuckExecutions returns executions in any of the given statuses
// whose updated_at is older than the given duration. Used by the
// reaper sweep (issue #678) to find rows stuck in approved/running
// after the synchronous executor failed mid-flight without flipping
// them to a terminal state. Oldest-stuck-first (ORDER BY updated_at
// ASC), capped at MaxListLimit per sweep.
ListStuckExecutions(ctx context.Context, statuses []string, olderThan time.Duration) ([]PurchaseExecution, error)
// GetScheduledExecutionsDue returns purchase_executions with
// status='scheduled' whose scheduled_execution_at is in the past
// (scheduled_execution_at <= NOW()). Used by the Gmail-style pre-fire
// delay scheduler tick (issue #291 wave-2) to find rows ready to fire.
// Oldest-due-first (ORDER BY scheduled_execution_at ASC), capped at
// MaxListLimit per sweep.
GetScheduledExecutionsDue(ctx context.Context) ([]PurchaseExecution, error)
// Purchase history
SavePurchaseHistory(ctx context.Context, record *PurchaseHistoryRecord) error
GetPurchaseHistory(ctx context.Context, accountID string, limit int) ([]PurchaseHistoryRecord, error)
GetAllPurchaseHistory(ctx context.Context, limit int) ([]PurchaseHistoryRecord, error)
// GetActivePurchaseHistory returns every purchase_history row whose commitment
// is still within its term at asOf (term > 0 AND timestamp + term years >= asOf;
// the expiry boundary is inclusive, matching the API layer's isActiveCommitment),
// newest-first, optionally scoped to a set of accounts. The account scope uses
// the same dual-column predicate as GetPurchaseHistoryFiltered: accountIDs
// match cloud_account_id (cloud_accounts UUIDs) and externalIDsByProvider
// matches account_id per provider, OR'd together so rows carrying only one
// identifier are still returned (issues #701/#498/#866). Both empty means all
// accounts. Unlike GetAllPurchaseHistory it is not row-capped: the analytics
// collector, dashboard KPIs, and inventory endpoints need the complete active
// set, and filtering expired commitments in SQL keeps the result bounded by
// the number of live commitments rather than by all history ever recorded (so
// it cannot silently truncate older-but-still-active 1y/3y commitments the
// way a newest-first capped page does, issue #1140).
GetActivePurchaseHistory(ctx context.Context, asOf time.Time, accountIDs []string, externalIDsByProvider map[string][]string) ([]PurchaseHistoryRecord, error)
// GetPurchaseHistoryFiltered reads purchase_history rows matching the
// PurchaseHistoryFilter, newest-first, capped at filter.Limit. Each field is
// applied independently and only when populated (see PurchaseHistoryFilter).
// The account predicate matches BOTH identifier columns:
// (cloud_account_id = ANY(AccountIDs)
// OR (provider = $p AND account_id = ANY(ExternalIDsByProvider[p])) OR ...)
// because purchase_history rows carry the cloud_accounts UUID FK
// (cloud_account_id, NULL on direct-execute/ambient/pre-000011 rows) and the
// cloud-provider external number (account_id, always populated) independently.
// The top-bar Account chip emits the UUID; matching only one column silently
// dropped rows that carried only the other (issues #701/#498/#866). The caller
// resolves AccountIDs to their external account numbers grouped by provider
// and populates ExternalIDsByProvider so rows that carry only account_id are
// also matched, while the per-provider grouping keeps a reused external number
// across providers (aws/123 vs azure/123) from leaking the wrong rows.
GetPurchaseHistoryFiltered(ctx context.Context, filter PurchaseHistoryFilter) ([]PurchaseHistoryRecord, error)
// GetPurchaseHistoryByPurchaseID returns the single purchase_history row
// whose purchase_id matches (AWS ReservedInstancesId / Azure reservation
// ID). Returns (nil, nil) when no row is found. Used by the revoke
// endpoint to load the record before calling the provider cancel API
// (issue #290) and by the marketplace-list handler to validate
// offering_class and look up the cloud account (issue #292).
GetPurchaseHistoryByPurchaseID(ctx context.Context, purchaseID string) (*PurchaseHistoryRecord, error)
// MarkPurchaseRevoked stamps revoked_at, revoked_via, and optionally
// support_case_id on a purchase_history row identified by purchase_id.
// calcRefundAmount and calcRefundCurrency capture the Azure CalculateRefund
// quote for audit (migration 000071, Finding #4); both nil/empty for
// non-Azure paths or legacy rows written before the migration.
// Returns a not-found error when no row matches. Idempotent: a second
// call for the same row is a no-op (revoked_at is not overwritten when
// it is already non-null). Used by the revoke endpoint (issue #290).
MarkPurchaseRevoked(ctx context.Context, purchaseID string, revokedAt time.Time, revokedVia string, supportCaseID string, calcRefundAmount *float64, calcRefundCurrency string) error
// FlipPurchaseRevocationInFlight atomically sets revocation_in_flight=true
// on a purchase_history row. Called immediately before the Azure Return API
// call so that the row can be identified by the finalize sweep if the
// subsequent MarkPurchaseRevoked DB write fails (partial-success reconciliation,
// issue #290 Finding #6, migration 000072). No-op when the flag is already
// true (idempotent). Returns a not-found error when no row matches.
FlipPurchaseRevocationInFlight(ctx context.Context, purchaseID string) error
// ClearRevocationInFlight resets revocation_in_flight=false on a
// purchase_history row. Called when the Azure Return call fails with a
// transient or client error (not "already returned"), so the row is not
// left in a permanently-sticky in-flight state that would prevent future
// retries or mislead the finalize_revocations sweep (issue #290, second-wave
// CR Finding D). No-op when the row is already false. Best-effort: callers
// should log on error but not surface it to the user.
ClearRevocationInFlight(ctx context.Context, purchaseID string) error
// GetPurchaseHistoryInFlight returns all purchase_history rows with
// revocation_in_flight=true and revoked_at IS NULL. These are rows where
// the Azure Return call succeeded but MarkPurchaseRevoked failed; the
// finalize_revocations scheduled sweep calls this to retry the DB write.
GetPurchaseHistoryInFlight(ctx context.Context) ([]*PurchaseHistoryRecord, error)
// UpdatePurchaseHistoryListing stamps the AWS marketplace listing_id and
// listing_state onto a purchase_history row. Called after
// CreateReservedInstancesListing succeeds (listing_state="active") and
// on subsequent poll/cancel transitions (issue #292).
UpdatePurchaseHistoryListing(ctx context.Context, purchaseID, listingID, listingState string) error
// StampOfferingClass writes the offering_class value to a purchase_history
// row identified by purchase_id. Called by the marketplace-list handler
// when offering_class is absent in the DB (pre-migration 000087 rows and
// externally-created Standard RIs): after fetching the class from AWS
// DescribeReservedInstances it is persisted so subsequent requests do not
// incur an extra AWS API call.
StampOfferingClass(ctx context.Context, purchaseID, offeringClass string) error
// ClaimMarketplaceListingSlot atomically reserves the marketplace-listing
// slot for a purchase_history row so two concurrent marketplace-list
// requests cannot both proceed to create a duplicate AWS listing (issue
// #292). It transitions listing_state to ListingStatePending only when the
// row is not already listed or mid-listing, and reports whether this call
// won the claim: (true, nil) means the caller reserved the slot and must
// then persist the real listing on success or release the slot back to its
// prior state on failure; (false, nil) means another request already holds
// an active or pending listing (the caller maps this to a 409). Modeled on
// FlipPurchaseRevocationInFlight.
ClaimMarketplaceListingSlot(ctx context.Context, purchaseID string) (bool, error)
// ClaimRIExchangeIdempotencyKey atomically claims key for an RI exchange
// submit, so a client that retries a timed-out execute request cannot
// commit the same exchange twice (issue #1642). key is a fingerprint of
// what the request buys; the caller derives it and claims it immediately
// before the irreversible provider call.
//
// Returns (true, nil) when this call won the claim and may proceed to
// commit, and (false, nil) when another submit of the same fingerprint
// claimed it less than window ago (the caller maps this to a 409).
// A claim older than window is reclaimable, so a genuine intentional
// repeat of the same exchange is not blocked forever.
//
// There is no release: once taken, a claim stands for the rest of the
// window whatever the provider call does, including when it fails. Past
// the point of submission the outcome is ambiguous, and holding the claim
// is the fail-closed choice. The consequence for the caller's 409 is that
// it cannot promise the earlier submit committed, only that it claimed the
// window.
ClaimRIExchangeIdempotencyKey(ctx context.Context, key string, window time.Duration) (bool, error)
// RI Exchange history
SaveRIExchangeRecord(ctx context.Context, record *RIExchangeRecord) error
GetRIExchangeRecord(ctx context.Context, id string) (*RIExchangeRecord, error)
GetRIExchangeRecordByToken(ctx context.Context, token string) (*RIExchangeRecord, error)
GetRIExchangeHistory(ctx context.Context, since time.Time, limit int) ([]RIExchangeRecord, error)
// TransitionRIExchangeStatus atomically transitions an RI exchange record status.
// actor is the UUID of the user performing the transition (nil for system-initiated paths).
TransitionRIExchangeStatus(ctx context.Context, id string, fromStatus string, toStatus string, actor *string) (*RIExchangeRecord, error)
CompleteRIExchange(ctx context.Context, id string, exchangeID string) error
// CompleteRIExchangeWithPayment marks an RI exchange as completed and
// updates payment_due to the amount AWS actually accepted. Use this instead
// of CompleteRIExchange on the manual-approval path so the daily-spend
// ledger (GetRIExchangeDailySpend) reflects the real accepted amount rather
// than the stale pre-execution quote (H3 fix).
CompleteRIExchangeWithPayment(ctx context.Context, id string, exchangeID string, acceptedPaymentDue string) error
// StampRIExchangeApprovedBy sets the approved_by column on a completed
// exchange row (issue #300). Called after CompleteRIExchangeWithPayment when
// the approval came from a session-authed user rather than an email token.
StampRIExchangeApprovedBy(ctx context.Context, id string, approverEmail string) error
FailRIExchange(ctx context.Context, id string, errorMsg string) error
GetRIExchangeDailySpend(ctx context.Context, date time.Time) (string, error)
CancelAllPendingExchanges(ctx context.Context) (int64, error)
// CancelPendingExchangesByOrigin cancels only pending records whose origin
// matches:
// - common.ExchangeOriginStandalone: cancels WHERE ladder_run_id IS NULL
// - common.ExchangeOriginLadder: cancels WHERE ladder_run_id IS NOT NULL
// The origin is validated at the boundary and an unknown value fails loud.
// This prevents the standalone ri_exchange_reshape task from wiping out
// ladder-linked pending reshapes and vice versa (gap G10 / issue #1348).
CancelPendingExchangesByOrigin(ctx context.Context, origin common.ExchangeOrigin) (int64, error)
GetStaleProcessingExchanges(ctx context.Context, olderThan time.Duration) ([]RIExchangeRecord, error)
// Cloud accounts
CreateCloudAccount(ctx context.Context, account *CloudAccount) error
GetCloudAccount(ctx context.Context, id string) (*CloudAccount, error)
// GetCloudAccountByExternalID looks up a cloud account by its
// (provider, external_id) pair. Used by the scheduler's ambient
// collector path to tag rec rows with the registered host-account's
// UUID when the Lambda's STS identity matches a registered (but
// possibly disabled) account — so the approve-modal Account column
// shows the account name instead of `(ambient)`. Returns
// (nil, nil) when no row matches (the caller treats this as the
// genuine orphan case). The underlying
// `UNIQUE(provider, external_id)` constraint on cloud_accounts
// guarantees the lookup hits an index.
GetCloudAccountByExternalID(ctx context.Context, provider, externalID string) (*CloudAccount, error)
UpdateCloudAccount(ctx context.Context, account *CloudAccount) error
DeleteCloudAccount(ctx context.Context, id string) error
ListCloudAccounts(ctx context.Context, filter CloudAccountFilter) ([]CloudAccount, error)
// Account credentials (encrypted blobs; never returned via API)
SaveAccountCredential(ctx context.Context, accountID, credentialType, encryptedBlob string) error
GetAccountCredential(ctx context.Context, accountID, credentialType string) (string, error)
DeleteAccountCredentials(ctx context.Context, accountID string) error
HasAccountCredentials(ctx context.Context, accountID string) (bool, error)
// Account service overrides
GetAccountServiceOverride(ctx context.Context, accountID, provider, service string) (*AccountServiceOverride, error)
SaveAccountServiceOverride(ctx context.Context, override *AccountServiceOverride) error
DeleteAccountServiceOverride(ctx context.Context, accountID, provider, service string) error
ListAccountServiceOverrides(ctx context.Context, accountID string) ([]AccountServiceOverride, error)
// Plan ↔ account association
SetPlanAccounts(ctx context.Context, planID string, accountIDs []string) error
GetPlanAccounts(ctx context.Context, planID string) ([]CloudAccount, error)
// Recommendations cache (ADR: store recommendations in Postgres so the
// dashboard serves provider-switch clicks from SQL instead of live cloud
// API calls). ReplaceRecommendations is the "force full resync" path;
// UpsertRecommendations is the steady-state write path and takes a list
// of (provider, account) pairs that successfully collected this cycle.
// Stale-row eviction is scoped to that union so a partially-failed
// provider preserves the failed accounts' previous-cycle rows. See
// SuccessfulCollect for the per-row semantics.
ReplaceRecommendations(ctx context.Context, collectedAt time.Time, recs []RecommendationRecord) error
UpsertRecommendations(ctx context.Context, collectedAt time.Time, recs []RecommendationRecord, successfulCollects []SuccessfulCollect) error
ListStoredRecommendations(ctx context.Context, filter RecommendationFilter) ([]RecommendationRecord, error)
GetRecommendationsFreshness(ctx context.Context) (*RecommendationsFreshness, error)
SetRecommendationsCollectionError(ctx context.Context, errMsg string) error
// MarkCollectionStarted atomically sets last_collection_started_at = now
// only when no in-flight collection is running (last_collection_started_at IS NULL
// OR older than 5 minutes), and stamps a freshly generated owner token
// alongside it. Returns the token and true when this caller won the race
// and should proceed with the async invoke, passing the token through so
// only this caller can later clear the marker; ("", false, nil) when
// another collection is already in flight and the caller should return
// 409.
MarkCollectionStarted(ctx context.Context) (token string, ok bool, err error)
// ClearCollectionStarted clears last_collection_started_at, but only if
// last_collection_owner_id still matches token: a compare-and-clear
// guard (issue #261) so a caller that never won MarkCollectionStarted
// (cron, cold-start) cannot wipe another caller's in-flight marker. A
// mismatched token is a documented silent no-op (the marker belongs to
// someone else); an empty token is a boundary error, since only a
// caller that actually owns a marker should ever call Clear.
ClearCollectionStarted(ctx context.Context, token string) error
// RI utilization cache. Postgres-backed TTL cache for Cost Explorer
// GetReservationUtilization; shared across Lambda containers so
// dashboard loads don't each fan out to a paid CE API call. A
// per-process in-memory cache effectively never hits because each
// cold container starts empty. GetRIUtilizationCache returns nil
// when the (region, lookback_days) key is absent — callers treat
// that as a miss and re-fetch.
GetRIUtilizationCache(ctx context.Context, region string, lookbackDays int) (*RIUtilizationCacheEntry, error)
UpsertRIUtilizationCache(ctx context.Context, region string, lookbackDays int, payload []byte, fetchedAt time.Time) error
// Account registrations (self-service enrollment via federation IaC)
CreateAccountRegistration(ctx context.Context, reg *AccountRegistration) error
GetAccountRegistration(ctx context.Context, id string) (*AccountRegistration, error)
GetAccountRegistrationByToken(ctx context.Context, token string) (*AccountRegistration, error)
ListAccountRegistrations(ctx context.Context, filter AccountRegistrationFilter) ([]AccountRegistration, error)
UpdateAccountRegistration(ctx context.Context, reg *AccountRegistration) error
// TransitionRegistrationStatus atomically updates a registration's workflow fields.
// actor is the UUID of the reviewer (nil for system-initiated transitions).
TransitionRegistrationStatus(ctx context.Context, reg *AccountRegistration, fromStatus string, actor *string) error
DeleteAccountRegistration(ctx context.Context, id string) error
// Purchase suppressions. Written inside a WithTx block during bulk
// purchase submit so the execution insert + the suppression rows
// commit atomically. Deleted on cancel/expire of the execution,
// also inside a WithTx block paired with the status update.
//
// The plain variants (no Tx) open their own single-call transaction
// — useful for tests and one-off admin operations. The Tx variants
// reuse a caller-provided transaction so multi-write operations
// can roll back atomically.
CreateSuppression(ctx context.Context, sup *PurchaseSuppression) error
CreateSuppressionTx(ctx context.Context, tx pgx.Tx, sup *PurchaseSuppression) error
DeleteSuppressionsByExecution(ctx context.Context, executionID string) error
DeleteSuppressionsByExecutionTx(ctx context.Context, tx pgx.Tx, executionID string) error
ListActiveSuppressions(ctx context.Context) ([]PurchaseSuppression, error)
// SavePurchaseExecutionTx is the tx-accepting variant of
// SavePurchaseExecution. Used from executePurchase's WithTx block
// so the execution insert + suppression writes commit atomically.
SavePurchaseExecutionTx(ctx context.Context, tx pgx.Tx, execution *PurchaseExecution) error
// GetPendingExecutionsTx is the tx-accepting variant of
// GetPendingExecutions. Used inside the executePurchase WithTx block
// so the duplicate-detection read and the new-execution insert are
// atomic under the same transaction, eliminating the TOCTOU race (#643).
GetPendingExecutionsTx(ctx context.Context, tx pgx.Tx) ([]PurchaseExecution, error)
// WithTx opens a pgx transaction, runs fn, and commits on success or
// rolls back on error. fn can call any *Tx method on the store to
// participate in the transaction. Nested transactions are not
// supported — fn must not call WithTx recursively.
WithTx(ctx context.Context, fn func(tx pgx.Tx) error) error
// Ladder configuration (per-account, per-provider).
// GetLadderConfigs returns all rows, newest first.
// GetLadderConfig returns the single row for (cloudAccountID, provider),
// or (nil, nil) when no row exists.
// UpsertLadderConfig inserts or updates via the UNIQUE(cloud_account_id, provider)
// constraint and returns the persisted row with all DB-stamped fields populated.
GetLadderConfigs(ctx context.Context) ([]LadderConfigDB, error)
GetLadderConfig(ctx context.Context, cloudAccountID, provider string) (*LadderConfigDB, error)
UpsertLadderConfig(ctx context.Context, cfg *LadderConfigDB) (*LadderConfigDB, error)
// Ladder run/tranche persistence (migration 000080/000081, PR-2).
//
// SaveLadderRun inserts a new ladder_runs row, returning the persisted row
// with all DB-stamped fields (id, created_at, updated_at) populated.
// If run.ID is empty, a new UUID is generated before the insert.
//
// GetLadderRun returns the row for the given id, or (nil, nil) when no
// row exists (mirrors GetLadderConfig semantics).
//
// SaveLadderTranches inserts a batch of ladder_tranches rows inside a
// single transaction. Each tranche must carry a non-empty ID; duplicate
// IDs within the batch are rejected at the DB UNIQUE constraint level.
//
// LatestLadderRunStartedAt returns the maximum started_at for the given
// config_id, or nil when no run has been recorded yet. Powers the per-cadence
// self-gate in the scheduler (Q6).
//
// TransitionLadderRunStatus atomically updates the status of a ladder_runs
// row from one of the fromStatuses to toStatus, returning the updated row.
// Returns (nil, nil) when zero rows are affected (CAS race lost or wrong
// current status), so callers can distinguish a race from a hard error.
// Statuses are typed ladder.RunStatus so callers cannot pass an arbitrary
// string that would never match a stored status.
// SaveLadderRunWithTranches inserts the run row and its tranches in ONE
// transaction: a tranche-insert failure rolls back the run row too, so a
// status=planned run never persists without its tranches (which would let
// the cadence gate suppress the retry for a full window).
//
// L5 append-only: every ladder run persists its new scheduled tranches via
// this method and leaves any existing scheduled tranches untouched. In-flight
// netting (GetInFlightLadderCommitUSDHr) already subtracts existing
// scheduled tranches from the gap, so each run appends exactly the delta
// needed to reach target-E. Appending (never superseding) keeps prior
// tranches' original fire dates, converges to target on drift-up (run N
// tops up the gap the netting leaves), and cannot oscillate.
//
// GetInFlightLadderCommitUSDHr returns the total hourly USD commitment in
// flight for the given config: the sum of amount_usd_hr for tranches with
// status = 'scheduled' ONLY. Fired/completed tranches are executed
// purchases already reflected in the engine's ExistingUSDPerHour (the
// provider adapters fold payment-pending and active commitments into E),
// so summing them here too would double-count and under-purchase. Returns
// a non-nil pointer (zero when no scheduled tranches exist) so callers can
// pass it directly to AllocationInput.InFlightUSDPerHour. Never returns
// nil without an error.
SaveLadderRun(ctx context.Context, run *LadderRunDB) (*LadderRunDB, error)
SaveLadderRunWithTranches(ctx context.Context, run *LadderRunDB, tranches []LadderTrancheDB) (*LadderRunDB, error)
GetInFlightLadderCommitUSDHr(ctx context.Context, configID string) (*float64, error)
GetLadderRun(ctx context.Context, id string) (*LadderRunDB, error)
SaveLadderTranches(ctx context.Context, tranches []LadderTrancheDB) error
LatestLadderRunStartedAt(ctx context.Context, configID string) (*time.Time, error)
TransitionLadderRunStatus(ctx context.Context, id string, fromStatuses []ladder.RunStatus, toStatus ladder.RunStatus) (*LadderRunDB, error)
// Notification mutes (issue #297 / migration 000091).
// UpsertNotificationMute inserts or updates a mute row for (email, scope).
// Idempotent for row existence: calling it again for an already-muted
// address refreshes muted_at and replaces unmute_token if the token changes.
UpsertNotificationMute(ctx context.Context, recipientEmail, scope, unmuteToken string) error
// IsNotificationMuted returns true when (email, scope) has a row in
// muted_recipients. The email comparison is case-insensitive.
IsNotificationMuted(ctx context.Context, recipientEmail, scope string) (bool, error)
}
StoreInterface defines the methods required for configuration storage.
type SuccessfulCollect ¶
SuccessfulCollect identifies a (provider, account) pair whose collection completed in the current cycle. UpsertRecommendations scopes the stale-row eviction DELETE to the union of these pairs so a partially- failed provider preserves the failed accounts' previous-cycle rows (the dashboard for those accounts isn't blanked out by transient cloud-API failures).
CloudAccountID is nil for the AWS ambient-credentials path (no registered account); the eviction collapses nil to the zero UUID via the same generated-column rule that applies to inserts, so ambient rows are evicted independently of any registered-account rows under the same provider.
Source Files
¶
- constants.go
- defaults.go
- errors.go
- interfaces.go
- purchase_plan_providers.go
- recommendation_overrides.go
- resolver.go
- store_postgres.go
- store_postgres_ladder.go
- store_postgres_ramp_step.go
- store_postgres_recommendations.go
- store_postgres_registrations.go
- store_postgres_ri_utilization.go
- store_postgres_suppressions.go
- types.go
- validation.go