Help us shape the future of Go! Take the 2026 Go Developer Survey

receipts

package
v0.40.22 Latest Latest
Warning

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

Go to latest
Published: Nov 2, 2026 License: MIT Imports: 17 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, 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 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) 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) 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.

type UserFencer added in v0.40.13

type UserFencer interface {
	FenceUser(ctx context.Context, uid string) (fence.Token, fence.State, error)
}

UserFencer resolves a user to the fence state of their group: the lease store implements it.

Jump to

Keyboard shortcuts

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