You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: .agents/skills/charges/SKILL.md
+28-20Lines changed: 28 additions & 20 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -83,9 +83,9 @@ Important types:
83
83
-`AdvanceAfter`
84
84
-`usagebased.RealizationRunBase` stores:
85
85
-`Type`
86
-
-`AsOf`
87
-
-`CollectionEnd`
88
-
-`MeterValue`
86
+
-`StoredAtLT`
87
+
-`ServicePeriodTo`
88
+
-`MeteredQuantity`
89
89
-`Totals`
90
90
-`usagebased.RealizationRun` can expand:
91
91
-`DetailedLines`
@@ -159,7 +159,7 @@ Rules:
159
159
-`meta.NormalizeClosedPeriod(...)` and `Intent.Normalized()` helpers are the domain-level normalization entrypoints
160
160
- normalize intent timestamps before validation and before any derived calculation that depends on durations or boundaries
161
161
- flat-fee proration must use normalized periods, otherwise sub-second inputs can change `AmountAfterProration`
162
-
- for usage-based lifecycle timestamps (`AdvanceAfter`, `AsOf`, `CollectionEnd`, `storedAtOffset`), normalize the computed timestamp before persisting it or handing it to downstream persistence callbacks
162
+
- for usage-based lifecycle timestamps (`AdvanceAfter`, `StoredAtLT`, `ServicePeriodTo`), normalize the computed timestamp before persisting it or handing it to downstream persistence callbacks
163
163
164
164
Important timestamp surfaces:
165
165
@@ -170,15 +170,15 @@ Important timestamp surfaces:
- prefer domain-side normalization when constructing or mutating intents and state (`Intent.Normalized()`, state-machine transition logic, temporary patch remap)
180
180
- keep a persistence backstop in shared write helpers such as `charges/models/chargemeta`
181
-
- in adapters, normalize at the actual write setter (`SetInvoiceAt(...)`, `SetAsof(...)`, `SetCollectionEnd(...)`, `SetOrClearAdvanceAfter(...)`) rather than rewriting the whole input object at the top of the adapter method
181
+
- in adapters, normalize at the actual write setter (`SetInvoiceAt(...)`, `SetStoredAtLt(...)`, `SetServicePeriodTo(...)`, `SetOrClearAdvanceAfter(...)`) rather than rewriting the whole input object at the top of the adapter method
182
182
- do not add redundant `.UTC()` calls after `meta.NormalizeTimestamp(...)`; the helper already returns UTC
183
183
184
184
## Currency Normalization
@@ -431,13 +431,17 @@ The collection-period logic is central to this package.
431
431
Rules:
432
432
433
433
-`usagebased.InternalCollectionPeriod` is `1 minute`
- usage-based standard invoice lines should set `OverrideCollectionPeriodEnd = StoredAtLT + InternalCollectionPeriod` so invoice collection waits for the same internal buffer as the charge state machine
@@ -450,9 +454,10 @@ Usage-based quantity is derived through `snapshotQuantity(...)`.
450
454
451
455
Important behavior:
452
456
453
-
- query window uses the charge service period
457
+
- query window starts at the charge intent's service-period start
458
+
- query window ends at the run's `ServicePeriodTo`
454
459
- stored-at filtering uses `stored_at < cutoff`
455
-
- the cutoff is the current `storedAtOffset`
460
+
- the cutoff is the run's `StoredAtLT`
456
461
- the service-period end is expected to behave as exclusive in lifecycle tests
457
462
458
463
This means late-arriving events can become eligible in later advances if their `stored_at` was previously too new but later falls before the next cutoff.
@@ -464,7 +469,7 @@ Realization runs are the persisted checkpoint for collection progress.
464
469
Important rules:
465
470
466
471
- the first final-realization advance creates a run
467
-
-`CollectionEnd` must be persisted on the run and mapped back into the domain model
472
+
-`StoredAtLT`, `ServicePeriodTo`, and `MeteredQuantity` must be persisted on the run and mapped back into the domain model
468
473
-`CurrentRealizationRunID` points at the active run while waiting/finalizing
469
474
- finalization must clear `CurrentRealizationRunID`
470
475
@@ -504,12 +509,15 @@ Use these conventions for lifecycle tests:
504
509
- if a returned charge is non-`nil`, at minimum match its status to the DB-loaded charge
505
510
- install usage-based handler callbacks only in the subtests that expect them (handler is reset in `TearDownTest`)
506
511
- use `streaming/testutils.WithStoredAt(...)` to simulate late events
507
-
- prefer `clock.FreezeTime(...)` for exact `AsOf` / `AllocateAt` assertions
512
+
- when testing stored-at cutoffs, remember the predicate is exclusive: an event with `stored_at == StoredAtLT` is excluded, and an event with `stored_at` before `StoredAtLT` is included
513
+
- when testing service-period cutoffs, remember the event-time window is half-open: an event with `event_time == ServicePeriodTo` is excluded
514
+
- prefer `clock.FreezeTime(...)` for exact `StoredAtLT` / `AllocateAt` assertions
508
515
- rely on the default billing profile unless the test explicitly needs customer-specific override behavior
509
516
- for credit-only charges (usage-based or flat fee), `Create(...)` itself may return an already-advanced charge — assert the returned charge's status, do not assume it will be `created`
510
517
- for flat fee credit-only tests, use `mustAdvanceFlatFeeCharges(...)` helper — it filters the advance result to flat fee charges only
511
518
- flat fee credit-only handler callbacks (`onCreditsOnlyUsageAccrued`) must return credit allocations that sum to the input `AmountToAllocate`
512
519
- when testing timestamp truncation, use sub-second fixtures and assert the persisted charge/run fields are second-aligned after create/advance
520
+
-`time.Time` fields on domain models are value typed; use `s.False(ts.IsZero())` instead of `s.NotNil(ts)` when asserting they are populated
513
521
- cover the temporary shrink/extend remap path as well; it synthesizes new intents and must normalize the replacement period ends before re-create
514
522
515
523
Test suite teardown:
@@ -549,7 +557,7 @@ When changing usage-based charges:
549
557
- confirm whether the change belongs in the facade, usage-based service, state machine, or adapter
550
558
- preserve the `nil means noop` contract for `AdvanceCharge(...)`
551
559
- preserve merged-profile based collection-period resolution
552
-
- keep `CollectionEnd` persisted on realization runs
560
+
- keep `StoredAtLT`, `ServicePeriodTo`, and `MeteredQuantity` persisted on realization runs
553
561
- keep the `stored_at < cutoff` behavior explicit in tests
554
562
- update lifecycle tests if late-event visibility changes
Copy file name to clipboardExpand all lines: .agents/skills/ent/SKILL.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -32,6 +32,7 @@ After any schema change, regenerate with `make generate` before running tests.
32
32
-**Soft-delete unique indexes** include `deleted_at` in the unique constraint (e.g., `index.Fields("namespace", "key", "deleted_at").Unique()`) — always filter with `Where(<entity>db.DeletedAtIsNil())` in queries.
33
33
-**Foreign keys** use `char(26)` schema type to match ULID IDs.
34
34
-**Cascade deletes** use `entsql.OnDelete(entsql.Cascade)` on the parent edge.
35
+
-**PostgreSQL identifier length** is 63 bytes by default (PostgreSQL docs, “Lexical Structure” / `NAMEDATALEN`). Long Ent-generated table, index, and FK names can truncate and collide even when their full names differ. When a schema/entity/edge name is verbose, proactively shorten generated FK symbols with `StorageKey(edge.Symbol("..."))` and shorten index names with `StorageKey("...")` before generating migrations.
35
36
-**JSONB fields** use `entutils.JSONStringValueScanner` — see `openmeter/ent/schema/llmcostprice.go`.
36
37
-**Non-empty strings at the DB layer**: `field.String(...).NotEmpty()` enforces Ent-side validation, but Atlas may still diff only `SET NOT NULL` for existing tables. If the database must reject empty strings too, add an explicit `entsql.Checks(...)` annotation in the schema or mixin alongside `NotEmpty()`.
0 commit comments