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
6 changes: 5 additions & 1 deletion atlas.hcl
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
data "external_schema" "ent" {
program = ["go", "run", "-mod=readonly", "./tools/migrate/cmd/entschema"]
}

env "local" {
src = "${local.schema_src}"

Expand Down Expand Up @@ -45,7 +49,7 @@ env "ci" {

locals {
// Define the directory where the schema definition resides.
schema_src = "ent://openmeter/ent/schema"
schema_src = data.external_schema.ent.url
// Define the initial migration timestamp
init_migration_ts = "20240826120919"
// Define the directory where the migrations are stored.
Expand Down
5 changes: 0 additions & 5 deletions openmeter/ent/entc.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import (
"github.com/openmeterio/openmeter/pkg/framework/entutils/entmixinaccessor"
"github.com/openmeterio/openmeter/pkg/framework/entutils/entpaginate"
"github.com/openmeterio/openmeter/pkg/framework/entutils/entsetorclear"
"github.com/openmeterio/openmeter/tools/migrate/viewgen"
)

func main() {
Expand All @@ -41,8 +40,4 @@ func main() {
if err != nil {
log.Fatal("running ent codegen:", err)
}

if err := viewgen.GenerateFile("./schema", "../../tools/migrate/views.sql"); err != nil {
log.Fatal("generating views SQL:", err)
}
}
1 change: 1 addition & 0 deletions openmeter/ent/generate.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
package entdb

//go:generate go run -mod=readonly entc.go
//go:generate go run -mod=readonly ../../tools/migrate/cmd/viewgen -schema ./schema -out ../../tools/migrate/views.sql
4 changes: 2 additions & 2 deletions openmeter/ent/schema/ledger_breakage_record.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ type LedgerBreakageRecord struct {
ent.Schema
}

// Breakage records intentionally keep ledger references as plain IDs: they are
// projection rows, not accounting source of truth. See ledger/breakage/README.md.
// Ledger-reference FKs are deferred by tools/migrate/deferred_constraints.sql:
// bookkeeping is written before the journal group in the same transaction.
func (LedgerBreakageRecord) Mixin() []ent.Mixin {
return []ent.Mixin{
entutils.IDMixin{},
Expand Down
2 changes: 2 additions & 0 deletions openmeter/ledger/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ facts stored independently from the journal.
currency's precision.
- `CommitGroup` validates the whole input, locks every affected parent account,
and books the group atomically in the caller's database transaction.
- IDs can be assigned to resolved postings before commit; `ledger.PreassignIDs`
generates missing transaction and entry IDs without posting.
- Default routing rules constrain allowed account-type combinations, flow
direction, authorization stages, route compatibility, and dimension scope.
A balanced transaction can still be invalid.
Expand Down
5 changes: 3 additions & 2 deletions openmeter/ledger/advance/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,5 +57,6 @@ the purchase was fully consumed by backfill and issued no ordinary FBO credit.

`PlanLegacyCorrection` uses original transaction groups for legacy histories.
It returns deferred template corrections so the collector can merge selections
against the same transaction before resolving them. Both paths return breakage
records for the caller to persist with the committed group; neither writes lineage.
against the same transaction before resolving them. Both paths write breakage
bookkeeping in the caller's transaction and return its postings for the same group;
neither writes lineage.
33 changes: 17 additions & 16 deletions openmeter/ledger/advance/correction.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,13 @@ type BackfillCorrection struct {
}

type CorrectionInput struct {
CustomerID customer.CustomerID
At time.Time
Amount alpacadecimal.Decimal
Collection CorrectionSource
Issue CorrectionSource
Backfills []BackfillCorrection
BreakagePosting breakage.PostingInput
CustomerID customer.CustomerID
At time.Time
Amount alpacadecimal.Decimal
Collection CorrectionSource
Issue CorrectionSource
Backfills []BackfillCorrection
}

func (i CorrectionInput) Validate() error {
Expand Down Expand Up @@ -99,22 +100,22 @@ func (i CorrectionInput) Validate() error {
}

type CorrectionPlan struct {
Inputs []ledger.TransactionInput
BreakagePending []breakage.PendingRecord
Inputs []ledger.TransactionInput
// LegacyCorrections remain unresolved so the caller can merge selections
// from the same original transaction before invoking template correction.
LegacyCorrections []transactions.CorrectionInput
}

type LegacyCorrectionInput struct {
CustomerID customer.CustomerID
ChargeID string
At time.Time
Amount alpacadecimal.Decimal
OriginalGroup ledger.TransactionGroup
Collection ledger.Transaction
Issue ledger.Transaction
BackingGroupID *string
BreakagePosting breakage.PostingInput
CustomerID customer.CustomerID
ChargeID string
At time.Time
Amount alpacadecimal.Decimal
OriginalGroup ledger.TransactionGroup
Collection ledger.Transaction
Issue ledger.Transaction
BackingGroupID *string
}

func (i LegacyCorrectionInput) Validate() error {
Expand Down
5 changes: 2 additions & 3 deletions openmeter/ledger/advance/service/correction.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ func (s *service) PlanCorrection(ctx context.Context, input advance.CorrectionIn
}

out.Inputs = append(out.Inputs, planned.Inputs...)
out.BreakagePending = append(out.BreakagePending, planned.BreakagePending...)
}

for _, source := range []advance.CorrectionSource{input.Collection, input.Issue} {
Expand Down Expand Up @@ -92,7 +91,8 @@ func (s *service) correctBackfill(ctx context.Context, input advance.CorrectionI
continue
}

reopened, pending, err := s.breakage.ReopenRelease(ctx, breakage.ReopenReleaseInput{
reopened, err := s.breakage.ReopenRelease(ctx, breakage.ReopenReleaseInput{
PostingInput: input.BreakagePosting,
Release: release,
Amount: take,
SourceKind: breakage.SourceKindUsageCorrection,
Expand All @@ -105,7 +105,6 @@ func (s *service) correctBackfill(ctx context.Context, input advance.CorrectionI
}

out.Inputs = append(out.Inputs, reopened)
out.BreakagePending = append(out.BreakagePending, pending)
remaining = remaining.Sub(take)
}

Expand Down
15 changes: 7 additions & 8 deletions openmeter/ledger/advance/service/legacy_correction.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ func (s *service) PlanLegacyCorrection(ctx context.Context, input advance.Legacy
OriginalGroup: group,
})

out.Inputs, out.BreakagePending, err = s.resolveAdvanceBackfillBreakageReopenInputs(ctx, input, group, input.Amount)
out.Inputs, err = s.resolveAdvanceBackfillBreakageReopenInputs(ctx, input, group, input.Amount)
if err != nil {
return out, err
}
Expand Down Expand Up @@ -140,18 +140,17 @@ func (s *service) reissueBackfilledCredit(ctx context.Context, input advance.Leg
return out, nil
}

func (s *service) resolveAdvanceBackfillBreakageReopenInputs(ctx context.Context, input advance.LegacyCorrectionInput, backingGroup ledger.TransactionGroup, amount alpacadecimal.Decimal) ([]ledger.TransactionInput, []breakage.PendingRecord, error) {
func (s *service) resolveAdvanceBackfillBreakageReopenInputs(ctx context.Context, input advance.LegacyCorrectionInput, backingGroup ledger.TransactionGroup, amount alpacadecimal.Decimal) ([]ledger.TransactionInput, error) {
releases, err := s.breakage.ListReleases(ctx, breakage.ListReleasesInput{
CustomerID: input.CustomerID,
SourceTransactionGroupID: []string{backingGroup.ID().ID},
ReleaseSourceKind: []breakage.SourceKind{breakage.SourceKindAdvanceBackfill},
})
if err != nil {
return nil, nil, fmt.Errorf("list advance-backfill breakage releases: %w", err)
return nil, fmt.Errorf("list advance-backfill breakage releases: %w", err)
}

inputs := make([]ledger.TransactionInput, 0, len(releases))
pending := make([]breakage.PendingRecord, 0, len(releases))
releaseFactsByTransactionID := breakageReleaseFactsByTransactionID(backingGroup)
remaining := amount
for _, release := range releases {
Expand All @@ -169,23 +168,23 @@ func (s *service) resolveAdvanceBackfillBreakageReopenInputs(ctx context.Context
continue
}

reopenInput, reopenRecord, err := s.breakage.ReopenRelease(ctx, breakage.ReopenReleaseInput{
reopenInput, err := s.breakage.ReopenRelease(ctx, breakage.ReopenReleaseInput{
PostingInput: input.BreakagePosting,
Release: release,
Amount: reopenAmount,
SourceKind: breakage.SourceKindUsageCorrection,
SourceChargeID: releaseFacts.SourceChargeID,
SpendChargeID: releaseFacts.SpendChargeID,
})
if err != nil {
return nil, nil, fmt.Errorf("resolve advance-backfill breakage reopen: %w", err)
return nil, fmt.Errorf("resolve advance-backfill breakage reopen: %w", err)
}

inputs = append(inputs, reopenInput)
pending = append(pending, reopenRecord)
remaining = remaining.Sub(reopenAmount)
}

return inputs, pending, nil
return inputs, nil
}

func breakageReleaseFactsByTransactionID(group ledger.TransactionGroup) map[string]ledger.EntryIdentityParts {
Expand Down
10 changes: 8 additions & 2 deletions openmeter/ledger/breakage/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ This package keeps future credit-expiration ledger entries aligned with actual c

The ledger is the accounting source of truth. Breakage records are an allocation/index layer: they let later collection and correction flows find open planned breakage, reopen released breakage, and project customer-visible expired credit.

Breakage operations write complete bookkeeping using preassigned ledger IDs and
return postings for the caller's group. Both must run in one database transaction;
deferred ledger FKs reject unposted references at commit. Later operations in that
transaction see the updated bookkeeping. Source links are typed record fields;
annotations provide context and backlinks.

## Notation

- `FBO(r)` is the customer credit account route.
Expand Down Expand Up @@ -54,13 +60,13 @@ would leave the wrong credit available.

Use route dimensions for fields that define collection eligibility, ordering, or routing validation.

Use annotations or breakage records for metadata and links.
Use breakage records for accounting links and annotations for context.

That means:

- `credit_priority` is a route dimension because it affects normal FBO collection.
- `expires_at` is represented by planned breakage `booked_at`.
- breakage kind and source links are annotations/record data.
- breakage records own source links; annotations identify breakage activity.
- breakage account routes should carry only dimensions relevant to breakage accounting/revenue recognition.

Breakage-generated FBO entries must not be treated as normal credit issuance or usage. They are marked as breakage activity:
Expand Down
16 changes: 6 additions & 10 deletions openmeter/ledger/breakage/noop.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,16 @@ type NoopService struct{}

var _ Service = NoopService{}

func (NoopService) PlanIssuance(context.Context, PlanIssuanceInput) ([]ledger.TransactionInput, []PendingRecord, error) {
return nil, nil, nil
func (NoopService) PlanIssuance(context.Context, PlanIssuanceInput) ([]ledger.TransactionInput, error) {
return nil, nil
}

func (NoopService) ReleasePlan(context.Context, ReleasePlanInput) (ledger.TransactionInput, PendingRecord, error) {
return nil, PendingRecord{}, nil
func (NoopService) ReleasePlan(context.Context, ReleasePlanInput) (ledger.TransactionInput, error) {
return nil, nil
}

func (NoopService) ReopenRelease(context.Context, ReopenReleaseInput) (ledger.TransactionInput, PendingRecord, error) {
return nil, PendingRecord{}, nil
func (NoopService) ReopenRelease(context.Context, ReopenReleaseInput) (ledger.TransactionInput, error) {
return nil, nil
}

func (NoopService) ListPlans(context.Context, ListPlansInput) ([]Plan, error) {
Expand All @@ -41,10 +41,6 @@ func (NoopService) ListExpiredBreakageImpacts(context.Context, ListExpiredBreaka
return ListExpiredBreakageImpactsResult{}, nil
}

func (NoopService) PersistCommittedRecords(context.Context, []PendingRecord, ledger.TransactionGroup) error {
return nil
}

// NewNoopService returns a breakage service that never plans or releases
// breakage.
func NewNoopService() Service {
Expand Down
Loading
Loading