Documentation
¶
Overview ¶
Package receipts syncs what Rivian actually billed for each charging session (its Charging history) next to our own estimate.
Index ¶
- Constants
- Variables
- func PaidRates(rs []Receipt, now time.Time, currency string) map[string]PaidRate
- type ChargeRef
- type EnergyCand
- type EnergyUpdate
- type PaidRate
- type Pair
- type Receipt
- type Result
- type Store
- func (s *Store) Apply(ctx context.Context, uid uuid.UUID, pairs []Pair) error
- func (s *Store) ApplyEnergy(ctx context.Context, uid uuid.UUID, c EnergyCand, up EnergyUpdate) (bool, error)
- func (s *Store) CandidateCharges(ctx context.Context, uid uuid.UUID, from, to time.Time) ([]ChargeRef, error)
- func (s *Store) ClaimDetails(ctx context.Context, uid uuid.UUID, minInterval time.Duration) (bool, error)
- func (s *Store) ClaimSummaries(ctx context.Context, uid uuid.UUID, minInterval time.Duration) (bool, error)
- func (s *Store) EnergyCandidates(ctx context.Context, uid uuid.UUID, since time.Time) ([]EnergyCand, error)
- func (s *Store) ListAll(ctx context.Context, uid uuid.UUID) ([]Receipt, error)
- func (s *Store) MarkResult(ctx context.Context, uid uuid.UUID, runErr error) error
- func (s *Store) NeedDetail(ctx context.Context, uid uuid.UUID, limit int) ([]string, error)
- func (s *Store) PaidRates(ctx context.Context, uid uuid.UUID, now time.Time, currency string) (map[string]PaidRate, error)
- func (s *Store) Purge(ctx context.Context, uid uuid.UUID) error
- func (s *Store) PurgeNotOptedIn(ctx context.Context) (int64, error)
- func (s *Store) RefreshMatched(ctx context.Context, uid uuid.UUID) error
- func (s *Store) SaveDetail(ctx context.Context, uid uuid.UUID, txn string, c *rivian.SessionCharger) error
- func (s *Store) Unmatched(ctx context.Context, uid uuid.UUID) ([]Receipt, error)
- func (s *Store) UpsertReceipts(ctx context.Context, uid uuid.UUID, in []rivian.SessionSummary) error
- type Syncer
- func (s *Syncer) OnChargeClose(uid uuid.UUID)
- func (s *Syncer) Run(ctx context.Context)
- func (s *Syncer) SetEnergy(mode string, observe func(outcome string, deltaPct float64))
- func (s *Syncer) SetFence(f UserFencer)
- func (s *Syncer) SetObserve(f func(trigger, outcome string))
- func (s *Syncer) Sync(ctx context.Context, uid uuid.UUID, minInterval time.Duration, ...) (Result, error)
- func (s *Syncer) SyncOnRequest(ctx context.Context, uid uuid.UUID) (Result, error)
- type UserFencer
Constants ¶
const ( EnergyOff = "off" EnergyObserve = "observe" EnergyApply = "apply" )
Energy modes (RIVOLT_RECEIPT_ENERGY): Observe measures and counts only, Apply also writes.
const ( OutcomeApplied = "applied" OutcomeWouldApply = "would_apply" OutcomeHome = "home" OutcomeNoDetail = "no_detail" OutcomeNoEnergy = "no_energy" OutcomeOpen = "open" OutcomeBelow = "below_recorded" OutcomeAbove = "above_tail_limit" )
Outcomes of looking at one matched receipt, for the metrics.
const ( // PaidMinSessions is how many usable receipts a network needs before a figure is // shown; fewer is noise (one off-peak session, one promotion). PaidMinSessions = 3 // PaidMaxSessions caps it at the most recent sessions, so an old price does not // outweigh the current one. PaidMaxSessions = 10 // PaidMaxAge drops receipts older than this: a price paid long ago is not // today's price. PaidMaxAge = 365 * 24 * time.Hour )
const ( TriggerForward = "forward" TriggerSweep = "sweep" TriggerClose = "close" )
Triggers and outcomes of a summaries pass, for rivolt_receipts_sync_total. Outcomes: ok (the pass ran), skipped (opted out, no receipt source, or the 30 min claim is held by an earlier pass on any pod), not_owner (another pod owns the user's group), error.
const SourceReceipt = "rivian_receipt"
SourceReceipt is the metadata energy_source of a charge whose energy came from its receipt.
Variables ¶
var ( Triggers = []string{TriggerForward, TriggerSweep, TriggerClose} Outcomes = []string{"ok", "skipped", "not_owner", "error"} )
Functions ¶
func PaidRates ¶ added in v0.38.7
PaidRates computes the figures from a user's receipts: per network (the vendor and charger name matched to the network table), over the most recent PaidMaxSessions receipts that are within PaidMaxAge, not at a home charger, in the given currency, with a positive amount and energy. A network with fewer than PaidMinSessions such receipts has no entry. Pure.
Types ¶
type EnergyCand ¶ added in v0.41.10
type EnergyCand struct {
ChargeID uuid.UUID
ReceiptID uuid.UUID
TxnID string
FinalState string
ChargeEnd time.Time
MaxPowerKW float64
// Energy is the charge's energy now; Recorded is what the recorder saved first (the metadata
// original once a receipt value is in, else Energy); AvgRecorded and CostRecorded the same for
// the average power and the cost.
Energy, Recorded, AvgPower float64
AvgRecorded *float64
Cost *float64
CostRecorded *float64
CostSource string
PricePerKWh float64
FromReceipt bool
ReceiptStart, ReceiptEnd time.Time
ReceiptEnergy *float64
IsHome *bool
DetailFetched bool
}
EnergyCand is one matched receipt next to its charge, as the check needs them.
type EnergyUpdate ¶ added in v0.41.10
type EnergyUpdate struct {
Energy float64
AvgPower float64
Cost *float64 // nil: leave the cost alone
Meta map[string]any
ExpectedNow float64
}
EnergyUpdate is what to write for a candidate that passed.
func EvaluateEnergy ¶ added in v0.41.10
func EvaluateEnergy(c EnergyCand) (outcome string, deltaPct float64, up *EnergyUpdate)
EvaluateEnergy checks one candidate. outcome is OutcomeWouldApply when the receipt's energy should be taken (nil update means nothing to write: it already is), deltaPct the receipt's difference from what we recorded as a percentage of it (0 when not computed).
type PaidRate ¶ added in v0.38.7
type PaidRate struct {
Slug string
Currency string
// PerKWh is total paid over total energy across Sessions receipts (energy-weighted,
// not an average of per-session prices).
PerKWh float64
Sessions int
}
PaidRate is one network's figure for one user.
type Pair ¶
Pair says which charge a receipt belongs to.
func Match ¶
Match pairs unmatched receipts with unmatched charges one to one.
A receipt and a charge are candidates when both their starts and their ends are within 5 minutes, or when one of the two is within 5 minutes and the energies agree (a long trickle session can end at different moments on the two clocks). When both energies are known and disagree the pair is never a candidate, however close the times. Candidates are taken best first by |start gap| + |end gap|.
type Receipt ¶
type Receipt struct {
ID uuid.UUID
TransactionID string
StartedAt time.Time
EndedAt time.Time
TotalCost *float64
Currency string
EnergyKWh *float64
RangeAddedKm *float64
Vendor string
City string
ChargerName string
IsHome *bool
DetailFetched bool
ChargeID *uuid.UUID
}
Receipt is one Rivian charging-history row.
type Result ¶
type Result struct {
Skipped bool // claimed by another run within the interval, or not this pod's user
NotOwner bool // skipped because another pod owns the user's group (lead mode)
Fetched int // summaries returned
Matched int // new receipt-to-charge links
Details int // detail calls that succeeded
DetailsLeft bool
}
Result is what one Sync did, for logs and tests.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store is the Postgres side: rivian_receipts, rivian_receipt_sync and the charges.receipt_* columns (migration 0055). It never touches a charge's cost, cost_source or price_per_kwh.
func (*Store) Apply ¶
Apply links each pair: the receipt gets charge_id, the charge gets the billed amount in receipt_*. A charge's cost, cost_source and price_per_kwh are left alone.
func (*Store) ApplyEnergy ¶ added in v0.41.10
func (s *Store) ApplyEnergy(ctx context.Context, uid uuid.UUID, c EnergyCand, up EnergyUpdate) (bool, error)
ApplyEnergy writes one update to the charge, only while it is still closed, still linked to the receipt and still holding the energy the check saw. It reports whether a row changed.
func (*Store) CandidateCharges ¶
func (s *Store) CandidateCharges(ctx context.Context, uid uuid.UUID, from, to time.Time) ([]ChargeRef, error)
CandidateCharges lists the user's charges without a receipt whose window touches [from, to].
func (*Store) ClaimDetails ¶
func (s *Store) ClaimDetails(ctx context.Context, uid uuid.UUID, minInterval time.Duration) (bool, error)
ClaimDetails is the same for a details-only pass (no summaries call).
func (*Store) ClaimSummaries ¶
func (s *Store) ClaimSummaries(ctx context.Context, uid uuid.UUID, minInterval time.Duration) (bool, error)
ClaimSummaries records an attempt to fetch the user's summaries and reports whether this caller won it: no attempt in the last minInterval, across pods and restarts.
func (*Store) EnergyCandidates ¶ added in v0.41.10
func (s *Store) EnergyCandidates(ctx context.Context, uid uuid.UUID, since time.Time) ([]EnergyCand, error)
EnergyCandidates lists the user's receipts matched since `since` with their charges.
func (*Store) MarkResult ¶
MarkResult stores the outcome of a claimed attempt. The error text is kept short and is only ever our own classification, never a response body.
func (*Store) NeedDetail ¶
NeedDetail returns up to limit transaction ids of matched receipts whose charger details were never fetched.
func (*Store) PaidRates ¶ added in v0.38.7
func (s *Store) PaidRates(ctx context.Context, uid uuid.UUID, now time.Time, currency string) (map[string]PaidRate, error)
PaidRates reads the user's receipts and returns their paid $/kWh per network. Empty for a user without receipts (they must have opted in for any to exist).
func (*Store) Purge ¶ added in v0.36.10
Purge removes everything the receipts sync stored for a user: the receipts, the sync clock, and the receipt_* columns on their charges. The estimate on the charge is untouched. Used when the user opts out.
func (*Store) PurgeNotOptedIn ¶ added in v0.36.10
PurgeNotOptedIn is the daily backstop for Purge: it removes the receipts of every user who is not opted in, catching a pass that saved rows after the user opted out. It returns how many receipts it deleted.
func (*Store) RefreshMatched ¶
RefreshMatched copies a changed amount or currency from a receipt to the charge it is linked to.
func (*Store) SaveDetail ¶
func (s *Store) SaveDetail(ctx context.Context, uid uuid.UUID, txn string, c *rivian.SessionCharger) error
SaveDetail stores the charger fields. For a home charger only the flag is kept: its name can be the owner's own place. A nil charger still marks the row fetched so it is not asked again.
func (*Store) UpsertReceipts ¶
func (s *Store) UpsertReceipts(ctx context.Context, uid uuid.UUID, in []rivian.SessionSummary) error
UpsertReceipts stores the summaries, refreshing amounts of rows we already have (a receipt can finalize later) and keeping detail and match state.
type Syncer ¶
type Syncer struct {
// contains filtered or unexported fields
}
Syncer pulls receipts for users whose flag is on.
func NewSyncer ¶
func NewSyncer(st *Store, accounts rivian.AccountRegistry, enabled func(uuid.UUID) bool, users func(context.Context) ([]uuid.UUID, error), logger *slog.Logger) *Syncer
NewSyncer wires a Syncer. enabled gates a user (the rivian_receipts flag); users lists the users with a Rivian session for the sweep.
func (*Syncer) OnChargeClose ¶
OnChargeClose schedules a sync for the user after closeDelay. One timer per user: further closes while it is pending are absorbed.
func (*Syncer) Run ¶
Run sweeps until ctx ends: every sweepEvery it syncs each enabled user once a day (summaries) and gives users with unfetched details a details-only pass, one user at a time, userPace apart.
func (*Syncer) SetEnergy ¶ added in v0.41.10
SetEnergy sets the energy mode and the observer of its outcomes (outcome, delta percent; the percent is NaN-free and 0 when not computed). Default (never called) is off.
func (*Syncer) SetFence ¶ added in v0.40.13
func (s *Syncer) SetFence(f UserFencer)
SetFence makes the sync run only on the pod that owns the user's group, with its writes guarded by that lease (lead mode); with lead mode off the store says Off and nothing changes.
func (*Syncer) SetObserve ¶ added in v0.41.10
SetObserve installs the counter hook for summaries passes (before Run and before the first charge close).
func (*Syncer) Sync ¶
func (s *Syncer) Sync(ctx context.Context, uid uuid.UUID, minInterval time.Duration, detailsOnly bool) (Result, error)
Sync runs one pass for a user: unless detailsOnly, fetch the summaries, store them and match them to charges; then fetch up to maxDetailPerRun charger details for matched receipts. minInterval is the claim window.
func (*Syncer) SyncOnRequest ¶ added in v0.41.10
SyncOnRequest is the owner's side of a forwarded receipts_sync: one summaries-and-match pass (no detail calls, so it fits the kind timeout) with the close-time claim window, so the cross-pod claim lets at most one summaries call per user through every 30 min however often it is asked for. The fence check inside Sync comes before the claim.