customer

package
v1.801.299 Latest Latest
Warning

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

Go to latest
Published: Jul 28, 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 added in v1.801.264

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 added in v1.801.299

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 added in v1.801.299

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 added in v1.801.299

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 added in v1.801.299

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

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

type GrantIn added in v1.801.299

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 added in v1.801.264

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 added in v1.801.299

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 added in v1.801.299

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

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

type OrgIn added in v1.801.299

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