customer

package
v1.801.381 Latest Latest
Warning

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

Go to latest
Published: Aug 3, 2026 License: Apache-2.0 Imports: 13 Imported by: 0

Documentation

Overview

Package customer is the CUSTOMER management surface (/v1/admin/customers*) — the operator cockpit's core: the live fleet customer list (incl. new self-serve signups), one-customer detail, and the audited management ACTIONS (grant credit, suspend, reactivate).

It aggregates the SAME real upstreams the rest of admin reads — IAM for the org directory + user/owner/status, commerce for balance/spend/plan/ledger — and adds the two write levers an operator needs:

  • GRANT CREDIT is a real commerce Deposit landing in the org's own wallet, via the ONE core credit-write path (core.ApplyGrant).
  • SUSPEND / REACTIVATE flips IAM `isForbidden` on the org's users — IAM refuses a forbidden user at login AND at token issuance, so a suspended customer cannot sign in or mint a fresh token. Fully reversible.

SECURITY. Every op calls core.Admit (SuperAdmin only, fail-closed) on its first line. The write actions REPLAY THE CALLER'S OWN SuperAdmin credential to IAM, and each is recorded to cloud's tamper-evident audit trail with a redacted BEFORE/AFTER.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func GrantFilter

func GrantFilter(org, result string, limit int) audit.Filter

GrantFilter is the ONE audit query that identifies a credit grant. Both the grants ledger and the consolidated money board select rows through it, so "what counts as a grant" is defined once. org filters the ACTOR's org; result is "" (all), "success" or "error".

func Routes

func Routes(z *zip.App, s *cloud.Service[core.State])

Routes registers the customer-management surface (SuperAdmin only). List (static) precedes the :org param route; the write actions are POST (distinct method), so none collide. The grants ledger + the org-in-body issue-grant share the ONE credit path.

Types

type AccessChange

type AccessChange struct {
	// Org is the tenant acted on.
	Org string `json:"org"`
	// Suspended is the state applied: true for suspend, false for reactivate.
	Suspended bool `json:"suspended"`
	// Affected lists the usernames that were updated.
	Affected []string `json:"affected"`
	// Failed lists the usernames that were NOT updated. Non-empty means the org is in
	// a mixed state and the action should be retried.
	Failed []string `json:"failed"`
}

AccessChange is what a suspend or reactivate DID, per user. A partial failure is reported honestly here rather than masked as a clean success.

type AccessOut

type AccessOut struct {
	Status string        `json:"status"`
	Msg    string        `json:"msg"`
	Data   *AccessChange `json:"data"`
}

AccessOut is the envelope of the suspend and reactivate ops.

type CustomerDetailData

type CustomerDetailData struct {
	Org          string         `json:"org"`
	Display      string         `json:"display"`
	OwnerEmail   string         `json:"ownerEmail"`
	Plan         string         `json:"plan"`
	Status       string         `json:"status"`
	Created      string         `json:"created"`
	BalanceCents int64          `json:"balanceCents"`
	SpendCents   int64          `json:"spendCents"`
	MRRCents     int64          `json:"mrrCents"`
	APIKeys      int            `json:"apiKeys"`
	Users        []CustomerUser `json:"users"`
	Transactions []CustomerTxn  `json:"transactions"`
}

CustomerDetailData is the GET /v1/admin/customers/:org payload.

type CustomerDetailOut

type CustomerDetailOut struct {
	Status string              `json:"status"`
	Msg    string              `json:"msg"`
	Data   *CustomerDetailData `json:"data"`
}

CustomerDetailOut is the GET /v1/admin/customers/:org envelope.

type CustomerRow

type CustomerRow struct {
	Org          string `json:"org"`
	Display      string `json:"display"`
	OwnerEmail   string `json:"ownerEmail"`
	Plan         string `json:"plan"`
	Status       string `json:"status"` // "active" | "suspended"
	Users        int    `json:"users"`
	BalanceCents int64  `json:"balanceCents"`
	SpendCents   int64  `json:"spendCents"`
	MRRCents     int64  `json:"mrrCents"`
	Created      string `json:"created"`
	LastActive   string `json:"lastActive"`
}

CustomerRow is one row in GET /v1/admin/customers — a fleet customer at a glance.

type CustomerTxn

