Files
member-console/internal/integration/outbox.go
T
cgalo5758 dd3962990b Adopt entity keys and add invoice numbers
Replace the entity slugs on organizations, workspaces, resource pools,
and
plan ladders with nullable `key` columns and add keys to products,
prices,
and entitlement sets. Rename `providers.slug` to `provider` and add
partial
unique indexes for system and org role names.

Assign invoice numbers per billing account from a gapless transactional
counter; Stripe's number moves to the invoice mapping as an external
reference.

Seeds, fixtures, and the operator lookup address rows by key, and the
returning-login resync no longer blanks a display name when the IdP
sends
no `name` claim.
2026-08-29 20:12:04 -05:00

52 lines
2.4 KiB
Go

package integration
import (
"context"
"encoding/json"
"fmt"
)
// Enqueue inserts a pending core.outbox row — the transactional dispatch
// handoff ratified in design.md Decision 4 for the integration-extraction
// change: a domain write and its outbox row commit atomically inside the
// caller's Postgres transaction, and a separate poller (e.g. the Stripe
// outbox drainer, internal/integrations/stripe/workflows's
// PollIntegrationOutbox) executes the action against the external provider
// later.
//
// This is the only supported write path into core.outbox; core code must
// not INSERT into it directly (see Decision 4's "concession to
// coherence"). It replaces the raw `INSERT INTO core.outbox` statements
// formerly hand-written in internal/server/operator_billing.go and
// internal/integrations/stripe/store/ensure_customer.go.
//
// db accepts a *sql.DB or a *sql.Tx via this package's DBTX interface
// (generated by sqlc for the provider-registry querier but equally
// satisfied by both). Pass a *sql.Tx to keep the enqueue atomic with the
// caller's other writes in the same transaction — the pattern both
// converted call sites rely on. A single logical action (e.g. "create the
// product, then its price") that needs more than one outbox row is two
// separate Enqueue calls against the same *sql.Tx, not a batch API: this
// keeps the helper's shape simple and the atomicity guarantee is already
// provided by the shared transaction.
//
// payload is marshaled to JSON here — pass a struct or map, not
// pre-marshaled bytes. providerKey and actionType are the outbox's
// existing free-form vocabulary (e.g. "stripe" / "create_stripe_customer");
// this helper does not validate or constrain that vocabulary (the Stripe
// action-type vocabulary baked into core is the payments-seam follow-up
// noted in design.md Decision 4, out of scope here).
func Enqueue(ctx context.Context, db DBTX, providerKey, actionType string, payload any) error {
payloadJSON, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("marshal outbox payload for %s/%s: %w", providerKey, actionType, err)
}
if _, err := db.ExecContext(ctx,
`INSERT INTO core.outbox (provider, action_type, payload) VALUES ($1, $2, $3)`,
providerKey, actionType, payloadJSON,
); err != nil {
return fmt.Errorf("enqueue %s/%s outbox entry: %w", providerKey, actionType, err)
}
return nil
}