Documentation
¶
Overview ¶
Package ads mounts the Hanzo Cloud /v1/ads/* surface: a native-Go, per-org ad-campaign store on Base/SQLite. It is the NET-NEW ads domain built directly on the ONE cloud framework (zip/Fiber + cloud.Deps + per-org SQLite) — the same shape every other in-repo subsystem uses (clients/crm and clients/marketing are the twins), NOT a proxy to a standalone ads pod (there is none — ads.hanzo.ai is net-new).
The Campaign entity is the root of the ad hierarchy (campaign → ad sets → ads): a named campaign on an ad Platform (meta/google/tiktok/x), a lifecycle Status (draft/active/paused/completed), an Objective, and Budget/Spend in minor units (cents). The ad-set and ad legs of the hierarchy are follow-ups that hang off this seam; this is the reviewable domain + campaign CRUD they attach to.
Tenant isolation is enforced SERVER-SIDE on every request: the org is principal.Org(c) — the value SanitizeIdentity minted from the VALIDATED bearer owner claim (HIP-0026) — and NEVER a client-supplied header. Every store query filters WHERE org=?, so one tenant can never read or mutate another's data.
Surface (all org-scoped; /v1 only):
GET /v1/ads/summary per-org roll-up (total/active/budget/spend)
GET /v1/ads/campaigns list campaigns (?status=) -> {data:[…]}
POST /v1/ads/campaigns create a campaign -> Campaign (201)
GET /v1/ads/campaigns/:id campaign detail -> Campaign
PUT /v1/ads/campaigns/:id update a campaign -> Campaign
DELETE /v1/ads/campaigns/:id delete a campaign
serve.go auto-registers GET /v1/ads/health (this subsystem does not set OwnsHealth, so the generic always-ok liveness route serves it).
Index ¶
- func Mount(app cloud.Router, deps cloud.Deps) error
- func PaidSpend(ctx context.Context, org string, ref PaidRef) (int64, error)
- func PausePaid(ctx context.Context, org string, ref PaidRef) error
- func Shutdown() error
- type Campaign
- type PaidPlan
- type PaidRef
- type Store
- func (s *Store) Close() error
- func (s *Store) Counts(ctx context.Context, org string) (total, active int, budget, spend int64, err error)
- func (s *Store) CreateCampaign(ctx context.Context, c Campaign) (Campaign, error)
- func (s *Store) DeleteCampaign(ctx context.Context, org, id string) (bool, error)
- func (s *Store) GetCampaign(ctx context.Context, org, id string) (Campaign, error)
- func (s *Store) ListCampaigns(ctx context.Context, org, status string, limit int) ([]Campaign, error)
- func (s *Store) MarkLaunched(ctx context.Context, org, id, account, externalID string, updatedAt int64) (Campaign, error)
- func (s *Store) UpdateCampaign(ctx context.Context, c Campaign) (Campaign, error)
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Mount ¶
Mount wires the ads surface onto app per HIP-0106. It keeps a package global (mounted) for Shutdown, so it constructs the Service value directly — the same "complex flavour" clients/crm uses.
func PaidSpend ¶
PaidSpend reads the provider-reported spend (minor units) for a launched ad campaign. Fail-closed on the token; honest 0 when the platform is unsupported.
Types ¶
type Campaign ¶
type Campaign struct {
ID string `json:"id"`
Org string `json:"-"`
Name string `json:"name"`
Platform string `json:"platform"`
Account string `json:"account,omitempty"` // provider ad-account ref (Meta act_<id>)
ExternalID string `json:"externalId,omitempty"` // provider campaign id after a launch
Status string `json:"status"`
Objective string `json:"objective"`
Budget int64 `json:"budget"`
Spend int64 `json:"spend"`
CreatedAt int64 `json:"createdAt"`
UpdatedAt int64 `json:"updatedAt"`
}
Campaign is an org-scoped ad campaign — the root of the ad hierarchy (campaign → ad sets → ads; the ad-set/ad legs hang off this seam as follow-ups). Budget and Spend are minor units (cents). Platform is the ad network (meta/google/tiktok/x); Status is the lifecycle (draft/active/paused/completed) — both validated at the write layer against the fixed vocabularies in ads.go.
type PaidPlan ¶
type PaidPlan struct {
Platform string
Account string // provider ad-account ref (Meta: act_<id> or <id>)
Name string
Objective string // provider objective enum; defaulted per-provider when empty
BudgetCents int64
ScheduleAt int64
}
PaidPlan is the standalone contract for launching one ad campaign — the campaign paid channel adapts campaign.Plan onto it (apps/wire_seams.go). BudgetCents is the org's own (connector-paid) budget; ScheduleAt an optional start time.
type PaidRef ¶
type PaidRef struct {
Platform string
Account string
ExternalID string
Status string
Detail string
}
PaidRef is a launched ad campaign's durable handle: the provider-side id plus the platform/account needed to read spend or pause it.
func LaunchPaid ¶
LaunchPaid creates the ad campaign on its provider using the org's connector token. Fail-closed: it resolves the token BEFORE any provider call. Meta is executed for real; other connected platforms return errUnsupportedPlatform (the connector is verified, only the provider impl is the remaining gap).
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store is the ads database. ONE SQLite file ({DataDir}/ads.db) holds every org's records; tenant isolation is the `org` column, enforced on EVERY query. This mirrors clients/crm exactly (the ONE storage pattern). MaxOpenConns(1) serializes writes against the single-writer file.
func (*Store) Counts ¶
func (s *Store) Counts(ctx context.Context, org string) (total, active int, budget, spend int64, err error)
Counts returns the per-org campaign roll-up: total campaigns, how many are active, and the summed budget + spend (cents) — a real, non-fabricated summary for the ads module's overview cards.
func (*Store) CreateCampaign ¶
func (*Store) DeleteCampaign ¶
func (*Store) GetCampaign ¶
func (*Store) ListCampaigns ¶
func (s *Store) ListCampaigns(ctx context.Context, org, status string, limit int) ([]Campaign, error)
ListCampaigns lists the org's campaigns, optionally filtered by status (status=="" means all). Most-recently-updated first.
func (*Store) MarkLaunched ¶
func (s *Store) MarkLaunched(ctx context.Context, org, id, account, externalID string, updatedAt int64) (Campaign, error)
MarkLaunched records the provider execution on a stored campaign: its account + external id, and status=active. Org-scoped — a cross-tenant id affects zero rows (errNotFound), never a foreign mutation. This is the ONLY writer of external_id, so the launch link is never clobbered by a user edit.
func (*Store) UpdateCampaign ¶
UpdateCampaign edits the user-owned fields. external_id is deliberately NOT in the SET list: it is launch-owned (MarkLaunched sets it), so editing a campaign never clobbers the link to its live provider execution.