type CustomerTxn struct {
	ID       string `json:"id"`
	Type     string `json:"type"` // "deposit" (credit) | "withdraw" (usage)
	Cents    int64  `json:"cents"`
	Currency string `json:"currency"`
	Notes    string `json:"notes,omitempty"`
	Time     string `json:"time"`
}

CustomerTxn is one ledger row in the detail's top-up/usage history.

type CustomerUser

type CustomerUser struct {
	Name       string `json:"name"`
	Email      string `json:"email"`
	IsAdmin    bool   `json:"isAdmin"`
	Forbidden  bool   `json:"forbidden"`
	HasAPIKey  bool   `json:"hasApiKey"`
	LastSignin string `json:"lastSignin"`
	Created    string `json:"created"`
}

CustomerUser is one member in the customer detail (no secrets — the AccessKey PRESENCE is surfaced as hasApiKey, never the key itself).

type CustomersOut

type CustomersOut struct {
	Status string        `json:"status"`
	Msg    string        `json:"msg"`
	Data   []CustomerRow `json:"data"`
	Total  *int          `json:"total,omitempty"`
}

CustomersOut is the GET /v1/admin/customers envelope. total == len(data): the list is every customer, unpaginated.

type GrantIn

type GrantIn struct {
	// Org is the tenant to credit. Required.
	Org string `json:"org"`
	// User optionally names a MEMBER to credit, by bare IAM username. Empty credits
	// the org. Which of the two the money actually lands on is decided by
	// account.Payer, not here: a pooled org keeps one balance whatever is named.
	User string `json:"user"`
	// AmountCents is the credit, in whole cents. Must be positive and within the
	// per-grant cap.
	AmountCents int64 `json:"amountCents"`
	// Currency is the ISO code, lower-cased. Empty means usd.
	Currency string `json:"currency"`
	// Reason is the operator's justification, recorded on the audit row.
	Reason string `json:"reason"`
	// Source is the money bucket: "trial" (default) for a non-cash comp that is never
	// refundable, or "prepaid" for real money. Anything unknown falls back to trial.
	Source string `json:"source"`
}

GrantIn is the input of BOTH credit-grant ops. They differ only in where the target org comes from — the path on /v1/admin/customers/:org/credit, the body on /v1/admin/grants — and the URL wins where both are present, so one type serves both and there is one contract to read.

type GrantRow

type GrantRow struct {
	Org           string `json:"org"`
	AmountCents   int64  `json:"amountCents"`
	Currency      string `json:"currency"`
	Source        string `json:"source"` // "trial" | "prepaid"
	Reason        string `json:"reason,omitempty"`
	Actor         string `json:"actor"` // staff email (or sub) who issued it
	CreatedAt     string `json:"createdAt"`
	TransactionID string `json:"transactionId,omitempty"`
	Result        string `json:"result"` // success | error
}

GrantRow is one row in GET /v1/admin/grants.

func GrantRows

func GrantRows(s *cloud.Service[core.State], ctx context.Context, f audit.Filter) ([]GrantRow, int, error)

GrantRows projects the audit trail into grant rows. It is split out of the handler so the consolidated money board (/v1/admin/money) totals the SAME grants this endpoint lists — one projection of the trail, two views. No audit store is an honest empty result, not an error: the caller decides how to report that (the board marks the source not-ok).

type GrantsIn

type GrantsIn struct {
	// Org filters by the ACTOR's org (the staff org that issued the grant), which is
	// rarely what a reader wants — the target org is a row field, not a filter.
	Org string `json:"org"`
	// Result filters by outcome: "success" or "error". Empty returns both, which is
	// the point of this view — a refused grant is as interesting as a granted one.
	Result string `json:"result"`
	// Limit caps the rows returned. Default 200.
	Limit string `json:"limit"`
}

GrantsIn is the GET /v1/admin/grants filter.

type GrantsOut

type GrantsOut struct {
	Status string     `json:"status"`
	Msg    string     `json:"msg"`
	Data   []GrantRow `json:"data"`
	Total  *int       `json:"total,omitempty"`
}

GrantsOut is the GET /v1/admin/grants envelope. total is the store's total for the filter, which can exceed len(data) when limit truncates.

type OrgIn

type OrgIn struct {
	// Org is the tenant slug from the path.
	Org string `json:"org"`
}

OrgIn addresses ONE customer by the org slug in the path. It is the input of every per-customer op that carries no body.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL