summaryrefslogtreecommitdiff
path: root/internal/billing/ledger.go
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--internal/billing/ledger.go77
1 files changed, 77 insertions, 0 deletions
diff --git a/internal/billing/ledger.go b/internal/billing/ledger.go
index 7a00081..64f258c 100644
--- a/internal/billing/ledger.go
+++ b/internal/billing/ledger.go
@@ -190,6 +190,83 @@ func (s *Service) AdjustBalance(ctx context.Context, input AdjustmentInput) (Led
return result, nil
}
+// ReleaseUnmeteredReservation is an audited operational escape hatch for a
+// fail-closed success that cannot be reconciled. It never invents usage or
+// changes wallet balance; it only returns the existing hold to availability.
+func (s *Service) ReleaseUnmeteredReservation(ctx context.Context, tenantID, requestID string, input ReleaseReservationInput, actor ResolutionActor) (ReservationRelease, error) {
+ tenantID = strings.TrimSpace(tenantID)
+ requestID = strings.TrimSpace(requestID)
+ reason := normalizeDescription(input.Reason)
+ if tenantID == "" || requestID == "" || reason == "" || actor.ID == "" || actor.Type == "" {
+ return ReservationRelease{}, fmt.Errorf("%w: tenant, request, reason, and actor are required", ErrReservationNotReleasable)
+ }
+ tx, err := s.db.BeginTx(ctx, pgx.TxOptions{IsoLevel: pgx.ReadCommitted})
+ if err != nil {
+ return ReservationRelease{}, fmt.Errorf("begin reservation release: %w", err)
+ }
+ defer tx.Rollback(ctx)
+ var result ReservationRelease
+ var projectID, currency, status string
+ if err := tx.QueryRow(ctx, `SELECT request_id,tenant_id::text,project_id::text,currency,reserved_micros,status
+ FROM billing_reservations WHERE request_id=$1 AND tenant_id=$2 FOR UPDATE`, requestID, tenantID).Scan(
+ &result.RequestID, &result.TenantID, &projectID, &currency, &result.ReservedMicros, &status); errors.Is(err, pgx.ErrNoRows) {
+ return ReservationRelease{}, ErrReservationNotReleasable
+ } else if err != nil {
+ return ReservationRelease{}, fmt.Errorf("lock reservation for release: %w", err)
+ }
+ if status == "released" {
+ var evidenceExists bool
+ if err := tx.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM billing_ledger
+ WHERE source_type='unmetered_reservation' AND source_id=$1)`, requestID).Scan(&evidenceExists); err != nil {
+ return ReservationRelease{}, fmt.Errorf("read reservation release evidence: %w", err)
+ }
+ if !evidenceExists {
+ return ReservationRelease{}, fmt.Errorf("%w: reservation was released by normal settlement", ErrReservationNotReleasable)
+ }
+ if err := tx.QueryRow(ctx, `SELECT COALESCE(settled_at,created_at) FROM billing_reservations WHERE request_id=$1`, requestID).Scan(&result.ReleasedAt); err != nil {
+ return ReservationRelease{}, err
+ }
+ result.Status = status
+ return result, tx.Commit(ctx)
+ }
+ if status != "metering_failed" {
+ return ReservationRelease{}, fmt.Errorf("%w: reservation status is %s", ErrReservationNotReleasable, status)
+ }
+ var balance, held int64
+ if err := tx.QueryRow(ctx, `SELECT balance_micros,reserved_micros FROM tenant_wallets WHERE tenant_id=$1 FOR UPDATE`, tenantID).Scan(&balance, &held); err != nil {
+ return ReservationRelease{}, fmt.Errorf("lock wallet for reservation release: %w", err)
+ }
+ if result.ReservedMicros > held {
+ return ReservationRelease{}, errors.New("wallet reservation invariant violated during release")
+ }
+ if _, err := tx.Exec(ctx, `UPDATE tenant_wallets SET reserved_micros=reserved_micros-$2,updated_at=now() WHERE tenant_id=$1`, tenantID, result.ReservedMicros); err != nil {
+ return ReservationRelease{}, fmt.Errorf("release wallet hold: %w", err)
+ }
+ if err := tx.QueryRow(ctx, `UPDATE billing_reservations SET status='released',settled_at=now()
+ WHERE request_id=$1 RETURNING status,settled_at`, requestID).Scan(&result.Status, &result.ReleasedAt); err != nil {
+ return ReservationRelease{}, fmt.Errorf("mark reservation released: %w", err)
+ }
+ command, err := tx.Exec(ctx, `UPDATE usage_events SET metering_status='released_unmetered'
+ WHERE request_id=$1 AND tenant_id=$2 AND metering_status='missing' AND usage_reported=FALSE`, requestID, tenantID)
+ if err != nil {
+ return ReservationRelease{}, fmt.Errorf("mark unmetered usage resolved: %w", err)
+ }
+ if command.RowsAffected() != 1 {
+ return ReservationRelease{}, errors.New("unmetered usage invariant violated during release")
+ }
+ description := fmt.Sprintf("Unmetered reservation released by %s %s: %s", actor.Type, actor.ID, reason)
+ if _, err := tx.Exec(ctx, `INSERT INTO billing_ledger
+ (tenant_id,project_id,currency,amount_micros,balance_after_micros,kind,source_type,source_id,description)
+ VALUES ($1,$2,$3,0,$4,'release','unmetered_reservation',$5,$6)
+ ON CONFLICT (source_type,source_id) DO NOTHING`, tenantID, projectID, currency, balance, requestID, description); err != nil {
+ return ReservationRelease{}, fmt.Errorf("write reservation release evidence: %w", err)
+ }
+ if err := tx.Commit(ctx); err != nil {
+ return ReservationRelease{}, fmt.Errorf("commit reservation release: %w", err)
+ }
+ return result, nil
+}
+
func (s *Service) createTopUpOrder(ctx context.Context, input CheckoutInput) (string, int64, error) {
if !s.stripeEnabled {
return "", 0, ErrStripeDisabled