mirror of
https://github.com/warmbly/warmbly.git
synced 2026-08-24 16:00:39 +00:00
5954ac53c2
Support chained automation execution from native actions, guard automation recursion depth, carry idempotency context through campaign-launched automations, allow team task assignment in backend action execution, and reject deletes while campaign steps still reference an automation.
516 lines
22 KiB
Go
516 lines
22 KiB
Go
package models
|
|
|
|
import (
|
|
"encoding/json"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// IntegrationProvider identifies one third-party system Warmbly can connect
|
|
// to. Adding a new provider here is enough to make it visible in the
|
|
// dashboard's catalog. The actual connect/disconnect logic is handled in
|
|
// the integration service's per-provider switch.
|
|
type IntegrationProvider string
|
|
|
|
const (
|
|
// CRM
|
|
IntegrationHubSpot IntegrationProvider = "hubspot"
|
|
IntegrationSalesforce IntegrationProvider = "salesforce"
|
|
IntegrationPipedrive IntegrationProvider = "pipedrive"
|
|
IntegrationClose IntegrationProvider = "close"
|
|
|
|
// Automation
|
|
IntegrationZapier IntegrationProvider = "zapier"
|
|
IntegrationMake IntegrationProvider = "make"
|
|
IntegrationN8N IntegrationProvider = "n8n"
|
|
|
|
// Notifications
|
|
IntegrationSlack IntegrationProvider = "slack"
|
|
IntegrationDiscord IntegrationProvider = "discord"
|
|
|
|
// Meetings
|
|
IntegrationCalendly IntegrationProvider = "calendly"
|
|
IntegrationCalCom IntegrationProvider = "cal_com"
|
|
|
|
// Data
|
|
IntegrationGoogleSheets IntegrationProvider = "google_sheets"
|
|
)
|
|
|
|
// AllIntegrationProviders lists every provider the dashboard exposes. The
|
|
// order here is the catalog order users see.
|
|
var AllIntegrationProviders = []IntegrationProvider{
|
|
IntegrationHubSpot,
|
|
IntegrationSalesforce,
|
|
IntegrationPipedrive,
|
|
IntegrationClose,
|
|
IntegrationZapier,
|
|
IntegrationMake,
|
|
IntegrationN8N,
|
|
IntegrationSlack,
|
|
IntegrationDiscord,
|
|
IntegrationCalendly,
|
|
IntegrationCalCom,
|
|
IntegrationGoogleSheets,
|
|
}
|
|
|
|
func IsValidIntegrationProvider(s string) bool {
|
|
for _, p := range AllIntegrationProviders {
|
|
if string(p) == s {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// IntegrationAuthMethod describes how a connection is authenticated.
|
|
type IntegrationAuthMethod string
|
|
|
|
const (
|
|
// IntegrationAuthOAuth is a real OAuth 2.0 authorization-code handshake.
|
|
// The user clicks "Connect", authorizes in the provider's popup, and we
|
|
// store an encrypted access/refresh token pair — no pasting credentials.
|
|
IntegrationAuthOAuth IntegrationAuthMethod = "oauth"
|
|
// IntegrationAuthAPIKey is a provider-issued API token the user pastes
|
|
// (used only where the provider offers no OAuth app, e.g. Close).
|
|
IntegrationAuthAPIKey IntegrationAuthMethod = "api_key"
|
|
// IntegrationAuthWebhook is an inbound URL Warmbly mints (Calendly, Cal.com)
|
|
// or an outbound URL the user pastes (Discord).
|
|
IntegrationAuthWebhook IntegrationAuthMethod = "webhook"
|
|
)
|
|
|
|
// IntegrationStatus is the lifecycle state of a connection.
|
|
type IntegrationStatus string
|
|
|
|
const (
|
|
// IntegrationStatusPending — row created, not yet usable.
|
|
IntegrationStatusPending IntegrationStatus = "pending"
|
|
// IntegrationStatusAuthorizing — OAuth handshake is mid-flight.
|
|
IntegrationStatusAuthorizing IntegrationStatus = "authorizing"
|
|
// IntegrationStatusConnected — healthy, last interaction succeeded.
|
|
IntegrationStatusConnected IntegrationStatus = "connected"
|
|
// IntegrationStatusDegraded — connected but the last call errored.
|
|
IntegrationStatusDegraded IntegrationStatus = "degraded"
|
|
// IntegrationStatusReauthRequired — token revoked/expired; user must reconnect.
|
|
IntegrationStatusReauthRequired IntegrationStatus = "reauth_required"
|
|
// IntegrationStatusDisconnected — intentionally removed.
|
|
IntegrationStatusDisconnected IntegrationStatus = "disconnected"
|
|
)
|
|
|
|
// IntegrationHealth is the rolling health signal surfaced on the connection
|
|
// card, independent of the lifecycle status (a connection can be 'connected'
|
|
// but 'degraded' health after a transient provider error).
|
|
type IntegrationHealth string
|
|
|
|
const (
|
|
IntegrationHealthUnknown IntegrationHealth = "unknown"
|
|
IntegrationHealthHealthy IntegrationHealth = "healthy"
|
|
IntegrationHealthDegraded IntegrationHealth = "degraded"
|
|
IntegrationHealthDown IntegrationHealth = "down"
|
|
)
|
|
|
|
// IntegrationCategory groups providers in the dashboard.
|
|
type IntegrationCategory string
|
|
|
|
const (
|
|
IntegrationCategoryCRM IntegrationCategory = "crm"
|
|
IntegrationCategoryAutomation IntegrationCategory = "automation"
|
|
IntegrationCategoryNotifications IntegrationCategory = "notifications"
|
|
IntegrationCategoryMeetings IntegrationCategory = "meetings"
|
|
IntegrationCategoryData IntegrationCategory = "data"
|
|
)
|
|
|
|
// IntegrationCatalogEntry is the static metadata for one provider that the
|
|
// dashboard renders even when no connection exists yet.
|
|
type IntegrationCatalogEntry struct {
|
|
Provider IntegrationProvider `json:"provider"`
|
|
Name string `json:"name"`
|
|
Tagline string `json:"tagline"`
|
|
Category IntegrationCategory `json:"category"`
|
|
DocsURL string `json:"docs_url,omitempty"`
|
|
AuthMethod string `json:"auth_method"` // 'oauth' | 'api_key' | 'webhook'
|
|
BadgeColor string `json:"badge_color,omitempty"`
|
|
BetaFlag bool `json:"beta"`
|
|
|
|
// WebhookHint is shown to webhook-URL providers (Discord) and inbound
|
|
// providers (Calendly, Cal.com).
|
|
WebhookHint string `json:"webhook_hint,omitempty"`
|
|
|
|
// Highlights are short "what you get" bullets shown on the provider's
|
|
// detail screen during onboarding.
|
|
Highlights []string `json:"highlights,omitempty"`
|
|
|
|
// Scopes is the set of OAuth scopes requested at authorize time. Empty
|
|
// for non-OAuth providers. Surfaced so the consent screen is honest.
|
|
Scopes []string `json:"scopes,omitempty"`
|
|
|
|
// Events lists the Warmbly events this provider can react to (so the UI
|
|
// can offer "notify on positive reply", etc).
|
|
Events []string `json:"events,omitempty"`
|
|
|
|
// ActionTypes lists the provider action identifiers that have a real
|
|
// backend handler. The dashboard reads this so the automation builder only
|
|
// offers actions Warmbly can actually execute (a provider with no action
|
|
// types should not surface an automation/subscription builder at all).
|
|
ActionTypes []string `json:"action_types,omitempty"`
|
|
|
|
// SupportsPush reports whether this provider can be the target of the
|
|
// synchronous "push selected contacts" action surfaced contextually in the
|
|
// dashboard (Contacts, Deals). True only for CRM providers with an upsert
|
|
// handler.
|
|
SupportsPush bool `json:"supports_push"`
|
|
|
|
// Capability is the configurable-action descriptor the dashboard renders the
|
|
// onboarding + field-mapping UI from. Nil for providers with no descriptor.
|
|
Capability *ProviderCapability `json:"capability,omitempty"`
|
|
|
|
// Configured reports whether the server has OAuth client credentials wired
|
|
// for this provider. OAuth providers without credentials render as
|
|
// "coming soon" instead of a dead Connect button.
|
|
Configured bool `json:"configured"`
|
|
}
|
|
|
|
// IntegrationConnection is one org's link to one provider. Secrets
|
|
// (access/refresh tokens, pasted API keys) are never serialized here — only
|
|
// the encrypted columns in the DB hold them.
|
|
type IntegrationConnection struct {
|
|
ID uuid.UUID `json:"id"`
|
|
OrganizationID uuid.UUID `json:"organization_id"`
|
|
Provider IntegrationProvider `json:"provider"`
|
|
Label string `json:"label"`
|
|
Status IntegrationStatus `json:"status"`
|
|
AuthMethod string `json:"auth_method"`
|
|
DisplayFields json.RawMessage `json:"display_fields"`
|
|
|
|
// ConfigCapabilities is the per-connection onboarding/capability snapshot
|
|
// (selected objects, enabled use-cases, picker selections). Non-secret, read
|
|
// before execution. Distinct from the sealed secrets in config_encrypted.
|
|
ConfigCapabilities json.RawMessage `json:"config_capabilities,omitempty"`
|
|
// SyncDirection is the connection's data-flow direction: push | pull | both.
|
|
SyncDirection string `json:"sync_direction"`
|
|
|
|
ConnectedByUserID *uuid.UUID `json:"connected_by_user_id,omitempty"`
|
|
ExternalAccountID string `json:"external_account_id,omitempty"`
|
|
ExternalAccountName string `json:"external_account_name,omitempty"`
|
|
GrantedScopes []string `json:"granted_scopes,omitempty"`
|
|
TokenExpiresAt *time.Time `json:"token_expires_at,omitempty"`
|
|
|
|
Health string `json:"health"`
|
|
HealthDetail *string `json:"health_detail,omitempty"`
|
|
HealthCheckedAt *time.Time `json:"health_checked_at,omitempty"`
|
|
|
|
LastSyncedAt *time.Time `json:"last_synced_at,omitempty"`
|
|
LastError *string `json:"last_error,omitempty"`
|
|
LastErrorAt *time.Time `json:"last_error_at,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
|
|
// Returned only at create time for providers that POST inbound
|
|
// (Calendly, Cal.com). The dashboard surfaces the resulting URL once.
|
|
InboundWebhookURL string `json:"inbound_webhook_url,omitempty"`
|
|
}
|
|
|
|
// IntegrationTokens carries the freshly-exchanged OAuth material an
|
|
// implementation persists. Plaintext lives only in memory.
|
|
type IntegrationTokens struct {
|
|
AccessToken string
|
|
RefreshToken string
|
|
ExpiresAt *time.Time
|
|
Scopes []string
|
|
}
|
|
|
|
// IntegrationOAuthState is the short-lived CSRF/PKCE record minted at the
|
|
// start of an OAuth handshake and consumed on callback.
|
|
type IntegrationOAuthState struct {
|
|
ID uuid.UUID
|
|
OrganizationID uuid.UUID
|
|
UserID uuid.UUID
|
|
Provider IntegrationProvider
|
|
State string
|
|
CodeVerifier string
|
|
Label string
|
|
RequestedScopes []string
|
|
UsedAt *time.Time
|
|
ExpiresAt time.Time
|
|
CreatedAt time.Time
|
|
}
|
|
|
|
// IntegrationOAuthStartResponse is returned to the SPA so it can open the
|
|
// provider authorization popup.
|
|
type IntegrationOAuthStartResponse struct {
|
|
URL string `json:"url"`
|
|
State string `json:"state"`
|
|
}
|
|
|
|
// IntegrationAction is a provider-specific side effect fired by an event
|
|
// subscription.
|
|
type IntegrationAction string
|
|
|
|
const (
|
|
IntegrationActionSlackNotify IntegrationAction = "slack.notify"
|
|
IntegrationActionDiscordNotify IntegrationAction = "discord.notify"
|
|
IntegrationActionHubSpotUpsert IntegrationAction = "hubspot.upsert_contact"
|
|
IntegrationActionPipedriveUpsert IntegrationAction = "pipedrive.upsert_person"
|
|
IntegrationActionSalesforceUpsert IntegrationAction = "salesforce.upsert_contact"
|
|
IntegrationActionCloseUpsert IntegrationAction = "close.upsert_lead"
|
|
IntegrationActionGenericWebhookPing IntegrationAction = "webhook.ping"
|
|
|
|
// Native (Warmbly-internal) actions: CRM/contact mutations that need no
|
|
// external connection. Run against the contact resolved from the event data.
|
|
IntegrationActionAddTag IntegrationAction = "warmbly.add_tag"
|
|
IntegrationActionRemoveTag IntegrationAction = "warmbly.remove_tag"
|
|
IntegrationActionCreateTask IntegrationAction = "warmbly.create_task"
|
|
IntegrationActionCreateDeal IntegrationAction = "warmbly.create_deal"
|
|
IntegrationActionMoveDealStage IntegrationAction = "warmbly.move_deal_stage"
|
|
IntegrationActionUnsubscribe IntegrationAction = "warmbly.unsubscribe"
|
|
// IntegrationActionRunAutomation launches another automation's flow, passing
|
|
// the current event data through. Bounded by the chain-depth guard so it
|
|
// cannot loop forever or fan out unbounded compute.
|
|
IntegrationActionRunAutomation IntegrationAction = "warmbly.run_automation"
|
|
)
|
|
|
|
// IsNativeAction reports whether an action is a Warmbly-internal CRM/contact
|
|
// mutation (no external connection required).
|
|
func IsNativeAction(a IntegrationAction) bool {
|
|
switch a {
|
|
case IntegrationActionAddTag, IntegrationActionRemoveTag, IntegrationActionCreateTask,
|
|
IntegrationActionCreateDeal, IntegrationActionMoveDealStage, IntegrationActionUnsubscribe,
|
|
IntegrationActionRunAutomation:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// IntegrationEventSubscription routes a Warmbly event to a provider action.
|
|
type IntegrationEventSubscription struct {
|
|
ID uuid.UUID `json:"id"`
|
|
ConnectionID uuid.UUID `json:"connection_id"`
|
|
OrganizationID uuid.UUID `json:"organization_id"`
|
|
EventType string `json:"event_type"`
|
|
Action IntegrationAction `json:"action"`
|
|
Config json.RawMessage `json:"config"`
|
|
Enabled bool `json:"enabled"`
|
|
// UseCase is a discriminator describing what this automation is for (e.g.
|
|
// "crm_sync", "notify", "custom"). Drives projection + which handler runs.
|
|
UseCase string `json:"use_case"`
|
|
// AutomationID groups this subscription as one step of an Automation (the
|
|
// visual flow builder). Nil = a legacy/standalone subscription.
|
|
AutomationID *uuid.UUID `json:"automation_id,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
// Automation is a branching flow: "when <trigger_event> fires, walk this graph"
|
|
// — the data behind the visual flow builder. The graph holds a trigger node,
|
|
// condition (IF) nodes, and action nodes connected by edges; the executor walks
|
|
// it on each matching event, evaluating conditions and running the action nodes
|
|
// on the matched paths (reusing the event-subscription action handlers).
|
|
type Automation struct {
|
|
ID uuid.UUID `json:"id"`
|
|
OrganizationID uuid.UUID `json:"organization_id"`
|
|
Name string `json:"name"`
|
|
Enabled bool `json:"enabled"`
|
|
TriggerEvent string `json:"trigger_event"`
|
|
// Filter is an optional automation-wide gate (intents / min_confidence)
|
|
// applied to every action, on top of any condition nodes.
|
|
Filter json.RawMessage `json:"filter,omitempty"`
|
|
Graph AutomationGraph `json:"graph"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
// AutomationGraph is the editable flow: nodes + the edges connecting them.
|
|
type AutomationGraph struct {
|
|
Nodes []AutomationNode `json:"nodes"`
|
|
Edges []AutomationEdge `json:"edges"`
|
|
}
|
|
|
|
// AutomationNode is one node on the canvas. Type is "trigger" (exactly one, id
|
|
// "trigger"), "condition" (an IF with true/false outgoing edges), or "action".
|
|
type AutomationNode struct {
|
|
ID string `json:"id"`
|
|
Type string `json:"type"`
|
|
Action IntegrationAction `json:"action,omitempty"` // action nodes
|
|
ConnectionID *uuid.UUID `json:"connection_id,omitempty"` // action nodes
|
|
Config json.RawMessage `json:"config,omitempty"` // action node config
|
|
Condition *AutomationCondition `json:"condition,omitempty"` // condition nodes
|
|
X float64 `json:"x"`
|
|
Y float64 `json:"y"`
|
|
}
|
|
|
|
// AutomationEdge connects two nodes. When is "" for plain edges (from the
|
|
// trigger or after an action) and "true"/"false" for the two outgoing edges of
|
|
// a condition node.
|
|
type AutomationEdge struct {
|
|
ID string `json:"id"`
|
|
Source string `json:"source"`
|
|
Target string `json:"target"`
|
|
When string `json:"when,omitempty"`
|
|
}
|
|
|
|
// AutomationCondition is an IF test evaluated against the trigger event's data.
|
|
// For the generic "field" type, Key names the event-data key to test. For the
|
|
// "expression" type, Expression is a Go-template predicate (truthy when it
|
|
// renders a non-empty, non-false value) evaluated against the native event data,
|
|
// giving full string/number/boolean logic.
|
|
type AutomationCondition struct {
|
|
Field string `json:"field"`
|
|
Key string `json:"key,omitempty"`
|
|
Operator string `json:"operator"`
|
|
Value any `json:"value,omitempty"`
|
|
Expression string `json:"expression,omitempty"`
|
|
}
|
|
|
|
// AutomationWrite is the create/update payload from the flow builder.
|
|
type AutomationWrite struct {
|
|
Name string `json:"name"`
|
|
Enabled bool `json:"enabled"`
|
|
TriggerEvent string `json:"trigger_event"`
|
|
Filter json.RawMessage `json:"filter,omitempty"`
|
|
Graph AutomationGraph `json:"graph"`
|
|
}
|
|
|
|
// AutomationRun is one execution of an automation graph (per fired event or
|
|
// manual launch), with per-node outcomes for the builder's history panel.
|
|
type AutomationRun struct {
|
|
ID uuid.UUID `json:"id"`
|
|
AutomationID uuid.UUID `json:"automation_id"`
|
|
OrganizationID uuid.UUID `json:"organization_id"`
|
|
TriggerEvent string `json:"trigger_event"`
|
|
Status string `json:"status"` // running | success | error
|
|
NodeResults []AutomationNodeResult `json:"node_results"`
|
|
ErrorDetail string `json:"error_detail,omitempty"`
|
|
StartedAt time.Time `json:"started_at"`
|
|
FinishedAt *time.Time `json:"finished_at,omitempty"`
|
|
}
|
|
|
|
// AutomationNodeResult is one node's outcome in a run (or a dry-run trace).
|
|
type AutomationNodeResult struct {
|
|
NodeID string `json:"node_id"`
|
|
Type string `json:"type"` // trigger | condition | action
|
|
Action string `json:"action,omitempty"` // action nodes
|
|
Label string `json:"label,omitempty"` // human summary (e.g. "Slack · #sales")
|
|
Status string `json:"status"` // success | error | skipped | branch_true | branch_false
|
|
Error string `json:"error,omitempty"`
|
|
Preview map[string]any `json:"preview,omitempty"` // dry-run: what the action would send
|
|
}
|
|
|
|
// DryRunRequest tests an automation without side effects. Data is the sample
|
|
// event payload; when empty the server builds a sample from the trigger.
|
|
type DryRunRequest struct {
|
|
Data map[string]any `json:"data,omitempty"`
|
|
}
|
|
|
|
// DryRunResponse is the trace of a dry run.
|
|
type DryRunResponse struct {
|
|
Trace []AutomationNodeResult `json:"trace"`
|
|
Data map[string]any `json:"data"`
|
|
}
|
|
|
|
// IntegrationFieldMapping is one Warmbly-field -> provider-field mapping row.
|
|
// SubscriptionID scopes a mapping to a single automation; when nil the mapping
|
|
// is a connection default applied to every automation for that object/direction.
|
|
type IntegrationFieldMapping struct {
|
|
ID uuid.UUID `json:"id"`
|
|
ConnectionID uuid.UUID `json:"connection_id"`
|
|
OrganizationID uuid.UUID `json:"organization_id"`
|
|
SubscriptionID *uuid.UUID `json:"subscription_id,omitempty"`
|
|
Direction string `json:"direction"`
|
|
ObjectName string `json:"object_name"`
|
|
WarmblyField string `json:"warmbly_field"`
|
|
ExternalField string `json:"external_field"`
|
|
Transform string `json:"transform"`
|
|
StaticValue string `json:"static_value"`
|
|
IsDefault bool `json:"is_default"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
}
|
|
|
|
// IntegrationSyncRun is one observability record of work done against a
|
|
// connection (connect, token refresh, event dispatch, manual sync).
|
|
type IntegrationSyncRun struct {
|
|
ID uuid.UUID `json:"id"`
|
|
ConnectionID uuid.UUID `json:"connection_id"`
|
|
OrganizationID uuid.UUID `json:"organization_id"`
|
|
Kind string `json:"kind"`
|
|
Status string `json:"status"`
|
|
Detail string `json:"detail"`
|
|
RecordsProcessed int `json:"records_processed"`
|
|
StartedAt time.Time `json:"started_at"`
|
|
FinishedAt *time.Time `json:"finished_at,omitempty"`
|
|
}
|
|
|
|
// MeetingBookingStatus is the lifecycle state of a booked meeting.
|
|
type MeetingBookingStatus string
|
|
|
|
const (
|
|
MeetingBooked MeetingBookingStatus = "booked"
|
|
MeetingRescheduled MeetingBookingStatus = "rescheduled"
|
|
MeetingCanceled MeetingBookingStatus = "canceled"
|
|
MeetingCompleted MeetingBookingStatus = "completed"
|
|
MeetingNoShow MeetingBookingStatus = "no_show"
|
|
)
|
|
|
|
// MeetingBooking represents one booked meeting from Calendly/Cal.com, tracked
|
|
// through its full lifecycle (booked -> rescheduled / canceled).
|
|
type MeetingBooking struct {
|
|
ID uuid.UUID `json:"id"`
|
|
OrganizationID uuid.UUID `json:"organization_id"`
|
|
Source string `json:"source"`
|
|
ExternalEventID string `json:"external_event_id"`
|
|
Status MeetingBookingStatus `json:"status"`
|
|
InviteeEmail string `json:"invitee_email"`
|
|
InviteeName string `json:"invitee_name"`
|
|
EventName string `json:"event_name"`
|
|
EventType string `json:"event_type,omitempty"`
|
|
ScheduledFor *time.Time `json:"scheduled_for,omitempty"`
|
|
EndTime *time.Time `json:"end_time,omitempty"`
|
|
JoinURL string `json:"join_url,omitempty"`
|
|
Location string `json:"location,omitempty"`
|
|
CancelURL string `json:"cancel_url,omitempty"`
|
|
RescheduleURL string `json:"reschedule_url,omitempty"`
|
|
CanceledReason string `json:"canceled_reason,omitempty"`
|
|
ContactID *uuid.UUID `json:"contact_id,omitempty"`
|
|
CampaignID *uuid.UUID `json:"campaign_id,omitempty"`
|
|
RawPayload json.RawMessage `json:"raw_payload,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
|
|
// Joined for list display (not stored on the row).
|
|
ContactName string `json:"contact_name,omitempty"`
|
|
}
|
|
|
|
// MeetingBookingFilter scopes a Meetings-page search.
|
|
type MeetingBookingFilter struct {
|
|
// Timeframe: "upcoming" (scheduled_for >= now, not canceled),
|
|
// "past" (scheduled_for < now), or "" for all.
|
|
Timeframe string
|
|
Status string // exact status filter, or "" for any
|
|
Search string // matches invitee name/email or event name
|
|
Limit int
|
|
Offset int
|
|
}
|
|
|
|
// MeetingBookingSummary powers the sidebar count + page header stats.
|
|
type MeetingBookingSummary struct {
|
|
Upcoming int `json:"upcoming"`
|
|
Today int `json:"today"`
|
|
Total int `json:"total"`
|
|
Canceled int `json:"canceled"`
|
|
}
|
|
|
|
// MeetingBookingPage is an offset-paginated meetings result (Total is exact so
|
|
// the UI can show "N of M").
|
|
type MeetingBookingPage struct {
|
|
Data []MeetingBooking `json:"data"`
|
|
Pagination MeetingBookingPagination `json:"pagination"`
|
|
}
|
|
|
|
type MeetingBookingPagination struct {
|
|
Total int64 `json:"total"`
|
|
Limit int `json:"limit"`
|
|
Offset int `json:"offset"`
|
|
HasMore bool `json:"has_more"`
|
|
NextOffset *int `json:"next_offset,omitempty"`
|
|
}
|