milestones

package
v0.40.13 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 30, 2026 License: MIT Imports: 13 Imported by: 0

Documentation

Overview

Package milestones notices when a vehicle passes one of a few round numbers (1,000 miles recorded, 1 MWh charged at home, 100 charges, halfway through the lease, and a personal-best efficiency on a long drive), keeps a dismissible card for each, and pushes it to the devices that turned milestones on.

Each milestone is announced once per vehicle, ever: the (account, vehicle, key) row is both the card and the guard. Milestones already passed when this shipped are marked 'backfilled' by a one-off pass and never announced; until that pass is done nothing is announced at all. Pushes go through internal/engage (the monthly budget, quiet hours, the toggle read at send time) and give way when the weekly recap needs the budget: a milestone push is only sent while at least two budget slots are left. Aggregates only: no place, no coordinate.

Index

Constants

View Source
const (
	KindMiles   = "miles_1000"
	KindHomeMWh = "home_mwh_1"
	KindCharges = "charges_100"
	KindLease   = "lease_half"
	KindBestEff = "best_eff" // key best_eff:YYYY-MM
)

Kinds, and their key prefixes. A key is stored per vehicle.

View Source
const (
	MilesThreshold   = 1000.0
	HomeKWhThreshold = 1000.0
	ChargesThreshold = 100
	// A personal best needs a long drive, a history to beat and a clear margin.
	BestEffMinMiles  = 50.0
	BestEffMinPrior  = 5
	BestEffMargin    = 1.05
	StatusAchieved   = "achieved"
	StatusBackfilled = "backfilled"
	CardWindow       = 30 * 24 * time.Hour
	// PushReserve is how many budget slots a milestone push leaves for the weekly
	// recap: it is sent only while at most MonthlyBudget-PushReserve are used.
	PushReserve = 2
)

Thresholds.

View Source
const ReofferWindow = 72 * time.Hour

ReofferWindow is how long a milestone whose push was refused stays eligible for another try; older ones stay card-only.

Variables

This section is empty.

Functions

func PushAllowed

func PushAllowed(used int) bool

PushAllowed says whether a milestone push may use the account's budget now: it leaves PushReserve slots for the weekly recap.

func Text

func Text(kind string, d Detail) (title, body string)

Text is the card's title and body, also the push's: aggregates only, one sentence, no exclamation marks. The wording is growth's.

Types

type Checker

type Checker struct {
	Store Store
	// Input loads the vehicle's whole ledger input.
	Input func(ctx context.Context, uid uuid.UUID, rivianID string) (ledger.Input, error)
	// Lease says how far through its lease the vehicle is; Total 0 when none.
	Lease func(ctx context.Context, uid uuid.UUID, vehicle uuid.UUID) LeaseTerm
	// Push sends the milestone to the account's devices that have it on, through
	// engage; nil sends nothing (the card still appears).
	Push   func(ctx context.Context, uid uuid.UUID, vehicle uuid.UUID, r Row)
	Now    func() time.Time
	Logger *slog.Logger
}

Checker finds new milestones and announces them.

func (*Checker) Backfill

func (c *Checker) Backfill(ctx context.Context) error

Backfill marks every milestone already passed as backfilled (never announced), then marks the feature ready. Idempotent and safe on every replica.

func (*Checker) CatchUp

func (c *Checker) CatchUp(ctx context.Context) error

CatchUp checks the vehicles that drove or charged in the last two days, for a close a restart or a failed hook missed.

func (*Checker) Check

func (c *Checker) Check(ctx context.Context, uid uuid.UUID, rivianID, driveID string)

Check looks for new milestones after a drive or charge closed (driveID names the drive, empty for a charge or the catch-up pass). Until the backfill is done it announces nothing.

func (*Checker) Run

func (c *Checker) Run(ctx context.Context)

Run backfills shortly after start (if it has not been done) and then catches up every six hours, until ctx ends.

type Detail

