receipts

package
v0.38.13 Latest Latest
Warning

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

Go to latest
Published: Oct 21, 2026 License: MIT Imports: 14 Imported by: 0

Documentation

Overview

Package receipts syncs what Rivian actually billed for each charging session (its Charging history) next to our own estimate.

Index

Constants

View Source
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
)

Variables

This section is empty.

Functions

func PaidRates added in v0.38.7

func PaidRates(rs []Receipt, now time.Time, currency string) map[string]PaidRate

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 ChargeRef

type ChargeRef struct {
	ID        uuid.UUID
	StartedAt time.Time
	EndedAt   time.Time
	EnergyKWh float64
}

ChargeRef is the part of a charge row the matcher looks at.

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

type Pair struct {
	ReceiptID uuid.UUID
	ChargeID  uuid.UUID
}

Pair says which charge a receipt belongs to.

func Match

func Match(charges []ChargeRef, receipts []Receipt) []Pair

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
	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 NewStore

func NewStore(d *sql.DB) *Store

NewStore wraps a pool.

func (*Store) Apply

func (s *Store) Apply(ctx context.Context, uid uuid.UUID, pairs []Pair) error

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) 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) ListAll

func (s *Store) ListAll(ctx context.Context, uid uuid.UUID) ([]Receipt, error)

ListAll is every receipt of the user, for the data export.

func (*Store) MarkResult

func (s *Store) MarkResult(ctx context.Context, uid uuid.UUID, runErr error) error

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

func (s *Store) NeedDetail(ctx context.Context, uid uuid.UUID, limit int) ([]string, error)

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

func (s *Store) Purge(ctx context.Context, uid uuid.UUID) error

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

func (s *Store) PurgeNotOptedIn(ctx context.Context) (int64, error)

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

func (s *Store) RefreshMatched(ctx context.Context, uid uuid.UUID) error

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) Unmatched

func (s *Store) Unmatched(ctx context.Context, uid uuid.UUID) ([]Receipt, error)

Unmatched lists the user's receipts that no charge claims yet.

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

func (s *Syncer) OnChargeClose(uid uuid.UUID)

OnChargeClose schedules a sync for the user after closeDelay. One timer per user: further closes while it is pending are absorbed.

func (*Syncer) Run

func (s *Syncer) Run(ctx context.Context)

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) 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.

Jump to

Keyboard shortcuts

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