diff --git a/packages/go-sdk/README.md b/packages/go-sdk/README.md index 3085a96b..2edbe0eb 100644 --- a/packages/go-sdk/README.md +++ b/packages/go-sdk/README.md @@ -6,7 +6,7 @@ **Official Go SDK for TurboDocx** -The most developer-friendly **DocuSign & PandaDoc alternative** for **e-signatures** and **document generation**. Send documents for signature and automate document workflows programmatically. +The most developer-friendly **DocuSign & PandaDoc alternative** for **e-signatures**, **document generation**, and **partner management**. Send documents for signature, automate document workflows, and manage partner organizations programmatically. [![Go Reference](https://pkg.go.dev/badge/github.com/turbodocx/sdk.svg)](https://pkg.go.dev/github.com/turbodocx/sdk) [![Go Report Card](https://goreportcard.com/badge/github.com/turbodocx/sdk)](https://goreportcard.com/report/github.com/turbodocx/sdk) @@ -52,6 +52,7 @@ A modern, developer-first alternative to legacy e-signature platforms: - ๐Ÿงต **Concurrent Safe** โ€” Safe for use across goroutines - ๐Ÿ“ฆ **Zero Dependencies** โ€” Only standard library - ๐Ÿค– **100% n8n Parity** โ€” Same operations as our n8n community nodes +- ๐Ÿข **TurboPartner** โ€” Full partner portal API for managing organizations, users, API keys, and entitlements --- @@ -293,6 +294,217 @@ for _, entry := range audit.AuditTrail { The audit trail includes a cryptographic hash chain for tamper-evidence verification. +### TurboPartner + +TurboPartner provides partner portal API access for managing organizations, users, API keys, and entitlements. It uses a separate client with partner-level authentication. + +#### Configuration + +```go +partner, err := turbodocx.NewPartnerClient(turbodocx.PartnerConfig{ + PartnerAPIKey: os.Getenv("TURBODOCX_PARTNER_API_KEY"), // REQUIRED (TDXP-* prefix) + PartnerID: os.Getenv("TURBODOCX_PARTNER_ID"), // REQUIRED +}) +``` + +**Environment Variables:** + +```bash +export TURBODOCX_PARTNER_API_KEY=TDXP-your-partner-key +export TURBODOCX_PARTNER_ID=your-partner-uuid +``` + +#### Organization Management + +```go +// Create organization with entitlements +org, err := partner.CreateOrganization(ctx, &turbodocx.CreateOrganizationRequest{ + Name: "Acme Corp", + Features: &turbodocx.Features{ + MaxUsers: turbodocx.IntPtr(25), + MaxStorage: turbodocx.Int64Ptr(5 * 1024 * 1024 * 1024), // 5 GB + HasTDAI: turbodocx.BoolPtr(true), + }, +}) + +// List organizations +orgs, err := partner.ListOrganizations(ctx, &turbodocx.ListOrganizationsRequest{ + Limit: turbodocx.IntPtr(10), + Search: "acme", +}) + +// Get full details (includes features + usage tracking) +details, err := partner.GetOrganizationDetails(ctx, orgID) + +// Update organization name +updated, err := partner.UpdateOrganizationInfo(ctx, orgID, &turbodocx.UpdateOrganizationRequest{ + Name: "Acme Corporation", +}) + +// Update entitlements +entitlements, err := partner.UpdateOrganizationEntitlements(ctx, orgID, &turbodocx.UpdateEntitlementsRequest{ + Features: &turbodocx.Features{ + MaxUsers: turbodocx.IntPtr(50), + MaxSignatures: turbodocx.IntPtr(1000), + }, +}) + +// Delete organization +_, err := partner.DeleteOrganization(ctx, orgID) +``` + +#### Organization User Management + +```go +// Add user to organization +user, err := partner.AddUserToOrganization(ctx, orgID, &turbodocx.AddOrgUserRequest{ + Email: "admin@acme.com", + Role: "admin", +}) + +// List users +users, err := partner.ListOrganizationUsers(ctx, orgID, nil) + +// Update user role +_, err := partner.UpdateOrganizationUserRole(ctx, orgID, userID, &turbodocx.UpdateOrgUserRequest{ + Role: "member", +}) + +// Remove user +_, err := partner.RemoveUserFromOrganization(ctx, orgID, userID) + +// Resend invitation +_, err := partner.ResendOrganizationInvitationToUser(ctx, orgID, userID) +``` + +#### Organization API Key Management + +```go +// Create API key +key, err := partner.CreateOrganizationApiKey(ctx, orgID, &turbodocx.CreateOrgApiKeyRequest{ + Name: "Production Key", + Role: "admin", +}) +fmt.Printf("API Key: %s\n", key.Data.Key) + +// List API keys +keys, err := partner.ListOrganizationApiKeys(ctx, orgID, nil) + +// Update API key +_, err := partner.UpdateOrganizationApiKey(ctx, orgID, keyID, &turbodocx.UpdateOrgApiKeyRequest{ + Name: "Updated Key Name", +}) + +// Revoke API key +_, err := partner.RevokeOrganizationApiKey(ctx, orgID, keyID) +``` + +#### Partner API Key Management + +```go +// Create scoped partner API key +key, err := partner.CreatePartnerApiKey(ctx, &turbodocx.CreatePartnerApiKeyRequest{ + Name: "Monitoring Key", + Description: "Read-only access for dashboard", + Scopes: []string{turbodocx.ScopeOrgRead, turbodocx.ScopeAuditRead}, +}) + +// List partner API keys +keys, err := partner.ListPartnerApiKeys(ctx, nil) + +// Update partner API key +_, err := partner.UpdatePartnerApiKey(ctx, keyID, &turbodocx.UpdatePartnerApiKeyRequest{ + Name: "Updated Key", + Scopes: []string{turbodocx.ScopeOrgRead, turbodocx.ScopeOrgUpdate}, +}) + +// Revoke partner API key +_, err := partner.RevokePartnerApiKey(ctx, keyID) +``` + +#### Partner User Management + +```go +// Add partner portal user +user, err := partner.AddUserToPartnerPortal(ctx, &turbodocx.AddPartnerUserRequest{ + Email: "ops@yourcompany.com", + Role: "member", + Permissions: turbodocx.PartnerPermissions{ + CanManageOrgs: true, + CanManageOrgUsers: true, + CanViewAuditLogs: true, + }, +}) + +// List partner users +users, err := partner.ListPartnerPortalUsers(ctx, nil) + +// Update permissions +_, err := partner.UpdatePartnerUserPermissions(ctx, userID, &turbodocx.UpdatePartnerUserRequest{ + Role: "admin", + Permissions: &turbodocx.PartnerPermissions{ + CanManageOrgs: true, + CanManageOrgUsers: true, + CanManagePartnerUsers: true, + CanManageOrgAPIKeys: true, + CanManagePartnerAPIKeys: true, + CanUpdateEntitlements: true, + CanViewAuditLogs: true, + }, +}) + +// Remove partner user +_, err := partner.RemoveUserFromPartnerPortal(ctx, userID) + +// Resend invitation +_, err := partner.ResendPartnerPortalInvitationToUser(ctx, userID) +``` + +#### Audit Logs + +```go +// Get recent audit logs +logs, err := partner.GetPartnerAuditLogs(ctx, &turbodocx.ListAuditLogsRequest{ + Limit: turbodocx.IntPtr(50), +}) + +// Filter by action and date range +logs, err := partner.GetPartnerAuditLogs(ctx, &turbodocx.ListAuditLogsRequest{ + Action: "org:create", + ResourceType: "organization", + StartDate: "2024-01-01", + EndDate: "2024-12-31", + Success: turbodocx.BoolPtr(true), +}) +``` + +#### Available Scopes + +| Scope | Description | +|:------|:------------| +| `org:create` | Create organizations | +| `org:read` | View organizations | +| `org:update` | Update organizations | +| `org:delete` | Delete organizations | +| `entitlements:update` | Update organization entitlements | +| `org-users:create` | Add users to organizations | +| `org-users:read` | View organization users | +| `org-users:update` | Update organization users | +| `org-users:delete` | Remove organization users | +| `org-apikeys:create` | Create organization API keys | +| `org-apikeys:read` | View organization API keys | +| `org-apikeys:update` | Update organization API keys | +| `org-apikeys:delete` | Revoke organization API keys | +| `partner-apikeys:create` | Create partner API keys | +| `partner-apikeys:read` | View partner API keys | +| `partner-apikeys:update` | Update partner API keys | +| `partner-apikeys:delete` | Revoke partner API keys | +| `partner-users:create` | Add partner portal users | +| `partner-users:read` | View partner portal users | +| `partner-users:update` | Update partner portal users | +| `partner-users:delete` | Remove partner portal users | +| `audit:read` | View audit logs | + --- ## Field Types @@ -458,12 +670,17 @@ type ResendEmailResponse struct { ## Examples -For complete, working examples including template anchors, advanced field types, and various workflows, see the [`examples/`](./examples/) directory: +For complete, working examples see the [`examples/`](./examples/) directory: +**TurboSign:** - [`turbosign_send_simple.go`](./examples/turbosign_send_simple.go) - Send document directly with template anchors - [`turbosign_basic.go`](./examples/turbosign_basic.go) - Create review link first, then send manually - [`turbosign_advanced.go`](./examples/turbosign_advanced.go) - Advanced field types (checkbox, readonly, multiline text, etc.) +**TurboPartner:** +- [`turbopartner_basic.go`](./examples/turbopartner_basic.go) - Full organization lifecycle (create, users, API keys) +- [`turbopartner_api_keys.go`](./examples/turbopartner_api_keys.go) - Partner API keys, portal users, and audit logs + ### Sequential Signing ```go diff --git a/packages/go-sdk/examples/turbopartner_api_keys.go b/packages/go-sdk/examples/turbopartner_api_keys.go new file mode 100644 index 00000000..0e26d783 --- /dev/null +++ b/packages/go-sdk/examples/turbopartner_api_keys.go @@ -0,0 +1,131 @@ +//go:build ignore +// +build ignore + +// TurboPartner Example: API Key & User Management +// +// This example demonstrates partner-level management: +// - Partner API key creation with scoped permissions +// - Partner portal user management +// - Audit log querying +// +// Set environment variables before running: +// export TURBODOCX_PARTNER_API_KEY=TDXP-your-key +// export TURBODOCX_PARTNER_ID=your-partner-uuid + +package main + +import ( + "context" + "fmt" + "os" + + turbodocx "github.com/TurboDocx/SDK/packages/go-sdk" +) + +func main() { + partner, err := turbodocx.NewPartnerClient(turbodocx.PartnerConfig{ + PartnerAPIKey: getEnv("TURBODOCX_PARTNER_API_KEY", "TDXP-your-key-here"), + PartnerID: getEnv("TURBODOCX_PARTNER_ID", "your-partner-uuid"), + }) + if err != nil { + fmt.Printf("Error: %v\n", err) + return + } + + ctx := context.Background() + + // --- Partner API Keys --- + + // Create a scoped partner API key (read-only for orgs and audit) + fmt.Println("Creating scoped partner API key...") + key, err := partner.CreatePartnerApiKey(ctx, &turbodocx.CreatePartnerApiKeyRequest{ + Name: "Read-Only Monitoring Key", + Description: "For monitoring dashboard - read-only access", + Scopes: []string{ + turbodocx.ScopeOrgRead, + turbodocx.ScopeOrgUsersRead, + turbodocx.ScopeAuditRead, + }, + }) + if err != nil { + fmt.Printf("Error creating partner API key: %v\n", err) + return + } + fmt.Printf("Created key: %s\n", key.Data.Name) + fmt.Printf("Key value: %s\n", key.Data.Key) + fmt.Printf("Scopes: %v\n\n", key.Data.Scopes) + + // List all partner API keys + fmt.Println("Listing partner API keys...") + keys, err := partner.ListPartnerApiKeys(ctx, nil) + if err != nil { + fmt.Printf("Error listing keys: %v\n", err) + return + } + for _, k := range keys.Data.Results { + fmt.Printf(" - %s (ID: %s)\n", k.Name, k.ID) + } + fmt.Println() + + // --- Partner Portal Users --- + + // Add a user to the partner portal with specific permissions + fmt.Println("Adding partner portal user...") + user, err := partner.AddUserToPartnerPortal(ctx, &turbodocx.AddPartnerUserRequest{ + Email: "ops@yourcompany.com", + Role: "member", + Permissions: turbodocx.PartnerPermissions{ + CanManageOrgs: true, + CanManageOrgUsers: true, + CanViewAuditLogs: true, + // Other permissions default to false + }, + }) + if err != nil { + fmt.Printf("Error adding partner user: %v\n", err) + return + } + fmt.Printf("Added partner user: %s (Role: %s)\n\n", user.Data.Email, user.Data.Role) + + // List partner portal users + fmt.Println("Listing partner portal users...") + users, err := partner.ListPartnerPortalUsers(ctx, nil) + if err != nil { + fmt.Printf("Error listing users: %v\n", err) + return + } + for _, u := range users.Data.Results { + admin := "" + if u.IsPrimaryAdmin { + admin = " [PRIMARY ADMIN]" + } + fmt.Printf(" - %s (%s)%s\n", u.Email, u.Role, admin) + } + fmt.Println() + + // --- Audit Logs --- + + // Query recent audit logs + fmt.Println("Fetching recent audit logs...") + logs, err := partner.GetPartnerAuditLogs(ctx, &turbodocx.ListAuditLogsRequest{ + Limit: turbodocx.IntPtr(5), + }) + if err != nil { + fmt.Printf("Error fetching audit logs: %v\n", err) + return + } + fmt.Printf("Total log entries: %d (showing first %d)\n", logs.Data.TotalRecords, len(logs.Data.Results)) + for _, entry := range logs.Data.Results { + fmt.Printf(" [%s] %s %s (success: %t)\n", + entry.CreatedOn, entry.Action, entry.ResourceType, entry.Success) + } + + fmt.Println("\nDone!") +} + +func getEnv(key, fallback string) string { + if value := os.Getenv(key); value != "" { + return value + } + return fallback +} diff --git a/packages/go-sdk/examples/turbopartner_basic.go b/packages/go-sdk/examples/turbopartner_basic.go new file mode 100644 index 00000000..7df43769 --- /dev/null +++ b/packages/go-sdk/examples/turbopartner_basic.go @@ -0,0 +1,137 @@ +//go:build ignore +// +build ignore + +// TurboPartner Example: Organization Lifecycle +// +// This example demonstrates the full TurboPartner partner management flow: +// 1. Create an organization with entitlements +// 2. Add a user to the organization +// 3. Create an API key for the organization +// 4. List organizations and users +// 5. Clean up resources +// +// Set environment variables before running: +// export TURBODOCX_PARTNER_API_KEY=TDXP-your-key +// export TURBODOCX_PARTNER_ID=your-partner-uuid + +package main + +import ( + "context" + "fmt" + "os" + + turbodocx "github.com/TurboDocx/SDK/packages/go-sdk" +) + +func main() { + // 1. Configure the partner client + partner, err := turbodocx.NewPartnerClient(turbodocx.PartnerConfig{ + PartnerAPIKey: getEnv("TURBODOCX_PARTNER_API_KEY", "TDXP-your-key-here"), + PartnerID: getEnv("TURBODOCX_PARTNER_ID", "your-partner-uuid"), + }) + if err != nil { + fmt.Printf("Error creating partner client: %v\n", err) + return + } + + ctx := context.Background() + + // 2. Create an organization with entitlements + fmt.Println("Creating organization...") + org, err := partner.CreateOrganization(ctx, &turbodocx.CreateOrganizationRequest{ + Name: "Acme Corporation", + Features: &turbodocx.Features{ + MaxUsers: turbodocx.IntPtr(25), // 25 user seats + MaxStorage: turbodocx.Int64Ptr(5 * 1024 * 1024 * 1024), // 5 GB storage + MaxTemplates: turbodocx.IntPtr(100), // 100 templates + MaxSignatures: turbodocx.IntPtr(500), // 500 signatures/month + HasTDAI: turbodocx.BoolPtr(true), // AI features enabled + HasPptx: turbodocx.BoolPtr(true), // PowerPoint generation + HasFileDownload: turbodocx.BoolPtr(true), // File downloads enabled + }, + }) + if err != nil { + fmt.Printf("Error creating organization: %v\n", err) + return + } + orgID := org.Data.ID + fmt.Printf("Created organization: %s (ID: %s)\n\n", org.Data.Name, orgID) + + // 3. Add a user to the organization + fmt.Println("Adding user to organization...") + user, err := partner.AddUserToOrganization(ctx, orgID, &turbodocx.AddOrgUserRequest{ + Email: "admin@acme.com", + Role: "admin", + }) + if err != nil { + fmt.Printf("Error adding user: %v\n", err) + return + } + fmt.Printf("Added user: %s (Role: %s)\n\n", user.Data.Email, user.Data.Role) + + // 4. Create an API key for the organization + fmt.Println("Creating organization API key...") + apiKey, err := partner.CreateOrganizationApiKey(ctx, orgID, &turbodocx.CreateOrgApiKeyRequest{ + Name: "Production Key", + Role: "admin", + }) + if err != nil { + fmt.Printf("Error creating API key: %v\n", err) + return + } + fmt.Printf("Created API key: %s\n", apiKey.Data.Name) + fmt.Printf("Key value: %s\n\n", apiKey.Data.Key) + + // 5. List all organizations + fmt.Println("Listing organizations...") + orgs, err := partner.ListOrganizations(ctx, &turbodocx.ListOrganizationsRequest{ + Limit: turbodocx.IntPtr(10), + }) + if err != nil { + fmt.Printf("Error listing organizations: %v\n", err) + return + } + fmt.Printf("Total organizations: %d\n", orgs.Data.TotalRecords) + for _, o := range orgs.Data.Results { + fmt.Printf(" - %s (ID: %s)\n", o.Name, o.ID) + } + fmt.Println() + + // 6. Get full organization details (includes features + tracking) + fmt.Println("Getting organization details...") + details, err := partner.GetOrganizationDetails(ctx, orgID) + if err != nil { + fmt.Printf("Error getting details: %v\n", err) + return + } + fmt.Printf("Organization: %s\n", details.Data.Name) + if details.Data.Features != nil && details.Data.Features.MaxUsers != nil { + fmt.Printf(" Max Users: %d\n", *details.Data.Features.MaxUsers) + } + if details.Data.Tracking != nil { + fmt.Printf(" Current Users: %d\n", details.Data.Tracking.NumUsers) + } + fmt.Println() + + // 7. List users in the organization + fmt.Println("Listing organization users...") + users, err := partner.ListOrganizationUsers(ctx, orgID, nil) + if err != nil { + fmt.Printf("Error listing users: %v\n", err) + return + } + fmt.Printf("Total users: %d\n", users.Data.TotalRecords) + for _, u := range users.Data.Results { + fmt.Printf(" - %s (%s)\n", u.Email, u.Role) + } + + fmt.Println("\nDone! Organization is fully provisioned.") +} + +func getEnv(key, fallback string) string { + if value := os.Getenv(key); value != "" { + return value + } + return fallback +} diff --git a/packages/go-sdk/http.go b/packages/go-sdk/http.go index a5e06645..bdfb987f 100644 --- a/packages/go-sdk/http.go +++ b/packages/go-sdk/http.go @@ -324,6 +324,57 @@ func (c *HTTPClient) Post(ctx context.Context, path string, data interface{}, re return c.handleResponse(resp, result) } +// Patch performs a PATCH request with JSON body +func (c *HTTPClient) Patch(ctx context.Context, path string, data interface{}, result interface{}) error { + var body io.Reader + if data != nil { + jsonData, err := json.Marshal(data) + if err != nil { + return fmt.Errorf("failed to marshal request body: %w", err) + } + body = bytes.NewReader(jsonData) + } + + req, err := http.NewRequestWithContext(ctx, "PATCH", c.baseURL+path, body) + if err != nil { + return &NetworkError{TurboDocxError: TurboDocxError{ + Message: fmt.Sprintf("failed to create request: %v", err), + }} + } + + c.setHeaders(req, "application/json") + + resp, err := c.client.Do(req) + if err != nil { + return &NetworkError{TurboDocxError: TurboDocxError{ + Message: fmt.Sprintf("request failed: %v", err), + }} + } + + return c.handleResponse(resp, result) +} + +// Delete performs a DELETE request +func (c *HTTPClient) Delete(ctx context.Context, path string, result interface{}) error { + req, err := http.NewRequestWithContext(ctx, "DELETE", c.baseURL+path, nil) + if err != nil { + return &NetworkError{TurboDocxError: TurboDocxError{ + Message: fmt.Sprintf("failed to create request: %v", err), + }} + } + + c.setHeaders(req, "application/json") + + resp, err := c.client.Do(req) + if err != nil { + return &NetworkError{TurboDocxError: TurboDocxError{ + Message: fmt.Sprintf("request failed: %v", err), + }} + } + + return c.handleResponse(resp, result) +} + // UploadFile performs a multipart file upload // file can be either a file path (string) or file content ([]byte) func (c *HTTPClient) UploadFile(ctx context.Context, path string, file interface{}, fileName string, additionalData map[string]string, result interface{}) error { diff --git a/packages/go-sdk/turbopartner.go b/packages/go-sdk/turbopartner.go new file mode 100644 index 00000000..b9ef7f4f --- /dev/null +++ b/packages/go-sdk/turbopartner.go @@ -0,0 +1,879 @@ +package turbodocx + +import ( + "context" + "fmt" + "net/url" + "os" + "strconv" +) + +// PartnerConfig holds configuration for the TurboPartner client +type PartnerConfig struct { + // PartnerAPIKey is your partner API key (required, must start with TDXP-) + PartnerAPIKey string + + // PartnerID is your partner UUID (required) + PartnerID string + + // BaseURL is the API base URL (optional, default: https://api.turbodocx.com) + BaseURL string +} + +// PartnerClient provides TurboPartner partner management operations +type PartnerClient struct { + http *HTTPClient + partnerID string +} + +// NewPartnerClient creates a new TurboPartner client with the given config +func NewPartnerClient(config PartnerConfig) (*PartnerClient, error) { + if config.PartnerAPIKey == "" { + config.PartnerAPIKey = os.Getenv("TURBODOCX_PARTNER_API_KEY") + } + if config.PartnerID == "" { + config.PartnerID = os.Getenv("TURBODOCX_PARTNER_ID") + } + if config.BaseURL == "" { + config.BaseURL = os.Getenv("TURBODOCX_BASE_URL") + } + if config.BaseURL == "" { + config.BaseURL = "https://api.turbodocx.com" + } + + if config.PartnerAPIKey == "" { + return nil, &AuthenticationError{TurboDocxError: TurboDocxError{ + Message: "Partner API key is required. Set PartnerAPIKey in config or TURBODOCX_PARTNER_API_KEY environment variable.", + StatusCode: 401, + }} + } + if config.PartnerID == "" { + return nil, &AuthenticationError{TurboDocxError: TurboDocxError{ + Message: "Partner ID is required. Set PartnerID in config or TURBODOCX_PARTNER_ID environment variable.", + StatusCode: 401, + }} + } + + httpClient := NewHTTPClient(ClientConfig{ + APIKey: config.PartnerAPIKey, + BaseURL: config.BaseURL, + }) + + return &PartnerClient{ + http: httpClient, + partnerID: config.PartnerID, + }, nil +} + +func (c *PartnerClient) basePath() string { + return "/partner/" + c.partnerID +} + +// --- Query param helpers --- + +func buildQuery(params url.Values) string { + encoded := params.Encode() + if encoded == "" { + return "" + } + return "?" + encoded +} + +func addPaginationParams(q url.Values, limit, offset *int, search string) { + if limit != nil { + q.Set("limit", strconv.Itoa(*limit)) + } + if offset != nil { + q.Set("offset", strconv.Itoa(*offset)) + } + if search != "" { + q.Set("search", search) + } +} + +// ============================================= +// Domain Types +// ============================================= + +// Organization represents a partner organization +type Organization struct { + ID string `json:"id"` + Name string `json:"name"` + PartnerID string `json:"partnerId,omitempty"` + CreatedOn string `json:"createdOn,omitempty"` + UpdatedOn string `json:"updatedOn,omitempty"` + CreatedBy string `json:"createdBy,omitempty"` + IsActive bool `json:"isActive,omitempty"` + UserCount int `json:"userCount,omitempty"` + StorageUsed int64 `json:"storageUsed,omitempty"` + Metadata map[string]interface{} `json:"metadata,omitempty"` +} + +// OrganizationUser represents a user in an organization +type OrganizationUser struct { + ID string `json:"id"` + Email string `json:"email"` + FirstName string `json:"firstName,omitempty"` + LastName string `json:"lastName,omitempty"` + SsoID string `json:"ssoId,omitempty"` + Role string `json:"role,omitempty"` + CreatedOn string `json:"createdOn,omitempty"` + IsActive bool `json:"isActive,omitempty"` +} + +// OrgApiKey represents an organization API key +type OrgApiKey struct { + ID string `json:"id"` + Name string `json:"name"` + Key string `json:"key,omitempty"` + Role string `json:"role,omitempty"` + Scopes []string `json:"scopes,omitempty"` + CreatedOn string `json:"createdOn,omitempty"` + CreatedBy string `json:"createdBy,omitempty"` + LastUsedOn string `json:"lastUsedOn,omitempty"` + LastUsedIP string `json:"lastUsedIP,omitempty"` + UpdatedOn string `json:"updatedOn,omitempty"` +} + +// PartnerApiKey represents a partner-level API key +type PartnerApiKey struct { + ID string `json:"id"` + Name string `json:"name"` + Key string `json:"key,omitempty"` + Description string `json:"description,omitempty"` + Scopes []string `json:"scopes,omitempty"` + CreatedOn string `json:"createdOn,omitempty"` + CreatedBy string `json:"createdBy,omitempty"` + LastUsedOn string `json:"lastUsedOn,omitempty"` + LastUsedIP string `json:"lastUsedIP,omitempty"` + UpdatedOn string `json:"updatedOn,omitempty"` +} + +// PartnerUser represents a user in the partner portal +type PartnerUser struct { + ID string `json:"id"` + Email string `json:"email"` + FirstName string `json:"firstName,omitempty"` + LastName string `json:"lastName,omitempty"` + SsoID string `json:"ssoId,omitempty"` + Role string `json:"role,omitempty"` + Permissions *PartnerPermissions `json:"permissions,omitempty"` + IsPrimaryAdmin bool `json:"isPrimaryAdmin,omitempty"` + CreatedOn string `json:"createdOn,omitempty"` + IsActive bool `json:"isActive,omitempty"` +} + +// PartnerPermissions represents the permissions for a partner portal user +type PartnerPermissions struct { + CanManageOrgs bool `json:"canManageOrgs"` + CanManageOrgUsers bool `json:"canManageOrgUsers"` + CanManagePartnerUsers bool `json:"canManagePartnerUsers"` + CanManageOrgAPIKeys bool `json:"canManageOrgAPIKeys"` + CanManagePartnerAPIKeys bool `json:"canManagePartnerAPIKeys"` + CanUpdateEntitlements bool `json:"canUpdateEntitlements"` + CanViewAuditLogs bool `json:"canViewAuditLogs"` +} + +// Features represents settable entitlement limits for an organization +type Features struct { + OrgID string `json:"orgId,omitempty"` + MaxUsers *int `json:"maxUsers,omitempty"` + MaxProjectspaces *int `json:"maxProjectspaces,omitempty"` + MaxTemplates *int `json:"maxTemplates,omitempty"` + MaxStorage *int64 `json:"maxStorage,omitempty"` + MaxGeneratedDeliverables *int `json:"maxGeneratedDeliverables,omitempty"` + MaxSignatures *int `json:"maxSignatures,omitempty"` + MaxAICredits *int `json:"maxAICredits,omitempty"` + RdWatermark *bool `json:"rdWatermark,omitempty"` + HasFileDownload *bool `json:"hasFileDownload,omitempty"` + HasAdvancedDateFormats *bool `json:"hasAdvancedDateFormats,omitempty"` + HasGDrive *bool `json:"hasGDrive,omitempty"` + HasSharepoint *bool `json:"hasSharepoint,omitempty"` + HasSharepointOnly *bool `json:"hasSharepointOnly,omitempty"` + HasTDAI *bool `json:"hasTDAI,omitempty"` + HasPptx *bool `json:"hasPptx,omitempty"` + HasTDWriter *bool `json:"hasTDWriter,omitempty"` + HasSalesforce *bool `json:"hasSalesforce,omitempty"` + HasWrike *bool `json:"hasWrike,omitempty"` + HasVariableStack *bool `json:"hasVariableStack,omitempty"` + HasSubvariables *bool `json:"hasSubvariables,omitempty"` + HasZapier *bool `json:"hasZapier,omitempty"` + HasBYOM *bool `json:"hasBYOM,omitempty"` + HasBYOVS *bool `json:"hasBYOVS,omitempty"` + HasBetaFeatures *bool `json:"hasBetaFeatures,omitempty"` + EnableBulkSending *bool `json:"enableBulkSending,omitempty"` + CreatedBy string `json:"createdBy,omitempty"` +} + +// Tracking represents read-only usage counters for an organization +type Tracking struct { + NumUsers int `json:"numUsers,omitempty"` + NumProjectspaces int `json:"numProjectspaces,omitempty"` + NumTemplates int `json:"numTemplates,omitempty"` + StorageUsed int64 `json:"storageUsed,omitempty"` + NumGeneratedDeliverables int `json:"numGeneratedDeliverables,omitempty"` + NumSignaturesUsed int `json:"numSignaturesUsed,omitempty"` + CurrentAICredits int `json:"currentAICredits,omitempty"` +} + +// AuditLogEntry represents a single audit log entry +type AuditLogEntry struct { + ID string `json:"id"` + PartnerID string `json:"partnerId"` + PartnerAPIKeyID string `json:"partnerAPIKeyId,omitempty"` + Action string `json:"action,omitempty"` + ResourceType string `json:"resourceType,omitempty"` + ResourceID string `json:"resourceId,omitempty"` + Details map[string]interface{} `json:"details,omitempty"` + Success bool `json:"success,omitempty"` + IPAddress string `json:"ipAddress,omitempty"` + UserAgent string `json:"userAgent,omitempty"` + CreatedOn string `json:"createdOn,omitempty"` +} + +// ============================================= +// Partner Scope Constants +// ============================================= + +const ( + ScopeOrgCreate = "org:create" + ScopeOrgRead = "org:read" + ScopeOrgUpdate = "org:update" + ScopeOrgDelete = "org:delete" + ScopeEntitlementsUpdate = "entitlements:update" + ScopeOrgUsersCreate = "org-users:create" + ScopeOrgUsersRead = "org-users:read" + ScopeOrgUsersUpdate = "org-users:update" + ScopeOrgUsersDelete = "org-users:delete" + ScopePartnerUsersCreate = "partner-users:create" + ScopePartnerUsersRead = "partner-users:read" + ScopePartnerUsersUpdate = "partner-users:update" + ScopePartnerUsersDelete = "partner-users:delete" + ScopeOrgApikeysCreate = "org-apikeys:create" + ScopeOrgApikeysRead = "org-apikeys:read" + ScopeOrgApikeysUpdate = "org-apikeys:update" + ScopeOrgApikeysDelete = "org-apikeys:delete" + ScopePartnerApikeysCreate = "partner-apikeys:create" + ScopePartnerApikeysRead = "partner-apikeys:read" + ScopePartnerApikeysUpdate = "partner-apikeys:update" + ScopePartnerApikeysDelete = "partner-apikeys:delete" + ScopeAuditRead = "audit:read" +) + +// ============================================= +// Request Types +// ============================================= + +// CreateOrganizationRequest is the request to create an organization +type CreateOrganizationRequest struct { + Name string `json:"name"` + Metadata map[string]interface{} `json:"metadata,omitempty"` + Features *Features `json:"features,omitempty"` +} + +// ListOrganizationsRequest is the request to list organizations +type ListOrganizationsRequest struct { + Limit *int + Offset *int + Search string +} + +// UpdateOrganizationRequest is the request to update an organization +type UpdateOrganizationRequest struct { + Name string `json:"name"` +} + +// UpdateEntitlementsRequest is the request to update organization entitlements +type UpdateEntitlementsRequest struct { + Features *Features `json:"features,omitempty"` + Tracking *Tracking `json:"tracking,omitempty"` +} + +// AddOrgUserRequest is the request to add a user to an organization +type AddOrgUserRequest struct { + Email string `json:"email"` + Role string `json:"role"` +} + +// ListOrgUsersRequest is the request to list organization users +type ListOrgUsersRequest struct { + Limit *int + Offset *int + Search string +} + +// UpdateOrgUserRequest is the request to update an organization user's role +type UpdateOrgUserRequest struct { + Role string `json:"role"` +} + +// CreateOrgApiKeyRequest is the request to create an organization API key +type CreateOrgApiKeyRequest struct { + Name string `json:"name"` + Role string `json:"role"` +} + +// ListOrgApiKeysRequest is the request to list organization API keys +type ListOrgApiKeysRequest struct { + Limit *int + Offset *int + Search string +} + +// UpdateOrgApiKeyRequest is the request to update an organization API key +type UpdateOrgApiKeyRequest struct { + Name string `json:"name,omitempty"` + Role string `json:"role,omitempty"` +} + +// CreatePartnerApiKeyRequest is the request to create a partner API key +type CreatePartnerApiKeyRequest struct { + Name string `json:"name"` + Scopes []string `json:"scopes"` + Description string `json:"description,omitempty"` +} + +// ListPartnerApiKeysRequest is the request to list partner API keys +type ListPartnerApiKeysRequest struct { + Limit *int + Offset *int + Search string +} + +// UpdatePartnerApiKeyRequest is the request to update a partner API key +type UpdatePartnerApiKeyRequest struct { + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + Scopes []string `json:"scopes,omitempty"` +} + +// AddPartnerUserRequest is the request to add a user to the partner portal +type AddPartnerUserRequest struct { + Email string `json:"email"` + Role string `json:"role"` + Permissions PartnerPermissions `json:"permissions"` +} + +// ListPartnerUsersRequest is the request to list partner portal users +type ListPartnerUsersRequest struct { + Limit *int + Offset *int + Search string +} + +// UpdatePartnerUserRequest is the request to update a partner user +type UpdatePartnerUserRequest struct { + Role string `json:"role,omitempty"` + Permissions *PartnerPermissions `json:"permissions,omitempty"` +} + +// ListAuditLogsRequest is the request to list audit logs +type ListAuditLogsRequest struct { + Limit *int + Offset *int + Search string + Action string + ResourceType string + ResourceID string + Success *bool + StartDate string + EndDate string +} + +// ============================================= +// Response Types +// ============================================= + +// SuccessResponse is a generic success response +type SuccessResponse struct { + Success bool `json:"success"` + Message string `json:"message,omitempty"` +} + +// OrganizationResponse is the response for organization create/update +type OrganizationResponse struct { + Success bool `json:"success"` + Data Organization `json:"data"` +} + +// OrganizationListResponse is the response for listing organizations +type OrganizationListResponse struct { + Success bool `json:"success"` + Data struct { + Results []Organization `json:"results"` + TotalRecords int `json:"totalRecords"` + Limit int `json:"limit"` + Offset int `json:"offset"` + } `json:"data"` +} + +// OrganizationDetailResponse is the response for getting organization details +type OrganizationDetailResponse struct { + Success bool `json:"success"` + Data struct { + Organization + Features *Features `json:"features,omitempty"` + Tracking *Tracking `json:"tracking,omitempty"` + } `json:"data"` +} + +// EntitlementsResponse is the response for entitlement updates +type EntitlementsResponse struct { + Success bool `json:"success"` + Data struct { + Features *Features `json:"features,omitempty"` + Tracking *Tracking `json:"tracking,omitempty"` + } `json:"data"` +} + +// OrgUserResponse is the response for organization user operations +type OrgUserResponse struct { + Success bool `json:"success"` + Data OrganizationUser `json:"data"` +} + +// OrgUserListResponse is the response for listing organization users +type OrgUserListResponse struct { + Success bool `json:"success"` + Data struct { + Results []OrganizationUser `json:"results"` + TotalRecords int `json:"totalRecords"` + Limit int `json:"limit"` + Offset int `json:"offset"` + } `json:"data"` + UserLimit map[string]interface{} `json:"userLimit,omitempty"` +} + +// OrgApiKeyResponse is the response for organization API key creation +type OrgApiKeyResponse struct { + Success bool `json:"success"` + Data OrgApiKey `json:"data"` + Message string `json:"message,omitempty"` +} + +// OrgApiKeyUpdateResponse is the response for organization API key updates +type OrgApiKeyUpdateResponse struct { + Success bool `json:"success"` + Message string `json:"message,omitempty"` + ApiKey struct { + ID string `json:"id"` + Name string `json:"name"` + Role string `json:"role,omitempty"` + UpdatedOn string `json:"updatedOn,omitempty"` + } `json:"apiKey"` +} + +// OrgApiKeyListResponse is the response for listing organization API keys +type OrgApiKeyListResponse struct { + Success bool `json:"success"` + Data struct { + Results []OrgApiKey `json:"results"` + TotalRecords int `json:"totalRecords"` + Limit int `json:"limit"` + Offset int `json:"offset"` + } `json:"data"` +} + +// PartnerApiKeyResponse is the response for partner API key creation +type PartnerApiKeyResponse struct { + Success bool `json:"success"` + Data PartnerApiKey `json:"data"` + Message string `json:"message,omitempty"` +} + +// PartnerApiKeyUpdateResponse is the response for partner API key updates +type PartnerApiKeyUpdateResponse struct { + Success bool `json:"success"` + Message string `json:"message,omitempty"` + ApiKey struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + Scopes []string `json:"scopes,omitempty"` + UpdatedOn string `json:"updatedOn,omitempty"` + } `json:"apiKey"` +} + +// PartnerApiKeyListResponse is the response for listing partner API keys +type PartnerApiKeyListResponse struct { + Success bool `json:"success"` + Data struct { + Results []PartnerApiKey `json:"results"` + TotalRecords int `json:"totalRecords"` + Limit int `json:"limit"` + Offset int `json:"offset"` + } `json:"data"` +} + +// PartnerUserResponse is the response for partner user operations +type PartnerUserResponse struct { + Success bool `json:"success"` + Data PartnerUser `json:"data"` +} + +// PartnerUserUpdateResponse is the response for partner user updates +type PartnerUserUpdateResponse struct { + Success bool `json:"success"` + Data struct { + UserID string `json:"userId"` + Role string `json:"role"` + Permissions PartnerPermissions `json:"permissions"` + } `json:"data"` +} + +// PartnerUserListResponse is the response for listing partner users +type PartnerUserListResponse struct { + Success bool `json:"success"` + Data struct { + Results []PartnerUser `json:"results"` + TotalRecords int `json:"totalRecords"` + Limit int `json:"limit"` + Offset int `json:"offset"` + } `json:"data"` +} + +// AuditLogListResponse is the response for listing audit logs +type AuditLogListResponse struct { + Success bool `json:"success"` + Data struct { + Results []AuditLogEntry `json:"results"` + TotalRecords int `json:"totalRecords"` + Limit int `json:"limit"` + Offset int `json:"offset"` + } `json:"data"` +} + +// ============================================= +// Organization Management +// ============================================= + +// CreateOrganization creates a new organization under the partner account +func (c *PartnerClient) CreateOrganization(ctx context.Context, req *CreateOrganizationRequest) (*OrganizationResponse, error) { + var response OrganizationResponse + err := c.http.Post(ctx, c.basePath()+"/organization", req, &response) + if err != nil { + return nil, err + } + return &response, nil +} + +// ListOrganizations lists all organizations with optional pagination and search +func (c *PartnerClient) ListOrganizations(ctx context.Context, req *ListOrganizationsRequest) (*OrganizationListResponse, error) { + q := url.Values{} + if req != nil { + addPaginationParams(q, req.Limit, req.Offset, req.Search) + } + + var response OrganizationListResponse + err := c.http.Get(ctx, c.basePath()+"/organizations"+buildQuery(q), &response) + if err != nil { + return nil, err + } + return &response, nil +} + +// GetOrganizationDetails gets full details for an organization including features and tracking +func (c *PartnerClient) GetOrganizationDetails(ctx context.Context, organizationID string) (*OrganizationDetailResponse, error) { + var response OrganizationDetailResponse + err := c.http.Get(ctx, c.basePath()+"/organizations/"+organizationID, &response) + if err != nil { + return nil, err + } + return &response, nil +} + +// UpdateOrganizationInfo updates an organization's name +func (c *PartnerClient) UpdateOrganizationInfo(ctx context.Context, organizationID string, req *UpdateOrganizationRequest) (*OrganizationResponse, error) { + var response OrganizationResponse + err := c.http.Patch(ctx, c.basePath()+"/organizations/"+organizationID, req, &response) + if err != nil { + return nil, err + } + return &response, nil +} + +// DeleteOrganization soft-deletes an organization +func (c *PartnerClient) DeleteOrganization(ctx context.Context, organizationID string) (*SuccessResponse, error) { + var response SuccessResponse + err := c.http.Delete(ctx, c.basePath()+"/organizations/"+organizationID, &response) + if err != nil { + return nil, err + } + return &response, nil +} + +// UpdateOrganizationEntitlements updates an organization's feature limits and capabilities +func (c *PartnerClient) UpdateOrganizationEntitlements(ctx context.Context, organizationID string, req *UpdateEntitlementsRequest) (*EntitlementsResponse, error) { + var response EntitlementsResponse + err := c.http.Patch(ctx, c.basePath()+"/organizations/"+organizationID+"/entitlements", req, &response) + if err != nil { + return nil, err + } + return &response, nil +} + +// ============================================= +// Organization User Management +// ============================================= + +// ListOrganizationUsers lists all users in an organization +func (c *PartnerClient) ListOrganizationUsers(ctx context.Context, organizationID string, req *ListOrgUsersRequest) (*OrgUserListResponse, error) { + q := url.Values{} + if req != nil { + addPaginationParams(q, req.Limit, req.Offset, req.Search) + } + + var response OrgUserListResponse + err := c.http.Get(ctx, c.basePath()+"/organizations/"+organizationID+"/users"+buildQuery(q), &response) + if err != nil { + return nil, err + } + return &response, nil +} + +// AddUserToOrganization adds a user to an organization with a specific role +func (c *PartnerClient) AddUserToOrganization(ctx context.Context, organizationID string, req *AddOrgUserRequest) (*OrgUserResponse, error) { + var response OrgUserResponse + err := c.http.Post(ctx, c.basePath()+"/organizations/"+organizationID+"/users", req, &response) + if err != nil { + return nil, err + } + return &response, nil +} + +// UpdateOrganizationUserRole updates a user's role within an organization +func (c *PartnerClient) UpdateOrganizationUserRole(ctx context.Context, organizationID, userID string, req *UpdateOrgUserRequest) (*OrgUserResponse, error) { + var response OrgUserResponse + err := c.http.Patch(ctx, c.basePath()+"/organizations/"+organizationID+"/users/"+userID, req, &response) + if err != nil { + return nil, err + } + return &response, nil +} + +// RemoveUserFromOrganization removes a user from an organization +func (c *PartnerClient) RemoveUserFromOrganization(ctx context.Context, organizationID, userID string) (*SuccessResponse, error) { + var response SuccessResponse + err := c.http.Delete(ctx, c.basePath()+"/organizations/"+organizationID+"/users/"+userID, &response) + if err != nil { + return nil, err + } + return &response, nil +} + +// ResendOrganizationInvitationToUser resends the invitation email to a pending user +func (c *PartnerClient) ResendOrganizationInvitationToUser(ctx context.Context, organizationID, userID string) (*SuccessResponse, error) { + var response SuccessResponse + err := c.http.Post(ctx, c.basePath()+"/organizations/"+organizationID+"/users/"+userID+"/resend-invitation", nil, &response) + if err != nil { + return nil, err + } + return &response, nil +} + +// ============================================= +// Organization API Key Management +// ============================================= + +// ListOrganizationApiKeys lists all API keys for an organization +func (c *PartnerClient) ListOrganizationApiKeys(ctx context.Context, organizationID string, req *ListOrgApiKeysRequest) (*OrgApiKeyListResponse, error) { + q := url.Values{} + if req != nil { + addPaginationParams(q, req.Limit, req.Offset, req.Search) + } + + var response OrgApiKeyListResponse + err := c.http.Get(ctx, c.basePath()+"/organizations/"+organizationID+"/apikeys"+buildQuery(q), &response) + if err != nil { + return nil, err + } + return &response, nil +} + +// CreateOrganizationApiKey creates an API key for an organization +func (c *PartnerClient) CreateOrganizationApiKey(ctx context.Context, organizationID string, req *CreateOrgApiKeyRequest) (*OrgApiKeyResponse, error) { + var response OrgApiKeyResponse + err := c.http.Post(ctx, c.basePath()+"/organizations/"+organizationID+"/apikeys", req, &response) + if err != nil { + return nil, err + } + return &response, nil +} + +// UpdateOrganizationApiKey updates an organization API key +func (c *PartnerClient) UpdateOrganizationApiKey(ctx context.Context, organizationID, apiKeyID string, req *UpdateOrgApiKeyRequest) (*OrgApiKeyUpdateResponse, error) { + var response OrgApiKeyUpdateResponse + err := c.http.Patch(ctx, c.basePath()+"/organizations/"+organizationID+"/apikeys/"+apiKeyID, req, &response) + if err != nil { + return nil, err + } + return &response, nil +} + +// RevokeOrganizationApiKey revokes an organization API key +func (c *PartnerClient) RevokeOrganizationApiKey(ctx context.Context, organizationID, apiKeyID string) (*SuccessResponse, error) { + var response SuccessResponse + err := c.http.Delete(ctx, c.basePath()+"/organizations/"+organizationID+"/apikeys/"+apiKeyID, &response) + if err != nil { + return nil, err + } + return &response, nil +} + +// ============================================= +// Partner API Key Management +// ============================================= + +// ListPartnerApiKeys lists all partner API keys +func (c *PartnerClient) ListPartnerApiKeys(ctx context.Context, req *ListPartnerApiKeysRequest) (*PartnerApiKeyListResponse, error) { + q := url.Values{} + if req != nil { + addPaginationParams(q, req.Limit, req.Offset, req.Search) + } + + var response PartnerApiKeyListResponse + err := c.http.Get(ctx, c.basePath()+"/api-keys"+buildQuery(q), &response) + if err != nil { + return nil, err + } + return &response, nil +} + +// CreatePartnerApiKey creates a new partner-level API key with specific scopes +func (c *PartnerClient) CreatePartnerApiKey(ctx context.Context, req *CreatePartnerApiKeyRequest) (*PartnerApiKeyResponse, error) { + var response PartnerApiKeyResponse + err := c.http.Post(ctx, c.basePath()+"/api-keys", req, &response) + if err != nil { + return nil, err + } + return &response, nil +} + +// UpdatePartnerApiKey updates a partner API key +func (c *PartnerClient) UpdatePartnerApiKey(ctx context.Context, keyID string, req *UpdatePartnerApiKeyRequest) (*PartnerApiKeyUpdateResponse, error) { + var response PartnerApiKeyUpdateResponse + err := c.http.Patch(ctx, c.basePath()+"/api-keys/"+keyID, req, &response) + if err != nil { + return nil, err + } + return &response, nil +} + +// RevokePartnerApiKey revokes a partner API key +func (c *PartnerClient) RevokePartnerApiKey(ctx context.Context, keyID string) (*SuccessResponse, error) { + var response SuccessResponse + err := c.http.Delete(ctx, c.basePath()+"/api-keys/"+keyID, &response) + if err != nil { + return nil, err + } + return &response, nil +} + +// ============================================= +// Partner User Management +// ============================================= + +// ListPartnerPortalUsers lists all partner portal users +func (c *PartnerClient) ListPartnerPortalUsers(ctx context.Context, req *ListPartnerUsersRequest) (*PartnerUserListResponse, error) { + q := url.Values{} + if req != nil { + addPaginationParams(q, req.Limit, req.Offset, req.Search) + } + + var response PartnerUserListResponse + err := c.http.Get(ctx, c.basePath()+"/users"+buildQuery(q), &response) + if err != nil { + return nil, err + } + return &response, nil +} + +// AddUserToPartnerPortal adds a user to the partner portal with specific permissions +func (c *PartnerClient) AddUserToPartnerPortal(ctx context.Context, req *AddPartnerUserRequest) (*PartnerUserResponse, error) { + var response PartnerUserResponse + err := c.http.Post(ctx, c.basePath()+"/users", req, &response) + if err != nil { + return nil, err + } + return &response, nil +} + +// UpdatePartnerUserPermissions updates a partner user's role and permissions +func (c *PartnerClient) UpdatePartnerUserPermissions(ctx context.Context, userID string, req *UpdatePartnerUserRequest) (*PartnerUserUpdateResponse, error) { + var response PartnerUserUpdateResponse + err := c.http.Patch(ctx, c.basePath()+"/users/"+userID, req, &response) + if err != nil { + return nil, err + } + return &response, nil +} + +// RemoveUserFromPartnerPortal removes a user from the partner portal +func (c *PartnerClient) RemoveUserFromPartnerPortal(ctx context.Context, userID string) (*SuccessResponse, error) { + var response SuccessResponse + err := c.http.Delete(ctx, c.basePath()+"/users/"+userID, &response) + if err != nil { + return nil, err + } + return &response, nil +} + +// ResendPartnerPortalInvitationToUser resends the invitation email to a pending partner user +func (c *PartnerClient) ResendPartnerPortalInvitationToUser(ctx context.Context, userID string) (*SuccessResponse, error) { + var response SuccessResponse + err := c.http.Post(ctx, c.basePath()+"/users/"+userID+"/resend-invitation", nil, &response) + if err != nil { + return nil, err + } + return &response, nil +} + +// ============================================= +// Audit Logs +// ============================================= + +// GetPartnerAuditLogs gets audit logs for all partner activities with filtering +func (c *PartnerClient) GetPartnerAuditLogs(ctx context.Context, req *ListAuditLogsRequest) (*AuditLogListResponse, error) { + q := url.Values{} + if req != nil { + addPaginationParams(q, req.Limit, req.Offset, req.Search) + if req.Action != "" { + q.Set("action", req.Action) + } + if req.ResourceType != "" { + q.Set("resourceType", req.ResourceType) + } + if req.ResourceID != "" { + q.Set("resourceId", req.ResourceID) + } + if req.Success != nil { + q.Set("success", fmt.Sprintf("%t", *req.Success)) + } + if req.StartDate != "" { + q.Set("startDate", req.StartDate) + } + if req.EndDate != "" { + q.Set("endDate", req.EndDate) + } + } + + var response AuditLogListResponse + err := c.http.Get(ctx, c.basePath()+"/audit-logs"+buildQuery(q), &response) + if err != nil { + return nil, err + } + return &response, nil +} + +// ============================================= +// Convenience Helpers +// ============================================= + +// IntPtr returns a pointer to the given int value (helper for optional int fields) +func IntPtr(v int) *int { return &v } + +// Int64Ptr returns a pointer to the given int64 value (helper for optional int64 fields) +func Int64Ptr(v int64) *int64 { return &v } + +// BoolPtr returns a pointer to the given bool value (helper for optional bool fields) +func BoolPtr(v bool) *bool { return &v } diff --git a/packages/go-sdk/turbopartner_test.go b/packages/go-sdk/turbopartner_test.go new file mode 100644 index 00000000..821bc89d --- /dev/null +++ b/packages/go-sdk/turbopartner_test.go @@ -0,0 +1,931 @@ +package turbodocx + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func newTestPartnerClient(t *testing.T, serverURL string) *PartnerClient { + t.Helper() + client, err := NewPartnerClient(PartnerConfig{ + PartnerAPIKey: "TDXP-test-key", + PartnerID: "test-partner-id", + BaseURL: serverURL, + }) + require.NoError(t, err) + return client +} + +// ============================================= +// Configuration Tests +// ============================================= + +func TestNewPartnerClient(t *testing.T) { + t.Run("creates client with valid config", func(t *testing.T) { + client, err := NewPartnerClient(PartnerConfig{ + PartnerAPIKey: "TDXP-test-key", + PartnerID: "test-partner-id", + }) + require.NoError(t, err) + assert.NotNil(t, client) + assert.Equal(t, "test-partner-id", client.partnerID) + }) + + t.Run("returns error when partner API key is missing", func(t *testing.T) { + _, err := NewPartnerClient(PartnerConfig{ + PartnerID: "test-partner-id", + }) + require.Error(t, err) + authErr, ok := err.(*AuthenticationError) + require.True(t, ok, "expected AuthenticationError") + assert.Contains(t, authErr.Message, "Partner API key is required") + }) + + t.Run("returns error when partner ID is missing", func(t *testing.T) { + _, err := NewPartnerClient(PartnerConfig{ + PartnerAPIKey: "TDXP-test-key", + }) + require.Error(t, err) + authErr, ok := err.(*AuthenticationError) + require.True(t, ok, "expected AuthenticationError") + assert.Contains(t, authErr.Message, "Partner ID is required") + }) + + t.Run("uses default base URL", func(t *testing.T) { + client, err := NewPartnerClient(PartnerConfig{ + PartnerAPIKey: "TDXP-test-key", + PartnerID: "test-partner-id", + }) + require.NoError(t, err) + assert.Equal(t, "https://api.turbodocx.com", client.http.baseURL) + }) + + t.Run("uses custom base URL", func(t *testing.T) { + client, err := NewPartnerClient(PartnerConfig{ + PartnerAPIKey: "TDXP-test-key", + PartnerID: "test-partner-id", + BaseURL: "https://custom.api.com", + }) + require.NoError(t, err) + assert.Equal(t, "https://custom.api.com", client.http.baseURL) + }) + + t.Run("reads config from environment variables", func(t *testing.T) { + t.Setenv("TURBODOCX_PARTNER_API_KEY", "TDXP-env-key") + t.Setenv("TURBODOCX_PARTNER_ID", "env-partner-id") + + client, err := NewPartnerClient(PartnerConfig{}) + require.NoError(t, err) + assert.Equal(t, "env-partner-id", client.partnerID) + }) +} + +// ============================================= +// Organization Management Tests +// ============================================= + +func TestCreateOrganization(t *testing.T) { + t.Run("creates organization successfully", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + assert.Equal(t, "/partner/test-partner-id/organization", r.URL.Path) + assert.Equal(t, "Bearer TDXP-test-key", r.Header.Get("Authorization")) + + var body CreateOrganizationRequest + json.NewDecoder(r.Body).Decode(&body) + assert.Equal(t, "Acme Corp", body.Name) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "success": true, + "data": map[string]interface{}{ + "id": "org-123", + "name": "Acme Corp", + }, + }) + })) + defer server.Close() + + client := newTestPartnerClient(t, server.URL) + result, err := client.CreateOrganization(context.Background(), &CreateOrganizationRequest{ + Name: "Acme Corp", + }) + + require.NoError(t, err) + assert.True(t, result.Success) + assert.Equal(t, "org-123", result.Data.ID) + assert.Equal(t, "Acme Corp", result.Data.Name) + }) + + t.Run("creates organization with features", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var body CreateOrganizationRequest + json.NewDecoder(r.Body).Decode(&body) + assert.Equal(t, "Acme Corp", body.Name) + assert.NotNil(t, body.Features) + assert.Equal(t, 25, *body.Features.MaxUsers) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "success": true, + "data": map[string]interface{}{"id": "org-123", "name": "Acme Corp"}, + }) + })) + defer server.Close() + + client := newTestPartnerClient(t, server.URL) + result, err := client.CreateOrganization(context.Background(), &CreateOrganizationRequest{ + Name: "Acme Corp", + Features: &Features{MaxUsers: IntPtr(25)}, + }) + + require.NoError(t, err) + assert.Equal(t, "org-123", result.Data.ID) + }) +} + +func TestListOrganizations(t *testing.T) { + t.Run("lists organizations with pagination", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "GET", r.Method) + assert.Equal(t, "/partner/test-partner-id/organizations", r.URL.Path) + assert.Equal(t, "25", r.URL.Query().Get("limit")) + assert.Equal(t, "0", r.URL.Query().Get("offset")) + assert.Equal(t, "Acme", r.URL.Query().Get("search")) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "success": true, + "data": map[string]interface{}{ + "results": []map[string]interface{}{{"id": "org-1", "name": "Acme Corp"}}, + "totalRecords": 1, + "limit": 25, + "offset": 0, + }, + }) + })) + defer server.Close() + + client := newTestPartnerClient(t, server.URL) + result, err := client.ListOrganizations(context.Background(), &ListOrganizationsRequest{ + Limit: IntPtr(25), + Offset: IntPtr(0), + Search: "Acme", + }) + + require.NoError(t, err) + assert.True(t, result.Success) + assert.Equal(t, 1, result.Data.TotalRecords) + assert.Len(t, result.Data.Results, 1) + assert.Equal(t, "Acme Corp", result.Data.Results[0].Name) + }) + + t.Run("lists organizations with nil request", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/partner/test-partner-id/organizations", r.URL.Path) + assert.Empty(t, r.URL.RawQuery) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "success": true, + "data": map[string]interface{}{"results": []interface{}{}, "totalRecords": 0, "limit": 25, "offset": 0}, + }) + })) + defer server.Close() + + client := newTestPartnerClient(t, server.URL) + result, err := client.ListOrganizations(context.Background(), nil) + + require.NoError(t, err) + assert.Equal(t, 0, result.Data.TotalRecords) + }) +} + +func TestGetOrganizationDetails(t *testing.T) { + t.Run("gets organization details with features and tracking", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "GET", r.Method) + assert.Equal(t, "/partner/test-partner-id/organizations/org-123", r.URL.Path) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "success": true, + "data": map[string]interface{}{ + "id": "org-123", + "name": "Acme Corp", + "isActive": true, + "features": map[string]interface{}{"maxUsers": 25, "hasTDAI": true}, + "tracking": map[string]interface{}{"numUsers": 5, "storageUsed": 1024}, + }, + }) + })) + defer server.Close() + + client := newTestPartnerClient(t, server.URL) + result, err := client.GetOrganizationDetails(context.Background(), "org-123") + + require.NoError(t, err) + assert.True(t, result.Success) + assert.Equal(t, "org-123", result.Data.ID) + assert.Equal(t, "Acme Corp", result.Data.Name) + assert.NotNil(t, result.Data.Features) + assert.Equal(t, 25, *result.Data.Features.MaxUsers) + assert.NotNil(t, result.Data.Tracking) + assert.Equal(t, 5, result.Data.Tracking.NumUsers) + }) +} + +func TestUpdateOrganizationInfo(t *testing.T) { + t.Run("updates organization name", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "PATCH", r.Method) + assert.Equal(t, "/partner/test-partner-id/organizations/org-123", r.URL.Path) + + var body UpdateOrganizationRequest + json.NewDecoder(r.Body).Decode(&body) + assert.Equal(t, "Acme Corp Updated", body.Name) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "success": true, + "data": map[string]interface{}{"id": "org-123", "name": "Acme Corp Updated"}, + }) + })) + defer server.Close() + + client := newTestPartnerClient(t, server.URL) + result, err := client.UpdateOrganizationInfo(context.Background(), "org-123", &UpdateOrganizationRequest{ + Name: "Acme Corp Updated", + }) + + require.NoError(t, err) + assert.Equal(t, "Acme Corp Updated", result.Data.Name) + }) +} + +func TestDeleteOrganization(t *testing.T) { + t.Run("deletes organization", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "DELETE", r.Method) + assert.Equal(t, "/partner/test-partner-id/organizations/org-123", r.URL.Path) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{"success": true}) + })) + defer server.Close() + + client := newTestPartnerClient(t, server.URL) + result, err := client.DeleteOrganization(context.Background(), "org-123") + + require.NoError(t, err) + assert.True(t, result.Success) + }) +} + +func TestUpdateOrganizationEntitlements(t *testing.T) { + t.Run("updates entitlements", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "PATCH", r.Method) + assert.Equal(t, "/partner/test-partner-id/organizations/org-123/entitlements", r.URL.Path) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "success": true, + "data": map[string]interface{}{ + "features": map[string]interface{}{"maxUsers": 100, "hasTDAI": true}, + }, + }) + })) + defer server.Close() + + client := newTestPartnerClient(t, server.URL) + result, err := client.UpdateOrganizationEntitlements(context.Background(), "org-123", &UpdateEntitlementsRequest{ + Features: &Features{ + MaxUsers: IntPtr(100), + HasTDAI: BoolPtr(true), + }, + }) + + require.NoError(t, err) + assert.True(t, result.Success) + assert.Equal(t, 100, *result.Data.Features.MaxUsers) + }) +} + +// ============================================= +// Organization User Management Tests +// ============================================= + +func TestAddUserToOrganization(t *testing.T) { + t.Run("adds user to organization", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + assert.Equal(t, "/partner/test-partner-id/organizations/org-123/users", r.URL.Path) + + var body AddOrgUserRequest + json.NewDecoder(r.Body).Decode(&body) + assert.Equal(t, "user@example.com", body.Email) + assert.Equal(t, "admin", body.Role) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "success": true, + "data": map[string]interface{}{"id": "user-123", "email": "user@example.com", "role": "admin"}, + }) + })) + defer server.Close() + + client := newTestPartnerClient(t, server.URL) + result, err := client.AddUserToOrganization(context.Background(), "org-123", &AddOrgUserRequest{ + Email: "user@example.com", + Role: "admin", + }) + + require.NoError(t, err) + assert.Equal(t, "user-123", result.Data.ID) + assert.Equal(t, "user@example.com", result.Data.Email) + }) +} + +func TestListOrganizationUsers(t *testing.T) { + t.Run("lists organization users", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "GET", r.Method) + assert.Equal(t, "/partner/test-partner-id/organizations/org-123/users", r.URL.Path) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "success": true, + "data": map[string]interface{}{ + "results": []map[string]interface{}{{"id": "user-1", "email": "a@b.com", "role": "admin"}}, + "totalRecords": 1, "limit": 50, "offset": 0, + }, + }) + })) + defer server.Close() + + client := newTestPartnerClient(t, server.URL) + result, err := client.ListOrganizationUsers(context.Background(), "org-123", &ListOrgUsersRequest{Limit: IntPtr(50)}) + + require.NoError(t, err) + assert.Equal(t, 1, result.Data.TotalRecords) + assert.Equal(t, "a@b.com", result.Data.Results[0].Email) + }) +} + +func TestUpdateOrganizationUserRole(t *testing.T) { + t.Run("updates user role", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "PATCH", r.Method) + assert.Equal(t, "/partner/test-partner-id/organizations/org-123/users/user-123", r.URL.Path) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "success": true, + "data": map[string]interface{}{"id": "user-123", "email": "a@b.com", "role": "contributor"}, + }) + })) + defer server.Close() + + client := newTestPartnerClient(t, server.URL) + result, err := client.UpdateOrganizationUserRole(context.Background(), "org-123", "user-123", &UpdateOrgUserRequest{Role: "contributor"}) + + require.NoError(t, err) + assert.Equal(t, "contributor", result.Data.Role) + }) +} + +func TestRemoveUserFromOrganization(t *testing.T) { + t.Run("removes user from organization", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "DELETE", r.Method) + assert.Equal(t, "/partner/test-partner-id/organizations/org-123/users/user-123", r.URL.Path) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{"success": true}) + })) + defer server.Close() + + client := newTestPartnerClient(t, server.URL) + result, err := client.RemoveUserFromOrganization(context.Background(), "org-123", "user-123") + + require.NoError(t, err) + assert.True(t, result.Success) + }) +} + +func TestResendOrganizationInvitationToUser(t *testing.T) { + t.Run("resends invitation", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + assert.Equal(t, "/partner/test-partner-id/organizations/org-123/users/user-123/resend-invitation", r.URL.Path) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{"success": true}) + })) + defer server.Close() + + client := newTestPartnerClient(t, server.URL) + result, err := client.ResendOrganizationInvitationToUser(context.Background(), "org-123", "user-123") + + require.NoError(t, err) + assert.True(t, result.Success) + }) +} + +// ============================================= +// Organization API Key Management Tests +// ============================================= + +func TestCreateOrganizationApiKey(t *testing.T) { + t.Run("creates organization API key", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + assert.Equal(t, "/partner/test-partner-id/organizations/org-123/apikeys", r.URL.Path) + + var body CreateOrgApiKeyRequest + json.NewDecoder(r.Body).Decode(&body) + assert.Equal(t, "Production Key", body.Name) + assert.Equal(t, "admin", body.Role) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "success": true, + "data": map[string]interface{}{"id": "key-123", "name": "Production Key", "key": "TDX-full-key-value"}, + }) + })) + defer server.Close() + + client := newTestPartnerClient(t, server.URL) + result, err := client.CreateOrganizationApiKey(context.Background(), "org-123", &CreateOrgApiKeyRequest{ + Name: "Production Key", + Role: "admin", + }) + + require.NoError(t, err) + assert.Equal(t, "key-123", result.Data.ID) + assert.Equal(t, "TDX-full-key-value", result.Data.Key) + }) +} + +func TestListOrganizationApiKeys(t *testing.T) { + t.Run("lists organization API keys", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "GET", r.Method) + assert.Equal(t, "/partner/test-partner-id/organizations/org-123/apikeys", r.URL.Path) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "success": true, + "data": map[string]interface{}{ + "results": []map[string]interface{}{{"id": "key-1", "name": "My Key", "role": "admin"}}, + "totalRecords": 1, "limit": 50, "offset": 0, + }, + }) + })) + defer server.Close() + + client := newTestPartnerClient(t, server.URL) + result, err := client.ListOrganizationApiKeys(context.Background(), "org-123", nil) + + require.NoError(t, err) + assert.Equal(t, 1, result.Data.TotalRecords) + }) +} + +func TestUpdateOrganizationApiKey(t *testing.T) { + t.Run("updates organization API key", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "PATCH", r.Method) + assert.Equal(t, "/partner/test-partner-id/organizations/org-123/apikeys/key-123", r.URL.Path) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "success": true, + "apiKey": map[string]interface{}{"id": "key-123", "name": "Updated Key"}, + }) + })) + defer server.Close() + + client := newTestPartnerClient(t, server.URL) + result, err := client.UpdateOrganizationApiKey(context.Background(), "org-123", "key-123", &UpdateOrgApiKeyRequest{ + Name: "Updated Key", + }) + + require.NoError(t, err) + assert.Equal(t, "key-123", result.ApiKey.ID) + assert.Equal(t, "Updated Key", result.ApiKey.Name) + }) +} + +func TestRevokeOrganizationApiKey(t *testing.T) { + t.Run("revokes organization API key", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "DELETE", r.Method) + assert.Equal(t, "/partner/test-partner-id/organizations/org-123/apikeys/key-123", r.URL.Path) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{"success": true}) + })) + defer server.Close() + + client := newTestPartnerClient(t, server.URL) + result, err := client.RevokeOrganizationApiKey(context.Background(), "org-123", "key-123") + + require.NoError(t, err) + assert.True(t, result.Success) + }) +} + +// ============================================= +// Partner API Key Management Tests +// ============================================= + +func TestCreatePartnerApiKey(t *testing.T) { + t.Run("creates partner API key with scopes", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + assert.Equal(t, "/partner/test-partner-id/api-keys", r.URL.Path) + + var body CreatePartnerApiKeyRequest + json.NewDecoder(r.Body).Decode(&body) + assert.Equal(t, "Integration Key", body.Name) + assert.Contains(t, body.Scopes, ScopeOrgCreate) + assert.Contains(t, body.Scopes, ScopeOrgRead) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "success": true, + "data": map[string]interface{}{"id": "pkey-123", "name": "Integration Key", "key": "TDXP-full-key"}, + }) + })) + defer server.Close() + + client := newTestPartnerClient(t, server.URL) + result, err := client.CreatePartnerApiKey(context.Background(), &CreatePartnerApiKeyRequest{ + Name: "Integration Key", + Scopes: []string{ScopeOrgCreate, ScopeOrgRead, ScopeAuditRead}, + }) + + require.NoError(t, err) + assert.Equal(t, "pkey-123", result.Data.ID) + assert.Equal(t, "TDXP-full-key", result.Data.Key) + }) +} + +func TestListPartnerApiKeys(t *testing.T) { + t.Run("lists partner API keys", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "GET", r.Method) + assert.Equal(t, "/partner/test-partner-id/api-keys", r.URL.Path) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "success": true, + "data": map[string]interface{}{ + "results": []map[string]interface{}{{"id": "pkey-1", "name": "Key 1"}}, + "totalRecords": 1, "limit": 50, "offset": 0, + }, + }) + })) + defer server.Close() + + client := newTestPartnerClient(t, server.URL) + result, err := client.ListPartnerApiKeys(context.Background(), nil) + + require.NoError(t, err) + assert.Equal(t, 1, result.Data.TotalRecords) + }) +} + +func TestUpdatePartnerApiKey(t *testing.T) { + t.Run("updates partner API key", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "PATCH", r.Method) + assert.Equal(t, "/partner/test-partner-id/api-keys/pkey-123", r.URL.Path) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "success": true, + "apiKey": map[string]interface{}{"id": "pkey-123", "name": "Updated Key", "description": "Updated"}, + }) + })) + defer server.Close() + + client := newTestPartnerClient(t, server.URL) + result, err := client.UpdatePartnerApiKey(context.Background(), "pkey-123", &UpdatePartnerApiKeyRequest{ + Name: "Updated Key", + Description: "Updated", + }) + + require.NoError(t, err) + assert.Equal(t, "pkey-123", result.ApiKey.ID) + }) +} + +func TestRevokePartnerApiKey(t *testing.T) { + t.Run("revokes partner API key", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "DELETE", r.Method) + assert.Equal(t, "/partner/test-partner-id/api-keys/pkey-123", r.URL.Path) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{"success": true}) + })) + defer server.Close() + + client := newTestPartnerClient(t, server.URL) + result, err := client.RevokePartnerApiKey(context.Background(), "pkey-123") + + require.NoError(t, err) + assert.True(t, result.Success) + }) +} + +// ============================================= +// Partner User Management Tests +// ============================================= + +func TestAddUserToPartnerPortal(t *testing.T) { + t.Run("adds user to partner portal", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + assert.Equal(t, "/partner/test-partner-id/users", r.URL.Path) + + var body AddPartnerUserRequest + json.NewDecoder(r.Body).Decode(&body) + assert.Equal(t, "admin@partner.com", body.Email) + assert.Equal(t, "admin", body.Role) + assert.True(t, body.Permissions.CanManageOrgs) + assert.False(t, body.Permissions.CanManagePartnerUsers) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "success": true, + "data": map[string]interface{}{"id": "puser-123", "email": "admin@partner.com", "role": "admin"}, + }) + })) + defer server.Close() + + client := newTestPartnerClient(t, server.URL) + result, err := client.AddUserToPartnerPortal(context.Background(), &AddPartnerUserRequest{ + Email: "admin@partner.com", + Role: "admin", + Permissions: PartnerPermissions{ + CanManageOrgs: true, + CanManageOrgUsers: true, + CanManagePartnerUsers: false, + CanManageOrgAPIKeys: true, + CanManagePartnerAPIKeys: false, + CanUpdateEntitlements: true, + CanViewAuditLogs: true, + }, + }) + + require.NoError(t, err) + assert.Equal(t, "puser-123", result.Data.ID) + }) +} + +func TestListPartnerPortalUsers(t *testing.T) { + t.Run("lists partner portal users", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "GET", r.Method) + assert.Equal(t, "/partner/test-partner-id/users", r.URL.Path) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "success": true, + "data": map[string]interface{}{ + "results": []map[string]interface{}{{"id": "puser-1", "email": "a@b.com", "role": "admin"}}, + "totalRecords": 1, "limit": 50, "offset": 0, + }, + }) + })) + defer server.Close() + + client := newTestPartnerClient(t, server.URL) + result, err := client.ListPartnerPortalUsers(context.Background(), nil) + + require.NoError(t, err) + assert.Equal(t, 1, result.Data.TotalRecords) + }) +} + +func TestUpdatePartnerUserPermissions(t *testing.T) { + t.Run("updates partner user permissions", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "PATCH", r.Method) + assert.Equal(t, "/partner/test-partner-id/users/puser-123", r.URL.Path) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "success": true, + "data": map[string]interface{}{ + "userId": "puser-123", + "role": "admin", + "permissions": map[string]interface{}{ + "canManageOrgs": true, "canManageOrgUsers": true, + "canManagePartnerUsers": true, "canManageOrgAPIKeys": true, + "canManagePartnerAPIKeys": true, "canUpdateEntitlements": true, + "canViewAuditLogs": true, + }, + }, + }) + })) + defer server.Close() + + client := newTestPartnerClient(t, server.URL) + result, err := client.UpdatePartnerUserPermissions(context.Background(), "puser-123", &UpdatePartnerUserRequest{ + Role: "admin", + Permissions: &PartnerPermissions{ + CanManageOrgs: true, + CanManageOrgUsers: true, + CanManagePartnerUsers: true, + CanManageOrgAPIKeys: true, + CanManagePartnerAPIKeys: true, + CanUpdateEntitlements: true, + CanViewAuditLogs: true, + }, + }) + + require.NoError(t, err) + assert.Equal(t, "puser-123", result.Data.UserID) + assert.Equal(t, "admin", result.Data.Role) + assert.True(t, result.Data.Permissions.CanManageOrgs) + }) +} + +func TestRemoveUserFromPartnerPortal(t *testing.T) { + t.Run("removes user from partner portal", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "DELETE", r.Method) + assert.Equal(t, "/partner/test-partner-id/users/puser-123", r.URL.Path) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{"success": true}) + })) + defer server.Close() + + client := newTestPartnerClient(t, server.URL) + result, err := client.RemoveUserFromPartnerPortal(context.Background(), "puser-123") + + require.NoError(t, err) + assert.True(t, result.Success) + }) +} + +func TestResendPartnerPortalInvitationToUser(t *testing.T) { + t.Run("resends partner portal invitation", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + assert.Equal(t, "/partner/test-partner-id/users/puser-123/resend-invitation", r.URL.Path) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{"success": true}) + })) + defer server.Close() + + client := newTestPartnerClient(t, server.URL) + result, err := client.ResendPartnerPortalInvitationToUser(context.Background(), "puser-123") + + require.NoError(t, err) + assert.True(t, result.Success) + }) +} + +// ============================================= +// Audit Log Tests +// ============================================= + +func TestGetPartnerAuditLogs(t *testing.T) { + t.Run("gets audit logs with filters", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "GET", r.Method) + assert.Equal(t, "/partner/test-partner-id/audit-logs", r.URL.Path) + assert.Equal(t, "org.created", r.URL.Query().Get("action")) + assert.Equal(t, "organization", r.URL.Query().Get("resourceType")) + assert.Equal(t, "true", r.URL.Query().Get("success")) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "success": true, + "data": map[string]interface{}{ + "results": []map[string]interface{}{ + {"id": "log-1", "action": "org.created", "resourceType": "organization", "success": true, "createdOn": "2024-01-01"}, + }, + "totalRecords": 1, "limit": 50, "offset": 0, + }, + }) + })) + defer server.Close() + + client := newTestPartnerClient(t, server.URL) + result, err := client.GetPartnerAuditLogs(context.Background(), &ListAuditLogsRequest{ + Limit: IntPtr(50), + Action: "org.created", + ResourceType: "organization", + Success: BoolPtr(true), + }) + + require.NoError(t, err) + assert.Equal(t, 1, result.Data.TotalRecords) + assert.Equal(t, "org.created", result.Data.Results[0].Action) + assert.True(t, result.Data.Results[0].Success) + }) + + t.Run("gets audit logs with nil request", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Empty(t, r.URL.RawQuery) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "success": true, + "data": map[string]interface{}{"results": []interface{}{}, "totalRecords": 0, "limit": 50, "offset": 0}, + }) + })) + defer server.Close() + + client := newTestPartnerClient(t, server.URL) + result, err := client.GetPartnerAuditLogs(context.Background(), nil) + + require.NoError(t, err) + assert.Equal(t, 0, result.Data.TotalRecords) + }) +} + +// ============================================= +// Error Handling Tests +// ============================================= + +func TestPartnerErrorHandling(t *testing.T) { + t.Run("handles 401 authentication error", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(401) + json.NewEncoder(w).Encode(map[string]interface{}{"message": "Invalid API key"}) + })) + defer server.Close() + + client := newTestPartnerClient(t, server.URL) + _, err := client.ListOrganizations(context.Background(), nil) + + require.Error(t, err) + _, ok := err.(*AuthenticationError) + assert.True(t, ok, "expected AuthenticationError") + }) + + t.Run("handles 404 not found error", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(404) + json.NewEncoder(w).Encode(map[string]interface{}{"message": "Organization not found"}) + })) + defer server.Close() + + client := newTestPartnerClient(t, server.URL) + _, err := client.GetOrganizationDetails(context.Background(), "nonexistent") + + require.Error(t, err) + _, ok := err.(*NotFoundError) + assert.True(t, ok, "expected NotFoundError") + }) + + t.Run("handles 400 validation error", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + json.NewEncoder(w).Encode(map[string]interface{}{"message": "Name is required"}) + })) + defer server.Close() + + client := newTestPartnerClient(t, server.URL) + _, err := client.CreateOrganization(context.Background(), &CreateOrganizationRequest{}) + + require.Error(t, err) + _, ok := err.(*ValidationError) + assert.True(t, ok, "expected ValidationError") + }) + + t.Run("handles 429 rate limit error", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(429) + json.NewEncoder(w).Encode(map[string]interface{}{"message": "Rate limit exceeded"}) + })) + defer server.Close() + + client := newTestPartnerClient(t, server.URL) + _, err := client.ListOrganizations(context.Background(), nil) + + require.Error(t, err) + _, ok := err.(*RateLimitError) + assert.True(t, ok, "expected RateLimitError") + }) +}