Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,21 @@ See [Testing](docs/development/testing.md) for harness patterns and examples.
- Preserve explanatory comments during refactors unless the change makes them
false or misleading.
- Keep project documentation short enough that humans will read it.
- Write comments like you're explaining the code to a teammate: plain words,
concrete reasons, no formal filler. Keep the useful explanation:

Bad:
```go
// scopedEntries applies the account, route, provenance, and whole-history
// exclusions shared by both branches. AsOf is left to each branch: filtering
// here would hide future postings whose balance impact must be preserved.
```

Good:
```go
// Apply the shared filters, but keep entries after AsOf: we need those to see
// how low the balance gets later.
```

## Further guidance

Expand Down
10 changes: 6 additions & 4 deletions openmeter/ledger/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ within a namespace.

The historical ledger stores immutable transaction groups, transactions, and
entries. Balances are projections over those entries; they are not mutable
facts stored independently from the journal.
facts stored independently from the journal. Account balances sum the selected
entries. Minimum balance buckets take the lowest balance from `AsOf` onward;
[FBO spendable balances](fbo/README.md) apply the credit and breakage policy.

## Ownership and boundaries

Expand All @@ -35,9 +37,9 @@ facts stored independently from the journal.
- `chargeadapter` translates charge lifecycle events into ledger templates.
Charges decide when an effect is due; the ledger decides how that effect is
represented and validated. See the [charges domain](../billing/charges/README.md).
- The collector owns source selection and correction unwind order. Callers
provide the target amount and attribution; they must not recreate collection
policy.
- [FBO](fbo/README.md) owns consumption source selection, expiry release, and
advance coordination. The collector maps consumptions to billing allocations,
commits their group, and owns correction unwind order.
- `customerbalance` is a customer-facing projection over booked ledger state,
breakage, and not-yet-booked charge impacts. It does not own posting,
collection, or correction rules.
Expand Down
31 changes: 31 additions & 0 deletions openmeter/ledger/balance.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,36 @@ type BalanceBucket struct {
PendingAmount alpacadecimal.Decimal
}

// MinimumBalanceBucketQuery returns each bucket's lowest balance from AsOf
// through all recorded future postings. AsOf is inclusive; simultaneous postings
// are netted before comparing balances. A future-only bucket starts at zero.
type MinimumBalanceBucketQuery struct {
BalanceBucketQuery

// ExcludeFutureAnnotationFilters omits matching movements strictly after
// AsOf. The opening balance still includes them.
ExcludeFutureAnnotationFilters map[string]string
}

var _ models.Validator = MinimumBalanceBucketQuery{}

func (q MinimumBalanceBucketQuery) Validate() error {
var errs []error
if err := q.BalanceBucketQuery.Validate(); err != nil {
errs = append(errs, err)
}

if q.Filters.AsOf == nil {
errs = append(errs, errors.New("as of is required"))
}

if q.Filters.After != nil || q.Filters.BookedAtPeriod != nil {
errs = append(errs, errors.New("minimum balance cannot truncate the posting history"))
}

return models.NewNillableGenericValidationError(errors.Join(errs...))
}

// GetBalancesAtBoundariesInput describes independent persisted balance boundaries.
// Results follow Queries order, including zero balances for empty scopes.
type GetBalancesAtBoundariesInput struct {
Expand Down Expand Up @@ -101,4 +131,5 @@ type BalanceQuerier interface {
GetAccountBalance(ctx context.Context, account Account, route RouteFilter, query BalanceQuery) (Balance, error)
GetSubAccountBalance(ctx context.Context, subAccount SubAccount, query BalanceQuery) (Balance, error)
GetBalanceBuckets(ctx context.Context, query BalanceBucketQuery) ([]BalanceBucket, error)
GetMinimumBalanceBuckets(ctx context.Context, query MinimumBalanceBucketQuery) ([]BalanceBucket, error)
}
13 changes: 8 additions & 5 deletions openmeter/ledger/collector/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@

This package turns selected customer FBO credit into accrued value and, for
custom-currency `credit_then_invoice` overage, fiat receivable coverage.
Credit-only accrual asks [advance](../advance/README.md) to create an advance for
an uncovered amount. Source order is preserved so correction and breakage can
undo the same collected amounts.
The [FBO plan](../fbo/README.md) owns source selection, consumption, expiry
release, and credit-only advance for an uncovered amount. The collector commits
the complete group and maps each consumption's transactions to billing
allocations. Source order is preserved so correction and breakage can undo the
same collected amounts.

## Vocab

- `BookedAt`: timestamp used for the ledger transactions being written.
- `SourceBalanceAsOf`: timestamp used to decide which FBO sources are available.
- `source`: one spendable FBO slice selected by the collector.
- `source`: one eligible FBO slice selected by the FBO service.
- `source entry`: the concrete negative FBO ledger entry created by collection.
- `allocation`: billing's collapsed record of collected credit.
- `advance`: value moved through FBO/accrued before real credit exists to cover it.
Expand Down Expand Up @@ -42,7 +44,8 @@ stable cursor asc

Non-expiring credit sorts after expiring credit with the same priority.

This order must match breakage release order. If the collector consumes an expiring source, it also asks breakage to release the matching planned breakage for that same source.
This order must match breakage release order. Consuming an expiring source also
releases the matching planned breakage for that source.

## Forward Collection Example

Expand Down
Loading
Loading