mirror of
https://github.com/warmbly/warmbly.git
synced 2026-09-05 16:02:48 +00:00
976 lines
41 KiB
Go
976 lines
41 KiB
Go
package models
|
|
|
|
import (
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// AdminUserSearch represents search parameters for admin user listing
|
|
type AdminUserSearch struct {
|
|
Query string `form:"q"`
|
|
Status string `form:"status"` // active, banned, all
|
|
IsAdmin *bool `form:"is_admin"`
|
|
HasOverrides bool `form:"has_overrides"` // has custom rate limits
|
|
FreeTrialUsed bool `form:"free_trial_used"` // has consumed the free trial
|
|
CreatedWithin int `form:"created_within"` // signup window, in days
|
|
|
|
// Plan / subscription
|
|
PlanID *uuid.UUID `form:"plan_id"` // subscribed to THIS plan
|
|
SubscriptionStatus string `form:"subscription_status"` // trialing, active, past_due, ...
|
|
IsEnterprise bool `form:"is_enterprise"`
|
|
HasSubscription bool `form:"has_subscription"`
|
|
HasActiveSubscription bool `form:"has_active_subscription"`
|
|
|
|
// Account state
|
|
OnboardingCompleted bool `form:"onboarding_completed"`
|
|
DeletionScheduled bool `form:"deletion_scheduled"`
|
|
HasAvatar bool `form:"has_avatar"`
|
|
HasActiveCampaign bool `form:"has_active_campaign"`
|
|
HasBanRecord bool `form:"has_ban_record"`
|
|
HasDedicatedWorker bool `form:"has_dedicated_worker"`
|
|
|
|
// Count / numeric ranges
|
|
OrgCountMin *int `form:"org_count_min"`
|
|
OrgCountMax *int `form:"org_count_max"`
|
|
EmailAccountCountMin *int `form:"email_account_count_min"`
|
|
EmailAccountCountMax *int `form:"email_account_count_max"`
|
|
CampaignCountMin *int `form:"campaign_count_min"`
|
|
CampaignCountMax *int `form:"campaign_count_max"`
|
|
MaxOrganizationsMin *int `form:"max_organizations_min"`
|
|
MaxOrganizationsMax *int `form:"max_organizations_max"`
|
|
|
|
// Date ranges (YYYY-MM-DD, parsed as UTC)
|
|
CreatedAfter *time.Time `form:"created_after" time_format:"2006-01-02" time_utc:"true"`
|
|
CreatedBefore *time.Time `form:"created_before" time_format:"2006-01-02" time_utc:"true"`
|
|
AdminGrantedAfter *time.Time `form:"admin_granted_after" time_format:"2006-01-02" time_utc:"true"`
|
|
AdminGrantedBefore *time.Time `form:"admin_granted_before" time_format:"2006-01-02" time_utc:"true"`
|
|
BannedAfter *time.Time `form:"banned_after" time_format:"2006-01-02" time_utc:"true"`
|
|
BannedBefore *time.Time `form:"banned_before" time_format:"2006-01-02" time_utc:"true"`
|
|
UpdatedAfter *time.Time `form:"updated_after" time_format:"2006-01-02" time_utc:"true"`
|
|
UpdatedBefore *time.Time `form:"updated_before" time_format:"2006-01-02" time_utc:"true"`
|
|
|
|
Cursor *uuid.UUID `form:"cursor"`
|
|
Limit int `form:"limit"`
|
|
SortBy string `form:"sort_by"` // created_at, email, name
|
|
SortDesc bool `form:"sort_desc"`
|
|
}
|
|
|
|
// AdminUserDetail represents a user with admin-relevant statistics
|
|
type AdminUserDetail struct {
|
|
ID uuid.UUID `json:"id"`
|
|
FirstName string `json:"first_name"`
|
|
LastName string `json:"last_name"`
|
|
Email string `json:"email"`
|
|
MaxOrganizations int `json:"max_organizations"`
|
|
FreeTrialUsed bool `json:"free_trial_used"`
|
|
AdminPermissions AdminPermission `json:"admin_permissions"`
|
|
AdminGrantedAt *time.Time `json:"admin_granted_at,omitempty"`
|
|
AdminGrantedBy *uuid.UUID `json:"admin_granted_by,omitempty"`
|
|
BannedAt *time.Time `json:"banned_at,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
|
|
// Statistics
|
|
OrganizationCount int `json:"organization_count"`
|
|
EmailAccountCount int `json:"email_account_count"`
|
|
CampaignCount int `json:"campaign_count"`
|
|
}
|
|
|
|
// AdminUsersResult represents paginated user listing
|
|
type AdminUsersResult struct {
|
|
Data []AdminUserDetail `json:"data"`
|
|
Pagination Pagination `json:"pagination"`
|
|
}
|
|
|
|
// UserBan represents a ban record for a user
|
|
type UserBan struct {
|
|
ID uuid.UUID `json:"id"`
|
|
UserID uuid.UUID `json:"user_id"`
|
|
BannedBy uuid.UUID `json:"banned_by"`
|
|
Reason string `json:"reason"`
|
|
BannedAt time.Time `json:"banned_at"`
|
|
UnbannedAt *time.Time `json:"unbanned_at,omitempty"`
|
|
UnbannedBy *uuid.UUID `json:"unbanned_by,omitempty"`
|
|
UnbanReason *string `json:"unban_reason,omitempty"`
|
|
|
|
// Joined data
|
|
BannedByUser *AdminUserSummary `json:"banned_by_user,omitempty"`
|
|
UnbannedByUser *AdminUserSummary `json:"unbanned_by_user,omitempty"`
|
|
}
|
|
|
|
// AdminUserSummary is a minimal user representation for joined data
|
|
type AdminUserSummary struct {
|
|
ID uuid.UUID `json:"id"`
|
|
FirstName string `json:"first_name"`
|
|
LastName string `json:"last_name"`
|
|
Email string `json:"email"`
|
|
}
|
|
|
|
// BanScope is a bitmask describing which actions are blocked while a
|
|
// user is banned. The existing banned_at column still marks "banned",
|
|
// but the scope decides what concretely stops working. Kept in sync
|
|
// with the values in 000045_ban_scope.up.sql.
|
|
type BanScope uint32
|
|
|
|
const (
|
|
BanScopeLogin BanScope = 1 << iota // 1 — block authentication
|
|
BanScopeOrgCreate // 2 — refuse new workspace creation
|
|
BanScopeSend // 4 — block outbound campaign sends
|
|
)
|
|
|
|
// BanScopeAll is the legacy "everything" mask applied at migration to
|
|
// preserve the pre-bitmask "fully banned" semantics. New bans should
|
|
// pick a more specific scope where possible.
|
|
const BanScopeAll = BanScopeLogin | BanScopeOrgCreate | BanScopeSend
|
|
|
|
func (s BanScope) Has(flag BanScope) bool { return s&flag == flag }
|
|
|
|
// BanUserRequest represents the request to ban a user
|
|
type BanUserRequest struct {
|
|
Reason string `json:"reason" binding:"required"`
|
|
// Scope is a BanScope bitmask. Zero / missing falls back to
|
|
// BanScopeLogin so the historic "you can't log in" behaviour
|
|
// stays the default for ambiguous calls.
|
|
Scope BanScope `json:"scope,omitempty"`
|
|
}
|
|
|
|
// UnbanUserRequest represents the request to unban a user
|
|
type UnbanUserRequest struct {
|
|
Reason string `json:"reason" binding:"required"`
|
|
}
|
|
|
|
// AdminWorkerDetail represents a worker with admin-relevant details
|
|
type AdminWorkerDetail struct {
|
|
ID uuid.UUID `json:"id"`
|
|
Name string `json:"name"`
|
|
Notes string `json:"notes"`
|
|
IPAddr string `json:"ip_addr"`
|
|
Active bool `json:"active"`
|
|
FreeTier bool `json:"free_tier"`
|
|
WorkerType WorkerType `json:"worker_type"`
|
|
AccountCount int `json:"account_count"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
|
|
// Statistics
|
|
EmailsSentToday int `json:"emails_sent_today"`
|
|
EmailsSentTotal int `json:"emails_sent_total"`
|
|
ActiveCampaigns int `json:"active_campaigns"`
|
|
ConnectedEmails int `json:"connected_emails"`
|
|
WarmupEmails int `json:"warmup_emails"`
|
|
}
|
|
|
|
// AdminWorkersResult represents paginated worker listing
|
|
type AdminWorkersResult struct {
|
|
Data []AdminWorkerDetail `json:"data"`
|
|
Pagination Pagination `json:"pagination"`
|
|
}
|
|
|
|
// AdminUpdateWorker represents the request to update a worker
|
|
type AdminUpdateWorker struct {
|
|
Name *string `json:"name,omitempty"`
|
|
Notes *string `json:"notes,omitempty"`
|
|
Active *bool `json:"active,omitempty"`
|
|
WorkerType *WorkerType `json:"worker_type,omitempty"`
|
|
}
|
|
|
|
// AdminWorkerEmail represents an email account connected to a worker, including
|
|
// the per-mailbox health signals (risk band + worst warmup health state) so the
|
|
// admin worker view can show how healthy the inboxes on a worker are.
|
|
type AdminWorkerEmail struct {
|
|
ID uuid.UUID `json:"id"`
|
|
Email string `json:"email"`
|
|
UserID uuid.UUID `json:"user_id"`
|
|
OrganizationID *uuid.UUID `json:"organization_id,omitempty"`
|
|
Status string `json:"status"`
|
|
Provider string `json:"provider"`
|
|
WarmupEnabled bool `json:"warmup_enabled"`
|
|
// NULL until the mailbox syncs for the first time.
|
|
LastSyncedAt *time.Time `json:"last_synced_at"`
|
|
RiskBand string `json:"risk_band"` // clean | risky | quarantine
|
|
RiskEvaluatedAt *time.Time `json:"risk_evaluated_at,omitempty"`
|
|
WarmupHealth string `json:"warmup_health,omitempty"` // worst warmup health_state, "" if not in a pool
|
|
SpamScore *int `json:"spam_score,omitempty"`
|
|
BlockedUntil *time.Time `json:"blocked_until,omitempty"`
|
|
}
|
|
|
|
// ReassignEmailsRequest represents the request to reassign emails
|
|
type ReassignEmailsRequest struct {
|
|
EmailIDs []uuid.UUID `json:"email_ids" binding:"required"`
|
|
NewWorkerID uuid.UUID `json:"new_worker_id" binding:"required"`
|
|
}
|
|
|
|
// WarmupAppealStatus represents the status of a warmup appeal
|
|
type WarmupAppealStatus string
|
|
|
|
const (
|
|
WarmupAppealStatusPending WarmupAppealStatus = "pending"
|
|
WarmupAppealStatusApproved WarmupAppealStatus = "approved"
|
|
WarmupAppealStatusRejected WarmupAppealStatus = "rejected"
|
|
)
|
|
|
|
// WarmupAppeal represents an appeal for a blocked warmup account
|
|
type WarmupAppeal struct {
|
|
ID uuid.UUID `json:"id"`
|
|
EmailAccountID uuid.UUID `json:"email_account_id"`
|
|
UserID uuid.UUID `json:"user_id"`
|
|
Reason string `json:"reason"`
|
|
Status WarmupAppealStatus `json:"status"`
|
|
ReviewedBy *uuid.UUID `json:"reviewed_by,omitempty"`
|
|
ReviewedAt *time.Time `json:"reviewed_at,omitempty"`
|
|
ReviewNotes *string `json:"review_notes,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
|
|
// Joined data
|
|
User *AdminUserSummary `json:"user,omitempty"`
|
|
EmailAccount *AdminWorkerEmail `json:"email_account,omitempty"`
|
|
ReviewedByUser *AdminUserSummary `json:"reviewed_by_user,omitempty"`
|
|
}
|
|
|
|
// WarmupAppealsResult represents paginated appeal listing
|
|
type WarmupAppealsResult struct {
|
|
Data []WarmupAppeal `json:"data"`
|
|
Pagination Pagination `json:"pagination"`
|
|
}
|
|
|
|
// ReviewAppealRequest represents the request to review an appeal
|
|
type ReviewAppealRequest struct {
|
|
Approved bool `json:"approved"`
|
|
Notes string `json:"notes"`
|
|
}
|
|
|
|
// AdminBlockedAccount represents a blocked warmup account
|
|
type AdminBlockedAccount struct {
|
|
ID uuid.UUID `json:"id"`
|
|
Email string `json:"email"`
|
|
UserID uuid.UUID `json:"user_id"`
|
|
BlockedAt time.Time `json:"blocked_at"`
|
|
BlockedBy *uuid.UUID `json:"blocked_by,omitempty"`
|
|
BlockReason string `json:"block_reason"`
|
|
HasAppeal bool `json:"has_appeal"`
|
|
AppealStatus *WarmupAppealStatus `json:"appeal_status,omitempty"`
|
|
|
|
// Joined data
|
|
User *AdminUserSummary `json:"user,omitempty"`
|
|
}
|
|
|
|
// AdminBlockedAccountsResult represents paginated blocked accounts
|
|
type AdminBlockedAccountsResult struct {
|
|
Data []AdminBlockedAccount `json:"data"`
|
|
Pagination Pagination `json:"pagination"`
|
|
}
|
|
|
|
// BlockAccountRequest represents the request to block an account
|
|
type BlockAccountRequest struct {
|
|
Reason string `json:"reason" binding:"required"`
|
|
}
|
|
|
|
// AdminCampaignDetail represents a campaign with admin-relevant details
|
|
type AdminCampaignDetail struct {
|
|
ID uuid.UUID `json:"id"`
|
|
Name string `json:"name"`
|
|
UserID uuid.UUID `json:"user_id"`
|
|
OrganizationID uuid.UUID `json:"organization_id"`
|
|
Status string `json:"status"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
StartedAt *time.Time `json:"started_at,omitempty"`
|
|
StoppedAt *time.Time `json:"stopped_at,omitempty"`
|
|
|
|
// Statistics
|
|
TotalContacts int `json:"total_contacts"`
|
|
EmailsSent int `json:"emails_sent"`
|
|
EmailsOpened int `json:"emails_opened"`
|
|
EmailsClicked int `json:"emails_clicked"`
|
|
EmailsReplied int `json:"emails_replied"`
|
|
EmailsBounced int `json:"emails_bounced"`
|
|
|
|
// Joined data
|
|
User *AdminUserSummary `json:"user,omitempty"`
|
|
Organization *Organization `json:"organization,omitempty"`
|
|
}
|
|
|
|
// AdminStopCampaignRequest carries the reason the audit trail and the owner's
|
|
// campaign feed show, so it is required.
|
|
type AdminStopCampaignRequest struct {
|
|
Reason string `json:"reason" binding:"required"`
|
|
}
|
|
|
|
// AdminCampaignsResult represents paginated campaign listing
|
|
type AdminCampaignsResult struct {
|
|
Data []AdminCampaignDetail `json:"data"`
|
|
Pagination Pagination `json:"pagination"`
|
|
}
|
|
|
|
// AdminCampaignSearch represents search parameters for campaigns. form tags
|
|
// must stay byte-identical to the TS AdminCampaignSearch keys; date facets are
|
|
// *time.Time with time_format/time_utc, mirroring AdminOrgSearch.
|
|
type AdminCampaignSearch struct {
|
|
Query string `form:"q"`
|
|
UserID *uuid.UUID `form:"user_id"`
|
|
OrgID *uuid.UUID `form:"org_id"`
|
|
Status string `form:"status"` // draft, active, paused, completed, paused_trial_expired, paused_no_accounts
|
|
|
|
// Boolean flags
|
|
OpenTracking bool `form:"open_tracking"`
|
|
LinkTracking bool `form:"link_tracking"`
|
|
StopOnReply bool `form:"stop_on_reply"`
|
|
TextOnly bool `form:"text_only"`
|
|
UnsubscribeHeader bool `form:"unsubscribe_header"`
|
|
|
|
// Relationship existence
|
|
HasContacts bool `form:"has_contacts"`
|
|
HasBounces bool `form:"has_bounces"`
|
|
|
|
// Count ranges
|
|
DailyLimitMin *int `form:"daily_limit_min"`
|
|
DailyLimitMax *int `form:"daily_limit_max"`
|
|
ContactCountMin *int `form:"contact_count_min"`
|
|
ContactCountMax *int `form:"contact_count_max"`
|
|
SentCountMin *int `form:"sent_count_min"`
|
|
SentCountMax *int `form:"sent_count_max"`
|
|
|
|
// Date ranges (YYYY-MM-DD, UTC)
|
|
CreatedWithin int `form:"created_within"` // days; 0 = any
|
|
CreatedAfter *time.Time `form:"created_after" time_format:"2006-01-02" time_utc:"true"`
|
|
CreatedBefore *time.Time `form:"created_before" time_format:"2006-01-02" time_utc:"true"`
|
|
StartDateAfter *time.Time `form:"start_date_after" time_format:"2006-01-02" time_utc:"true"`
|
|
StartDateBefore *time.Time `form:"start_date_before" time_format:"2006-01-02" time_utc:"true"`
|
|
UpdatedAfter *time.Time `form:"updated_after" time_format:"2006-01-02" time_utc:"true"`
|
|
UpdatedBefore *time.Time `form:"updated_before" time_format:"2006-01-02" time_utc:"true"`
|
|
|
|
Cursor *uuid.UUID `form:"cursor"`
|
|
Limit int `form:"limit"`
|
|
SortBy string `form:"sort_by"`
|
|
SortDesc bool `form:"sort_desc"`
|
|
}
|
|
|
|
// AdminAuditLog represents an audit log entry for admin actions
|
|
type AdminAuditLog struct {
|
|
ID uuid.UUID `json:"id"`
|
|
AdminUserID uuid.UUID `json:"admin_user_id"`
|
|
Action string `json:"action"`
|
|
TargetType string `json:"target_type"`
|
|
TargetID uuid.UUID `json:"target_id"`
|
|
Details map[string]any `json:"details,omitempty"`
|
|
IPAddress string `json:"ip_address"`
|
|
UserAgent string `json:"user_agent"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
|
|
// Joined data
|
|
AdminUser *AdminUserSummary `json:"admin_user,omitempty"`
|
|
}
|
|
|
|
// AdminAuditLogsResult represents paginated audit logs
|
|
type AdminAuditLogsResult struct {
|
|
Data []AdminAuditLog `json:"data"`
|
|
Pagination Pagination `json:"pagination"`
|
|
}
|
|
|
|
// AdminAuditLogSearch represents search parameters for audit logs
|
|
type AdminAuditLogSearch struct {
|
|
AdminUserID *uuid.UUID `form:"admin_user_id"`
|
|
Action string `form:"action"`
|
|
TargetType string `form:"target_type"`
|
|
TargetID *uuid.UUID `form:"target_id"`
|
|
StartDate *time.Time `form:"start_date"`
|
|
EndDate *time.Time `form:"end_date"`
|
|
Cursor *uuid.UUID `form:"cursor"`
|
|
Limit int `form:"limit"`
|
|
}
|
|
|
|
// PlatformOverview represents high-level platform statistics
|
|
type PlatformOverview struct {
|
|
TotalUsers int64 `json:"total_users"`
|
|
ActiveUsers int64 `json:"active_users"` // Active in last 30 days
|
|
NewUsersToday int64 `json:"new_users_today"`
|
|
NewUsersThisWeek int64 `json:"new_users_this_week"`
|
|
|
|
TotalCampaigns int64 `json:"total_campaigns"`
|
|
ActiveCampaigns int64 `json:"active_campaigns"`
|
|
|
|
TotalEmailsSent int64 `json:"total_emails_sent"`
|
|
EmailsSentToday int64 `json:"emails_sent_today"`
|
|
|
|
TotalWorkers int64 `json:"total_workers"`
|
|
ActiveWorkers int64 `json:"active_workers"`
|
|
|
|
WarmupBlockedCount int64 `json:"warmup_blocked_count"`
|
|
PendingAppeals int64 `json:"pending_appeals"`
|
|
|
|
ActiveSubscriptions int64 `json:"active_subscriptions"`
|
|
TrialingUsers int64 `json:"trialing_users"`
|
|
}
|
|
|
|
// AdminMailboxRow is one row in the platform-wide mailbox admin list.
|
|
// Joins the connected mailbox with owner + workspace so the table can
|
|
// answer "whose mailbox is this and where does it live" without
|
|
// fan-out fetches.
|
|
type AdminMailboxRow struct {
|
|
ID uuid.UUID `json:"id"`
|
|
Email string `json:"email"`
|
|
Provider string `json:"provider"`
|
|
Status string `json:"status"`
|
|
UserID uuid.UUID `json:"user_id"`
|
|
OwnerEmail string `json:"owner_email"`
|
|
OrganizationID *uuid.UUID `json:"organization_id,omitempty"`
|
|
OrgName *string `json:"org_name,omitempty"`
|
|
WorkerID *uuid.UUID `json:"worker_id,omitempty"`
|
|
WarmupEnabled bool `json:"warmup_enabled"`
|
|
RiskBand string `json:"risk_band"`
|
|
WarmupPoolType *string `json:"warmup_pool_type,omitempty"`
|
|
CampaignLimit int `json:"campaign_limit"`
|
|
LastSyncedAt *time.Time `json:"last_synced_at,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
}
|
|
|
|
// AdminMailboxesResult is the paginated wrapper.
|
|
type AdminMailboxesResult struct {
|
|
Data []AdminMailboxRow `json:"data"`
|
|
Pagination Pagination `json:"pagination"`
|
|
}
|
|
|
|
// AdminMailboxSearch covers the query knobs the admin table needs.
|
|
// All fields optional; empty status = active, "all" returns every
|
|
// status. provider lets ops filter to "all gmail mailboxes" quickly.
|
|
type AdminMailboxSearch struct {
|
|
Query string `form:"q"`
|
|
Status string `form:"status"`
|
|
Provider string `form:"provider"`
|
|
Warmup string `form:"warmup"` // "on", "off", or "" for any
|
|
CreatedWithin int `form:"created_within"` // connected window, in days
|
|
OrgID *uuid.UUID `form:"org_id"` // browse a single org's mailboxes
|
|
|
|
// Ownership / placement
|
|
UserID *uuid.UUID `form:"user_id"` // mailboxes owned by THIS user
|
|
WorkerID *uuid.UUID `form:"worker_id"` // mailboxes on THIS worker
|
|
|
|
// Classification
|
|
RiskBand string `form:"risk_band"` // clean, risky, quarantine
|
|
WarmupPoolType string `form:"warmup_pool_type"` // free, premium
|
|
SyncedStatus string `form:"synced_status"` // never, stale, recent
|
|
|
|
// Flags
|
|
WarmupPaused bool `form:"warmup_paused"`
|
|
TrackingDomainVerified bool `form:"tracking_domain_verified"`
|
|
HasTrackingDomain bool `form:"has_tracking_domain"`
|
|
HasOrganization bool `form:"has_organization"`
|
|
SignatureSync bool `form:"signature_sync"`
|
|
HasOAuth bool `form:"has_oauth"`
|
|
HasSMTPImap bool `form:"has_smtp_imap"`
|
|
|
|
// Numeric ranges
|
|
CampaignLimitMin *int `form:"campaign_limit_min"`
|
|
CampaignLimitMax *int `form:"campaign_limit_max"`
|
|
MinWaitTimeMin *int `form:"min_wait_time_min"`
|
|
MinWaitTimeMax *int `form:"min_wait_time_max"`
|
|
|
|
// Date ranges (YYYY-MM-DD, UTC)
|
|
CreatedAfter *time.Time `form:"created_after" time_format:"2006-01-02" time_utc:"true"`
|
|
CreatedBefore *time.Time `form:"created_before" time_format:"2006-01-02" time_utc:"true"`
|
|
LastSyncedAfter *time.Time `form:"last_synced_after" time_format:"2006-01-02" time_utc:"true"`
|
|
LastSyncedBefore *time.Time `form:"last_synced_before" time_format:"2006-01-02" time_utc:"true"`
|
|
|
|
Cursor *uuid.UUID `form:"cursor"`
|
|
Limit int `form:"limit"`
|
|
SortBy string `form:"sort_by"` // email, created_at, last_synced_at, campaign_limit
|
|
SortDesc bool `form:"sort_desc"`
|
|
}
|
|
|
|
// DailyEmailStats represents daily email statistics for graphs
|
|
type DailyEmailStats struct {
|
|
Date time.Time `json:"date"`
|
|
TotalSent int64 `json:"total_sent"`
|
|
TotalDelivered int64 `json:"total_delivered"`
|
|
TotalBounced int64 `json:"total_bounced"`
|
|
TotalOpened int64 `json:"total_opened"`
|
|
TotalClicked int64 `json:"total_clicked"`
|
|
TotalReplied int64 `json:"total_replied"`
|
|
}
|
|
|
|
// HourlyEmailStats represents hourly email statistics
|
|
type HourlyEmailStats struct {
|
|
Hour int `json:"hour"`
|
|
TotalSent int64 `json:"total_sent"`
|
|
}
|
|
|
|
// WorkerLoadStats represents worker load statistics
|
|
type WorkerLoadStats struct {
|
|
WorkerID uuid.UUID `json:"worker_id"`
|
|
WorkerName string `json:"worker_name"`
|
|
EmailsSentToday int64 `json:"emails_sent_today"`
|
|
QueuedEmails int64 `json:"queued_emails"`
|
|
ConnectedEmails int64 `json:"connected_emails"`
|
|
CPUUsage float64 `json:"cpu_usage,omitempty"`
|
|
MemoryUsage float64 `json:"memory_usage,omitempty"`
|
|
}
|
|
|
|
// UserGrowthStats represents user growth statistics
|
|
type UserGrowthStats struct {
|
|
Date time.Time `json:"date"`
|
|
NewUsers int64 `json:"new_users"`
|
|
TotalUsers int64 `json:"total_users"`
|
|
}
|
|
|
|
// AnalyticsTrends represents trend data for analytics
|
|
type AnalyticsTrends struct {
|
|
UsersGrowthPercent float64 `json:"users_growth_percent"`
|
|
EmailsGrowthPercent float64 `json:"emails_growth_percent"`
|
|
CampaignsGrowthPercent float64 `json:"campaigns_growth_percent"`
|
|
RevenueGrowthPercent float64 `json:"revenue_growth_percent"`
|
|
}
|
|
|
|
// CreatePlanRequest represents the request to create a custom plan
|
|
type CreatePlanRequest struct {
|
|
Name string `json:"name" binding:"required"`
|
|
MaxContacts uint `json:"max_contacts"`
|
|
DailyEmails uint `json:"daily_emails"`
|
|
AIGeneration bool `json:"ai_generation"`
|
|
AccountLimit uint `json:"account_limit"`
|
|
Price float32 `json:"price"`
|
|
DiscountedPrice float32 `json:"discounted_price"`
|
|
Duration Duration `json:"duration"` // month/year
|
|
DedicatedWorkers int `json:"dedicated_workers"`
|
|
DailyCampaignLimit *int `json:"daily_campaign_limit,omitempty"`
|
|
MaxCampaigns *int `json:"max_campaigns,omitempty"`
|
|
MaxActiveCampaigns *int `json:"max_active_campaigns,omitempty"`
|
|
MaxTeamMembers *int `json:"max_team_members,omitempty"`
|
|
MaxEmailAccounts *int `json:"max_email_accounts,omitempty"`
|
|
Public bool `json:"public"` // false for enterprise-only
|
|
}
|
|
|
|
// UpdatePlanRequest represents the request to update a plan
|
|
type UpdatePlanRequest struct {
|
|
Name *string `json:"name,omitempty"`
|
|
MaxContacts *uint `json:"max_contacts,omitempty"`
|
|
DailyEmails *uint `json:"daily_emails,omitempty"`
|
|
AIGeneration *bool `json:"ai_generation,omitempty"`
|
|
AccountLimit *uint `json:"account_limit,omitempty"`
|
|
Price *float32 `json:"price,omitempty"`
|
|
DiscountedPrice *float32 `json:"discounted_price,omitempty"`
|
|
Duration *Duration `json:"duration,omitempty"`
|
|
DedicatedWorkers *int `json:"dedicated_workers,omitempty"`
|
|
DailyCampaignLimit *int `json:"daily_campaign_limit,omitempty"`
|
|
MaxCampaigns *int `json:"max_campaigns,omitempty"`
|
|
MaxActiveCampaigns *int `json:"max_active_campaigns,omitempty"`
|
|
MaxTeamMembers *int `json:"max_team_members,omitempty"`
|
|
MaxEmailAccounts *int `json:"max_email_accounts,omitempty"`
|
|
Public *bool `json:"public,omitempty"`
|
|
}
|
|
|
|
// AdminEnterpriseInquiry represents an enterprise inquiry with admin details
|
|
type AdminEnterpriseInquiry struct {
|
|
ID uuid.UUID `json:"id"`
|
|
UserID *uuid.UUID `json:"user_id,omitempty"`
|
|
CompanyName string `json:"company_name"`
|
|
ContactName string `json:"contact_name"`
|
|
ContactEmail string `json:"contact_email"`
|
|
Phone *string `json:"phone,omitempty"`
|
|
TeamSize *string `json:"team_size,omitempty"`
|
|
EstimatedVolume *int `json:"estimated_volume,omitempty"`
|
|
MonthlyEmailVolume *string `json:"monthly_email_volume,omitempty"`
|
|
Message *string `json:"message,omitempty"`
|
|
Status string `json:"status"`
|
|
AssignedTo *uuid.UUID `json:"assigned_to,omitempty"`
|
|
Notes *string `json:"notes,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
|
|
// Joined data
|
|
User *AdminUserSummary `json:"user,omitempty"`
|
|
AssignedAdmin *AdminUserSummary `json:"assigned_admin,omitempty"`
|
|
}
|
|
|
|
// AdminEnterpriseInquiriesResult represents paginated inquiries
|
|
type AdminEnterpriseInquiriesResult struct {
|
|
Data []AdminEnterpriseInquiry `json:"data"`
|
|
Pagination Pagination `json:"pagination"`
|
|
}
|
|
|
|
// AdminEnterpriseInquirySearch are the query params for the enterprise inquiry
|
|
// queue. Mirrors AdminOrgSearch tag conventions.
|
|
type AdminEnterpriseInquirySearch struct {
|
|
Query string `form:"q"`
|
|
Status string `form:"status"` // pending, contacted, converted, declined, all
|
|
Assignment string `form:"assignment"` // assigned, unassigned
|
|
Linkage string `form:"linkage"` // linked, anonymous
|
|
|
|
HasNotes bool `form:"has_notes"`
|
|
HasPhone bool `form:"has_phone"`
|
|
Processed bool `form:"processed"`
|
|
|
|
// Count ranges
|
|
TeamSizeMin *int `form:"team_size_min"`
|
|
TeamSizeMax *int `form:"team_size_max"`
|
|
EstimatedVolumeMin *int `form:"estimated_volume_min"`
|
|
EstimatedVolumeMax *int `form:"estimated_volume_max"`
|
|
|
|
// Date ranges (YYYY-MM-DD, UTC)
|
|
CreatedWithin int `form:"created_within"`
|
|
CreatedAfter *time.Time `form:"created_after" time_format:"2006-01-02" time_utc:"true"`
|
|
CreatedBefore *time.Time `form:"created_before" time_format:"2006-01-02" time_utc:"true"`
|
|
UpdatedAfter *time.Time `form:"updated_after" time_format:"2006-01-02" time_utc:"true"`
|
|
UpdatedBefore *time.Time `form:"updated_before" time_format:"2006-01-02" time_utc:"true"`
|
|
|
|
Cursor *uuid.UUID `form:"cursor"`
|
|
Limit int `form:"limit"`
|
|
SortBy string `form:"sort_by"` // created_at, updated_at, company_name, contact_email, status, team_size, estimated_volume
|
|
SortDesc bool `form:"sort_desc"`
|
|
}
|
|
|
|
// UpdateEnterpriseInquiryRequest represents the request to update an inquiry
|
|
type UpdateEnterpriseInquiryRequest struct {
|
|
Status *string `json:"status,omitempty"`
|
|
AssignedTo *uuid.UUID `json:"assigned_to,omitempty"`
|
|
Notes *string `json:"notes,omitempty"`
|
|
}
|
|
|
|
// AdminInfo represents an admin user for listing
|
|
type AdminInfo struct {
|
|
ID uuid.UUID `json:"id"`
|
|
FirstName string `json:"first_name"`
|
|
LastName string `json:"last_name"`
|
|
Email string `json:"email"`
|
|
AdminPermissions AdminPermission `json:"admin_permissions"`
|
|
AdminGrantedAt *time.Time `json:"admin_granted_at,omitempty"`
|
|
AdminGrantedBy *uuid.UUID `json:"admin_granted_by,omitempty"`
|
|
|
|
// Joined
|
|
GrantedByUser *AdminUserSummary `json:"granted_by_user,omitempty"`
|
|
}
|
|
|
|
// AdminsResult represents paginated admin listing
|
|
type AdminsResult struct {
|
|
Data []AdminInfo `json:"data"`
|
|
Pagination Pagination `json:"pagination"`
|
|
}
|
|
|
|
// GrantAdminRequest represents the request to grant admin permissions
|
|
type GrantAdminRequest struct {
|
|
Permissions AdminPermission `json:"permissions" binding:"required"`
|
|
}
|
|
|
|
// AdminUserRateLimits is one user's row in user_rate_limits: the API and
|
|
// realtime throughput they are allowed. Mail volume is not a rate limit.
|
|
type AdminUserRateLimits struct {
|
|
UserID uuid.UUID `json:"user_id"`
|
|
|
|
LimitReadPM int `json:"limit_read_pm"`
|
|
LimitWritePM int `json:"limit_write_pm"`
|
|
LimitBulkPM int `json:"limit_bulk_pm"`
|
|
LimitUniboxPM int `json:"limit_unibox_pm"`
|
|
LimitAnalyticsPM int `json:"limit_analytics_pm"`
|
|
|
|
LimitAPICallsDaily int `json:"limit_api_calls_daily"`
|
|
LimitBulkOpsDaily int `json:"limit_bulk_ops_daily"`
|
|
|
|
LimitWSMessagePM int `json:"limit_ws_message_pm"`
|
|
LimitWSJoinPM int `json:"limit_ws_join_pm"`
|
|
LimitWSEventPM int `json:"limit_ws_event_pm"`
|
|
MaxConnections int `json:"max_connections"`
|
|
|
|
Notes *string `json:"notes,omitempty"`
|
|
UpdatedBy *uuid.UUID `json:"updated_by,omitempty"`
|
|
// Absent when the user has no override row and these are the defaults.
|
|
UpdatedAt *time.Time `json:"updated_at,omitempty"`
|
|
}
|
|
|
|
// UpdateUserRateLimitsRequest patches user_rate_limits. An omitted field is left
|
|
// alone; there is no null state, so clearing means typing the default back in.
|
|
type UpdateUserRateLimitsRequest struct {
|
|
LimitReadPM *int `json:"limit_read_pm,omitempty"`
|
|
LimitWritePM *int `json:"limit_write_pm,omitempty"`
|
|
LimitBulkPM *int `json:"limit_bulk_pm,omitempty"`
|
|
LimitUniboxPM *int `json:"limit_unibox_pm,omitempty"`
|
|
LimitAnalyticsPM *int `json:"limit_analytics_pm,omitempty"`
|
|
|
|
LimitAPICallsDaily *int `json:"limit_api_calls_daily,omitempty"`
|
|
LimitBulkOpsDaily *int `json:"limit_bulk_ops_daily,omitempty"`
|
|
|
|
LimitWSMessagePM *int `json:"limit_ws_message_pm,omitempty"`
|
|
LimitWSJoinPM *int `json:"limit_ws_join_pm,omitempty"`
|
|
LimitWSEventPM *int `json:"limit_ws_event_pm,omitempty"`
|
|
MaxConnections *int `json:"max_connections,omitempty"`
|
|
|
|
Notes *string `json:"notes,omitempty"`
|
|
}
|
|
|
|
// DefaultAdminUserRateLimits is what the enforcement path applies to a user with
|
|
// no override row, so the editor shows the limits actually in force.
|
|
func DefaultAdminUserRateLimits(userID uuid.UUID) *AdminUserRateLimits {
|
|
d := DefaultRateLimits()
|
|
return &AdminUserRateLimits{
|
|
UserID: userID,
|
|
LimitReadPM: d.LimitReadPM,
|
|
LimitWritePM: d.LimitWritePM,
|
|
LimitBulkPM: d.LimitBulkPM,
|
|
LimitUniboxPM: d.LimitUniboxPM,
|
|
LimitAnalyticsPM: d.LimitAnalyticsPM,
|
|
LimitAPICallsDaily: d.LimitAPICallsDaily,
|
|
LimitBulkOpsDaily: d.LimitBulkOpsDaily,
|
|
LimitWSMessagePM: d.LimitWSMessagePM,
|
|
LimitWSJoinPM: d.LimitWSJoinPM,
|
|
LimitWSEventPM: d.LimitWSEventPM,
|
|
MaxConnections: d.MaxConnections,
|
|
}
|
|
}
|
|
|
|
// AdminUserPreview represents a full preview of a user's account
|
|
type AdminUserPreview struct {
|
|
User AdminUserDetail `json:"user"`
|
|
Organizations []Organization `json:"organizations"`
|
|
Subscriptions []Subscription `json:"subscriptions"`
|
|
EmailAccounts []AdminWorkerEmail `json:"email_accounts"`
|
|
RecentBans []UserBan `json:"recent_bans"`
|
|
RateLimits *AdminUserRateLimits `json:"rate_limits,omitempty"`
|
|
}
|
|
|
|
// WarmupPoolInfo represents information about a warmup pool
|
|
type WarmupPoolInfo struct {
|
|
Type string `json:"type"`
|
|
TotalParticipants int64 `json:"total_participants"`
|
|
ActiveParticipants int64 `json:"active_participants"`
|
|
BlockedCount int64 `json:"blocked_count"`
|
|
}
|
|
|
|
// WarmupPoolParticipant represents a participant in a warmup pool
|
|
type WarmupPoolParticipant struct {
|
|
ID uuid.UUID `json:"id"`
|
|
Email string `json:"email"`
|
|
UserID uuid.UUID `json:"user_id"`
|
|
JoinedAt time.Time `json:"joined_at"`
|
|
EmailsSent int64 `json:"emails_sent"`
|
|
EmailsReceived int64 `json:"emails_received"`
|
|
ReputationScore float64 `json:"reputation_score"`
|
|
IsBlocked bool `json:"is_blocked"`
|
|
BlockedAt *time.Time `json:"blocked_at,omitempty"`
|
|
|
|
// Joined data
|
|
User *AdminUserSummary `json:"user,omitempty"`
|
|
}
|
|
|
|
// WarmupPoolParticipantsResult represents paginated pool participants
|
|
type WarmupPoolParticipantsResult struct {
|
|
Data []WarmupPoolParticipant `json:"data"`
|
|
Pagination Pagination `json:"pagination"`
|
|
}
|
|
|
|
// WorkerStats represents statistics for a specific worker
|
|
type WorkerStats struct {
|
|
WorkerID uuid.UUID `json:"worker_id"`
|
|
TotalEmailsSent int64 `json:"total_emails_sent"`
|
|
EmailsSentToday int64 `json:"emails_sent_today"`
|
|
EmailsSentThisWeek int64 `json:"emails_sent_this_week"`
|
|
AverageDeliveryTime float64 `json:"average_delivery_time_ms"`
|
|
SuccessRate float64 `json:"success_rate"`
|
|
QueueDepth int64 `json:"queue_depth"`
|
|
}
|
|
|
|
// EmailDistribution represents email distribution across workers
|
|
type EmailDistribution struct {
|
|
WorkerID uuid.UUID `json:"worker_id"`
|
|
WorkerName string `json:"worker_name"`
|
|
EmailCount int64 `json:"email_count"`
|
|
Percentage float64 `json:"percentage"`
|
|
}
|
|
|
|
// AdminOrgSearch are the query params for the admin organization listing.
|
|
type AdminOrgSearch struct {
|
|
Query string `form:"q"`
|
|
Status string `form:"status"` // active, pending_deletion, all
|
|
PlanID *uuid.UUID `form:"plan_id"`
|
|
PlanVisibility string `form:"plan_visibility"` // public, private, none
|
|
CreatedWithin int `form:"created_within"` // days; 0 = any
|
|
HasOverrides bool `form:"has_overrides"` // has organization_limit_overrides
|
|
RiskState string `form:"risk_state"` // exact posture: trusted|watch|restricted|suspended
|
|
RiskFlagged bool `form:"risk_flagged"` // any posture other than trusted
|
|
Enterprise bool `form:"enterprise"` // has an enterprise subscription
|
|
|
|
// Subscription state
|
|
SubscriptionStatus string `form:"subscription_status"`
|
|
CancelAtPeriodEnd bool `form:"cancel_at_period_end"`
|
|
HasActiveSubscription bool `form:"has_active_subscription"`
|
|
NoSubscription bool `form:"no_subscription"`
|
|
OwnerBanned bool `form:"owner_banned"`
|
|
|
|
// Relationship existence
|
|
HasActiveCampaigns bool `form:"has_active_campaigns"`
|
|
HasEmailAccounts bool `form:"has_email_accounts"`
|
|
|
|
// Count ranges
|
|
MemberCountMin *int `form:"member_count_min"`
|
|
MemberCountMax *int `form:"member_count_max"`
|
|
EmailAccountCountMin *int `form:"email_account_count_min"`
|
|
EmailAccountCountMax *int `form:"email_account_count_max"`
|
|
CampaignCountMin *int `form:"campaign_count_min"`
|
|
CampaignCountMax *int `form:"campaign_count_max"`
|
|
|
|
// Date ranges (YYYY-MM-DD, UTC)
|
|
CreatedAfter *time.Time `form:"created_after" time_format:"2006-01-02" time_utc:"true"`
|
|
CreatedBefore *time.Time `form:"created_before" time_format:"2006-01-02" time_utc:"true"`
|
|
TrialEndAfter *time.Time `form:"trial_end_after" time_format:"2006-01-02" time_utc:"true"`
|
|
TrialEndBefore *time.Time `form:"trial_end_before" time_format:"2006-01-02" time_utc:"true"`
|
|
CurrentPeriodEndAfter *time.Time `form:"current_period_end_after" time_format:"2006-01-02" time_utc:"true"`
|
|
CurrentPeriodEndBefore *time.Time `form:"current_period_end_before" time_format:"2006-01-02" time_utc:"true"`
|
|
UpdatedAfter *time.Time `form:"updated_after" time_format:"2006-01-02" time_utc:"true"`
|
|
UpdatedBefore *time.Time `form:"updated_before" time_format:"2006-01-02" time_utc:"true"`
|
|
|
|
Cursor *uuid.UUID `form:"cursor"`
|
|
Limit int `form:"limit"`
|
|
SortBy string `form:"sort_by"` // created_at, name, owner_email, member_count, campaign_count, email_account_count
|
|
SortDesc bool `form:"sort_desc"`
|
|
}
|
|
|
|
// AdminOrgListItem is one row in the admin org list. It carries enough
|
|
// summary state for the table (owner, counts, deletion status) without
|
|
// joining to plans or subscriptions — those land on the detail endpoint.
|
|
type AdminOrgListItem struct {
|
|
ID uuid.UUID `json:"id"`
|
|
Name string `json:"name"`
|
|
Slug *string `json:"slug,omitempty"`
|
|
OwnerUserID uuid.UUID `json:"owner_user_id"`
|
|
OwnerEmail string `json:"owner_email"`
|
|
OwnerFirstName string `json:"owner_first_name"`
|
|
OwnerLastName string `json:"owner_last_name"`
|
|
OwnerBannedAt *time.Time `json:"owner_banned_at,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
DeletionScheduledFor *time.Time `json:"deletion_scheduled_for,omitempty"`
|
|
|
|
// Resource counts. Cheap enough to inline on the list query so the
|
|
// table can show usage at a glance without an extra round-trip.
|
|
MemberCount int `json:"member_count"`
|
|
EmailAccountCount int `json:"email_account_count"`
|
|
CampaignCount int `json:"campaign_count"`
|
|
ActiveCampaigns int `json:"active_campaigns"`
|
|
|
|
// RiskState is the fused abuse posture, inlined so the table can show
|
|
// which workspaces a detector has acted on without a call per row.
|
|
RiskState OrgRiskState `json:"risk_state,omitempty"`
|
|
|
|
// Plan summary (LEFT JOINed via the org's single active subscription).
|
|
PlanName *string `json:"plan_name,omitempty"`
|
|
PlanPublic *bool `json:"plan_public,omitempty"`
|
|
IsEnterprise bool `json:"is_enterprise"`
|
|
}
|
|
|
|
// AdminOrgsResult is the paginated response for the admin org listing.
|
|
type AdminOrgsResult struct {
|
|
Data []AdminOrgListItem `json:"data"`
|
|
Pagination Pagination `json:"pagination"`
|
|
}
|
|
|
|
// AdminLimitRequestSearch are the query params for the admin limit-request
|
|
// queue. Mirrors AdminOrgSearch: *time.Time date facets, *int range bounds,
|
|
// *uuid.UUID for cursor/org/submitter. The form tags must stay byte-identical
|
|
// to the frontend param keys and the TS AdminLimitRequestSearch interface.
|
|
type AdminLimitRequestSearch struct {
|
|
Query string `form:"q"`
|
|
Status string `form:"status"` // pending, approved, rejected, cancelled, all
|
|
Field string `form:"field"` // app-validated: max_email_accounts ... daily_campaign_limit
|
|
|
|
OrgID *uuid.UUID `form:"org_id"`
|
|
SubmittedBy *uuid.UUID `form:"submitted_by"`
|
|
|
|
// Flags
|
|
Reviewed bool `form:"reviewed"` // reviewed_at IS NOT NULL
|
|
Unreviewed bool `form:"unreviewed"` // reviewed_at IS NULL
|
|
|
|
// Numeric ranges
|
|
RequestedMin *int `form:"requested_min"`
|
|
RequestedMax *int `form:"requested_max"`
|
|
CurrentEffectiveMin *int `form:"current_effective_min"`
|
|
CurrentEffectiveMax *int `form:"current_effective_max"`
|
|
|
|
// Date ranges
|
|
SubmittedWithin int `form:"submitted_within"` // days; 0 = any
|
|
SubmittedAfter *time.Time `form:"submitted_after" time_format:"2006-01-02" time_utc:"true"`
|
|
SubmittedBefore *time.Time `form:"submitted_before" time_format:"2006-01-02" time_utc:"true"`
|
|
ReviewedAfter *time.Time `form:"reviewed_after" time_format:"2006-01-02" time_utc:"true"`
|
|
ReviewedBefore *time.Time `form:"reviewed_before" time_format:"2006-01-02" time_utc:"true"`
|
|
|
|
Cursor *uuid.UUID `form:"cursor"`
|
|
Limit int `form:"limit"`
|
|
SortBy string `form:"sort_by"` // submitted_at, requested, current_effective, reviewed_at, status, field, org_name
|
|
SortDesc bool `form:"sort_desc"`
|
|
}
|
|
|
|
// AdminLimitRequestsResult is the paginated admin limit-request response.
|
|
type AdminLimitRequestsResult struct {
|
|
Data []LimitIncreaseRequest `json:"data"`
|
|
Pagination Pagination `json:"pagination"`
|
|
}
|
|
|
|
// AdminPlanSearch are the query params for the admin plan catalog listing.
|
|
// Mirrors AdminOrgSearch tag conventions. price_* bind to *int (whole-dollar
|
|
// filtering against the numeric price column).
|
|
type AdminPlanSearch struct {
|
|
Query string `form:"q"`
|
|
Visibility string `form:"visibility"` // public, private, "" = any
|
|
Duration string `form:"duration"` // month, year, "" = any (durations.title)
|
|
|
|
AIGeneration bool `form:"ai_generation"`
|
|
HasStripe bool `form:"has_stripe"`
|
|
HasSubscribers bool `form:"has_subscribers"`
|
|
|
|
// Numeric ranges
|
|
PriceMin *int `form:"price_min"`
|
|
PriceMax *int `form:"price_max"`
|
|
DailyEmailsMin *int `form:"daily_emails_min"`
|
|
DailyEmailsMax *int `form:"daily_emails_max"`
|
|
AccountLimitMin *int `form:"account_limit_min"`
|
|
AccountLimitMax *int `form:"account_limit_max"`
|
|
|
|
// Date range
|
|
CreatedWithin int `form:"created_within"` // days; 0 = any
|
|
CreatedAfter *time.Time `form:"created_after" time_format:"2006-01-02" time_utc:"true"`
|
|
CreatedBefore *time.Time `form:"created_before" time_format:"2006-01-02" time_utc:"true"`
|
|
|
|
Cursor *uuid.UUID `form:"cursor"`
|
|
Limit int `form:"limit"`
|
|
SortBy string `form:"sort_by"` // price, name, daily_emails, account_limit, created_at
|
|
SortDesc bool `form:"sort_desc"`
|
|
}
|
|
|
|
// AdminPlansResult is the paginated response for the admin plan listing.
|
|
type AdminPlansResult struct {
|
|
Data []Plan `json:"data"`
|
|
Pagination Pagination `json:"pagination"`
|
|
}
|
|
|
|
// AdminOrgDetail is the full payload for the org detail page. Carries
|
|
// three limit blocks side-by-side so the UI can explain *why* each
|
|
// effective number is what it is:
|
|
//
|
|
// - Limits — plan defaults (nil = unlimited)
|
|
// - Overrides — raw override row (0 per field = inherit)
|
|
// - EffectiveLimits — what the runtime actually enforces
|
|
type AdminOrgDetail struct {
|
|
AdminOrgListItem
|
|
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
DeletionScheduledAt *time.Time `json:"deletion_scheduled_at,omitempty"`
|
|
Limits *OrganizationLimits `json:"limits,omitempty"`
|
|
Overrides *OrganizationLimitOverrides `json:"overrides,omitempty"`
|
|
EffectiveLimits *OrganizationLimits `json:"effective_limits,omitempty"`
|
|
Counts *OrganizationCounts `json:"counts,omitempty"`
|
|
|
|
// Plan / subscription context. Either may be nil (org with no active
|
|
// subscription — e.g. trial or freshly created).
|
|
PlanName *string `json:"plan_name,omitempty"`
|
|
SubscriptionStatus *string `json:"subscription_status,omitempty"`
|
|
IsEnterprise bool `json:"is_enterprise"`
|
|
CurrentPeriodEnd *time.Time `json:"current_period_end,omitempty"`
|
|
TrialEnd *time.Time `json:"trial_end,omitempty"`
|
|
}
|
|
|
|
// AdminOrgMember is a member row enriched with the joined user.
|
|
type AdminOrgMember struct {
|
|
OrganizationMember
|
|
User *AdminUserSummary `json:"user,omitempty"`
|
|
}
|
|
|
|
// AdminOrgMembersResult is the response for /admin/organizations/:id/members.
|
|
type AdminOrgMembersResult struct {
|
|
Data []AdminOrgMember `json:"data"`
|
|
}
|