type Detail struct {
	Miles       float64 `json:"miles,omitempty"`
	MiPerKWh    float64 `json:"mi_per_kwh,omitempty"`
	CostPerMile float64 `json:"cost_per_mile,omitempty"`
	Currency    string  `json:"currency,omitempty"`
	HomeKWh     float64 `json:"home_kwh,omitempty"`
	HomeCost    float64 `json:"home_cost,omitempty"`
	Charges     int     `json:"charges,omitempty"`
	KWhAdded    float64 `json:"kwh_added,omitempty"`
	DriveMiles  float64 `json:"drive_miles,omitempty"`
	DriveMiKWh  float64 `json:"drive_mi_per_kwh,omitempty"`
	LeaseDays   int     `json:"lease_days_elapsed,omitempty"`
	LeaseTotal  int     `json:"lease_days_total,omitempty"`
}

Detail is what a milestone records: numbers only.

type Found

type Found struct {
	Key    string
	Kind   string
	Detail Detail
}

Found is a milestone that has been passed.

func Evaluate

func Evaluate(in ledger.Input, now time.Time, lease LeaseTerm, driveID string) []Found

Evaluate lists the milestones the vehicle has passed: the totals ones, the lease one when it has a lease, and the personal best when driveID names a drive that just closed. It reports every one passed, announced or not; the caller keeps only the new ones.

type LeaseTerm

type LeaseTerm struct{ Elapsed, Total int }

LeaseTerm is how far through its lease a vehicle is; Total 0 means no lease.

type PGStore

type PGStore struct {
	// contains filtered or unexported fields
}

PGStore is the Postgres store; it also serves the card API.

func NewStore

func NewStore(db *sql.DB) *PGStore

NewStore returns the Postgres store.

func (*PGStore) Dismiss

func (s *PGStore) Dismiss(ctx context.Context, uid uuid.UUID, rivianID, key string, now time.Time) (bool, error)

Dismiss hides a card; false when there is no such card for the account.

func (*PGStore) Insert

func (s *PGStore) Insert(ctx context.Context, r Row) (bool, error)

func (*PGStore) List

func (s *PGStore) List(ctx context.Context, uid uuid.UUID, since time.Time) ([]Row, error)

List returns the account's milestone cards: achieved since the cutoff and not dismissed, newest first.

func (*PGStore) MarkReady

func (s *PGStore) MarkReady(ctx context.Context) error

func (*PGStore) PendingPush

func (s *PGStore) PendingPush(ctx context.Context, since time.Time) ([]Row, error)

PendingPush: see Store. The claim key is the one the push is sent under (milestone:<key>:<vehicle id>), so "pushed" is read from engagement_sends and nothing new is stored. Pending means no engagement_sends row at all: engage leaves no row for a refused send, and never re-sends a claimed key (sent, failed or stuck), so a failed push must not hold the account's one re-offer slot.

func (*PGStore) Ready

func (s *PGStore) Ready(ctx context.Context) (bool, error)

func (*PGStore) Vehicle

func (s *PGStore) Vehicle(ctx context.Context, uid uuid.UUID, rivianID string) (uuid.UUID, bool, error)

func (*PGStore) Vehicles

func (s *PGStore) Vehicles(ctx context.Context, active time.Time) ([]VehicleRef, error)

type Row

type Row struct {
	User        uuid.UUID
	Vehicle     uuid.UUID
	RivianID    string
	Key, Kind   string
	Status      string
	AchievedAt  time.Time
	Detail      Detail
	DismissedAt *time.Time
}

Row is a stored milestone.

type Store

type Store interface {
	// Ready reports whether the one-off backfill has finished.
	Ready(ctx context.Context) (bool, error)
	MarkReady(ctx context.Context) error
	// Insert adds a milestone row; false when it already exists.
	Insert(ctx context.Context, r Row) (bool, error)
	// Vehicle maps a Rivian vehicle id to the account's vehicle row.
	Vehicle(ctx context.Context, uid uuid.UUID, rivianID string) (uuid.UUID, bool, error)
	// Vehicles lists accounts' vehicles; active limits it to those with a drive or
	// charge since the given time (the zero time: all).
	Vehicles(ctx context.Context, active time.Time) ([]VehicleRef, error)
	// PendingPush lists achieved, undismissed milestones achieved since the
	// cutoff whose push was never claimed (no engagement_sends row for their
	// claim key, whatever its status), oldest first.
	PendingPush(ctx context.Context, since time.Time) ([]Row, error)
}

Store is the persistence the checker needs.

type VehicleRef

type VehicleRef struct {
	User     uuid.UUID
	ID       uuid.UUID
	RivianID string
}

VehicleRef names a vehicle row and its owner.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL