receipts

package
v0.36.13 Latest Latest
Warning

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

Go to latest
Published: Oct 17, 2026 License: MIT Imports: 12 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

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

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