diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa17052 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Claude AI +.claude/ + +# Coverage +coverage.out + +# Compiled example binaries (go build artifacts) +address_verifications +did_groups +did_history +dids +emergency_calling_services +emergency_requirement_validations +emergency_requirements +emergency_verifications +exports +identities +orders +orders_emergency +shared_capacity_groups +voice_in_trunk_groups +voice_out_trunks +/emergency_scenario +/did_trunk_assignment diff --git a/README.md b/README.md index c88e175..1381a30 100644 --- a/README.md +++ b/README.md @@ -15,9 +15,12 @@ This SDK implements JSON:API serialization and deserialization without external Read more https://doc.didww.com/api This SDK targets DIDWW API v3 documentation version: -[https://doc.didww.com/api3/2022-05-10/index.html](https://doc.didww.com/api3/2022-05-10/index.html) +[https://doc.didww.com/api3/2026-04-16/index.html](https://doc.didww.com/api3/2026-04-16/index.html) -The client sends the `X-DIDWW-API-Version: 2022-05-10` header with each request. +The client sends the `X-DIDWW-API-Version: 2026-04-16` header with each request. + +Version **3.x** targets API version `2026-04-16`. +Version **2.x** (branch `release-2`) targets API version `2022-05-10`. ## Requirements @@ -29,6 +32,11 @@ The client sends the `X-DIDWW-API-Version: 2022-05-10` header with each request. go get github.com/didww/didww-api-3-go-sdk ``` +**Note on module path:** This module intentionally does not use a `/v3` suffix, +following the same convention established in v2. The major version is bumped in +`go.mod` and tagged releases, but the import path stays +`github.com/didww/didww-api-3-go-sdk` for backward compatibility. + ## Usage ```go @@ -161,8 +169,14 @@ proofTypes, _ := client.ProofTypes().List(ctx, nil) // Public Keys publicKeys, _ := client.PublicKeys().List(ctx, nil) -// Requirements -requirements, _ := client.Requirements().List(ctx, nil) +// Address Requirements +requirements, _ := client.AddressRequirements().List(ctx, nil) + +// Emergency Requirements (2026-04-16) +emergReqs, _ := client.EmergencyRequirements().List(ctx, nil) + +// DID History (2026-04-16) +history, _ := client.DIDHistory().List(ctx, nil) // Supporting Document Templates templates, _ := client.SupportingDocumentTemplates().List(ctx, nil) @@ -232,19 +246,32 @@ created, _ := client.VoiceInTrunkGroups().Create(ctx, group) > **Note:** Voice Out Trunks require additional account configuration. Contact DIDWW support to enable. > The `replace_cli` and `randomize_cli` values of `OnCliMismatchAction` also require account configuration. +Voice Out Trunks use a polymorphic `AuthenticationMethod` (2026-04-16). Three types are supported: + +- **`credentials_and_ip`** -- default method; `Username` and `Password` are server-generated and returned in the response. +- **`twilio`** -- requires a `TwilioAccountSid`. +- **`ip_only`** -- read-only; can only be configured by DIDWW staff upon request. Cannot be set via the API. + ```go -import "github.com/didww/didww-api-3-go-sdk/resource/enums" +import ( + "github.com/didww/didww-api-3-go-sdk/resource/authenticationmethod" + "github.com/didww/didww-api-3-go-sdk/resource/enums" +) +// NOTE: 203.0.113.0/24 is RFC 5737 TEST-NET-3 documentation space. +// Replace with the real CIDR of your SIP infrastructure. trunk := &didww.VoiceOutTrunk{ - Name: "My Outbound Trunk", - AllowedSipIPs: []string{"0.0.0.0/0"}, - AllowedRtpIPs: []string{"0.0.0.0/0"}, - DstPrefixes: []string{}, + Name: "My Outbound Trunk", + AuthenticationMethod: &authenticationmethod.CredentialsAndIp{ + AllowedSipIPs: []string{"203.0.113.0/24"}, + }, DefaultDstAction: enums.DefaultDstActionAllowAll, OnCliMismatchAction: enums.OnCliMismatchActionRejectCall, MediaEncryptionMode: enums.MediaEncryptionModeDisabled, } created, _ := client.VoiceOutTrunks().Create(ctx, trunk) +// created.AuthenticationMethod.(*authenticationmethod.CredentialsAndIp).Username -- server-generated +// created.AuthenticationMethod.(*authenticationmethod.CredentialsAndIp).Password -- server-generated ``` ### Orders @@ -335,6 +362,33 @@ verification := &didww.AddressVerification{ created, _ := client.AddressVerifications().Create(ctx, verification) ``` +### Emergency Services (2026-04-16) + +```go +// List emergency requirements filtered by country +params := didww.NewQueryParams().Filter("country.id", "country-uuid") +reqs, _ := client.EmergencyRequirements().List(ctx, params) + +// Create emergency verification +verification := &didww.EmergencyVerification{ + AddressID: "address-uuid", + IdentityID: "identity-uuid", + DIDIDs: []string{"did-uuid"}, +} +created, _ := client.EmergencyVerifications().Create(ctx, verification) + +// List emergency calling services +services, _ := client.EmergencyCallingServices().List(ctx, nil) +``` + +### DID History (2026-04-16) + +```go +// List DID history entries +params := didww.NewQueryParams().Filter("did.id", "did-uuid") +history, _ := client.DIDHistory().List(ctx, params) +``` + ### Exports ```go @@ -342,7 +396,7 @@ import "github.com/didww/didww-api-3-go-sdk/resource/enums" export := &didww.Export{ ExportType: enums.ExportTypeCdrIn, - Filters: map[string]interface{}{"year": 2025, "month": 1}, + Filters: map[string]interface{}{"from": "2026-04-01 00:00:00", "to": "2026-04-16 00:00:00"}, } created, _ := client.Exports().Create(ctx, export) ``` @@ -439,6 +493,7 @@ updated, _ := client.VoiceInTrunks().Update(ctx, trunk) | Available DID | `available_did_order_items` | | Reservation DID | `reservation_did_order_items` | | Capacity | `capacity_order_items` | +| Emergency | `emergency_order_items` | | Generic (response only) | `generic_order_items` | ## Error Handling @@ -478,7 +533,9 @@ if err != nil { | AvailableDID | `client.AvailableDIDs()` | list, find | | ProofType | `client.ProofTypes()` | list, find | | PublicKey | `client.PublicKeys()` | list | -| Requirement | `client.Requirements()` | list, find | +| AddressRequirement | `client.AddressRequirements()` | list, find | +| EmergencyRequirement | `client.EmergencyRequirements()` | list, find | +| DIDHistory | `client.DIDHistory()` | list | | SupportingDocumentTemplate | `client.SupportingDocumentTemplates()` | list, find | | Balance | `client.Balance()` | find | | DID | `client.DIDs()` | list, find, update, delete | @@ -490,14 +547,17 @@ if err != nil { | CapacityPool | `client.CapacityPools()` | list, find, update | | SharedCapacityGroup | `client.SharedCapacityGroups()` | list, find, create, update, delete | | Order | `client.Orders()` | list, find, create, delete | -| Export | `client.Exports()` | list, find, create | +| Export | `client.Exports()` | list, find, create, update | | Address | `client.Addresses()` | list, find, create, update, delete | -| AddressVerification | `client.AddressVerifications()` | list, find, create | +| AddressVerification | `client.AddressVerifications()` | list, find, create, update | +| EmergencyCallingService | `client.EmergencyCallingServices()` | list, find, delete | +| EmergencyVerification | `client.EmergencyVerifications()` | list, find, create, update | +| EmergencyRequirementValidation | `client.EmergencyRequirementValidations()` | create | | Identity | `client.Identities()` | list, find, create, update, delete | | EncryptedFile | `client.EncryptedFiles()` | list, find, delete | | PermanentSupportingDocument | `client.PermanentSupportingDocuments()` | create | | Proof | `client.Proofs()` | create | -| RequirementValidation | `client.RequirementValidations()` | create | +| AddressRequirementValidation | `client.AddressRequirementValidations()` | create | | StockKeepingUnit | include on `DIDGroups` | — | | QtyBasedPricing | include on `CapacityPools` | — | @@ -509,9 +569,22 @@ if err != nil { The SDK distinguishes between date-only and datetime fields: - **Datetime fields** are deserialized as `time.Time` (UTC) when always present, or `*time.Time` when optional (nil if the API omits the value): - - All `CreatedAt` fields — `time.Time`, present on most resources - - Expiry fields — `*time.Time`: `DID.ExpiresAt`, `Proof.ExpiresAt`, `EncryptedFile.ExpireAt`; `DIDReservation.ExpireAt` is `time.Time` (always present) -- **Date-only fields** (`Identity.BirthDate`, `CapacityPool.RenewDate`, order item `BilledFrom`/`BilledTo`) remain as `string` in `"YYYY-MM-DD"` format — Go has no separate date-only type, so the raw string avoids timezone ambiguity. + - `CreatedAt` — `time.Time`, present on most resources + - `ExpiresAt` — `*time.Time`: `DID`, `DIDReservation`, `Proof`, `EncryptedFile` + - `ActivatedAt` — `*time.Time`: `EmergencyCallingService` (nullable) + - `CanceledAt` — `*time.Time`: `EmergencyCallingService` (nullable) +- **Date-only fields** remain as `string` in `"YYYY-MM-DD"` format — Go has no separate date-only type, so the raw string avoids timezone ambiguity: + - `Identity.BirthDate` + - `CapacityPool.RenewDate`, `EmergencyCallingService.RenewDate` (nullable) + - Order item `BilledFrom` / `BilledTo` +- **String fields** (not numeric): + - `EmergencyRequirement.EstimateSetupTime` — e.g. `"7-14 days"`, `"1"` + - `EmergencyRequirement.RequirementRestrictionMessage` — nullable + +**Important changes from previous API versions:** +- `ExpiresAt` replaces `ExpireAt` on `DIDReservation` and `EncryptedFile` +- `RenewDate` is a date-only string, NOT a `time.Time` +- `EstimateSetupTime` is a string, NOT an integer ```go did, _ := client.DIDs().Find(ctx, "uuid") @@ -528,6 +601,7 @@ The SDK provides enum types in `github.com/didww/didww-api-3-go-sdk/resource/enu `CallbackMethod`, `IdentityType`, `OrderStatus`, `ExportType`, `ExportStatus`, `CliFormat`, `OnCliMismatchAction`\*, `MediaEncryptionMode`, `DefaultDstAction`, `VoiceOutTrunkStatus`, +`EmergencyCallingServiceStatus`, `EmergencyVerificationStatus`, `DiversionRelayPolicy`, `TransportProtocol`, `Codec`, `RxDtmfFormat`, `TxDtmfFormat`, `SstRefreshMethod`, `ReroutingDisconnectCode`, `Feature`, `AreaLevel`, `AddressVerificationStatus`, `StirShakenMode` diff --git a/address_verifications_test.go b/address_verifications_test.go index 3ba9cd6..590310d 100644 --- a/address_verifications_test.go +++ b/address_verifications_test.go @@ -29,7 +29,7 @@ func TestAddressVerificationsCreate(t *testing.T) { }) cbURL := "http://example.com" - cbMethod := "GET" + cbMethod := "get" av, err := server.client.AddressVerifications().Create(context.Background(), &resource.AddressVerification{ CallbackURL: &cbURL, CallbackMethod: &cbMethod, @@ -63,6 +63,47 @@ func TestAddressVerificationsFind(t *testing.T) { assert.Nil(t, av.RejectReasons) } +func TestAddressVerificationsUpdateExternalReferenceID(t *testing.T) { + server, capturedBodyPtr := captureRequestBody(t, map[string]testRoute{ + "PATCH /v3/address_verifications/c8e004b0-87ec-4987-b4fb-ee89db099f0e": {status: http.StatusOK, fixture: "address_verifications/update.json"}, + }) + + extRef := "ext-ref-123" + av, err := server.client.AddressVerifications().Update(context.Background(), &resource.AddressVerification{ + ID: "c8e004b0-87ec-4987-b4fb-ee89db099f0e", + ExternalReferenceID: &extRef, + }) + require.NoError(t, err) + + assert.Equal(t, "c8e004b0-87ec-4987-b4fb-ee89db099f0e", av.ID) + require.NotNil(t, av.ExternalReferenceID) + assert.Equal(t, "ext-ref-123", *av.ExternalReferenceID) + + assertRequestJSON(t, *capturedBodyPtr, "address_verifications/update_request.json") +} + +func TestAddressVerificationsUpdateExternalReferenceIDFromLoaded(t *testing.T) { + server, capturedBodyPtr := captureRequestBody(t, map[string]testRoute{ + "GET /v3/address_verifications/c8e004b0-87ec-4987-b4fb-ee89db099f0e": {status: http.StatusOK, fixture: "address_verifications/show.json"}, + "PATCH /v3/address_verifications/c8e004b0-87ec-4987-b4fb-ee89db099f0e": {status: http.StatusOK, fixture: "address_verifications/update.json"}, + }) + + av, err := server.client.AddressVerifications().Find(context.Background(), "c8e004b0-87ec-4987-b4fb-ee89db099f0e") + require.NoError(t, err) + + extRef := "ext-ref-123" + av.ExternalReferenceID = &extRef + + av, err = server.client.AddressVerifications().Update(context.Background(), av) + require.NoError(t, err) + + assert.Equal(t, "c8e004b0-87ec-4987-b4fb-ee89db099f0e", av.ID) + require.NotNil(t, av.ExternalReferenceID) + assert.Equal(t, "ext-ref-123", *av.ExternalReferenceID) + + assertRequestJSON(t, *capturedBodyPtr, "address_verifications/update_request.json") +} + func TestAddressVerificationsFindRejected(t *testing.T) { _, client := newTestServer(t, map[string]testRoute{ "GET /v3/address_verifications/429e6d4e-2ee9-4953-aa98-0b3ac07f0f96": {status: http.StatusOK, fixture: "address_verifications/show_rejected.json"}, diff --git a/client.go b/client.go index 86052d3..6bbeaa5 100644 --- a/client.go +++ b/client.go @@ -171,11 +171,11 @@ func (c *Client) Proofs() *Repository[resource.Proof] { return NewRepository[res func (c *Client) ProofTypes() *Repository[resource.ProofType] { return NewRepository[resource.ProofType](c) } -func (c *Client) Requirements() *Repository[resource.Requirement] { - return NewRepository[resource.Requirement](c) +func (c *Client) AddressRequirements() *Repository[resource.AddressRequirement] { + return NewRepository[resource.AddressRequirement](c) } -func (c *Client) RequirementValidations() *Repository[resource.RequirementValidation] { - return NewRepository[resource.RequirementValidation](c) +func (c *Client) AddressRequirementValidations() *Repository[resource.AddressRequirementValidation] { + return NewRepository[resource.AddressRequirementValidation](c) } func (c *Client) Exports() *Repository[resource.Export] { return NewRepository[resource.Export](c) } func (c *Client) CapacityPools() *Repository[resource.CapacityPool] { @@ -199,6 +199,21 @@ func (c *Client) PermanentSupportingDocuments() *Repository[resource.PermanentSu func (c *Client) NanpaPrefixes() *Repository[resource.NanpaPrefix] { return NewRepository[resource.NanpaPrefix](c) } +func (c *Client) EmergencyCallingServices() *Repository[resource.EmergencyCallingService] { + return NewRepository[resource.EmergencyCallingService](c) +} +func (c *Client) EmergencyVerifications() *Repository[resource.EmergencyVerification] { + return NewRepository[resource.EmergencyVerification](c) +} +func (c *Client) EmergencyRequirementValidations() *Repository[resource.EmergencyRequirementValidation] { + return NewRepository[resource.EmergencyRequirementValidation](c) +} +func (c *Client) EmergencyRequirements() *Repository[resource.EmergencyRequirement] { + return NewRepository[resource.EmergencyRequirement](c) +} +func (c *Client) DIDHistory() *Repository[resource.DIDHistory] { + return NewRepository[resource.DIDHistory](c) +} func (c *Client) VoiceOutTrunkRegenerateCredentials() *Repository[resource.VoiceOutTrunkRegenerateCredential] { return NewRepository[resource.VoiceOutTrunkRegenerateCredential](c) } diff --git a/client_test.go b/client_test.go index a3622fa..2f9d672 100644 --- a/client_test.go +++ b/client_test.go @@ -44,7 +44,7 @@ func TestClientSendsCorrectHeaders(t *testing.T) { assert.Equal(t, "application/vnd.api+json", receivedAccept) assert.Equal(t, "test-api-key", receivedAPIKey) assert.Equal(t, apiVersion, receivedAPIVersion) - assert.Equal(t, "didww-go-sdk/1.0.0", receivedUserAgent) + assert.Equal(t, "didww-go-sdk/3.0.0-dev", receivedUserAgent) } func TestClientHandlesHTTPErrors(t *testing.T) { diff --git a/did_groups_test.go b/did_groups_test.go index 5567dd7..8cd922b 100644 --- a/did_groups_test.go +++ b/did_groups_test.go @@ -60,7 +60,7 @@ func TestDIDGroupsFindWithIncludedRequirement(t *testing.T) { "GET /v3/did_groups/2187c36d-28fb-436f-8861-5a0f5b5a3ee1": {status: http.StatusOK, fixture: "did_groups/show_with_requirement.json"}, }) - params := NewQueryParams().Include("requirement") + params := NewQueryParams().Include("address_requirement") group, err := client.DIDGroups().Find(context.Background(), "2187c36d-28fb-436f-8861-5a0f5b5a3ee1", params) require.NoError(t, err) @@ -70,15 +70,15 @@ func TestDIDGroupsFindWithIncludedRequirement(t *testing.T) { assert.False(t, group.IsMetered) assert.True(t, group.AllowAdditionalChannels) - // Verify included requirement - require.NotNil(t, group.Requirement) - assert.Equal(t, "8da1e0b2-047c-4baf-9c57-57143f09b9ce", group.Requirement.ID) - assert.Equal(t, "Any", group.Requirement.IdentityType) - assert.Equal(t, "WorldWide", group.Requirement.PersonalAreaLevel) - assert.Equal(t, "Country", group.Requirement.BusinessAreaLevel) - assert.Equal(t, "City", group.Requirement.AddressAreaLevel) - assert.Equal(t, 1, group.Requirement.PersonalProofQty) - assert.Equal(t, 1, group.Requirement.BusinessProofQty) - assert.Equal(t, 1, group.Requirement.AddressProofQty) - assert.False(t, group.Requirement.ServiceDescriptionRequired) + // Verify included address_requirement + require.NotNil(t, group.AddressRequirement) + assert.Equal(t, "8da1e0b2-047c-4baf-9c57-57143f09b9ce", group.AddressRequirement.ID) + assert.Equal(t, "any", group.AddressRequirement.IdentityType) + assert.Equal(t, "world_wide", group.AddressRequirement.PersonalAreaLevel) + assert.Equal(t, "country", group.AddressRequirement.BusinessAreaLevel) + assert.Equal(t, "city", group.AddressRequirement.AddressAreaLevel) + assert.Equal(t, 1, group.AddressRequirement.PersonalProofQty) + assert.Equal(t, 1, group.AddressRequirement.BusinessProofQty) + assert.Equal(t, 1, group.AddressRequirement.AddressProofQty) + assert.False(t, group.AddressRequirement.ServiceDescriptionRequired) } diff --git a/did_history_test.go b/did_history_test.go new file mode 100644 index 0000000..db81a0f --- /dev/null +++ b/did_history_test.go @@ -0,0 +1,63 @@ +package didww + +import ( + "context" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDIDHistoryList(t *testing.T) { + _, client := newTestServer(t, map[string]testRoute{ + "GET /v3/did_history": {status: http.StatusOK, fixture: "did_history/index.json"}, + }) + + records, err := client.DIDHistory().List(context.Background(), nil) + require.NoError(t, err) + + require.Len(t, records, 2) + assert.Equal(t, "a1b2c3d4-e5f6-7890-abcd-ef1234567890", records[0].ID) + assert.Equal(t, "12025551234", records[0].DIDNumber) + assert.Equal(t, "assigned", records[0].Action) + assert.Equal(t, "api3", records[0].Method) + assert.Equal(t, "renewed", records[1].Action) + assert.Equal(t, "system", records[1].Method) +} + +func TestDIDHistoryFind(t *testing.T) { + _, client := newTestServer(t, map[string]testRoute{ + "GET /v3/did_history/a1b2c3d4-e5f6-7890-abcd-ef1234567890": {status: http.StatusOK, fixture: "did_history/show.json"}, + }) + + record, err := client.DIDHistory().Find(context.Background(), "a1b2c3d4-e5f6-7890-abcd-ef1234567890") + require.NoError(t, err) + + assert.Equal(t, "a1b2c3d4-e5f6-7890-abcd-ef1234567890", record.ID) + assert.Equal(t, "12025551234", record.DIDNumber) + assert.Equal(t, "assigned", record.Action) + assert.Equal(t, "api3", record.Method) + assert.False(t, record.CreatedAt.IsZero()) + // No meta for non-billing_cycles_count_changed actions + assert.Nil(t, record.Meta) +} + +func TestDIDHistoryFindBillingCyclesCountChanged(t *testing.T) { + _, client := newTestServer(t, map[string]testRoute{ + "GET /v3/did_history/c3d4e5f6-a7b8-9012-cdef-123456789012": {status: http.StatusOK, fixture: "did_history/show_billing_cycles_count_changed.json"}, + }) + + record, err := client.DIDHistory().Find(context.Background(), "c3d4e5f6-a7b8-9012-cdef-123456789012") + require.NoError(t, err) + + assert.Equal(t, "c3d4e5f6-a7b8-9012-cdef-123456789012", record.ID) + assert.Equal(t, "12025551234", record.DIDNumber) + assert.Equal(t, "billing_cycles_count_changed", record.Action) + assert.Equal(t, "system", record.Method) + assert.False(t, record.CreatedAt.IsZero()) + // Meta fields present for billing_cycles_count_changed + require.NotNil(t, record.Meta) + assert.Equal(t, "2", record.Meta["from"]) + assert.Equal(t, "1", record.Meta["to"]) +} diff --git a/dids_test.go b/dids_test.go index 986587f..3ee9369 100644 --- a/dids_test.go +++ b/dids_test.go @@ -60,7 +60,7 @@ func TestDIDsFindWithAddressVerificationAndDIDGroup(t *testing.T) { // Verify address verification require.NotNil(t, did.AddressVerification) assert.Equal(t, "75dc8d39-5e17-4470-a6f3-df42642c975f", did.AddressVerification.ID) - assert.Equal(t, enums.AddressVerificationStatus("Approved"), did.AddressVerification.Status) + assert.Equal(t, enums.AddressVerificationStatusApproved, did.AddressVerification.Status) // Verify DID group require.NotNil(t, did.DIDGroup) @@ -211,6 +211,24 @@ func TestDIDsUpdateAssignTrunkGroup(t *testing.T) { assert.Equal(t, "trunk group sample with 2 trunks", did.VoiceInTrunkGroup.Name) } +func TestDIDsUpdateUnassignEmergencyCallingService(t *testing.T) { + server, capturedBodyPtr := captureRequestBody(t, map[string]testRoute{ + "PATCH /v3/dids/44957076-778a-4802-b60c-d22db0cda284": {status: http.StatusOK, fixture: "dids/unassign_emergency_calling_service.json"}, + }) + + did, err := server.client.DIDs().Update(context.Background(), &resource.DID{ + ID: "44957076-778a-4802-b60c-d22db0cda284", + NullifyEmergencyCallingService: true, + }) + require.NoError(t, err) + + assertRequestJSON(t, *capturedBodyPtr, "dids/unassign_emergency_calling_service_request.json") + + assert.Equal(t, "44957076-778a-4802-b60c-d22db0cda284", did.ID) + assert.False(t, did.EmergencyEnabled) + assert.Nil(t, did.EmergencyCallingService) +} + func TestDIDsFindWithTrunkResolved(t *testing.T) { _, client := newTestServer(t, map[string]testRoute{ "GET /v3/dids/9df99644-f1a5-4a3c-99a4-559d758eb96b": {status: http.StatusOK, fixture: "dids/show_with_trunk.json"}, diff --git a/dirty_serialization_test.go b/dirty_serialization_test.go index 8d9d8bf..4f38b17 100644 --- a/dirty_serialization_test.go +++ b/dirty_serialization_test.go @@ -11,6 +11,7 @@ import ( "github.com/stretchr/testify/require" "github.com/didww/didww-api-3-go-sdk/resource" + "github.com/didww/didww-api-3-go-sdk/resource/authenticationmethod" ) const testDIDID = "9df99644-f1a5-4a3c-99a4-559d758eb96b" @@ -495,6 +496,74 @@ func TestDirtyPatch_LoadedSetSharedCapacityGroupOnlyRelChange(t *testing.T) { assertRelNull(t, doc.Rels, "capacity_pool") } +// TestDirtyPatch_VoiceOutTrunk_ReassignAuthenticationMethod verifies that after loading +// a VoiceOutTrunk from the API, reassigning authentication_method to a different +// polymorphic variant sends only that attribute. +func TestDirtyPatch_VoiceOutTrunk_ReassignAuthenticationMethod(t *testing.T) { + server, capturedBodyPtr := captureRequestBody(t, map[string]testRoute{ + "GET /v3/voice_out_trunks/" + testVoiceOutTrunkID: {status: http.StatusOK, fixture: "voice_out_trunks/show.json"}, + "PATCH /v3/voice_out_trunks/" + testVoiceOutTrunkID: {status: http.StatusOK, fixture: "voice_out_trunks/update.json"}, + }) + + trunk, err := server.client.VoiceOutTrunks().Find(context.Background(), testVoiceOutTrunkID) + require.NoError(t, err) + + // Reassign to a different polymorphic variant + trunk.AuthenticationMethod = &authenticationmethod.CredentialsAndIp{ + AllowedSipIPs: []string{"192.0.2.10/32"}, + TechPrefix: "99", + } + + _, err = server.client.VoiceOutTrunks().Update(context.Background(), trunk) + require.NoError(t, err) + + doc := parsePatchBody(t, *capturedBodyPtr) + + // Only authentication_method should be in attributes + require.Len(t, doc.Attrs, 1) + assert.Contains(t, doc.Attrs, "authentication_method") + + // No relationships should be present + assert.Empty(t, doc.Rels) +} + +// TestDirtyPatch_VoiceOutTrunk_ReplaceEmergencyDIDs verifies that setting +// EmergencyDIDIDs sends the relationship with the specified DID IDs. +func TestDirtyPatch_VoiceOutTrunk_ReplaceEmergencyDIDs(t *testing.T) { + const trunkID = "01234567-89ab-cdef-0123-456789abcdef" + server, capturedBodyPtr := captureRequestBody(t, map[string]testRoute{ + "PATCH /v3/voice_out_trunks/" + trunkID: {status: http.StatusOK, fixture: "voice_out_trunks/update_emergency_dids.json"}, + }) + + _, err := server.client.VoiceOutTrunks().Update(context.Background(), &resource.VoiceOutTrunk{ + ID: trunkID, + EmergencyDIDIDs: []string{ + "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", + "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", + }, + }) + require.NoError(t, err) + + assertRequestJSON(t, *capturedBodyPtr, "voice_out_trunks/update_emergency_dids_request.json") +} + +// TestDirtyPatch_VoiceOutTrunk_ClearEmergencyDIDs verifies that clearing +// emergency_dids sends an empty data array in the relationship. +func TestDirtyPatch_VoiceOutTrunk_ClearEmergencyDIDs(t *testing.T) { + const trunkID = "01234567-89ab-cdef-0123-456789abcdef" + server, capturedBodyPtr := captureRequestBody(t, map[string]testRoute{ + "PATCH /v3/voice_out_trunks/" + trunkID: {status: http.StatusOK, fixture: "voice_out_trunks/update_emergency_dids.json"}, + }) + + _, err := server.client.VoiceOutTrunks().Update(context.Background(), &resource.VoiceOutTrunk{ + ID: trunkID, + ClearEmergencyDIDs: true, + }) + require.NoError(t, err) + + assertRequestJSON(t, *capturedBodyPtr, "voice_out_trunks/update_emergency_dids_clear_request.json") +} + // --- test helpers --- type patchBodyDoc struct { diff --git a/emergency_calling_services_test.go b/emergency_calling_services_test.go new file mode 100644 index 0000000..60f5840 --- /dev/null +++ b/emergency_calling_services_test.go @@ -0,0 +1,66 @@ +package didww + +import ( + "context" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestEmergencyCallingServicesList(t *testing.T) { + _, client := newTestServer(t, map[string]testRoute{ + "GET /v3/emergency_calling_services": {status: http.StatusOK, fixture: "emergency_calling_services/index.json"}, + }) + + services, err := client.EmergencyCallingServices().List(context.Background(), nil) + require.NoError(t, err) + + require.Len(t, services, 1) + assert.Equal(t, "ecs-001-id", services[0].ID) + assert.Equal(t, "E911 Service US", services[0].Name) + assert.Equal(t, "ECS-12345", services[0].Reference) + assert.Equal(t, "active", services[0].Status) + assert.False(t, services[0].CreatedAt.IsZero()) + require.NotNil(t, services[0].ActivatedAt) + assert.Nil(t, services[0].CanceledAt) + assert.Equal(t, "2026-05-01", services[0].RenewDate) + + // Meta fields + require.NotNil(t, services[0].Meta) + assert.Equal(t, "0.0", services[0].Meta["setup_price"]) + assert.Equal(t, "1.5", services[0].Meta["monthly_price"]) +} + +func TestEmergencyCallingServicesFindWithIncludes(t *testing.T) { + _, client := newTestServer(t, map[string]testRoute{ + "GET /v3/emergency_calling_services/ecs-001-id": {status: http.StatusOK, fixture: "emergency_calling_services/show_with_includes.json"}, + }) + + params := NewQueryParams().Include("emergency_requirement,emergency_verification") + svc, err := client.EmergencyCallingServices().Find(context.Background(), "ecs-001-id", params) + require.NoError(t, err) + + assert.Equal(t, "ecs-001-id", svc.ID) + assert.Equal(t, "E911 Service US", svc.Name) + assert.Equal(t, "active", svc.Status) + + // Meta fields + require.NotNil(t, svc.Meta) + assert.Equal(t, "0.0", svc.Meta["setup_price"]) + assert.Equal(t, "1.5", svc.Meta["monthly_price"]) + + // Verify included emergency_requirement + require.NotNil(t, svc.EmergencyRequirement) + assert.Equal(t, "ereq-001-id", svc.EmergencyRequirement.ID) + assert.Equal(t, "personal", svc.EmergencyRequirement.IdentityType) + assert.Equal(t, "city", svc.EmergencyRequirement.AddressAreaLevel) + assert.Equal(t, []string{"city", "postal_code"}, svc.EmergencyRequirement.AddressMandatoryFields) + + // Verify included emergency_verification + require.NotNil(t, svc.EmergencyVerification) + assert.Equal(t, "ever-001-id", svc.EmergencyVerification.ID) + assert.Equal(t, "EVR-54321", svc.EmergencyVerification.Reference) + assert.Equal(t, "approved", svc.EmergencyVerification.Status) +} diff --git a/emergency_requirement_validations_test.go b/emergency_requirement_validations_test.go new file mode 100644 index 0000000..ad0bbf5 --- /dev/null +++ b/emergency_requirement_validations_test.go @@ -0,0 +1,28 @@ +package didww + +import ( + "context" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/didww/didww-api-3-go-sdk/resource" +) + +func TestEmergencyRequirementValidationsCreate(t *testing.T) { + server, capturedBodyPtr := captureRequestBody(t, map[string]testRoute{ + "POST /v3/emergency_requirement_validations": {status: http.StatusCreated, fixture: "emergency_requirement_validations/create.json"}, + }) + + rv, err := server.client.EmergencyRequirementValidations().Create(context.Background(), &resource.EmergencyRequirementValidation{ + EmergencyRequirementID: "c1d2e3f4-a5b6-7890-1234-567890abcdef", + AddressID: "d3414687-40f4-4346-a267-c2c65117d28c", + IdentityID: "5e9df058-50d2-4e34-b0d4-d1746b86f41a", + }) + require.NoError(t, err) + assert.Equal(t, "c1d2e3f4-a5b6-7890-1234-567890abcdef", rv.ID) + + assertRequestJSON(t, *capturedBodyPtr, "emergency_requirement_validations/create_request.json") +} diff --git a/emergency_requirements_test.go b/emergency_requirements_test.go new file mode 100644 index 0000000..e1b4d39 --- /dev/null +++ b/emergency_requirements_test.go @@ -0,0 +1,32 @@ +package didww + +import ( + "context" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestEmergencyRequirementsList(t *testing.T) { + _, client := newTestServer(t, map[string]testRoute{ + "GET /v3/emergency_requirements": {status: http.StatusOK, fixture: "emergency_requirements/index.json"}, + }) + + reqs, err := client.EmergencyRequirements().List(context.Background(), nil) + require.NoError(t, err) + + require.Len(t, reqs, 1) + assert.Equal(t, "c1d2e3f4-a5b6-7890-1234-567890abcdef", reqs[0].ID) + assert.Equal(t, "any", reqs[0].IdentityType) + assert.Equal(t, "city", reqs[0].AddressAreaLevel) + assert.Equal(t, []string{"city", "postal_code"}, reqs[0].AddressMandatoryFields) + assert.Equal(t, []string{"first_name", "last_name"}, reqs[0].PersonalMandatoryFields) + assert.Equal(t, "7-14 days", reqs[0].EstimateSetupTime) + + // Meta fields + require.NotNil(t, reqs[0].Meta) + assert.Equal(t, "0.0", reqs[0].Meta["setup_price"]) + assert.Equal(t, "1.5", reqs[0].Meta["monthly_price"]) +} diff --git a/emergency_verifications_test.go b/emergency_verifications_test.go new file mode 100644 index 0000000..3d84ef6 --- /dev/null +++ b/emergency_verifications_test.go @@ -0,0 +1,63 @@ +package didww + +import ( + "context" + "net/http" + "testing" + + "github.com/didww/didww-api-3-go-sdk/resource" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestEmergencyVerificationsList(t *testing.T) { + _, client := newTestServer(t, map[string]testRoute{ + "GET /v3/emergency_verifications": {status: http.StatusOK, fixture: "emergency_verifications/index.json"}, + }) + + evs, err := client.EmergencyVerifications().List(context.Background(), nil) + require.NoError(t, err) + + require.Len(t, evs, 1) + assert.Equal(t, "ev-001-id", evs[0].ID) + assert.Equal(t, "EV-123", evs[0].Reference) + assert.Equal(t, "pending", evs[0].Status) + assert.False(t, evs[0].CreatedAt.IsZero()) +} + +func TestEmergencyVerificationsUpdateExternalReferenceID(t *testing.T) { + server, capturedBodyPtr := captureRequestBody(t, map[string]testRoute{ + "PATCH /v3/emergency_verifications/ev-001-id": {status: http.StatusOK, fixture: "emergency_verifications/update.json"}, + }) + + extRef := "ev-ext-ref" + ev, err := server.client.EmergencyVerifications().Update(context.Background(), &resource.EmergencyVerification{ + ID: "ev-001-id", + ExternalReferenceID: &extRef, + }) + require.NoError(t, err) + + assert.Equal(t, "ev-001-id", ev.ID) + require.NotNil(t, ev.ExternalReferenceID) + assert.Equal(t, "ev-ext-ref", *ev.ExternalReferenceID) + + assertRequestJSON(t, *capturedBodyPtr, "emergency_verifications/update_request.json") +} + +func TestEmergencyVerificationsCreate(t *testing.T) { + server, capturedBodyPtr := captureRequestBody(t, map[string]testRoute{ + "POST /v3/emergency_verifications": {status: http.StatusCreated, fixture: "emergency_verifications/create.json"}, + }) + + ev, err := server.client.EmergencyVerifications().Create(context.Background(), &resource.EmergencyVerification{ + AddressID: "d3414687-40f4-4346-a267-c2c65117d28c", + EmergencyCallingServiceID: "ecs-001-id", + }) + require.NoError(t, err) + + assert.Equal(t, "ev-new-id", ev.ID) + assert.Equal(t, "pending", ev.Status) + + assertRequestJSON(t, *capturedBodyPtr, "emergency_verifications/create_request.json") +} diff --git a/encrypt.go b/encrypt.go index a191ac7..2cf7b5c 100644 --- a/encrypt.go +++ b/encrypt.go @@ -116,33 +116,33 @@ func CalculateFingerprint(publicKeyA, publicKeyB string) string { return fingerprintFor(publicKeyA) + ":::" + fingerprintFor(publicKeyB) } -// UploadEncryptedFile uploads an encrypted file via multipart/form-data POST. -// Returns the list of encrypted file IDs created by the API. -func (c *Client) UploadEncryptedFile(ctx context.Context, encryptedData []byte, fileName, fingerprint, description string) ([]string, error) { +// UploadEncryptedFile uploads a single encrypted file via multipart/form-data POST. +// Returns the created encrypted file ID. +func (c *Client) UploadEncryptedFile(ctx context.Context, encryptedData []byte, fileName, fingerprint, description string) (string, error) { var buf bytes.Buffer w := multipart.NewWriter(&buf) if err := w.WriteField("encrypted_files[encryption_fingerprint]", fingerprint); err != nil { - return nil, fmt.Errorf("failed to write fingerprint field: %w", err) + return "", fmt.Errorf("failed to write fingerprint field: %w", err) } - if err := w.WriteField("encrypted_files[items][][description]", description); err != nil { - return nil, fmt.Errorf("failed to write description field: %w", err) + if err := w.WriteField("encrypted_files[description]", description); err != nil { + return "", fmt.Errorf("failed to write description field: %w", err) } - part, err := w.CreateFormFile("encrypted_files[items][][file]", fileName) + part, err := w.CreateFormFile("encrypted_files[file]", fileName) if err != nil { - return nil, fmt.Errorf("failed to create file part: %w", err) + return "", fmt.Errorf("failed to create file part: %w", err) } if _, writeErr := part.Write(encryptedData); writeErr != nil { - return nil, fmt.Errorf("failed to write file data: %w", writeErr) + return "", fmt.Errorf("failed to write file data: %w", writeErr) } if closeErr := w.Close(); closeErr != nil { - return nil, fmt.Errorf("failed to close multipart writer: %w", closeErr) + return "", fmt.Errorf("failed to close multipart writer: %w", closeErr) } u := c.buildURL("encrypted_files") req, err := http.NewRequestWithContext(ctx, http.MethodPost, u, &buf) if err != nil { - return nil, &ClientError{Message: fmt.Sprintf("failed to create request: %v", err)} + return "", &ClientError{Message: fmt.Sprintf("failed to create request: %v", err)} } req.Header.Set("Content-Type", w.FormDataContentType()) req.Header.Set("Accept", "application/json") @@ -151,29 +151,31 @@ func (c *Client) UploadEncryptedFile(ctx context.Context, encryptedData []byte, resp, err := c.httpClient.Do(req) if err != nil { - return nil, &ClientError{Message: fmt.Sprintf("request failed: %v", err)} + return "", &ClientError{Message: fmt.Sprintf("request failed: %v", err)} } defer resp.Body.Close() //nolint:errcheck // best-effort close body, err := io.ReadAll(resp.Body) if err != nil { - return nil, &ClientError{Message: fmt.Sprintf("failed to read response: %v", err)} + return "", &ClientError{Message: fmt.Sprintf("failed to read response: %v", err)} } if resp.StatusCode >= 400 { - return nil, &ClientError{Message: fmt.Sprintf("upload failed: HTTP %d %s", resp.StatusCode, string(body))} + return "", &ClientError{Message: fmt.Sprintf("upload failed: HTTP %d %s", resp.StatusCode, string(body))} } var result struct { - IDs []string `json:"ids"` + Data struct { + ID string `json:"id"` + } `json:"data"` } if err := json.Unmarshal(body, &result); err != nil { - return nil, &ClientError{Message: fmt.Sprintf("unexpected upload response: %s", string(body))} + return "", &ClientError{Message: fmt.Sprintf("unexpected upload response: %s", string(body))} } - if result.IDs == nil { - return nil, &ClientError{Message: fmt.Sprintf("unexpected upload response: %s", string(body))} + if result.Data.ID == "" { + return "", &ClientError{Message: fmt.Sprintf("unexpected upload response: %s", string(body))} } - return result.IDs, nil + return result.Data.ID, nil } func encryptRSAOAEP(publicKeyPEM string, data []byte) ([]byte, error) { diff --git a/encrypt_test.go b/encrypt_test.go index 23cd06c..33180aa 100644 --- a/encrypt_test.go +++ b/encrypt_test.go @@ -131,7 +131,7 @@ func TestUploadEncryptedFile(t *testing.T) { capturedBody, _ = io.ReadAll(r.Body) }) - ids, err := server.client.UploadEncryptedFile( + id, err := server.client.UploadEncryptedFile( context.Background(), []byte("encrypted-content"), "sample.pdf.enc", @@ -140,9 +140,7 @@ func TestUploadEncryptedFile(t *testing.T) { ) require.NoError(t, err) - require.Len(t, ids, 2) - assert.Equal(t, "6eed102c-66a9-4a9b-a95f-4312d70ec12a", ids[0]) - assert.Equal(t, "371eafbd-ac6a-485c-aadf-9e3c5da37eb4", ids[1]) + assert.Equal(t, "6eed102c-66a9-4a9b-a95f-4312d70ec12a", id) // Verify multipart content type assert.Contains(t, capturedContentType, "multipart/form-data") diff --git a/encrypted_files_test.go b/encrypted_files_test.go index bdf17f6..87a4bdd 100644 --- a/encrypted_files_test.go +++ b/encrypted_files_test.go @@ -44,8 +44,8 @@ func TestEncryptedFilesFindWithExpiration(t *testing.T) { require.NoError(t, err) assert.Equal(t, "371eafbd-ac6a-485c-aadf-9e3c5da37eb4", file.ID) - require.NotNil(t, file.ExpireAt) - assert.Equal(t, time.Date(2021, 4, 6, 16, 38, 34, 437000000, time.UTC), *file.ExpireAt) + require.NotNil(t, file.ExpiresAt) + assert.Equal(t, time.Date(2021, 4, 6, 16, 38, 34, 437000000, time.UTC), *file.ExpiresAt) } func TestEncryptedFilesDelete(t *testing.T) { diff --git a/examples/README.md b/examples/README.md index f8ba202..bdff53e 100644 --- a/examples/README.md +++ b/examples/README.md @@ -25,6 +25,7 @@ DIDWW_API_KEY=your_api_key go run ./examples/balance/ | [`countries`](countries/) | Lists countries, demonstrates filtering, and fetches one country by ID. | | [`regions`](regions/) | Lists regions with filters/includes and fetches a specific region. | | [`did_groups`](did_groups/) | Fetches DID groups with included SKUs and shows group details. | +| [`did_trunk_assignment`](did_trunk_assignment/) | Demonstrates exclusive trunk/trunk group assignment on DIDs. | | [`dids`](dids/) | Updates DID routing/capacity by assigning trunk and capacity pool. | | [`trunks`](trunks/) | Lists trunks, creates SIP and PSTN trunks, updates and deletes them. | | [`shared_capacity_groups`](shared_capacity_groups/) | Creates a shared capacity group in a capacity pool. | @@ -35,10 +36,19 @@ DIDWW_API_KEY=your_api_key go run ./examples/balance/ | [`orders_available_dids`](orders_available_dids/) | Orders an available DID using included DID group SKU. | | [`orders_reservation_dids`](orders_reservation_dids/) | Reserves a DID and then places an order from that reservation. | | [`voice_in_trunk_groups`](voice_in_trunk_groups/) | CRUD for trunk groups with trunk relationships. | -| [`voice_out_trunks`](voice_out_trunks/) | CRUD for voice out trunks (requires account config). | +| [`voice_out_trunks`](voice_out_trunks/) | CRUD for voice out trunks using 2026-04-16 polymorphic authentication_method. | | [`did_reservations`](did_reservations/) | Creates, lists, finds and deletes DID reservations. | -| [`exports`](exports/) | Creates and lists CDR exports. | +| [`exports`](exports/) | Creates and lists CDR exports with 2026-04-16 external_reference_id. | | [`capacity_pools`](capacity_pools/) | Lists capacity pools with included shared capacity groups. | +| [`did_history`](did_history/) | Lists DID ownership history (2026-04-16). | +| [`identities`](identities/) | Lists identities with country and birth_country (2026-04-16). | +| [`emergency_requirements`](emergency_requirements/) | Lists emergency service requirements (2026-04-16). | +| [`emergency_calling_services`](emergency_calling_services/) | Lists emergency calling services (2026-04-16). | +| [`emergency_verifications`](emergency_verifications/) | Lists emergency verifications (2026-04-16). | +| [`emergency_requirement_validations`](emergency_requirement_validations/) | Validates emergency requirement data (2026-04-16). | +| [`emergency_scenario`](emergency_scenario/) | End-to-end: find DID → check requirements → validate → create verification → get service. | +| [`address_verifications`](address_verifications/) | Lists address verifications with reject_comment / external_reference_id (2026-04-16). | +| [`orders_emergency`](orders_emergency/) | Creates an emergency order with EmergencyOrderItem (2026-04-16). | ## Troubleshooting diff --git a/examples/address_verifications/main.go b/examples/address_verifications/main.go new file mode 100644 index 0000000..e565bd9 --- /dev/null +++ b/examples/address_verifications/main.go @@ -0,0 +1,75 @@ +// Lists Address Verifications (with 2026-04-16 reject_comment / external_reference_id). +// +// AddressVerification ties an address to one or more DIDs and a set of +// supporting documents so DIDWW compliance can approve or reject the +// declaration. 2026-04-16 adds: +// - reject_comment: free-form comment accompanying a rejection +// - external_reference_id: customer-supplied reference (max 100 chars) +// +// Usage: DIDWW_API_KEY=your_api_key go run ./examples/address_verifications/ +package main + +import ( + "context" + "fmt" + "strings" + + didww "github.com/didww/didww-api-3-go-sdk" + "github.com/didww/didww-api-3-go-sdk/examples" +) + +func main() { + client := examples.ClientFromEnv() + ctx := context.Background() + + fmt.Println("=== Address Verifications ===") + params := didww.NewQueryParams().Include("address,dids") + verifications, err := client.AddressVerifications().List(ctx, params) + if err != nil { + panic(err) + } + fmt.Printf("Found %d address verifications\n", len(verifications)) + + limit := 5 + if len(verifications) < limit { + limit = len(verifications) + } + for _, av := range verifications[:limit] { + fmt.Printf("\nVerification: %s\n", av.ID) + fmt.Printf(" Reference: %s\n", av.Reference) + fmt.Printf(" Status: %s\n", av.Status) + if av.ExternalReferenceID != nil { + fmt.Printf(" External Reference: %s\n", *av.ExternalReferenceID) + } + if av.ServiceDescription != nil { + fmt.Printf(" Service description: %s\n", *av.ServiceDescription) + } + if len(av.RejectReasons) > 0 { + fmt.Printf(" Reject reasons: %s\n", strings.Join(av.RejectReasons, ", ")) + } + if av.RejectComment != "" { + fmt.Printf(" Reject comment: %s\n", av.RejectComment) + } + } + + // Filter: only rejected verifications + fmt.Println("\n=== Rejected verifications ===") + params = didww.NewQueryParams().Filter("status", "rejected") + rejected, err := client.AddressVerifications().List(ctx, params) + if err != nil { + _ = rejected // silence unused + panic(err) + } + fmt.Printf("Found %d rejected verifications\n", len(rejected)) + limit = 3 + if len(rejected) < limit { + limit = len(rejected) + } + for _, av := range rejected[:limit] { + comment := av.RejectComment + if comment == "" && len(av.RejectReasons) > 0 { + comment = strings.Join(av.RejectReasons, ", ") + } + fmt.Printf(" %s: %s\n", av.Reference, comment) + } +} diff --git a/examples/did_groups/main.go b/examples/did_groups/main.go index 7732928..d074937 100644 --- a/examples/did_groups/main.go +++ b/examples/did_groups/main.go @@ -27,8 +27,11 @@ func main() { fmt.Printf("Found %d DID groups\n", len(didGroups)) for _, dg := range didGroups { - fmt.Printf("%s - %s prefix=%s features=%v metered=%v\n", - dg.ID, dg.AreaName, dg.Prefix, dg.Features, dg.IsMetered) + fmt.Printf("%s - %s prefix=%s features=%v metered=%v allow_additional_channels=%v\n", + dg.ID, dg.AreaName, dg.Prefix, dg.Features, dg.IsMetered, dg.AllowAdditionalChannels) + if dg.ServiceRestrictions != nil { + fmt.Printf(" Service restrictions: %s\n", *dg.ServiceRestrictions) + } } // Fetch a specific DID group diff --git a/examples/did_history/main.go b/examples/did_history/main.go new file mode 100644 index 0000000..b9fe1c1 --- /dev/null +++ b/examples/did_history/main.go @@ -0,0 +1,71 @@ +// Lists DID ownership history (2026-04-16). +// Records are retained for the last 90 days only. +// +// Server-side filters supported: +// +// did_number (eq), action (eq), method (eq), +// created_at_gteq, created_at_lteq +// +// Usage: DIDWW_API_KEY=your_api_key go run ./examples/did_history/ +package main + +import ( + "context" + "fmt" + + didww "github.com/didww/didww-api-3-go-sdk" + "github.com/didww/didww-api-3-go-sdk/examples" +) + +func main() { + client := examples.ClientFromEnv() + ctx := context.Background() + + // List most recent DID history events + fmt.Println("=== Recent DID History ===") + events, err := client.DIDHistory().List(ctx, nil) + if err != nil { + panic(err) + } + fmt.Printf("Found %d events in the last 90 days\n", len(events)) + + limit := 10 + if len(events) < limit { + limit = len(events) + } + for _, event := range events[:limit] { + fmt.Printf(" %s %-16s %-28s via %s\n", + event.CreatedAt.Format("2006-01-02T15:04:05Z"), + event.DIDNumber, + event.Action, + event.Method, + ) + } + + // Filter by action + fmt.Println("\n=== Only 'assigned' events ===") + params := didww.NewQueryParams().Filter("action", "assigned") + assigned, err := client.DIDHistory().List(ctx, params) + if err != nil { + panic(err) + } + fmt.Printf("Found %d assignments\n", len(assigned)) + + // Filter by a specific DID number + if len(events) > 0 { + number := events[0].DIDNumber + fmt.Printf("\n=== History for DID %s ===\n", number) + params = didww.NewQueryParams().Filter("did_number", number) + perNumber, err := client.DIDHistory().List(ctx, params) + if err != nil { + panic(err) + } + for _, event := range perNumber { + fmt.Printf(" %s %s via %s\n", + event.CreatedAt.Format("2006-01-02T15:04:05Z"), + event.Action, + event.Method, + ) + } + } +} diff --git a/examples/did_reservations/main.go b/examples/did_reservations/main.go index 4c87bbe..54c3b5a 100644 --- a/examples/did_reservations/main.go +++ b/examples/did_reservations/main.go @@ -40,7 +40,7 @@ func main() { } fmt.Println("Created reservation:", created.ID) fmt.Println(" description:", created.Description) - fmt.Println(" expires at:", created.ExpireAt) + fmt.Println(" expires at:", created.ExpiresAt) // List reservations with includes listParams := didww.NewQueryParams().Include("available_did") diff --git a/examples/did_trunk_assignment/main.go b/examples/did_trunk_assignment/main.go new file mode 100644 index 0000000..bebfc69 --- /dev/null +++ b/examples/did_trunk_assignment/main.go @@ -0,0 +1,130 @@ +// Demonstrates exclusive trunk/trunk group assignment on DIDs. +// Assigning a trunk auto-nullifies the trunk group and vice versa. +// +// Usage: DIDWW_API_KEY=your_api_key go run ./examples/did_trunk_assignment/ +package main + +import ( + "context" + "fmt" + + didww "github.com/didww/didww-api-3-go-sdk" + "github.com/didww/didww-api-3-go-sdk/examples" +) + +func main() { + client := examples.ClientFromEnv() + ctx := context.Background() + + // Get a DID to work with + fmt.Println("=== Finding DID ===") + didParams := didww.NewQueryParams(). + Include("voice_in_trunk,voice_in_trunk_group"). + Page(1, 1) + dids, err := client.DIDs().List(ctx, didParams) + if err != nil { + panic(err) + } + if len(dids) == 0 { + panic("No DIDs found. Please order a DID first.") + } + did := dids[0] + fmt.Printf("Using DID: %s (%s)\n", did.Number, did.ID) + + // Get a trunk + fmt.Println("\n=== Finding Trunk ===") + trunkParams := didww.NewQueryParams().Page(1, 1) + trunks, err := client.VoiceInTrunks().List(ctx, trunkParams) + if err != nil { + panic(err) + } + if len(trunks) == 0 { + panic("No trunks found. Please create a trunk first.") + } + trunk := trunks[0] + fmt.Printf("Selected trunk: %s (%s)\n", trunk.Name, trunk.ID) + + // Get a trunk group + fmt.Println("\n=== Finding Trunk Group ===") + groupParams := didww.NewQueryParams().Page(1, 1) + groups, err := client.VoiceInTrunkGroups().List(ctx, groupParams) + if err != nil { + panic(err) + } + if len(groups) == 0 { + panic("No trunk groups found. Please create a trunk group first.") + } + trunkGroup := groups[0] + fmt.Printf("Selected trunk group: %s (%s)\n", trunkGroup.Name, trunkGroup.ID) + + printDIDAssignment := func(didID string) { + p := didww.NewQueryParams().Include("voice_in_trunk,voice_in_trunk_group") + result, err := client.DIDs().Find(ctx, didID, p) + if err != nil { + panic(err) + } + trunkStr := "null" + if result.VoiceInTrunk != nil { + trunkStr = result.VoiceInTrunk.ID + } + groupStr := "null" + if result.VoiceInTrunkGroup != nil { + groupStr = result.VoiceInTrunkGroup.ID + } + fmt.Printf(" trunk = %s\n", trunkStr) + fmt.Printf(" group = %s\n", groupStr) + } + + // 1. Assign trunk to DID (auto-nullifies trunk group) + fmt.Println("\n=== 1. Assigning trunk to DID ===") + did.VoiceInTrunkID = trunk.ID + did.VoiceInTrunkGroupID = "" + _, err = client.DIDs().Update(ctx, did) + if err != nil { + panic(fmt.Sprintf("Error assigning trunk: %v", err)) + } + printDIDAssignment(did.ID) + + // 2. Assign trunk group to DID (auto-nullifies trunk) + fmt.Println("\n=== 2. Assigning trunk group to DID ===") + freshDID, err := client.DIDs().Find(ctx, did.ID, nil) + if err != nil { + panic(err) + } + freshDID.VoiceInTrunkGroupID = trunkGroup.ID + freshDID.VoiceInTrunkID = "" + _, err = client.DIDs().Update(ctx, freshDID) + if err != nil { + panic(fmt.Sprintf("Error assigning trunk group: %v", err)) + } + printDIDAssignment(did.ID) + + // 3. Re-assign trunk (auto-nullifies trunk group again) + fmt.Println("\n=== 3. Re-assigning trunk ===") + freshDID, err = client.DIDs().Find(ctx, did.ID, nil) + if err != nil { + panic(err) + } + freshDID.VoiceInTrunkID = trunk.ID + freshDID.VoiceInTrunkGroupID = "" + _, err = client.DIDs().Update(ctx, freshDID) + if err != nil { + panic(fmt.Sprintf("Error re-assigning trunk: %v", err)) + } + printDIDAssignment(did.ID) + + // 4. Update description only (trunk stays assigned) + fmt.Println("\n=== 4. Updating description only (trunk stays) ===") + freshDID, err = client.DIDs().Find(ctx, did.ID, nil) + if err != nil { + panic(err) + } + freshDID.Description = examples.Ptr("DID with trunk assigned") + _, err = client.DIDs().Update(ctx, freshDID) + if err != nil { + panic(fmt.Sprintf("Error updating description: %v", err)) + } + printDIDAssignment(did.ID) + + fmt.Println("\nDemonstration complete!") +} diff --git a/examples/dids/main.go b/examples/dids/main.go index f906287..df89f23 100644 --- a/examples/dids/main.go +++ b/examples/dids/main.go @@ -15,9 +15,10 @@ func main() { client := examples.ClientFromEnv() ctx := context.Background() - // Get last ordered DID + // Get last ordered DID (include emergency relationships, 2026-04-16) didParams := didww.NewQueryParams(). Sort("-created_at"). + Include("emergency_calling_service,emergency_verification,identity"). Page(1, 1) dids, err := client.DIDs().List(ctx, didParams) if err != nil { @@ -27,6 +28,20 @@ func main() { panic("No DIDs found. Order a DID first.") } did := dids[0] + fmt.Printf("DID %s (%s)\n", did.ID, did.Number) + fmt.Printf(" Emergency Enabled: %v\n", did.EmergencyEnabled) + if did.EmergencyCallingService != nil { + fmt.Printf(" Emergency Calling Service: %s (status: %s)\n", + did.EmergencyCallingService.ID, did.EmergencyCallingService.Status) + } + if did.EmergencyVerification != nil { + fmt.Printf(" Emergency Verification: %s (status: %s)\n", + did.EmergencyVerification.ID, did.EmergencyVerification.Status) + } + if did.Identity != nil { + fmt.Printf(" Identity: %s (%s %s)\n", + did.Identity.ID, did.Identity.FirstName, did.Identity.LastName) + } // Get last SIP trunk trunkParams := didww.NewQueryParams(). diff --git a/examples/emergency_calling_services/main.go b/examples/emergency_calling_services/main.go new file mode 100644 index 0000000..fb0a3ae --- /dev/null +++ b/examples/emergency_calling_services/main.go @@ -0,0 +1,61 @@ +// Lists emergency calling services (2026-04-16). +// +// Emergency calling services are customer-owned subscriptions that enable 911/112 +// on DIDs. They are created via the Orders API with an EmergencyOrderItem. +// +// Usage: DIDWW_API_KEY=your_api_key go run ./examples/emergency_calling_services/ +package main + +import ( + "context" + "fmt" + "time" + + didww "github.com/didww/didww-api-3-go-sdk" + "github.com/didww/didww-api-3-go-sdk/examples" +) + +func main() { + client := examples.ClientFromEnv() + ctx := context.Background() + + fmt.Println("=== Emergency Calling Services ===") + params := didww.NewQueryParams().Include("country,did_group_type") + services, err := client.EmergencyCallingServices().List(ctx, params) + if err != nil { + panic(err) + } + fmt.Printf("Found %d emergency calling services\n", len(services)) + + for _, svc := range services { + fmt.Printf("\nService: %s\n", svc.ID) + fmt.Printf(" Name: %s\n", svc.Name) + fmt.Printf(" Reference: %s\n", svc.Reference) + fmt.Printf(" Status: %s\n", svc.Status) + fmt.Printf(" Created: %s\n", svc.CreatedAt.Format(time.RFC3339)) + if svc.ActivatedAt != nil { + fmt.Printf(" Activated: %s\n", svc.ActivatedAt.Format(time.RFC3339)) + } + if svc.Country != nil { + fmt.Printf(" Country: %s\n", svc.Country.Name) + } + if svc.DIDGroupType != nil { + fmt.Printf(" DID Group Type: %s\n", svc.DIDGroupType.Name) + } + if svc.Meta != nil { + fmt.Printf(" Setup Price: %s\n", svc.Meta["setup_price"]) + fmt.Printf(" Monthly Price: %s\n", svc.Meta["monthly_price"]) + } + } + + // Find a specific service + if len(services) > 0 { + fmt.Printf("\n=== Details for %s ===\n", services[0].ID) + svc, err := client.EmergencyCallingServices().Find(ctx, services[0].ID) + if err != nil { + panic(err) + } + fmt.Printf(" Name: %s\n", svc.Name) + fmt.Printf(" Status: %s\n", svc.Status) + } +} diff --git a/examples/emergency_requirement_validations/main.go b/examples/emergency_requirement_validations/main.go new file mode 100644 index 0000000..da5f2bb --- /dev/null +++ b/examples/emergency_requirement_validations/main.go @@ -0,0 +1,51 @@ +// Validates emergency requirement data before ordering (2026-04-16). +// +// EmergencyRequirementValidation performs a dry-run check that the given +// address and identity satisfy an EmergencyRequirement. A successful POST +// returns 204 No Content, meaning the data is valid. +// +// Usage: DIDWW_API_KEY=your_api_key \ +// +// EMERGENCY_REQUIREMENT_ID=xxx ADDRESS_ID=yyy IDENTITY_ID=zzz \ +// go run ./examples/emergency_requirement_validations/ +package main + +import ( + "context" + "fmt" + "os" + + "github.com/didww/didww-api-3-go-sdk/examples" + "github.com/didww/didww-api-3-go-sdk/resource" +) + +func main() { + client := examples.ClientFromEnv() + ctx := context.Background() + + emergencyReqID := os.Getenv("EMERGENCY_REQUIREMENT_ID") + addressID := os.Getenv("ADDRESS_ID") + identityID := os.Getenv("IDENTITY_ID") + + if emergencyReqID == "" || addressID == "" || identityID == "" { + fmt.Fprintln(os.Stderr, "EMERGENCY_REQUIREMENT_ID, ADDRESS_ID, and IDENTITY_ID are required") + os.Exit(1) + } + + fmt.Println("=== Validating Emergency Requirement ===") + fmt.Printf(" Emergency Requirement: %s\n", emergencyReqID) + fmt.Printf(" Address: %s\n", addressID) + fmt.Printf(" Identity: %s\n", identityID) + + _, err := client.EmergencyRequirementValidations().Create(ctx, &resource.EmergencyRequirementValidation{ + EmergencyRequirementID: emergencyReqID, + AddressID: addressID, + IdentityID: identityID, + }) + if err != nil { + fmt.Fprintf(os.Stderr, "Validation failed: %v\n", err) + os.Exit(1) + } + + fmt.Println("\nValidation passed (204 No Content)") +} diff --git a/examples/emergency_requirements/main.go b/examples/emergency_requirements/main.go new file mode 100644 index 0000000..3755a0f --- /dev/null +++ b/examples/emergency_requirements/main.go @@ -0,0 +1,53 @@ +// Lists emergency service requirements for a country/did_group_type (2026-04-16). +// +// Emergency requirements describe what address precision, identity type, +// and supporting fields an end-customer must provide to enable 911/112 +// on a DID. +// +// Usage: DIDWW_API_KEY=your_api_key go run ./examples/emergency_requirements/ +package main + +import ( + "context" + "fmt" + "strings" + + didww "github.com/didww/didww-api-3-go-sdk" + "github.com/didww/didww-api-3-go-sdk/examples" +) + +func main() { + client := examples.ClientFromEnv() + ctx := context.Background() + + fmt.Println("=== Emergency Requirements ===") + params := didww.NewQueryParams().Include("country,did_group_type") + requirements, err := client.EmergencyRequirements().List(ctx, params) + if err != nil { + panic(err) + } + fmt.Printf("Found %d emergency requirements\n", len(requirements)) + + limit := 5 + if len(requirements) < limit { + limit = len(requirements) + } + for _, req := range requirements[:limit] { + fmt.Printf("\nRequirement: %s\n", req.ID) + if req.Country != nil { + fmt.Printf(" Country: %s\n", req.Country.Name) + } + if req.DIDGroupType != nil { + fmt.Printf(" DID Group Type: %s\n", req.DIDGroupType.Name) + } + fmt.Printf(" Identity type required: %s\n", req.IdentityType) + fmt.Printf(" Address area level: %s\n", req.AddressAreaLevel) + if len(req.AddressMandatoryFields) > 0 { + fmt.Printf(" Address mandatory fields: %s\n", strings.Join(req.AddressMandatoryFields, ", ")) + } + if req.Meta != nil { + fmt.Printf(" Setup Price: %s\n", req.Meta["setup_price"]) + fmt.Printf(" Monthly Price: %s\n", req.Meta["monthly_price"]) + } + } +} diff --git a/examples/emergency_scenario/main.go b/examples/emergency_scenario/main.go new file mode 100644 index 0000000..837c47b --- /dev/null +++ b/examples/emergency_scenario/main.go @@ -0,0 +1,297 @@ +// End-to-end Emergency Calling Service scenario (2026-04-16). +// +// This example walks through the full flow of purchasing an Emergency +// Calling Service: +// +// 0. Find an address with a country, find an available DID with emergency +// feature in that country, order via available DID, wait for completion. +// 1. Find a DID with the emergency feature that is not yet emergency-enabled. +// 2. Look up emergency requirements for that DID's country + did_group_type. +// 3. Find an existing identity on the account. +// 4. Find an existing address for that identity. +// 5. Validate the (emergency_requirement, address, identity) triple. +// 6. Create an emergency verification with callback_method="post". +// 7. Fetch the created verification to confirm its status. +// 8. Fetch the auto-created emergency_calling_service via the verification. +// +// Usage: DIDWW_API_KEY=your_api_key go run ./examples/emergency_scenario/ +package main + +import ( + "context" + "crypto/rand" + "encoding/hex" + "fmt" + "log" + "strings" + "time" + + didww "github.com/didww/didww-api-3-go-sdk" + "github.com/didww/didww-api-3-go-sdk/examples" + "github.com/didww/didww-api-3-go-sdk/resource" + "github.com/didww/didww-api-3-go-sdk/resource/orderitem" +) + +func randomHex(n int) string { + b := make([]byte, n) + _, _ = rand.Read(b) + return hex.EncodeToString(b) +} + +func main() { + client := examples.ClientFromEnv() + ctx := context.Background() + + // === Step 0: Order an available DID with emergency feature === + fmt.Println("=== Step 0: Order an available DID with emergency feature ===") + + // Find an address first so we know what country to order in + addrParams := didww.NewQueryParams().Include("country") + addresses, err := client.Addresses().List(ctx, addrParams) + if err != nil { + log.Fatal("Failed to list addresses: ", err) + } + if len(addresses) == 0 { + log.Fatal("No addresses on this account. Please create an address first.") + } + addressForOrder := addresses[0] + addressCountry := addressForOrder.Country + if addressCountry == nil { + log.Fatal("Address has no included country") + } + fmt.Printf(" Using address country: %s (%s)\n", addressCountry.Name, addressCountry.ID) + + // Find an available DID with emergency feature in that country + availParams := didww.NewQueryParams(). + Filter("did_group.features", "emergency"). + Filter("country.id", addressCountry.ID). + Include("did_group,did_group.stock_keeping_units"). + Page(1, 1) + availableDIDs, err := client.AvailableDIDs().List(ctx, availParams) + if err != nil { + log.Fatal("Failed to list available DIDs: ", err) + } + if len(availableDIDs) == 0 { + log.Fatal("No available DIDs with emergency feature in this country.") + } + + availableDID := availableDIDs[0] + didGroup := availableDID.DIDGroup + if didGroup == nil { + log.Fatal("Available DID has no included did_group") + } + if len(didGroup.StockKeepingUnits) == 0 { + log.Fatal("No SKU found for this DID group.") + } + sku := didGroup.StockKeepingUnits[0] + + fmt.Printf(" Available DID: %s\n", availableDID.Number) + fmt.Printf(" DID Group: %s\n", didGroup.AreaName) + + order, err := client.Orders().Create(ctx, &resource.Order{ + Items: []orderitem.OrderItem{ + &orderitem.AvailableDidOrderItem{ + DidOrderItem: orderitem.DidOrderItem{ + SkuID: sku.ID, + }, + AvailableDidID: availableDID.ID, + }, + }, + }) + if err != nil { + log.Fatal("Failed to create order: ", err) + } + fmt.Printf(" Order: %s -- %s\n", order.ID, order.Status) + + // Wait for order to complete + for i := 0; i < 10; i++ { + if order.Status == "completed" { + break + } + time.Sleep(5 * time.Second) + order, err = client.Orders().Find(ctx, order.ID) + if err != nil { + log.Fatal("Failed to fetch order: ", err) + } + } + if order.Status != "completed" { + log.Fatalf(" Order did not complete (status: %s).", order.Status) + } + fmt.Println(" Order completed") + + // === Step 1: Find the newly ordered DID === + fmt.Println("\n=== Step 1: Find the newly ordered DID ===") + didParams := didww.NewQueryParams(). + Filter("did_group.features", "emergency"). + Filter("emergency_enabled", "false"). + Include("did_group,did_group.country,did_group.did_group_type,emergency_calling_service"). + Sort("-created_at"). + Page(1, 10) + dids, err := client.DIDs().List(ctx, didParams) + if err != nil { + log.Fatal("Failed to list DIDs: ", err) + } + + // Pick a DID that is not yet assigned to an ECS + var did *resource.DID + for _, d := range dids { + if d.EmergencyCallingService == nil { + did = d + break + } + } + if did == nil { + log.Fatal("No available DID without an existing Emergency Calling Service.") + } + + didGroup = did.DIDGroup + if didGroup == nil { + log.Fatal("DID has no included did_group") + } + country := didGroup.Country + dgt := didGroup.DIDGroupType + + fmt.Printf(" DID: %s (%s)\n", did.Number, did.ID) + fmt.Printf(" DID Group: %s\n", didGroup.ID) + if country != nil { + fmt.Printf(" Country: %s (%s)\n", country.Name, country.ID) + } + if dgt != nil { + fmt.Printf(" DID Group Type: %s (%s)\n", dgt.Name, dgt.ID) + } + + // === Step 2: Get emergency requirements === + fmt.Println("\n=== Step 2: Get emergency requirements for country + did_group_type ===") + reqParams := didww.NewQueryParams() + if country != nil { + reqParams = reqParams.Filter("country.id", country.ID) + } + if dgt != nil { + reqParams = reqParams.Filter("did_group_type.id", dgt.ID) + } + requirements, err := client.EmergencyRequirements().List(ctx, reqParams) + if err != nil { + log.Fatal("Failed to list emergency requirements: ", err) + } + if len(requirements) == 0 { + log.Fatal("No emergency requirements found for this DID group") + } + req := requirements[0] + fmt.Printf(" Emergency Requirement: %s\n", req.ID) + fmt.Printf(" Identity type: %s\n", req.IdentityType) + fmt.Printf(" Address area level: %s\n", req.AddressAreaLevel) + + // === Step 3: Find an existing identity === + fmt.Println("\n=== Step 3: Find identity ===") + identityParams := didww.NewQueryParams().Page(1, 1) + identities, err := client.Identities().List(ctx, identityParams) + if err != nil { + log.Fatal("Failed to list identities: ", err) + } + if len(identities) == 0 { + log.Fatal("No identities found. Create an identity first.") + } + identity := identities[0] + fmt.Printf(" Identity: %s\n", identity.ID) + fmt.Printf(" Type: %s\n", identity.IdentityType) + + // === Step 4: Find an existing address === + fmt.Println("\n=== Step 4: Find address ===") + addrListParams := didww.NewQueryParams().Page(1, 1) + addrList, err := client.Addresses().List(ctx, addrListParams) + if err != nil { + log.Fatal("Failed to list addresses: ", err) + } + if len(addrList) == 0 { + log.Fatal("No addresses found. Create an address first.") + } + addr := addrList[0] + fmt.Printf(" Address: %s\n", addr.ID) + + // === Step 5: Validate emergency requirement (dry-run) === + fmt.Println("\n=== Step 5: Validate emergency requirement (requirement + address + identity) ===") + _, err = client.EmergencyRequirementValidations().Create(ctx, &resource.EmergencyRequirementValidation{ + EmergencyRequirementID: req.ID, + AddressID: addr.ID, + IdentityID: identity.ID, + }) + if err != nil { + log.Fatal("Validation failed: ", err) + } + fmt.Println(" Validation passed -- this combination can be used for emergency calling.") + + // === Step 6: Create an emergency verification === + fmt.Println("\n=== Step 6: Create emergency verification ===") + suffix := randomHex(4) + callbackMethod := "post" + externalRef := fmt.Sprintf("go-scenario-%s", suffix) + verification, err := client.EmergencyVerifications().Create(ctx, &resource.EmergencyVerification{ + CallbackURL: examples.Ptr("https://example.com/webhooks/emergency"), + CallbackMethod: &callbackMethod, + ExternalReferenceID: &externalRef, + AddressID: addr.ID, + DIDIDs: []string{did.ID}, + }) + if err != nil { + log.Fatal("Failed to create emergency verification: ", err) + } + fmt.Printf(" Created verification: %s\n", verification.ID) + fmt.Printf(" Reference: %s\n", verification.Reference) + fmt.Printf(" Status: %s\n", verification.Status) + if verification.ExternalReferenceID != nil { + fmt.Printf(" External Reference: %s\n", *verification.ExternalReferenceID) + } + + // === Step 7: Fetch the verification to confirm status === + fmt.Println("\n=== Step 7: Fetch the created verification ===") + fetchParams := didww.NewQueryParams().Include("address,emergency_calling_service,dids") + fetched, err := client.EmergencyVerifications().Find(ctx, verification.ID, fetchParams) + if err != nil { + log.Fatal("Failed to fetch verification: ", err) + } + fmt.Printf(" Verification: %s\n", fetched.ID) + fmt.Printf(" Status: %s\n", fetched.Status) + if fetched.DIDs != nil { + numbers := make([]string, len(fetched.DIDs)) + for i, d := range fetched.DIDs { + numbers[i] = d.Number + } + fmt.Printf(" DIDs: %s\n", strings.Join(numbers, ", ")) + } + if fetched.AddressRel != nil { + fmt.Printf(" Address: %s\n", fetched.AddressRel.ID) + } + + // === Step 8: Fetch the auto-created emergency_calling_service === + fmt.Println("\n=== Step 8: Fetch emergency calling service ===") + ecs := fetched.EmergencyCallingService + if ecs != nil { + // Re-fetch with includes for full details + svcParams := didww.NewQueryParams().Include("country,did_group_type,dids") + service, err := client.EmergencyCallingServices().Find(ctx, ecs.ID, svcParams) + if err != nil { + log.Fatal("Failed to fetch emergency calling service: ", err) + } + fmt.Printf(" Service: %s\n", service.ID) + fmt.Printf(" Name: %s\n", service.Name) + fmt.Printf(" Reference: %s\n", service.Reference) + fmt.Printf(" Status: %s\n", service.Status) + if service.Country != nil { + fmt.Printf(" Country: %s\n", service.Country.Name) + } + if service.DIDGroupType != nil { + fmt.Printf(" DID Group Type: %s\n", service.DIDGroupType.Name) + } + if service.DIDs != nil { + numbers := make([]string, len(service.DIDs)) + for i, d := range service.DIDs { + numbers[i] = d.Number + } + fmt.Printf(" Attached DIDs: %s\n", strings.Join(numbers, ", ")) + } + } else { + fmt.Println(" No emergency_calling_service linked yet (may be created asynchronously).") + } + + fmt.Println("\nDone! Emergency calling service flow completed.") +} diff --git a/examples/emergency_verifications/main.go b/examples/emergency_verifications/main.go new file mode 100644 index 0000000..3242cc4 --- /dev/null +++ b/examples/emergency_verifications/main.go @@ -0,0 +1,40 @@ +// Lists and creates emergency verifications (2026-04-16). +// +// Emergency verifications link an address to an emergency calling service, +// validating that the end-user's location meets regulatory requirements. +// +// Usage: DIDWW_API_KEY=your_api_key go run ./examples/emergency_verifications/ +package main + +import ( + "context" + "fmt" + "time" + + "github.com/didww/didww-api-3-go-sdk/examples" +) + +func main() { + client := examples.ClientFromEnv() + ctx := context.Background() + + fmt.Println("=== Emergency Verifications ===") + verifications, err := client.EmergencyVerifications().List(ctx, nil) + if err != nil { + panic(err) + } + fmt.Printf("Found %d emergency verifications\n", len(verifications)) + + for _, ev := range verifications { + fmt.Printf("\nVerification: %s\n", ev.ID) + fmt.Printf(" Reference: %s\n", ev.Reference) + fmt.Printf(" Status: %s\n", ev.Status) + fmt.Printf(" Created: %s\n", ev.CreatedAt.Format(time.RFC3339)) + if ev.ExternalReferenceID != nil { + fmt.Printf(" External Reference: %s\n", *ev.ExternalReferenceID) + } + if len(ev.RejectReasons) > 0 { + fmt.Printf(" Reject Reasons: %v\n", ev.RejectReasons) + } + } +} diff --git a/examples/exports/main.go b/examples/exports/main.go index c92c6e4..31deb65 100644 --- a/examples/exports/main.go +++ b/examples/exports/main.go @@ -1,4 +1,13 @@ -// Creates and lists CDR exports. +// Creates and lists CDR exports (cdr_in / cdr_out). +// +// 2026-04-16 additions: +// - external_reference_id: customer-supplied reference (max 100 chars) +// +// Filter semantics on CDR exports: +// - filters.from: lower bound, INCLUSIVE (server: time_start >= from) +// - filters.to: upper bound, EXCLUSIVE (server: time_start < to) +// +// To cover a whole day, pass from: "2026-04-15 00:00:00", to: "2026-04-16 00:00:00". // // Usage: DIDWW_API_KEY=your_api_key go run ./examples/exports/ package main @@ -6,6 +15,7 @@ package main import ( "context" "fmt" + "time" "github.com/didww/didww-api-3-go-sdk/examples" "github.com/didww/didww-api-3-go-sdk/resource" @@ -16,26 +26,69 @@ func main() { client := examples.ClientFromEnv() ctx := context.Background() - // Create an export + // List existing exports + fmt.Println("=== Existing Exports ===") + exports, err := client.Exports().List(ctx, nil) + if err != nil { + panic(err) + } + fmt.Printf("Found %d exports\n", len(exports)) + + limit := 5 + if len(exports) < limit { + limit = len(exports) + } + for _, e := range exports[:limit] { + fmt.Printf("Export: %s\n", e.ID) + fmt.Printf(" Type: %s\n", e.ExportType) + fmt.Printf(" Status: %s\n", e.Status) + fmt.Printf(" Created: %s\n", e.CreatedAt.Format(time.RFC3339)) + if e.URL != nil { + fmt.Printf(" URL: %s\n", *e.URL) + } + if e.ExternalReferenceID != nil { + fmt.Printf(" External Reference: %s\n", *e.ExternalReferenceID) + } + fmt.Println() + } + + // Create a CDR-In export for yesterday (from is inclusive, to is exclusive) + fmt.Println("\n=== Creating CDR-In Export (yesterday) ===") + now := time.Now().UTC() + yesterday := now.AddDate(0, 0, -1) + suffix := fmt.Sprintf("%d", now.UnixMilli())[:8] + extRef := fmt.Sprintf("go-cdr-in-%s", suffix) + export := &resource.Export{ ExportType: enums.ExportTypeCdrIn, - Filters: map[string]interface{}{"year": 2025, "month": 1}, + Filters: map[string]interface{}{ + "from": yesterday.Format("2006-01-02") + " 00:00:00", // inclusive + "to": now.Format("2006-01-02") + " 00:00:00", // exclusive + }, + ExternalReferenceID: &extRef, } created, err := client.Exports().Create(ctx, export) if err != nil { panic(err) } - fmt.Println("Created export:", created.ID) - fmt.Println(" type:", created.ExportType) - fmt.Println(" status:", created.Status) - - // List exports - exports, err := client.Exports().List(ctx, nil) - if err != nil { - panic(err) + fmt.Printf("Created CDR-In export: %s\n", created.ID) + fmt.Printf(" Status: %s\n", created.Status) + if created.ExternalReferenceID != nil { + fmt.Printf(" External Reference: %s\n", *created.ExternalReferenceID) } - fmt.Printf("\nAll exports (%d):\n", len(exports)) - for _, e := range exports { - fmt.Printf(" %s %s [%s]\n", e.ID, e.ExportType, e.Status) + + // Find and inspect the specific export + if len(exports) > 0 { + fmt.Println("\n=== Specific Export Details ===") + specific, err := client.Exports().Find(ctx, exports[0].ID) + if err != nil { + panic(err) + } + fmt.Printf("Export: %s\n", specific.ID) + fmt.Printf(" Type: %s\n", specific.ExportType) + fmt.Printf(" Status: %s\n", specific.Status) + if specific.ExternalReferenceID != nil { + fmt.Printf(" External Reference: %s\n", *specific.ExternalReferenceID) + } } } diff --git a/examples/identities/main.go b/examples/identities/main.go new file mode 100644 index 0000000..cfda928 --- /dev/null +++ b/examples/identities/main.go @@ -0,0 +1,47 @@ +// Lists identities with country and birth_country (2026-04-16). +// +// 2026-04-16 adds: +// - birth_country has_one relationship on Identity +// +// Usage: DIDWW_API_KEY=your_api_key go run ./examples/identities/ +package main + +import ( + "context" + "fmt" + + didww "github.com/didww/didww-api-3-go-sdk" + "github.com/didww/didww-api-3-go-sdk/examples" +) + +func main() { + client := examples.ClientFromEnv() + ctx := context.Background() + + fmt.Println("=== Identities ===") + params := didww.NewQueryParams().Include("country,birth_country") + identities, err := client.Identities().List(ctx, params) + if err != nil { + panic(err) + } + fmt.Printf("Found %d identities\n", len(identities)) + + limit := 10 + if len(identities) < limit { + limit = len(identities) + } + for _, id := range identities[:limit] { + fmt.Printf("\nIdentity: %s\n", id.ID) + fmt.Printf(" Name: %s %s\n", id.FirstName, id.LastName) + fmt.Printf(" Phone: %s\n", id.PhoneNumber) + fmt.Printf(" Type: %s\n", id.IdentityType) + if id.Country != nil { + fmt.Printf(" Country: %s\n", id.Country.Name) + } + if id.BirthCountry != nil { + fmt.Printf(" Birth Country: %s\n", id.BirthCountry.Name) + } + fmt.Printf(" Birth Date: %s\n", id.BirthDate) + fmt.Printf(" Verified: %v\n", id.Verified) + } +} diff --git a/examples/orders/main.go b/examples/orders/main.go index 8fc7b7c..9bb721e 100644 --- a/examples/orders/main.go +++ b/examples/orders/main.go @@ -23,7 +23,11 @@ func main() { panic(err) } for _, order := range orders { - fmt.Printf("Order %s: %s ($%s)\n", order.ID, order.Status, order.Amount) + fmt.Printf("Order %s: %s ($%s)", order.ID, order.Status, order.Amount) + if order.ExternalReferenceID != nil { + fmt.Printf(" [ref: %s]", *order.ExternalReferenceID) + } + fmt.Println() for _, item := range order.Items { fmt.Printf(" - %T\n", item) } @@ -42,7 +46,9 @@ func main() { } skuID := didGroups[0].StockKeepingUnits[0].ID + extRef := "go-order-example" newOrder := &resource.Order{ + ExternalReferenceID: &extRef, Items: []orderitem.OrderItem{ &orderitem.DidOrderItem{ SkuID: skuID, @@ -60,5 +66,5 @@ func main() { if err := client.Orders().Delete(ctx, created.ID); err != nil { panic(err) } - fmt.Println("Order cancelled") + fmt.Println("Order canceled") } diff --git a/examples/orders_emergency/main.go b/examples/orders_emergency/main.go new file mode 100644 index 0000000..309ed49 --- /dev/null +++ b/examples/orders_emergency/main.go @@ -0,0 +1,66 @@ +// Creates an Order with an EmergencyOrderItem (2026-04-16). +// +// This example demonstrates ordering an emergency calling service +// by submitting an EmergencyOrderItem with an emergency_calling_service_id +// obtained from the emergency requirements workflow. +// +// Usage: DIDWW_API_KEY=your_api_key EMERGENCY_CALLING_SERVICE_ID=xxx \ +// +// go run ./examples/orders_emergency/ +package main + +import ( + "context" + "fmt" + "os" + + "github.com/didww/didww-api-3-go-sdk/examples" + "github.com/didww/didww-api-3-go-sdk/resource" + "github.com/didww/didww-api-3-go-sdk/resource/orderitem" +) + +func main() { + client := examples.ClientFromEnv() + ctx := context.Background() + + ecsID := os.Getenv("EMERGENCY_CALLING_SERVICE_ID") + if ecsID == "" { + fmt.Fprintln(os.Stderr, "EMERGENCY_CALLING_SERVICE_ID is required") + os.Exit(1) + } + + fmt.Println("=== Creating Emergency Order ===") + extRef := "go-emergency-order" + order, err := client.Orders().Create(ctx, &resource.Order{ + ExternalReferenceID: &extRef, + Items: []orderitem.OrderItem{ + &orderitem.EmergencyOrderItem{ + EmergencyCallingServiceID: ecsID, + Qty: 1, + }, + }, + }) + if err != nil { + fmt.Fprintf(os.Stderr, "Error creating order: %v\n", err) + os.Exit(1) + } + + fmt.Printf("Order created: %s\n", order.ID) + fmt.Printf(" Reference: %s\n", order.Reference) + fmt.Printf(" Status: %s\n", order.Status) + fmt.Printf(" Amount: %s\n", order.Amount) + fmt.Printf(" Description: %s\n", order.Description) + if order.ExternalReferenceID != nil { + fmt.Printf(" External Reference: %s\n", *order.ExternalReferenceID) + } + + for i, item := range order.Items { + fmt.Printf("\n Item %d:\n", i+1) + if emItem, ok := item.(*orderitem.EmergencyOrderItem); ok { + fmt.Printf(" Type: emergency_order_items\n") + fmt.Printf(" Qty: %d\n", emItem.Qty) + fmt.Printf(" NRC: %s\n", emItem.Nrc) + fmt.Printf(" MRC: %s\n", emItem.Mrc) + } + } +} diff --git a/examples/orders_reservation_dids/main.go b/examples/orders_reservation_dids/main.go index d605cb8..ffa7e06 100644 --- a/examples/orders_reservation_dids/main.go +++ b/examples/orders_reservation_dids/main.go @@ -45,7 +45,7 @@ func main() { if err != nil { panic(err) } - fmt.Printf("Created reservation: %s (expires: %s)\n", created.ID, created.ExpireAt) + fmt.Printf("Created reservation: %s (expires: %s)\n", created.ID, created.ExpiresAt) // Order reserved DID order := &resource.Order{ diff --git a/examples/shared_capacity_groups/main.go b/examples/shared_capacity_groups/main.go index 4068e8e..9e4675e 100644 --- a/examples/shared_capacity_groups/main.go +++ b/examples/shared_capacity_groups/main.go @@ -27,11 +27,14 @@ func main() { pool := pools[0] // Create a shared capacity group + ts := time.Now().UnixMilli() + extRef := fmt.Sprintf("go-scg-%d", ts) group := &resource.SharedCapacityGroup{ - Name: fmt.Sprintf("SDK Channel Group %d", time.Now().UnixMilli()), + Name: fmt.Sprintf("SDK Channel Group %d", ts), MeteredChannelsCount: 10, SharedChannelsCount: 1, CapacityPoolID: pool.ID, + ExternalReferenceID: &extRef, } created, err := client.SharedCapacityGroups().Create(ctx, group) if err != nil { @@ -39,4 +42,7 @@ func main() { } fmt.Printf("Created: %s name=%s metered=%d shared=%d\n", created.ID, created.Name, created.MeteredChannelsCount, created.SharedChannelsCount) + if created.ExternalReferenceID != nil { + fmt.Printf(" External Reference: %s\n", *created.ExternalReferenceID) + } } diff --git a/examples/upload_encrypted_file/main.go b/examples/upload_encrypted_file/main.go index 5466f2f..b66f6ae 100644 --- a/examples/upload_encrypted_file/main.go +++ b/examples/upload_encrypted_file/main.go @@ -62,7 +62,7 @@ func main() { fmt.Printf("Encrypted size: %d bytes\n", len(encryptedData)) // Upload encrypted file - ids, err := client.UploadEncryptedFile( + id, err := client.UploadEncryptedFile( ctx, encryptedData, originalName+".enc", @@ -73,5 +73,5 @@ func main() { fmt.Fprintf(os.Stderr, "failed to upload: %v\n", err) os.Exit(1) } - fmt.Printf("Uploaded encrypted file IDs: %v\n", ids) + fmt.Printf("Uploaded encrypted file ID: %s\n", id) } diff --git a/examples/voice_in_trunk_groups/main.go b/examples/voice_in_trunk_groups/main.go index 8641a63..2b8170d 100644 --- a/examples/voice_in_trunk_groups/main.go +++ b/examples/voice_in_trunk_groups/main.go @@ -62,10 +62,12 @@ func main() { fmt.Println("Created trunk B:", trunkB.ID) // Create a trunk group with both trunks + extRef := fmt.Sprintf("go-vitg-%d", ts) group, err := client.VoiceInTrunkGroups().Create(ctx, &resource.VoiceInTrunkGroup{ - Name: fmt.Sprintf("SDK Trunk Group %d", ts), - CapacityLimit: examples.Ptr(10), - VoiceInTrunkIDs: []string{trunkA.ID, trunkB.ID}, + Name: fmt.Sprintf("SDK Trunk Group %d", ts), + CapacityLimit: examples.Ptr(10), + VoiceInTrunkIDs: []string{trunkA.ID, trunkB.ID}, + ExternalReferenceID: &extRef, }) if err != nil { panic(err) @@ -81,7 +83,11 @@ func main() { fmt.Printf("\nAll trunk groups (%d):\n", len(groups)) for _, g := range groups { trunkCount := len(g.VoiceInTrunks) - fmt.Printf(" %s (%d trunks)\n", g.Name, trunkCount) + fmt.Printf(" %s (%d trunks)", g.Name, trunkCount) + if g.ExternalReferenceID != nil { + fmt.Printf(" [ref: %s]", *g.ExternalReferenceID) + } + fmt.Println() } // Update group name diff --git a/examples/voice_out_trunks/main.go b/examples/voice_out_trunks/main.go index b0e9daf..dad8d64 100644 --- a/examples/voice_out_trunks/main.go +++ b/examples/voice_out_trunks/main.go @@ -1,4 +1,4 @@ -// CRUD for voice out trunks (requires account config). +// CRUD for voice out trunks using 2026-04-16 polymorphic authentication_method. // // Note: Voice Out Trunks and some OnCliMismatchAction values (e.g. replace_cli, randomize_cli) // require additional account configuration. Contact DIDWW support to enable. @@ -13,6 +13,7 @@ import ( "github.com/didww/didww-api-3-go-sdk/examples" "github.com/didww/didww-api-3-go-sdk/resource" + "github.com/didww/didww-api-3-go-sdk/resource/authenticationmethod" "github.com/didww/didww-api-3-go-sdk/resource/enums" ) @@ -20,49 +21,87 @@ func main() { client := examples.ClientFromEnv() ctx := context.Background() - // Create a voice out trunk + // List voice out trunks + trunks, err := client.VoiceOutTrunks().List(ctx, nil) + if err != nil { + panic(err) + } + fmt.Printf("Found %d voice out trunks\n", len(trunks)) + for _, t := range trunks { + fmt.Printf(" %s (%s)\n", t.Name, t.Status) + fmt.Printf(" ID: %s\n", t.ID) + if t.AuthenticationMethod != nil { + fmt.Printf(" Auth type: %s\n", t.AuthenticationMethod.AuthenticationType()) + switch am := t.AuthenticationMethod.(type) { + case *authenticationmethod.CredentialsAndIp: + fmt.Printf(" Username: %s\n", am.Username) + case *authenticationmethod.IpOnly: + fmt.Printf(" Allowed SIP IPs: %v\n", am.AllowedSipIPs) + case *authenticationmethod.Twilio: + fmt.Printf(" Twilio Account SID: %s\n", am.TwilioAccountSid) + } + } + if t.ExternalReferenceID != nil { + fmt.Printf(" External Reference ID: %s\n", *t.ExternalReferenceID) + } + fmt.Printf(" Emergency Enable All: %v\n", t.EmergencyEnableAll) + if t.RtpTimeout != nil { + fmt.Printf(" RTP Timeout: %d\n", *t.RtpTimeout) + } + } + + // Create a voice out trunk with credentials_and_ip authentication + // NOTE: 203.0.113.0/24 is RFC 5737 TEST-NET-3 documentation space. + // Replace with the real CIDR of your SIP infrastructure. + suffix := fmt.Sprintf("%d", time.Now().UnixMilli()) + extRef := fmt.Sprintf("go-example-%s", suffix[:8]) + rtpTimeout := 60 trunk := &resource.VoiceOutTrunk{ - Name: fmt.Sprintf("SDK Outbound Trunk %d", time.Now().UnixMilli()), - AllowedSipIPs: []string{"192.168.1.1"}, - AllowedRtpIPs: []string{"192.168.1.1"}, + Name: fmt.Sprintf("SDK Outbound Trunk %s", suffix), + AuthenticationMethod: &authenticationmethod.CredentialsAndIp{ + AllowedSipIPs: []string{"203.0.113.0/24"}, + }, + AllowedRtpIPs: []string{"203.0.113.1"}, DstPrefixes: []string{}, DefaultDstAction: enums.DefaultDstActionAllowAll, OnCliMismatchAction: enums.OnCliMismatchActionRejectCall, MediaEncryptionMode: enums.MediaEncryptionModeDisabled, ThresholdAmount: examples.Ptr("100.00"), + ExternalReferenceID: &extRef, + RtpTimeout: &rtpTimeout, } created, err := client.VoiceOutTrunks().Create(ctx, trunk) if err != nil { panic(err) } - fmt.Println("Created voice out trunk:", created.ID) + fmt.Println("\nCreated voice out trunk:", created.ID) fmt.Println(" name:", created.Name) - fmt.Println(" username:", created.Username) - fmt.Println(" password:", created.Password) - fmt.Println(" status:", created.Status) - - // List voice out trunks - trunks, err := client.VoiceOutTrunks().List(ctx, nil) - if err != nil { - panic(err) + fmt.Println(" auth type:", created.AuthenticationMethod.AuthenticationType()) + if cam, ok := created.AuthenticationMethod.(*authenticationmethod.CredentialsAndIp); ok { + fmt.Println(" username:", cam.Username) } - fmt.Printf("\nAll voice out trunks (%d):\n", len(trunks)) - for _, t := range trunks { - fmt.Printf(" %s (%s)\n", t.Name, t.Status) + fmt.Println(" status:", created.Status) + if created.ExternalReferenceID != nil { + fmt.Println(" external reference:", *created.ExternalReferenceID) } - // Update + // Update - change name and tech_prefix + fmt.Println("\n=== Updating Voice Out Trunk ===") created.Name = "Updated Outbound Trunk" - created.AllowedSipIPs = []string{"10.0.0.0/8"} + created.AuthenticationMethod = &authenticationmethod.CredentialsAndIp{ + AllowedSipIPs: []string{"203.0.113.0/24"}, + TechPrefix: "9", + } updated, err := client.VoiceOutTrunks().Update(ctx, created) if err != nil { panic(err) } - fmt.Println("\nUpdated name:", updated.Name) + fmt.Println("Updated name:", updated.Name) + fmt.Println(" New auth type:", updated.AuthenticationMethod.AuthenticationType()) // Delete if err := client.VoiceOutTrunks().Delete(ctx, created.ID); err != nil { panic(err) } - fmt.Println("Deleted voice out trunk") + fmt.Println("\nDeleted voice out trunk") } diff --git a/exports_test.go b/exports_test.go index b54a755..1e19b0e 100644 --- a/exports_test.go +++ b/exports_test.go @@ -41,8 +41,8 @@ func TestExportsCreate(t *testing.T) { ExportType: enums.ExportTypeCdrIn, Filters: map[string]interface{}{ "did_number": "1234556789", - "year": "2019", - "month": "01", + "from": "2026-04-01 00:00:00", + "to": "2026-04-15 23:59:59", }, }) require.NoError(t, err) @@ -62,8 +62,8 @@ func TestExportsCreateCdrOut(t *testing.T) { export, err := client.Exports().Create(context.Background(), &resource.Export{ ExportType: enums.ExportTypeCdrOut, Filters: map[string]interface{}{ - "year": "2019", - "month": "01", + "from": "2026-04-01 00:00:00", + "to": "2026-04-30 23:59:59", }, }) require.NoError(t, err) @@ -105,6 +105,47 @@ func TestExportsFind(t *testing.T) { assert.NotEmpty(t, *export.URL) } +func TestExportsUpdateExternalReferenceID(t *testing.T) { + server, capturedBodyPtr := captureRequestBody(t, map[string]testRoute{ + "PATCH /v3/exports/da15f006-5da4-45ca-b0df-735baeadf423": {status: http.StatusOK, fixture: "exports/update.json"}, + }) + + extRef := "export-ext-ref" + export, err := server.client.Exports().Update(context.Background(), &resource.Export{ + ID: "da15f006-5da4-45ca-b0df-735baeadf423", + ExternalReferenceID: &extRef, + }) + require.NoError(t, err) + + assert.Equal(t, "da15f006-5da4-45ca-b0df-735baeadf423", export.ID) + require.NotNil(t, export.ExternalReferenceID) + assert.Equal(t, "export-ext-ref", *export.ExternalReferenceID) + + assertRequestJSON(t, *capturedBodyPtr, "exports/update_request.json") +} + +func TestExportsUpdateExternalReferenceIDFromLoaded(t *testing.T) { + server, capturedBodyPtr := captureRequestBody(t, map[string]testRoute{ + "GET /v3/exports/da15f006-5da4-45ca-b0df-735baeadf423": {status: http.StatusOK, fixture: "exports/show.json"}, + "PATCH /v3/exports/da15f006-5da4-45ca-b0df-735baeadf423": {status: http.StatusOK, fixture: "exports/update.json"}, + }) + + export, err := server.client.Exports().Find(context.Background(), "da15f006-5da4-45ca-b0df-735baeadf423") + require.NoError(t, err) + + extRef := "export-ext-ref" + export.ExternalReferenceID = &extRef + + export, err = server.client.Exports().Update(context.Background(), export) + require.NoError(t, err) + + assert.Equal(t, "da15f006-5da4-45ca-b0df-735baeadf423", export.ID) + require.NotNil(t, export.ExternalReferenceID) + assert.Equal(t, "export-ext-ref", *export.ExternalReferenceID) + + assertRequestJSON(t, *capturedBodyPtr, "exports/update_request.json") +} + func TestDownloadExport(t *testing.T) { gzData := loadFixture(t, "exports/download.csv.gz") diff --git a/identities_test.go b/identities_test.go index 6977201..4ed68da 100644 --- a/identities_test.go +++ b/identities_test.go @@ -147,6 +147,30 @@ func TestIdentitiesFindWithContactEmail(t *testing.T) { assert.Equal(t, "john.doe@example.com", *identity.ContactEmail) } +func TestIdentitiesFindWithBirthCountry(t *testing.T) { + _, client := newTestServer(t, map[string]testRoute{ + "GET /v3/identities/e96ae7d1-11d5-42bc-a5c5-211f3c3788ae": {status: http.StatusOK, fixture: "identities/show_with_birth_country.json"}, + }) + + identity, err := client.Identities().Find(context.Background(), "e96ae7d1-11d5-42bc-a5c5-211f3c3788ae") + require.NoError(t, err) + + assert.Equal(t, "e96ae7d1-11d5-42bc-a5c5-211f3c3788ae", identity.ID) + assert.Equal(t, "John", identity.FirstName) + assert.Equal(t, enums.IdentityTypePersonal, identity.IdentityType) + + // Verify included country + require.NotNil(t, identity.Country) + assert.Equal(t, "United States", identity.Country.Name) + assert.Equal(t, "US", identity.Country.ISO) + + // Verify included birth_country + require.NotNil(t, identity.BirthCountry) + assert.Equal(t, "a2b3c4d5-e6f7-8901-abcd-ef1234567890", identity.BirthCountry.ID) + assert.Equal(t, "Canada", identity.BirthCountry.Name) + assert.Equal(t, "CA", identity.BirthCountry.ISO) +} + func TestIdentitiesDelete(t *testing.T) { _, client := newTestServer(t, map[string]testRoute{ "DELETE /v3/identities/e96ae7d1-11d5-42bc-a5c5-211f3c3788ae": {status: http.StatusNoContent}, diff --git a/jsonapi/jsonapi.go b/jsonapi/jsonapi.go index 55bd587..d445f5f 100644 --- a/jsonapi/jsonapi.go +++ b/jsonapi/jsonapi.go @@ -24,6 +24,12 @@ type jsonapiResource struct { Type string `json:"type"` Attributes json.RawMessage `json:"attributes"` Relationships map[string]json.RawMessage `json:"relationships,omitempty"` + Meta json.RawMessage `json:"meta,omitempty"` +} + +// MetaUnmarshaler is implemented by resources that parse resource-level JSON:API meta. +type MetaUnmarshaler interface { + UnmarshalMeta(raw json.RawMessage) error } // IncludedResources maps "type:id" to the raw JSON:API resource object. @@ -281,6 +287,15 @@ func unmarshalResourceWithIncluded[T any](data []byte, included IncludedResource } } + // Parse resource-level meta if the resource supports it + if len(res.Meta) > 0 && string(res.Meta) != jsonNull { + if mu, ok := any(&result).(MetaUnmarshaler); ok { + if err := mu.UnmarshalMeta(res.Meta); err != nil { + return nil, fmt.Errorf("failed to parse resource meta: %w", err) + } + } + } + _ = rememberCleanState(&result) return &result, nil diff --git a/orders_test.go b/orders_test.go index 83110d3..6850ca3 100644 --- a/orders_test.go +++ b/orders_test.go @@ -167,13 +167,46 @@ func TestOrdersCreateNanpa(t *testing.T) { assertRequestJSON(t, *capturedBodyPtr, "orders/create_request_nanpa.json") } +func TestOrdersCreateEmergency(t *testing.T) { + server, capturedBodyPtr := captureRequestBody(t, map[string]testRoute{ + "POST /v3/orders": {status: http.StatusCreated, fixture: "orders/create_emergency.json"}, + }) + + order, err := server.client.Orders().Create(context.Background(), &resource.Order{ + Items: []orderitem.OrderItem{ + &orderitem.EmergencyOrderItem{ + EmergencyCallingServiceID: "b6d9d793-578d-42d3-bc33-73dd8155e615", + Qty: 1, + }, + }, + }) + require.NoError(t, err) + + assert.Equal(t, "a1b2c3d4-e5f6-7890-abcd-ef1234567890", order.ID) + assert.Equal(t, "30.0", order.Amount) + assert.Equal(t, enums.OrderStatusPending, order.Status) + assert.Equal(t, "Emergency", order.Description) + assert.Equal(t, "EMG-100001", order.Reference) + require.Len(t, order.Items, 1) + + emItem, ok := order.Items[0].(*orderitem.EmergencyOrderItem) + require.True(t, ok, "expected EmergencyOrderItem") + assert.Equal(t, 1, emItem.Qty) + assert.Equal(t, "5.0", emItem.Nrc) + assert.Equal(t, "25.0", emItem.Mrc) + assert.Equal(t, false, emItem.ProratedMrc) + assert.Equal(t, "b6d9d793-578d-42d3-bc33-73dd8155e615", emItem.EmergencyCallingServiceID) + + assertRequestJSON(t, *capturedBodyPtr, "orders/create_request_emergency.json") +} + func TestOrdersCreateWithCallback(t *testing.T) { server, capturedBodyPtr := captureRequestBody(t, map[string]testRoute{ "POST /v3/orders": {status: http.StatusCreated, fixture: "orders_with_callback/create.json"}, }) cbURL := "https://example.com/callback" - cbMethod := "POST" + cbMethod := "post" _, err := server.client.Orders().Create(context.Background(), &resource.Order{ AllowBackOrdering: true, CallbackURL: &cbURL, diff --git a/repository.go b/repository.go index 7f67d2c..ba27fdf 100644 --- a/repository.go +++ b/repository.go @@ -120,8 +120,8 @@ func (r *SingletonRepository[T]) Find(ctx context.Context) (*T, error) { const ( jsonapiMediaType = "application/vnd.api+json" - apiVersion = "2022-05-10" - sdkVersion = "1.0.0" + apiVersion = "2026-04-16" + sdkVersion = "3.0.0-dev" ) // doRequest executes an HTTP request and returns the response body. diff --git a/requirement_validations_test.go b/requirement_validations_test.go index 85b9f4d..450c850 100644 --- a/requirement_validations_test.go +++ b/requirement_validations_test.go @@ -11,31 +11,31 @@ import ( "github.com/didww/didww-api-3-go-sdk/resource" ) -func TestRequirementValidationsCreate(t *testing.T) { +func TestAddressRequirementValidationsCreate(t *testing.T) { server, capturedBodyPtr := captureRequestBody(t, map[string]testRoute{ - "POST /v3/requirement_validations": {status: http.StatusCreated, fixture: "requirement_validations/create.json"}, + "POST /v3/address_requirement_validations": {status: http.StatusCreated, fixture: "address_requirement_validations/create.json"}, }) - rv, err := server.client.RequirementValidations().Create(context.Background(), &resource.RequirementValidation{ - AddressID: "d3414687-40f4-4346-a267-c2c65117d28c", - RequirementID: "aea92b24-a044-4864-9740-89d3e15b65c7", + rv, err := server.client.AddressRequirementValidations().Create(context.Background(), &resource.AddressRequirementValidation{ + AddressID: "d3414687-40f4-4346-a267-c2c65117d28c", + AddressRequirementID: "aea92b24-a044-4864-9740-89d3e15b65c7", }) require.NoError(t, err) assert.NotEmpty(t, rv.ID) - assertRequestJSON(t, *capturedBodyPtr, "requirement_validations/create_request.json") + assertRequestJSON(t, *capturedBodyPtr, "address_requirement_validations/create_request.json") } -func TestRequirementValidationsCreateError(t *testing.T) { +func TestAddressRequirementValidationsCreateError(t *testing.T) { server, capturedBodyPtr := captureRequestBody(t, map[string]testRoute{ - "POST /v3/requirement_validations": {status: http.StatusUnprocessableEntity, fixture: "requirement_validations/create_error_validation.json"}, + "POST /v3/address_requirement_validations": {status: http.StatusUnprocessableEntity, fixture: "address_requirement_validations/create_error_validation.json"}, }) - _, err := server.client.RequirementValidations().Create(context.Background(), &resource.RequirementValidation{ - IdentityID: "5e9df058-50d2-4e34-b0d4-d1746b86f41a", - AddressID: "d3414687-40f4-4346-a267-c2c65117d28c", - RequirementID: "2efc3427-8ba6-4d50-875d-f2de4a068de8", + _, err := server.client.AddressRequirementValidations().Create(context.Background(), &resource.AddressRequirementValidation{ + IdentityID: "5e9df058-50d2-4e34-b0d4-d1746b86f41a", + AddressID: "d3414687-40f4-4346-a267-c2c65117d28c", + AddressRequirementID: "2efc3427-8ba6-4d50-875d-f2de4a068de8", }) require.Error(t, err) @@ -43,5 +43,5 @@ func TestRequirementValidationsCreateError(t *testing.T) { require.True(t, ok, "expected *APIError") require.Len(t, apiErr.Errors, 3) - assertRequestJSON(t, *capturedBodyPtr, "requirement_validations/create_request_failed.json") + assertRequestJSON(t, *capturedBodyPtr, "address_requirement_validations/create_request_failed.json") } diff --git a/requirements_test.go b/requirements_test.go index 30825d9..be69f2d 100644 --- a/requirements_test.go +++ b/requirements_test.go @@ -9,28 +9,28 @@ import ( "github.com/stretchr/testify/require" ) -func TestRequirementsList(t *testing.T) { +func TestAddressRequirementsList(t *testing.T) { _, client := newTestServer(t, map[string]testRoute{ - "GET /v3/requirements": {status: http.StatusOK, fixture: "requirements/index.json"}, + "GET /v3/address_requirements": {status: http.StatusOK, fixture: "address_requirements/index.json"}, }) - requirements, err := client.Requirements().List(context.Background(), nil) + requirements, err := client.AddressRequirements().List(context.Background(), nil) require.NoError(t, err) require.NotEmpty(t, requirements) } -func TestRequirementsFindWithIncludes(t *testing.T) { +func TestAddressRequirementsFindWithIncludes(t *testing.T) { _, client := newTestServer(t, map[string]testRoute{ - "GET /v3/requirements/25d12afe-1ec6-4fe3-9621-b250dd1fb959": {status: http.StatusOK, fixture: "requirements/show.json"}, + "GET /v3/address_requirements/25d12afe-1ec6-4fe3-9621-b250dd1fb959": {status: http.StatusOK, fixture: "address_requirements/show.json"}, }) params := NewQueryParams().Include("country,did_group_type,personal_permanent_document,business_permanent_document,personal_onetime_document,business_onetime_document,personal_proof_types,business_proof_types,address_proof_types") - req, err := client.Requirements().Find(context.Background(), "25d12afe-1ec6-4fe3-9621-b250dd1fb959", params) + req, err := client.AddressRequirements().Find(context.Background(), "25d12afe-1ec6-4fe3-9621-b250dd1fb959", params) require.NoError(t, err) assert.Equal(t, "25d12afe-1ec6-4fe3-9621-b250dd1fb959", req.ID) - assert.Equal(t, "Any", req.IdentityType) + assert.Equal(t, "any", req.IdentityType) assert.Equal(t, 1, req.PersonalProofQty) assert.True(t, req.ServiceDescriptionRequired) diff --git a/resource/address.go b/resource/address.go index b9a542e..f37b960 100644 --- a/resource/address.go +++ b/resource/address.go @@ -4,13 +4,14 @@ import "time" // Address represents a customer address. type Address struct { - ID string `json:"-" jsonapi:"addresses"` - CityName string `json:"city_name"` - PostalCode string `json:"postal_code"` - Address string `json:"address"` - Description string `json:"description"` - CreatedAt time.Time `json:"created_at" api:"readonly"` - Verified bool `json:"verified" api:"readonly"` + ID string `json:"-" jsonapi:"addresses"` + CityName string `json:"city_name"` + PostalCode string `json:"postal_code"` + Address string `json:"address"` + Description string `json:"description"` + CreatedAt time.Time `json:"created_at" api:"readonly"` + Verified bool `json:"verified" api:"readonly"` + ExternalReferenceID *string `json:"external_reference_id,omitempty"` // Relationship IDs for create/update IdentityID string `json:"-" rel:"identity,identities"` CountryID string `json:"-" rel:"country,countries"` diff --git a/resource/address_verification.go b/resource/address_verification.go index 3447740..dbdec98 100644 --- a/resource/address_verification.go +++ b/resource/address_verification.go @@ -1,8 +1,6 @@ package resource import ( - "encoding/json" - "strings" "time" "github.com/didww/didww-api-3-go-sdk/resource/enums" @@ -10,14 +8,16 @@ import ( // AddressVerification represents an address verification request. type AddressVerification struct { - ID string `json:"-" jsonapi:"address_verifications"` - ServiceDescription *string `json:"service_description,omitempty"` - CallbackURL *string `json:"callback_url,omitempty"` - CallbackMethod *string `json:"callback_method,omitempty"` - Status enums.AddressVerificationStatus `json:"status" api:"readonly"` - RejectReasons []string `json:"reject_reasons" api:"readonly"` - CreatedAt time.Time `json:"created_at" api:"readonly"` - Reference string `json:"reference" api:"readonly"` + ID string `json:"-" jsonapi:"address_verifications"` + ServiceDescription *string `json:"service_description,omitempty"` + CallbackURL *string `json:"callback_url,omitempty"` + CallbackMethod *string `json:"callback_method,omitempty"` + Status enums.AddressVerificationStatus `json:"status" api:"readonly"` + RejectReasons []string `json:"reject_reasons" api:"readonly"` + CreatedAt time.Time `json:"created_at" api:"readonly"` + Reference string `json:"reference" api:"readonly"` + RejectComment string `json:"reject_comment" api:"readonly"` + ExternalReferenceID *string `json:"external_reference_id,omitempty"` // Relationship IDs for create/update AddressID string `json:"-" rel:"address,addresses"` DIDIDs []string `json:"-" rel:"dids,dids"` @@ -25,28 +25,17 @@ type AddressVerification struct { AddressRel *Address `json:"-" rel:"address"` } -// UnmarshalJSON splits the semicolon-separated reject_reasons string into a slice. -func (a *AddressVerification) UnmarshalJSON(data []byte) error { - type Alias AddressVerification - aux := &struct { - RejectReasons *string `json:"reject_reasons"` - *Alias - }{ - Alias: (*Alias)(a), - } - if err := json.Unmarshal(data, aux); err != nil { - return err - } - if aux.RejectReasons != nil { - rawItems := strings.Split(*aux.RejectReasons, "; ") - a.RejectReasons = make([]string, 0, len(rawItems)) - for _, item := range rawItems { - if item != "" { - a.RejectReasons = append(a.RejectReasons, item) - } - } - } else { - a.RejectReasons = nil - } - return nil +// IsPending returns true when the verification status is "pending". +func (a *AddressVerification) IsPending() bool { + return a.Status == enums.AddressVerificationStatusPending +} + +// IsApproved returns true when the verification status is "approved". +func (a *AddressVerification) IsApproved() bool { + return a.Status == enums.AddressVerificationStatusApproved +} + +// IsRejected returns true when the verification status is "rejected". +func (a *AddressVerification) IsRejected() bool { + return a.Status == enums.AddressVerificationStatusRejected } diff --git a/resource/authenticationmethod/authentication_method.go b/resource/authenticationmethod/authentication_method.go new file mode 100644 index 0000000..eb80129 --- /dev/null +++ b/resource/authenticationmethod/authentication_method.go @@ -0,0 +1,114 @@ +package authenticationmethod + +import ( + "encoding/json" + "fmt" +) + +// AuthenticationMethod is the interface for polymorphic authentication methods +// on VoiceOutTrunk resources. +type AuthenticationMethod interface { + AuthenticationType() string +} + +// IpOnly is a read-only authentication method. +// It can only be configured manually by DIDWW staff upon request +// and cannot be set via the API on create or update. +// Trunks with ip_only authentication can still be read and their +// non-auth attributes updated via the API. +type IpOnly struct { + AllowedSipIPs []string `json:"allowed_sip_ips,omitempty"` + TechPrefix string `json:"tech_prefix,omitempty"` +} + +func (a *IpOnly) AuthenticationType() string { return "ip_only" } + +// CredentialsAndIp uses credentials plus IP-based authentication. +// Username and Password are server-generated and returned in responses only. +type CredentialsAndIp struct { + AllowedSipIPs []string `json:"allowed_sip_ips,omitempty"` + TechPrefix string `json:"tech_prefix,omitempty"` + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` +} + +func (a *CredentialsAndIp) AuthenticationType() string { return "credentials_and_ip" } + +// Twilio uses Twilio SIP trunking authentication. +type Twilio struct { + TwilioAccountSid string `json:"twilio_account_sid,omitempty"` +} + +func (a *Twilio) AuthenticationType() string { return "twilio" } + +// Generic wraps an unknown authentication method type for forward-compatibility. +type Generic struct { + Type string `json:"-"` + Attributes map[string]interface{} `json:"-"` +} + +func (a *Generic) AuthenticationType() string { return a.Type } + +// MarshalJSON marshals an authentication method as { type: ..., attributes: ... }. +func MarshalJSON(am AuthenticationMethod) ([]byte, error) { + if am == nil { + return []byte("null"), nil + } + if g, ok := am.(*Generic); ok { + return json.Marshal(map[string]interface{}{ + "type": g.Type, + "attributes": g.Attributes, + }) + } + attrBytes, err := json.Marshal(am) + if err != nil { + return nil, err + } + return json.Marshal(map[string]json.RawMessage{ + "type": json.RawMessage(fmt.Sprintf("%q", am.AuthenticationType())), + "attributes": attrBytes, + }) +} + +// UnmarshalJSON unmarshals an authentication method from { type: ..., attributes: ... }. +func UnmarshalJSON(data []byte) (AuthenticationMethod, error) { + if string(data) == "null" { + return nil, nil + } + var wrapper struct { + Type string `json:"type"` + Attributes json.RawMessage `json:"attributes"` + } + if err := json.Unmarshal(data, &wrapper); err != nil { + return nil, err + } + + switch wrapper.Type { + case "ip_only": + var am IpOnly + if err := json.Unmarshal(wrapper.Attributes, &am); err != nil { + return nil, err + } + return &am, nil + case "credentials_and_ip": + var am CredentialsAndIp + if err := json.Unmarshal(wrapper.Attributes, &am); err != nil { + return nil, err + } + return &am, nil + case "twilio": + var am Twilio + if err := json.Unmarshal(wrapper.Attributes, &am); err != nil { + return nil, err + } + return &am, nil + default: + var attrs map[string]interface{} + if wrapper.Attributes != nil { + if err := json.Unmarshal(wrapper.Attributes, &attrs); err != nil { + return nil, err + } + } + return &Generic{Type: wrapper.Type, Attributes: attrs}, nil + } +} diff --git a/resource/available_did.go b/resource/available_did.go index 86cdb16..445d2c8 100644 --- a/resource/available_did.go +++ b/resource/available_did.go @@ -14,7 +14,7 @@ type AvailableDID struct { // DIDReservation represents a reserved DID. type DIDReservation struct { ID string `json:"-" jsonapi:"did_reservations"` - ExpireAt time.Time `json:"expire_at" api:"readonly"` + ExpiresAt time.Time `json:"expires_at" api:"readonly"` CreatedAt time.Time `json:"created_at" api:"readonly"` Description string `json:"description"` // Relationship IDs for create/update diff --git a/resource/capacity_pool.go b/resource/capacity_pool.go index a95a507..99ed843 100644 --- a/resource/capacity_pool.go +++ b/resource/capacity_pool.go @@ -27,6 +27,7 @@ type SharedCapacityGroup struct { SharedChannelsCount int `json:"shared_channels_count"` CreatedAt time.Time `json:"created_at" api:"readonly"` MeteredChannelsCount int `json:"metered_channels_count"` + ExternalReferenceID *string `json:"external_reference_id,omitempty"` // Relationship IDs for create/update CapacityPoolID string `json:"-" rel:"capacity_pool,capacity_pools"` // Resolved relationships diff --git a/resource/did.go b/resource/did.go index 6a2fe68..a138da0 100644 --- a/resource/did.go +++ b/resource/did.go @@ -20,19 +20,29 @@ type DID struct { ExpiresAt *time.Time `json:"expires_at" api:"readonly"` ChannelsIncludedCount int `json:"channels_included_count" api:"readonly"` DedicatedChannelsCount int `json:"dedicated_channels_count"` + EmergencyEnabled bool `json:"emergency_enabled" api:"readonly"` // Relationship IDs for create/update - VoiceInTrunkID string `json:"-" rel:"voice_in_trunk,voice_in_trunks"` - VoiceInTrunkGroupID string `json:"-" rel:"voice_in_trunk_group,voice_in_trunk_groups"` - CapacityPoolID string `json:"-" rel:"capacity_pool,capacity_pools"` - SharedCapacityGroupID string `json:"-" rel:"shared_capacity_group,shared_capacity_groups"` + VoiceInTrunkID string `json:"-" rel:"voice_in_trunk,voice_in_trunks"` + VoiceInTrunkGroupID string `json:"-" rel:"voice_in_trunk_group,voice_in_trunk_groups"` + CapacityPoolID string `json:"-" rel:"capacity_pool,capacity_pools"` + SharedCapacityGroupID string `json:"-" rel:"shared_capacity_group,shared_capacity_groups"` + EmergencyCallingServiceID string `json:"-" rel:"emergency_calling_service,emergency_calling_services"` + EmergencyVerificationID string `json:"-" rel:"emergency_verification,emergency_verifications"` + IdentityID string `json:"-" rel:"identity,identities"` + // NullifyEmergencyCallingService, when true, sends {"data": null} for the + // emergency_calling_service relationship (unassign the service from this DID). + NullifyEmergencyCallingService bool `json:"-"` // Resolved relationships - Order *Order `json:"-" rel:"order"` - AddressVerification *AddressVerification `json:"-" rel:"address_verification"` - DIDGroup *DIDGroup `json:"-" rel:"did_group"` - VoiceInTrunk *VoiceInTrunk `json:"-" rel:"voice_in_trunk"` - VoiceInTrunkGroup *VoiceInTrunkGroup `json:"-" rel:"voice_in_trunk_group"` - CapacityPool *CapacityPool `json:"-" rel:"capacity_pool"` - SharedCapacityGroup *SharedCapacityGroup `json:"-" rel:"shared_capacity_group"` + Order *Order `json:"-" rel:"order"` + AddressVerification *AddressVerification `json:"-" rel:"address_verification"` + DIDGroup *DIDGroup `json:"-" rel:"did_group"` + VoiceInTrunk *VoiceInTrunk `json:"-" rel:"voice_in_trunk"` + VoiceInTrunkGroup *VoiceInTrunkGroup `json:"-" rel:"voice_in_trunk_group"` + CapacityPool *CapacityPool `json:"-" rel:"capacity_pool"` + SharedCapacityGroup *SharedCapacityGroup `json:"-" rel:"shared_capacity_group"` + EmergencyCallingService *EmergencyCallingService `json:"-" rel:"emergency_calling_service"` + EmergencyVerification *EmergencyVerification `json:"-" rel:"emergency_verification"` + Identity *Identity `json:"-" rel:"identity"` } // MarshalRelationships implements RelationshipMarshaler for DID. @@ -51,5 +61,8 @@ func (d *DID) MarshalRelationships() (map[string]any, error) { if d.SharedCapacityGroupID != "" { rels["capacity_pool"] = jsonapi.NullRelationship() } + if d.NullifyEmergencyCallingService { + rels["emergency_calling_service"] = jsonapi.NullRelationship() + } return rels, nil } diff --git a/resource/did_group.go b/resource/did_group.go index 9f111b6..61983a6 100644 --- a/resource/did_group.go +++ b/resource/did_group.go @@ -10,13 +10,14 @@ type DIDGroup struct { IsMetered bool `json:"is_metered"` AreaName string `json:"area_name"` AllowAdditionalChannels bool `json:"allow_additional_channels"` + ServiceRestrictions *string `json:"service_restrictions"` // Resolved relationships - Country *Country `json:"-" rel:"country"` - City *City `json:"-" rel:"city"` - Region *Region `json:"-" rel:"region"` - DIDGroupType *DIDGroupType `json:"-" rel:"did_group_type"` - StockKeepingUnits []*StockKeepingUnit `json:"-" rel:"stock_keeping_units"` - Requirement *Requirement `json:"-" rel:"requirement"` + Country *Country `json:"-" rel:"country"` + City *City `json:"-" rel:"city"` + Region *Region `json:"-" rel:"region"` + DIDGroupType *DIDGroupType `json:"-" rel:"did_group_type"` + StockKeepingUnits []*StockKeepingUnit `json:"-" rel:"stock_keeping_units"` + AddressRequirement *AddressRequirement `json:"-" rel:"address_requirement"` } // DIDGroupType represents a type of DID group. diff --git a/resource/did_history.go b/resource/did_history.go new file mode 100644 index 0000000..d7479d3 --- /dev/null +++ b/resource/did_history.go @@ -0,0 +1,31 @@ +package resource + +import ( + "encoding/json" + "time" +) + +// DIDHistory represents a DID ownership history record. +// Introduced in API 2026-04-16. Records are retained for the last 90 days. +// +// When Action is "billing_cycles_count_changed", the JSON:API resource-level +// meta block contains "from" and "to" string fields indicating the previous +// and new billing_cycles_count values. Meta is nil for all other actions. +type DIDHistory struct { + ID string `json:"-" jsonapi:"did_history"` + DIDNumber string `json:"did_number" api:"readonly"` + Action string `json:"action" api:"readonly"` + Method string `json:"method" api:"readonly"` + CreatedAt time.Time `json:"created_at" api:"readonly"` + Meta map[string]string `json:"-"` +} + +// UnmarshalMeta parses the resource-level JSON:API meta block into a generic map. +func (d *DIDHistory) UnmarshalMeta(raw json.RawMessage) error { + var m map[string]string + if err := json.Unmarshal(raw, &m); err != nil { + return err + } + d.Meta = m + return nil +} diff --git a/resource/emergency_calling_service.go b/resource/emergency_calling_service.go new file mode 100644 index 0000000..15d42de --- /dev/null +++ b/resource/emergency_calling_service.go @@ -0,0 +1,76 @@ +package resource + +import ( + "encoding/json" + "time" +) + +// EmergencyCallingService represents a customer-owned subscription to emergency calling. +// Supported operations: index, show, destroy. Introduced in API 2026-04-16. +type EmergencyCallingService struct { + ID string `json:"-" jsonapi:"emergency_calling_services"` + // Name is the human-readable label for the service. + Name string `json:"name" api:"readonly"` + // Reference is the server-assigned reference code (e.g. "ECS-0042"). + Reference string `json:"reference" api:"readonly"` + // Status is the current lifecycle status ("active", "canceled", "new", "changes_required", "in_process", "pending_update"). + Status string `json:"status" api:"readonly"` + // ActivatedAt is when the service became active (nil if not yet activated). + ActivatedAt *time.Time `json:"activated_at" api:"readonly"` + // CanceledAt is when the service was canceled (nil if still active). + CanceledAt *time.Time `json:"canceled_at" api:"readonly"` + // CreatedAt is when the service was created. + CreatedAt time.Time `json:"created_at" api:"readonly"` + // RenewDate is the next renewal date for the service subscription (date-only, e.g. "2026-05-22"). + RenewDate string `json:"renew_date" api:"readonly"` + // Meta holds resource-level JSON:API meta (e.g. setup_price, monthly_price). + Meta map[string]string `json:"-"` + // Resolved relationships + Country *Country `json:"-" rel:"country"` + DIDGroupType *DIDGroupType `json:"-" rel:"did_group_type"` + Order *Order `json:"-" rel:"order"` + Address *Address `json:"-" rel:"address"` + EmergencyRequirement *EmergencyRequirement `json:"-" rel:"emergency_requirement"` + EmergencyVerification *EmergencyVerification `json:"-" rel:"emergency_verification"` + DIDs []*DID `json:"-" rel:"dids"` +} + +// UnmarshalMeta parses the resource-level JSON:API meta block into a generic map. +func (e *EmergencyCallingService) UnmarshalMeta(raw json.RawMessage) error { + var m map[string]string + if err := json.Unmarshal(raw, &m); err != nil { + return err + } + e.Meta = m + return nil +} + +// EmergencyCallingService status constants. +const ( + ECSStatusActive = "active" + ECSStatusCanceled = "canceled" + ECSStatusChangesRequired = "changes_required" + ECSStatusInProcess = "in_process" + ECSStatusNew = "new" + ECSStatusPendingUpdate = "pending_update" +) + +// IsActive returns true when the service status is "active". +func (e *EmergencyCallingService) IsActive() bool { return e.Status == ECSStatusActive } + +// IsCanceled returns true when the service status is "canceled". +func (e *EmergencyCallingService) IsCanceled() bool { return e.Status == ECSStatusCanceled } + +// IsChangesRequired returns true when the service status is "changes_required". +func (e *EmergencyCallingService) IsChangesRequired() bool { + return e.Status == ECSStatusChangesRequired +} + +// IsInProcess returns true when the service status is "in_process". +func (e *EmergencyCallingService) IsInProcess() bool { return e.Status == ECSStatusInProcess } + +// IsNew returns true when the service status is "new". +func (e *EmergencyCallingService) IsNew() bool { return e.Status == ECSStatusNew } + +// IsPendingUpdate returns true when the service status is "pending_update". +func (e *EmergencyCallingService) IsPendingUpdate() bool { return e.Status == ECSStatusPendingUpdate } diff --git a/resource/emergency_requirement.go b/resource/emergency_requirement.go new file mode 100644 index 0000000..8999397 --- /dev/null +++ b/resource/emergency_requirement.go @@ -0,0 +1,42 @@ +package resource + +import "encoding/json" + +// EmergencyRequirement represents the regulatory requirements for ordering +// an emergency calling service. Introduced in API 2026-04-16. +type EmergencyRequirement struct { + ID string `json:"-" jsonapi:"emergency_requirements"` + // IdentityType is the required identity type ("personal", "business", or "any"). + IdentityType string `json:"identity_type" api:"readonly"` + // AddressAreaLevel is the minimum geographic precision for the address ("country", "city", "street", etc.). + AddressAreaLevel string `json:"address_area_level" api:"readonly"` + // PersonalAreaLevel is the minimum geographic precision for personal identity addresses. + PersonalAreaLevel string `json:"personal_area_level" api:"readonly"` + // BusinessAreaLevel is the minimum geographic precision for business identity addresses. + BusinessAreaLevel string `json:"business_area_level" api:"readonly"` + // AddressMandatoryFields lists address fields required for this requirement. + AddressMandatoryFields []string `json:"address_mandatory_fields" api:"readonly"` + // PersonalMandatoryFields lists identity fields required for personal identities. + PersonalMandatoryFields []string `json:"personal_mandatory_fields" api:"readonly"` + // BusinessMandatoryFields lists identity fields required for business identities. + BusinessMandatoryFields []string `json:"business_mandatory_fields" api:"readonly"` + // EstimateSetupTime is the estimated time before emergency calling is enabled (e.g. "7-14 days"). + EstimateSetupTime string `json:"estimate_setup_time" api:"readonly"` + // RequirementRestrictionMessage is a human-readable restriction message. May be empty. + RequirementRestrictionMessage string `json:"requirement_restriction_message" api:"readonly"` + // Meta holds resource-level JSON:API meta (e.g. setup_price, monthly_price). + Meta map[string]string `json:"-"` + // Resolved relationships + Country *Country `json:"-" rel:"country"` + DIDGroupType *DIDGroupType `json:"-" rel:"did_group_type"` +} + +// UnmarshalMeta parses the resource-level JSON:API meta block into a generic map. +func (e *EmergencyRequirement) UnmarshalMeta(raw json.RawMessage) error { + var m map[string]string + if err := json.Unmarshal(raw, &m); err != nil { + return err + } + e.Meta = m + return nil +} diff --git a/resource/emergency_requirement_validation.go b/resource/emergency_requirement_validation.go new file mode 100644 index 0000000..ae5be30 --- /dev/null +++ b/resource/emergency_requirement_validation.go @@ -0,0 +1,13 @@ +package resource + +// EmergencyRequirementValidation validates a prospective emergency calling service +// order against an EmergencyRequirement. A successful POST returns 201 Created +// with the validation resource (id mirrors the submitted emergency_requirement_id). +// Introduced in API 2026-04-16. +type EmergencyRequirementValidation struct { + ID string `json:"-" jsonapi:"emergency_requirement_validations"` + // Relationship IDs for create + EmergencyRequirementID string `json:"-" rel:"emergency_requirement,emergency_requirements"` + AddressID string `json:"-" rel:"address,addresses"` + IdentityID string `json:"-" rel:"identity,identities"` +} diff --git a/resource/emergency_verification.go b/resource/emergency_verification.go new file mode 100644 index 0000000..a0f08a7 --- /dev/null +++ b/resource/emergency_verification.go @@ -0,0 +1,47 @@ +package resource + +import "time" + +// EmergencyVerification represents a verification record for an emergency calling service. +// Supported operations: index, show, create. Introduced in API 2026-04-16. +type EmergencyVerification struct { + ID string `json:"-" jsonapi:"emergency_verifications"` + Reference string `json:"reference" api:"readonly"` + Status string `json:"status" api:"readonly"` + RejectReasons []string `json:"reject_reasons" api:"readonly"` + RejectComment string `json:"reject_comment" api:"readonly"` + CallbackURL *string `json:"callback_url,omitempty"` + CallbackMethod *string `json:"callback_method,omitempty"` + ExternalReferenceID *string `json:"external_reference_id,omitempty"` + CreatedAt time.Time `json:"created_at" api:"readonly"` + // Relationship IDs for create/update + AddressID string `json:"-" rel:"address,addresses"` + EmergencyCallingServiceID string `json:"-" rel:"emergency_calling_service,emergency_calling_services"` + DIDIDs []string `json:"-" rel:"dids,dids"` + // Resolved relationships + AddressRel *Address `json:"-" rel:"address"` + EmergencyCallingService *EmergencyCallingService `json:"-" rel:"emergency_calling_service"` + DIDs []*DID `json:"-" rel:"dids"` +} + +// Emergency verification status constants (lowercase, per API). +const ( + EmergencyVerificationStatusPending = "pending" + EmergencyVerificationStatusApproved = "approved" + EmergencyVerificationStatusRejected = "rejected" +) + +// IsPending returns true when the verification status is "pending". +func (e *EmergencyVerification) IsPending() bool { + return e.Status == EmergencyVerificationStatusPending +} + +// IsApproved returns true when the verification status is "approved". +func (e *EmergencyVerification) IsApproved() bool { + return e.Status == EmergencyVerificationStatusApproved +} + +// IsRejected returns true when the verification status is "rejected". +func (e *EmergencyVerification) IsRejected() bool { + return e.Status == EmergencyVerificationStatusRejected +} diff --git a/resource/encrypted_file.go b/resource/encrypted_file.go index 34cc2d8..329bd9b 100644 --- a/resource/encrypted_file.go +++ b/resource/encrypted_file.go @@ -6,5 +6,5 @@ import "time" type EncryptedFile struct { ID string `json:"-" jsonapi:"encrypted_files"` Description string `json:"description"` - ExpireAt *time.Time `json:"expire_at" api:"readonly"` + ExpiresAt *time.Time `json:"expires_at" api:"readonly"` } diff --git a/resource/enums/address.go b/resource/enums/address.go index 047693f..3ef2509 100644 --- a/resource/enums/address.go +++ b/resource/enums/address.go @@ -4,17 +4,17 @@ package enums type AddressVerificationStatus string const ( - AddressVerificationStatusPending AddressVerificationStatus = "Pending" - AddressVerificationStatusApproved AddressVerificationStatus = "Approved" - AddressVerificationStatusRejected AddressVerificationStatus = "Rejected" + AddressVerificationStatusPending AddressVerificationStatus = "pending" + AddressVerificationStatusApproved AddressVerificationStatus = "approved" + AddressVerificationStatusRejected AddressVerificationStatus = "rejected" ) // AreaLevel defines the geographic area level for requirements and DID groups. type AreaLevel string const ( - AreaLevelWorldWide AreaLevel = "WorldWide" - AreaLevelCountry AreaLevel = "Country" - AreaLevelArea AreaLevel = "Area" - AreaLevelCity AreaLevel = "City" + AreaLevelWorldWide AreaLevel = "world_wide" + AreaLevelCountry AreaLevel = "country" + AreaLevelArea AreaLevel = "area" + AreaLevelCity AreaLevel = "city" ) diff --git a/resource/enums/address_test.go b/resource/enums/address_test.go index b622ce5..0ac8b71 100644 --- a/resource/enums/address_test.go +++ b/resource/enums/address_test.go @@ -8,9 +8,9 @@ func TestAddressVerificationStatus(t *testing.T) { value AddressVerificationStatus expected string }{ - {"Pending", AddressVerificationStatusPending, "Pending"}, - {"Approved", AddressVerificationStatusApproved, "Approved"}, - {"Rejected", AddressVerificationStatusRejected, "Rejected"}, + {"Pending", AddressVerificationStatusPending, "pending"}, + {"Approved", AddressVerificationStatusApproved, "approved"}, + {"Rejected", AddressVerificationStatusRejected, "rejected"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -27,10 +27,10 @@ func TestAreaLevel(t *testing.T) { value AreaLevel expected string }{ - {"WorldWide", AreaLevelWorldWide, "WorldWide"}, - {"Country", AreaLevelCountry, "Country"}, - {"Area", AreaLevelArea, "Area"}, - {"City", AreaLevelCity, "City"}, + {"WorldWide", AreaLevelWorldWide, "world_wide"}, + {"Country", AreaLevelCountry, "country"}, + {"Area", AreaLevelArea, "area"}, + {"City", AreaLevelCity, "city"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/resource/enums/diversion_relay_policy.go b/resource/enums/diversion_relay_policy.go new file mode 100644 index 0000000..c2163dc --- /dev/null +++ b/resource/enums/diversion_relay_policy.go @@ -0,0 +1,11 @@ +package enums + +// DiversionRelayPolicy defines the diversion header relay policy for SIP INVITE. +type DiversionRelayPolicy string + +const ( + DiversionRelayPolicyNone DiversionRelayPolicy = "none" + DiversionRelayPolicyAsIs DiversionRelayPolicy = "as_is" + DiversionRelayPolicySIP DiversionRelayPolicy = "sip" + DiversionRelayPolicyTel DiversionRelayPolicy = "tel" +) diff --git a/resource/enums/export.go b/resource/enums/export.go index 4235876..431c88f 100644 --- a/resource/enums/export.go +++ b/resource/enums/export.go @@ -12,15 +12,15 @@ const ( type ExportStatus string const ( - ExportStatusPending ExportStatus = "Pending" - ExportStatusProcessing ExportStatus = "Processing" - ExportStatusCompleted ExportStatus = "Completed" + ExportStatusPending ExportStatus = "pending" + ExportStatusProcessing ExportStatus = "processing" + ExportStatusCompleted ExportStatus = "completed" ) // CallbackMethod defines the HTTP method used for webhook callbacks. type CallbackMethod string const ( - CallbackMethodPOST CallbackMethod = "POST" - CallbackMethodGET CallbackMethod = "GET" + CallbackMethodPOST CallbackMethod = "post" + CallbackMethodGET CallbackMethod = "get" ) diff --git a/resource/enums/export_test.go b/resource/enums/export_test.go index 22919ce..9b9d6af 100644 --- a/resource/enums/export_test.go +++ b/resource/enums/export_test.go @@ -26,9 +26,9 @@ func TestExportStatus(t *testing.T) { value ExportStatus expected string }{ - {"Pending", ExportStatusPending, "Pending"}, - {"Processing", ExportStatusProcessing, "Processing"}, - {"Completed", ExportStatusCompleted, "Completed"}, + {"Pending", ExportStatusPending, "pending"}, + {"Processing", ExportStatusProcessing, "processing"}, + {"Completed", ExportStatusCompleted, "completed"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -45,8 +45,8 @@ func TestCallbackMethod(t *testing.T) { value CallbackMethod expected string }{ - {"POST", CallbackMethodPOST, "POST"}, - {"GET", CallbackMethodGET, "GET"}, + {"POST", CallbackMethodPOST, "post"}, + {"GET", CallbackMethodGET, "get"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/resource/enums/feature.go b/resource/enums/feature.go index 17a6bc8..2406b61 100644 --- a/resource/enums/feature.go +++ b/resource/enums/feature.go @@ -4,9 +4,12 @@ package enums type Feature string const ( - FeatureVoiceIn Feature = "voice_in" - FeatureVoiceOut Feature = "voice_out" - FeatureT38 Feature = "t38" - FeatureSmsIn Feature = "sms_in" - FeatureSmsOut Feature = "sms_out" + FeatureVoiceIn Feature = "voice_in" + FeatureVoiceOut Feature = "voice_out" + FeatureT38 Feature = "t38" + FeatureSmsIn Feature = "sms_in" + FeatureP2P Feature = "p2p" + FeatureA2P Feature = "a2p" + FeatureEmergency Feature = "emergency" + FeatureCnamOut Feature = "cnam_out" ) diff --git a/resource/enums/feature_test.go b/resource/enums/feature_test.go index 0a25618..c90db3d 100644 --- a/resource/enums/feature_test.go +++ b/resource/enums/feature_test.go @@ -12,7 +12,6 @@ func TestFeature(t *testing.T) { {"VoiceOut", FeatureVoiceOut, "voice_out"}, {"T38", FeatureT38, "t38"}, {"SmsIn", FeatureSmsIn, "sms_in"}, - {"SmsOut", FeatureSmsOut, "sms_out"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/resource/enums/identity.go b/resource/enums/identity.go index 17cbdce..2db08f0 100644 --- a/resource/enums/identity.go +++ b/resource/enums/identity.go @@ -4,7 +4,7 @@ package enums type IdentityType string const ( - IdentityTypePersonal IdentityType = "Personal" - IdentityTypeBusiness IdentityType = "Business" - IdentityTypeAny IdentityType = "Any" + IdentityTypePersonal IdentityType = "personal" + IdentityTypeBusiness IdentityType = "business" + IdentityTypeAny IdentityType = "any" ) diff --git a/resource/enums/identity_test.go b/resource/enums/identity_test.go index 5762bdf..463e3fa 100644 --- a/resource/enums/identity_test.go +++ b/resource/enums/identity_test.go @@ -8,9 +8,9 @@ func TestIdentityType(t *testing.T) { value IdentityType expected string }{ - {"Personal", IdentityTypePersonal, "Personal"}, - {"Business", IdentityTypeBusiness, "Business"}, - {"Any", IdentityTypeAny, "Any"}, + {"Personal", IdentityTypePersonal, "personal"}, + {"Business", IdentityTypeBusiness, "business"}, + {"Any", IdentityTypeAny, "any"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/resource/enums/order.go b/resource/enums/order.go index e984585..b8c9116 100644 --- a/resource/enums/order.go +++ b/resource/enums/order.go @@ -4,7 +4,7 @@ package enums type OrderStatus string const ( - OrderStatusPending OrderStatus = "Pending" - OrderStatusCanceled OrderStatus = "Canceled" - OrderStatusCompleted OrderStatus = "Completed" + OrderStatusPending OrderStatus = "pending" + OrderStatusCanceled OrderStatus = "canceled" + OrderStatusCompleted OrderStatus = "completed" ) diff --git a/resource/enums/order_test.go b/resource/enums/order_test.go index a21b55e..1c001df 100644 --- a/resource/enums/order_test.go +++ b/resource/enums/order_test.go @@ -8,9 +8,9 @@ func TestOrderStatus(t *testing.T) { value OrderStatus expected string }{ - {"Pending", OrderStatusPending, "Pending"}, - {"Canceled", OrderStatusCanceled, "Canceled"}, - {"Completed", OrderStatusCompleted, "Completed"}, + {"Pending", OrderStatusPending, "pending"}, + {"Canceled", OrderStatusCanceled, "canceled"}, + {"Completed", OrderStatusCompleted, "completed"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/resource/export.go b/resource/export.go index e8d6ec0..e936de1 100644 --- a/resource/export.go +++ b/resource/export.go @@ -7,13 +7,30 @@ import ( ) // Export represents a CDR export. +// +// Filters map keys for cdr_in / cdr_out exports: +// +// - "from": ISO 8601 / "YYYY-MM-DD HH:MM:SS" lower bound, INCLUSIVE (time_start >= from). +// - "to": ISO 8601 / "YYYY-MM-DD HH:MM:SS" upper bound, EXCLUSIVE (time_start < to). +// - "did_number": only for cdr_in exports. +// - "voice_out_trunk_id": only for cdr_out exports. type Export struct { - ID string `json:"-" jsonapi:"exports"` - Status enums.ExportStatus `json:"status" api:"readonly"` - CreatedAt time.Time `json:"created_at" api:"readonly"` - URL *string `json:"url" api:"readonly"` - CallbackURL *string `json:"callback_url,omitempty"` - CallbackMethod *string `json:"callback_method,omitempty"` - ExportType enums.ExportType `json:"export_type"` - Filters map[string]interface{} `json:"filters,omitempty"` + ID string `json:"-" jsonapi:"exports"` + Status enums.ExportStatus `json:"status" api:"readonly"` + CreatedAt time.Time `json:"created_at" api:"readonly"` + URL *string `json:"url" api:"readonly"` + CallbackURL *string `json:"callback_url,omitempty"` + CallbackMethod *string `json:"callback_method,omitempty"` + ExportType enums.ExportType `json:"export_type"` + Filters map[string]interface{} `json:"filters,omitempty"` + ExternalReferenceID *string `json:"external_reference_id,omitempty"` } + +// IsPending returns true when the export status is "pending". +func (e *Export) IsPending() bool { return e.Status == enums.ExportStatusPending } + +// IsProcessing returns true when the export status is "processing". +func (e *Export) IsProcessing() bool { return e.Status == enums.ExportStatusProcessing } + +// IsCompleted returns true when the export status is "completed". +func (e *Export) IsCompleted() bool { return e.Status == enums.ExportStatusCompleted } diff --git a/resource/identity.go b/resource/identity.go index 0b0ed2e..24eabe8 100644 --- a/resource/identity.go +++ b/resource/identity.go @@ -25,7 +25,9 @@ type Identity struct { Verified bool `json:"verified" api:"readonly"` ContactEmail *string `json:"contact_email"` // Relationship IDs for create/update - CountryID string `json:"-" rel:"country,countries"` + CountryID string `json:"-" rel:"country,countries"` + BirthCountryID string `json:"-" rel:"birth_country,countries"` // Resolved relationships - Country *Country `json:"-" rel:"country"` + Country *Country `json:"-" rel:"country"` + BirthCountry *Country `json:"-" rel:"birth_country"` } diff --git a/resource/order.go b/resource/order.go index a3f07ea..995f83b 100644 --- a/resource/order.go +++ b/resource/order.go @@ -10,18 +10,28 @@ import ( // Order represents a DIDWW order. type Order struct { - ID string `json:"-" jsonapi:"orders"` - Amount string `json:"amount" api:"readonly"` - Status enums.OrderStatus `json:"status" api:"readonly"` - CreatedAt time.Time `json:"created_at" api:"readonly"` - Description string `json:"description" api:"readonly"` - Reference string `json:"reference" api:"readonly"` - Items []orderitem.OrderItem `json:"items"` - AllowBackOrdering bool `json:"allow_back_ordering,omitempty"` - CallbackURL *string `json:"callback_url,omitempty"` - CallbackMethod *string `json:"callback_method,omitempty"` + ID string `json:"-" jsonapi:"orders"` + Amount string `json:"amount" api:"readonly"` + Status enums.OrderStatus `json:"status" api:"readonly"` + CreatedAt time.Time `json:"created_at" api:"readonly"` + Description string `json:"description" api:"readonly"` + Reference string `json:"reference" api:"readonly"` + Items []orderitem.OrderItem `json:"items"` + AllowBackOrdering bool `json:"allow_back_ordering,omitempty"` + CallbackURL *string `json:"callback_url,omitempty"` + CallbackMethod *string `json:"callback_method,omitempty"` + ExternalReferenceID *string `json:"external_reference_id,omitempty"` } +// IsPending returns true when the order status is "pending". +func (o *Order) IsPending() bool { return o.Status == enums.OrderStatusPending } + +// IsCompleted returns true when the order status is "completed". +func (o *Order) IsCompleted() bool { return o.Status == enums.OrderStatusCompleted } + +// IsCanceled returns true when the order status is "canceled". +func (o *Order) IsCanceled() bool { return o.Status == enums.OrderStatusCanceled } + // UnmarshalJSON implements custom unmarshaling for Order. func (o *Order) UnmarshalJSON(data []byte) error { type Alias Order diff --git a/resource/orderitem/emergency_order_item.go b/resource/orderitem/emergency_order_item.go new file mode 100644 index 0000000..8ad25c1 --- /dev/null +++ b/resource/orderitem/emergency_order_item.go @@ -0,0 +1,10 @@ +package orderitem + +// EmergencyOrderItem represents an emergency service order item. +type EmergencyOrderItem struct { + BaseOrderItem + EmergencyCallingServiceID string `json:"emergency_calling_service_id,omitempty"` + Qty int `json:"qty,omitempty"` +} + +func (i *EmergencyOrderItem) orderItemType() string { return typeEmergencyOrderItems } diff --git a/resource/orderitem/order_item.go b/resource/orderitem/order_item.go index cd61230..0df4d7b 100644 --- a/resource/orderitem/order_item.go +++ b/resource/orderitem/order_item.go @@ -7,9 +7,10 @@ import ( ) const ( - typeDidOrderItems = "did_order_items" - typeCapacityOrderItems = "capacity_order_items" - typeGenericOrderItems = "generic_order_items" + typeDidOrderItems = "did_order_items" + typeCapacityOrderItems = "capacity_order_items" + typeGenericOrderItems = "generic_order_items" + typeEmergencyOrderItems = "emergency_order_items" ) // OrderItem is the interface for all order item types. @@ -45,6 +46,12 @@ func Parse(data []byte) (OrderItem, error) { return nil, err } return &item, nil + case typeEmergencyOrderItems: + var item EmergencyOrderItem + if err := json.Unmarshal(env.Attributes, &item); err != nil { + return nil, err + } + return &item, nil default: return nil, nil } diff --git a/resource/proof.go b/resource/proof.go index 0fbe25f..7d67ab0 100644 --- a/resource/proof.go +++ b/resource/proof.go @@ -9,9 +9,10 @@ import ( // Proof represents a proof document. type Proof struct { - ID string `json:"-" jsonapi:"proofs"` - CreatedAt time.Time `json:"created_at" api:"readonly"` - ExpiresAt *time.Time `json:"expires_at" api:"readonly"` + ID string `json:"-" jsonapi:"proofs"` + CreatedAt time.Time `json:"created_at" api:"readonly"` + ExpiresAt *time.Time `json:"expires_at" api:"readonly"` + ExternalReferenceID *string `json:"external_reference_id,omitempty"` // Polymorphic entity relationship (type: "identities" or "addresses") EntityID string `json:"-"` EntityType string `json:"-"` diff --git a/resource/requirement.go b/resource/requirement.go index 9c311c8..d7cabe1 100644 --- a/resource/requirement.go +++ b/resource/requirement.go @@ -1,8 +1,8 @@ package resource -// Requirement represents a regulatory requirement. -type Requirement struct { - ID string `json:"-" jsonapi:"requirements"` +// AddressRequirement represents a regulatory address requirement. +type AddressRequirement struct { + ID string `json:"-" jsonapi:"address_requirements"` IdentityType string `json:"identity_type"` PersonalAreaLevel string `json:"personal_area_level"` BusinessAreaLevel string `json:"business_area_level"` @@ -26,11 +26,11 @@ type Requirement struct { AddressProofTypes []*ProofType `json:"-" rel:"address_proof_types"` } -// RequirementValidation represents a requirement validation result. -type RequirementValidation struct { - ID string `json:"-" jsonapi:"requirement_validations"` +// AddressRequirementValidation represents an address requirement validation result. +type AddressRequirementValidation struct { + ID string `json:"-" jsonapi:"address_requirement_validations"` // Relationship IDs for create - AddressID string `json:"-" rel:"address,addresses"` - IdentityID string `json:"-" rel:"identity,identities"` - RequirementID string `json:"-" rel:"requirement,requirements"` + AddressID string `json:"-" rel:"address,addresses"` + IdentityID string `json:"-" rel:"identity,identities"` + AddressRequirementID string `json:"-" rel:"address_requirement,address_requirements"` } diff --git a/resource/status_predicates_test.go b/resource/status_predicates_test.go new file mode 100644 index 0000000..ef7c4c1 --- /dev/null +++ b/resource/status_predicates_test.go @@ -0,0 +1,111 @@ +package resource + +import ( + "testing" + + "github.com/didww/didww-api-3-go-sdk/resource/enums" +) + +func TestAddressVerificationStatusPredicates(t *testing.T) { + tests := []struct { + status enums.AddressVerificationStatus + expectPending, expectApproved, expectRejected bool + }{ + {enums.AddressVerificationStatusPending, true, false, false}, + {enums.AddressVerificationStatusApproved, false, true, false}, + {enums.AddressVerificationStatusRejected, false, false, true}, + } + for _, tt := range tests { + av := &AddressVerification{Status: tt.status} + if got := av.IsPending(); got != tt.expectPending { + t.Errorf("IsPending() for %q = %v, want %v", tt.status, got, tt.expectPending) + } + if got := av.IsApproved(); got != tt.expectApproved { + t.Errorf("IsApproved() for %q = %v, want %v", tt.status, got, tt.expectApproved) + } + if got := av.IsRejected(); got != tt.expectRejected { + t.Errorf("IsRejected() for %q = %v, want %v", tt.status, got, tt.expectRejected) + } + } +} + +func TestEmergencyVerificationStatusPredicates(t *testing.T) { + tests := []struct { + status string + expectPending, expectApproved, expectRejected bool + }{ + {"pending", true, false, false}, + {"approved", false, true, false}, + {"rejected", false, false, true}, + } + for _, tt := range tests { + ev := &EmergencyVerification{Status: tt.status} + if got := ev.IsPending(); got != tt.expectPending { + t.Errorf("IsPending() for %q = %v, want %v", tt.status, got, tt.expectPending) + } + if got := ev.IsApproved(); got != tt.expectApproved { + t.Errorf("IsApproved() for %q = %v, want %v", tt.status, got, tt.expectApproved) + } + if got := ev.IsRejected(); got != tt.expectRejected { + t.Errorf("IsRejected() for %q = %v, want %v", tt.status, got, tt.expectRejected) + } + } +} + +func TestEmergencyCallingServiceStatusPredicates(t *testing.T) { + tests := []struct { + status string + active, canceled, changesRequired, inProcess, newStatus, pendingUpdate bool + }{ + {ECSStatusActive, true, false, false, false, false, false}, + {ECSStatusCanceled, false, true, false, false, false, false}, + {ECSStatusChangesRequired, false, false, true, false, false, false}, + {ECSStatusInProcess, false, false, false, true, false, false}, + {ECSStatusNew, false, false, false, false, true, false}, + {ECSStatusPendingUpdate, false, false, false, false, false, true}, + } + for _, tt := range tests { + ecs := &EmergencyCallingService{Status: tt.status} + if got := ecs.IsActive(); got != tt.active { + t.Errorf("IsActive() for %q = %v, want %v", tt.status, got, tt.active) + } + if got := ecs.IsCanceled(); got != tt.canceled { + t.Errorf("IsCanceled() for %q = %v, want %v", tt.status, got, tt.canceled) + } + if got := ecs.IsChangesRequired(); got != tt.changesRequired { + t.Errorf("IsChangesRequired() for %q = %v, want %v", tt.status, got, tt.changesRequired) + } + if got := ecs.IsInProcess(); got != tt.inProcess { + t.Errorf("IsInProcess() for %q = %v, want %v", tt.status, got, tt.inProcess) + } + if got := ecs.IsNew(); got != tt.newStatus { + t.Errorf("IsNew() for %q = %v, want %v", tt.status, got, tt.newStatus) + } + if got := ecs.IsPendingUpdate(); got != tt.pendingUpdate { + t.Errorf("IsPendingUpdate() for %q = %v, want %v", tt.status, got, tt.pendingUpdate) + } + } +} + +func TestOrderStatusPredicates(t *testing.T) { + tests := []struct { + status enums.OrderStatus + expectPending, expectCompleted, expectCanceled bool + }{ + {enums.OrderStatusPending, true, false, false}, + {enums.OrderStatusCompleted, false, true, false}, + {enums.OrderStatusCanceled, false, false, true}, + } + for _, tt := range tests { + o := &Order{Status: tt.status} + if got := o.IsPending(); got != tt.expectPending { + t.Errorf("IsPending() for %q = %v, want %v", tt.status, got, tt.expectPending) + } + if got := o.IsCompleted(); got != tt.expectCompleted { + t.Errorf("IsCompleted() for %q = %v, want %v", tt.status, got, tt.expectCompleted) + } + if got := o.IsCanceled(); got != tt.expectCanceled { + t.Errorf("IsCanceled() for %q = %v, want %v", tt.status, got, tt.expectCanceled) + } + } +} diff --git a/resource/supporting_document.go b/resource/supporting_document.go index 2b098e5..e6cdc9a 100644 --- a/resource/supporting_document.go +++ b/resource/supporting_document.go @@ -12,8 +12,9 @@ type SupportingDocumentTemplate struct { // PermanentSupportingDocument represents a permanent supporting document. type PermanentSupportingDocument struct { - ID string `json:"-" jsonapi:"permanent_supporting_documents"` - CreatedAt time.Time `json:"created_at" api:"readonly"` + ID string `json:"-" jsonapi:"permanent_supporting_documents"` + CreatedAt time.Time `json:"created_at" api:"readonly"` + ExternalReferenceID *string `json:"external_reference_id,omitempty"` // Relationship IDs for create/update TemplateID string `json:"-" rel:"template,supporting_document_templates"` IdentityID string `json:"-" rel:"identity,identities"` diff --git a/resource/trunkconfiguration/sip_configuration.go b/resource/trunkconfiguration/sip_configuration.go index c504632..f70e7da 100644 --- a/resource/trunkconfiguration/sip_configuration.go +++ b/resource/trunkconfiguration/sip_configuration.go @@ -35,6 +35,7 @@ type SIPConfiguration struct { MediaEncryptionMode enums.MediaEncryptionMode `json:"media_encryption_mode,omitempty"` StirShakenMode enums.StirShakenMode `json:"stir_shaken_mode,omitempty"` AllowedRtpIPs []string `json:"allowed_rtp_ips,omitempty"` + DiversionRelayPolicy enums.DiversionRelayPolicy `json:"diversion_relay_policy,omitempty"` } func (c *SIPConfiguration) ConfigurationType() string { return "sip_configurations" } diff --git a/resource/voice_in_trunk.go b/resource/voice_in_trunk.go index cc81d7f..7ab8d27 100644 --- a/resource/voice_in_trunk.go +++ b/resource/voice_in_trunk.go @@ -10,17 +10,18 @@ import ( // VoiceInTrunk represents a voice inbound trunk. type VoiceInTrunk struct { - ID string `json:"-" jsonapi:"voice_in_trunks"` - Priority int `json:"priority,omitempty"` - CapacityLimit *int `json:"capacity_limit,omitempty"` - Weight int `json:"weight,omitempty"` - Name string `json:"name,omitempty"` - CliFormat enums.CliFormat `json:"cli_format,omitempty"` - CliPrefix *string `json:"cli_prefix,omitempty"` - Description *string `json:"description,omitempty"` - RingingTimeout *int `json:"ringing_timeout,omitempty"` - Configuration trunkconfiguration.TrunkConfiguration `json:"-"` - CreatedAt time.Time `json:"created_at" api:"readonly"` + ID string `json:"-" jsonapi:"voice_in_trunks"` + Priority int `json:"priority,omitempty"` + CapacityLimit *int `json:"capacity_limit,omitempty"` + Weight int `json:"weight,omitempty"` + Name string `json:"name,omitempty"` + CliFormat enums.CliFormat `json:"cli_format,omitempty"` + CliPrefix *string `json:"cli_prefix,omitempty"` + Description *string `json:"description,omitempty"` + RingingTimeout *int `json:"ringing_timeout,omitempty"` + Configuration trunkconfiguration.TrunkConfiguration `json:"-"` + CreatedAt time.Time `json:"created_at" api:"readonly"` + ExternalReferenceID *string `json:"external_reference_id,omitempty"` // Resolved relationships Pop *Pop `json:"-" rel:"pop"` VoiceInTrunkGroup *VoiceInTrunkGroup `json:"-" rel:"voice_in_trunk_group"` @@ -77,10 +78,11 @@ func (v VoiceInTrunk) MarshalJSON() ([]byte, error) { //nolint:gocritic // value // VoiceInTrunkGroup represents a group of voice inbound trunks. type VoiceInTrunkGroup struct { - ID string `json:"-" jsonapi:"voice_in_trunk_groups"` - Name string `json:"name,omitempty"` - CapacityLimit *int `json:"capacity_limit,omitempty"` - CreatedAt time.Time `json:"created_at" api:"readonly"` + ID string `json:"-" jsonapi:"voice_in_trunk_groups"` + Name string `json:"name,omitempty"` + CapacityLimit *int `json:"capacity_limit,omitempty"` + CreatedAt time.Time `json:"created_at" api:"readonly"` + ExternalReferenceID *string `json:"external_reference_id,omitempty"` // Relationship IDs for create/update VoiceInTrunkIDs []string `json:"-" rel:"voice_in_trunks,voice_in_trunks"` // Resolved relationships diff --git a/resource/voice_out_trunk.go b/resource/voice_out_trunk.go index ad9f1d1..ac99b9e 100644 --- a/resource/voice_out_trunk.go +++ b/resource/voice_out_trunk.go @@ -1,40 +1,106 @@ package resource import ( + "encoding/json" "time" + "github.com/didww/didww-api-3-go-sdk/jsonapi" + "github.com/didww/didww-api-3-go-sdk/resource/authenticationmethod" "github.com/didww/didww-api-3-go-sdk/resource/enums" ) // VoiceOutTrunk represents a voice outbound trunk. type VoiceOutTrunk struct { - ID string `json:"-" jsonapi:"voice_out_trunks"` - AllowedSipIPs []string `json:"allowed_sip_ips,omitempty"` - AllowedRtpIPs []string `json:"allowed_rtp_ips,omitempty"` - AllowAnyDidAsCli bool `json:"allow_any_did_as_cli,omitempty"` - Status enums.VoiceOutTrunkStatus `json:"status" api:"readonly"` - OnCliMismatchAction enums.OnCliMismatchAction `json:"on_cli_mismatch_action,omitempty"` - Name string `json:"name,omitempty"` - CapacityLimit *int `json:"capacity_limit,omitempty"` - Username string `json:"username" api:"readonly"` - Password string `json:"password" api:"readonly"` - CreatedAt time.Time `json:"created_at" api:"readonly"` - ThresholdReached bool `json:"threshold_reached" api:"readonly"` - ThresholdAmount *string `json:"threshold_amount,omitempty"` - MediaEncryptionMode enums.MediaEncryptionMode `json:"media_encryption_mode,omitempty"` - DefaultDstAction enums.DefaultDstAction `json:"default_dst_action,omitempty"` - DstPrefixes []string `json:"dst_prefixes,omitempty"` - ForceSymmetricRtp bool `json:"force_symmetric_rtp,omitempty"` - RtpPing bool `json:"rtp_ping,omitempty"` - CallbackURL *string `json:"callback_url,omitempty"` + ID string `json:"-" jsonapi:"voice_out_trunks"` + AllowedRtpIPs []string `json:"allowed_rtp_ips,omitempty"` + AllowAnyDidAsCli bool `json:"allow_any_did_as_cli,omitempty"` + Status enums.VoiceOutTrunkStatus `json:"status" api:"readonly"` + OnCliMismatchAction enums.OnCliMismatchAction `json:"on_cli_mismatch_action,omitempty"` + Name string `json:"name,omitempty"` + CapacityLimit *int `json:"capacity_limit,omitempty"` + CreatedAt time.Time `json:"created_at" api:"readonly"` + ThresholdReached bool `json:"threshold_reached" api:"readonly"` + ThresholdAmount *string `json:"threshold_amount,omitempty"` + MediaEncryptionMode enums.MediaEncryptionMode `json:"media_encryption_mode,omitempty"` + DefaultDstAction enums.DefaultDstAction `json:"default_dst_action,omitempty"` + DstPrefixes []string `json:"dst_prefixes,omitempty"` + ForceSymmetricRtp bool `json:"force_symmetric_rtp,omitempty"` + RtpPing bool `json:"rtp_ping,omitempty"` + CallbackURL *string `json:"callback_url,omitempty"` + ExternalReferenceID *string `json:"external_reference_id,omitempty"` + EmergencyEnableAll bool `json:"emergency_enable_all,omitempty"` + RtpTimeout *int `json:"rtp_timeout,omitempty"` + AuthenticationMethod authenticationmethod.AuthenticationMethod `json:"-"` // Relationship IDs for create/update - DefaultDIDID string `json:"-" rel:"default_did,dids"` - DIDIDs []string `json:"-" rel:"dids,dids"` + DefaultDIDID string `json:"-" rel:"default_did,dids"` + DIDIDs []string `json:"-" rel:"dids,dids"` + EmergencyDIDIDs []string `json:"-" rel:"emergency_dids,dids"` + // ClearEmergencyDIDs, when true, sends {"data": []} for the + // emergency_dids relationship (remove all emergency DIDs from this trunk). + ClearEmergencyDIDs bool `json:"-"` // Resolved relationships - DefaultDID *DID `json:"-" rel:"default_did"` - DIDs []*DID `json:"-" rel:"dids"` + DefaultDID *DID `json:"-" rel:"default_did"` + DIDs []*DID `json:"-" rel:"dids"` + EmergencyDIDs []*DID `json:"-" rel:"emergency_dids"` } +// MarshalJSON handles custom serialization for VoiceOutTrunk. +func (v *VoiceOutTrunk) MarshalJSON() ([]byte, error) { + type Alias VoiceOutTrunk + aux := struct { + Alias + AuthenticationMethod json.RawMessage `json:"authentication_method,omitempty"` + }{ + Alias: Alias(*v), + } + if v.AuthenticationMethod != nil { + am, err := authenticationmethod.MarshalJSON(v.AuthenticationMethod) + if err != nil { + return nil, err + } + aux.AuthenticationMethod = am + } + return json.Marshal(aux) +} + +// UnmarshalJSON handles custom deserialization for VoiceOutTrunk. +func (v *VoiceOutTrunk) UnmarshalJSON(data []byte) error { + type Alias VoiceOutTrunk + aux := &struct { + *Alias + AuthenticationMethod json.RawMessage `json:"authentication_method"` + }{ + Alias: (*Alias)(v), + } + if err := json.Unmarshal(data, aux); err != nil { + return err + } + if len(aux.AuthenticationMethod) > 0 && string(aux.AuthenticationMethod) != "null" { + am, err := authenticationmethod.UnmarshalJSON(aux.AuthenticationMethod) + if err != nil { + return err + } + v.AuthenticationMethod = am + } + return nil +} + +// MarshalRelationships implements RelationshipMarshaler for VoiceOutTrunk. +// When ClearEmergencyDIDs is true, emits {"data": []} for emergency_dids. +func (v *VoiceOutTrunk) MarshalRelationships() (map[string]any, error) { + rels := make(map[string]any) + if v.ClearEmergencyDIDs { + rels["emergency_dids"] = jsonapi.ToManyRelationship([]jsonapi.RelationshipRef{}) + } + return rels, nil +} + +// IsActive returns true when the trunk status is "active". +func (v *VoiceOutTrunk) IsActive() bool { return v.Status == enums.VoiceOutTrunkStatusActive } + +// IsBlocked returns true when the trunk status is "blocked". +func (v *VoiceOutTrunk) IsBlocked() bool { return v.Status == enums.VoiceOutTrunkStatusBlocked } + // VoiceOutTrunkRegenerateCredential represents a credential regeneration for voice out trunks. type VoiceOutTrunkRegenerateCredential struct { ID string `json:"-" jsonapi:"voice_out_trunk_regenerate_credentials"` diff --git a/testdata/fixtures/requirement_validations/create.json b/testdata/fixtures/address_requirement_validations/create.json similarity index 71% rename from testdata/fixtures/requirement_validations/create.json rename to testdata/fixtures/address_requirement_validations/create.json index d431ab1..046f6e3 100644 --- a/testdata/fixtures/requirement_validations/create.json +++ b/testdata/fixtures/address_requirement_validations/create.json @@ -1,7 +1,7 @@ { "data": { "id": "aea92b24-a044-4864-9740-89d3e15b65c7", - "type": "requirement_validations" + "type": "address_requirement_validations" }, "meta": { "api_version": "2021-04-19" diff --git a/testdata/fixtures/requirement_validations/create_error_validation.json b/testdata/fixtures/address_requirement_validations/create_error_validation.json similarity index 100% rename from testdata/fixtures/requirement_validations/create_error_validation.json rename to testdata/fixtures/address_requirement_validations/create_error_validation.json diff --git a/testdata/fixtures/requirement_validations/create_request.json b/testdata/fixtures/address_requirement_validations/create_request.json similarity index 71% rename from testdata/fixtures/requirement_validations/create_request.json rename to testdata/fixtures/address_requirement_validations/create_request.json index 2131aab..dcae3a4 100644 --- a/testdata/fixtures/requirement_validations/create_request.json +++ b/testdata/fixtures/address_requirement_validations/create_request.json @@ -1,6 +1,6 @@ { "data": { - "type": "requirement_validations", + "type": "address_requirement_validations", "attributes": {}, "relationships": { "address": { @@ -9,9 +9,9 @@ "id": "d3414687-40f4-4346-a267-c2c65117d28c" } }, - "requirement": { + "address_requirement": { "data": { - "type": "requirements", + "type": "address_requirements", "id": "aea92b24-a044-4864-9740-89d3e15b65c7" } } diff --git a/testdata/fixtures/requirement_validations/create_request_failed.json b/testdata/fixtures/address_requirement_validations/create_request_failed.json similarity index 78% rename from testdata/fixtures/requirement_validations/create_request_failed.json rename to testdata/fixtures/address_requirement_validations/create_request_failed.json index bc7e8d4..aa7ee15 100644 --- a/testdata/fixtures/requirement_validations/create_request_failed.json +++ b/testdata/fixtures/address_requirement_validations/create_request_failed.json @@ -1,6 +1,6 @@ { "data": { - "type": "requirement_validations", + "type": "address_requirement_validations", "attributes": {}, "relationships": { "identity": { @@ -15,9 +15,9 @@ "id": "d3414687-40f4-4346-a267-c2c65117d28c" } }, - "requirement": { + "address_requirement": { "data": { - "type": "requirements", + "type": "address_requirements", "id": "2efc3427-8ba6-4d50-875d-f2de4a068de8" } } diff --git a/testdata/fixtures/requirements/index.json b/testdata/fixtures/address_requirements/index.json similarity index 94% rename from testdata/fixtures/requirements/index.json rename to testdata/fixtures/address_requirements/index.json index d4876b1..237991b 100644 --- a/testdata/fixtures/requirements/index.json +++ b/testdata/fixtures/address_requirements/index.json @@ -2,12 +2,12 @@ "data": [ { "id": "b6c80acb-3952-4d53-9e62-fe2348c0636b", - "type": "requirements", + "type": "address_requirements", "attributes": { - "identity_type": "Any", - "personal_area_level": "Country", - "business_area_level": "WorldWide", - "address_area_level": "Area", + "identity_type": "any", + "personal_area_level": "country", + "business_area_level": "world_wide", + "address_area_level": "area", "personal_proof_qty": 1, "business_proof_qty": 2, "address_proof_qty": 1, @@ -77,12 +77,12 @@ }, { "id": "51b293af-a496-4bf2-9c68-5221b3b0dc1e", - "type": "requirements", + "type": "address_requirements", "attributes": { - "identity_type": "Any", - "personal_area_level": "Country", - "business_area_level": "Country", - "address_area_level": "Country", + "identity_type": "any", + "personal_area_level": "country", + "business_area_level": "country", + "address_area_level": "country", "personal_proof_qty": 2, "business_proof_qty": 1, "address_proof_qty": 1, @@ -154,12 +154,12 @@ }, { "id": "f7c2a9a4-a40e-4f22-98ff-0f53c86052ac", - "type": "requirements", + "type": "address_requirements", "attributes": { - "identity_type": "Any", - "personal_area_level": "WorldWide", - "business_area_level": "Country", - "address_area_level": "WorldWide", + "identity_type": "any", + "personal_area_level": "world_wide", + "business_area_level": "country", + "address_area_level": "world_wide", "personal_proof_qty": 1, "business_proof_qty": 1, "address_proof_qty": 1, @@ -229,12 +229,12 @@ }, { "id": "edb71cff-d5e8-44ff-ba36-6f758066c175", - "type": "requirements", + "type": "address_requirements", "attributes": { - "identity_type": "Any", - "personal_area_level": "Country", - "business_area_level": "Country", - "address_area_level": "Country", + "identity_type": "any", + "personal_area_level": "country", + "business_area_level": "country", + "address_area_level": "country", "personal_proof_qty": 1, "business_proof_qty": 1, "address_proof_qty": 0, @@ -306,12 +306,12 @@ }, { "id": "90b72a1a-1ebd-4771-b818-f7123f8ff7ec", - "type": "requirements", + "type": "address_requirements", "attributes": { - "identity_type": "Any", - "personal_area_level": "WorldWide", - "business_area_level": "WorldWide", - "address_area_level": "WorldWide", + "identity_type": "any", + "personal_area_level": "world_wide", + "business_area_level": "world_wide", + "address_area_level": "world_wide", "personal_proof_qty": 0, "business_proof_qty": 0, "address_proof_qty": 0, diff --git a/testdata/fixtures/requirements/show.json b/testdata/fixtures/address_requirements/show.json similarity index 97% rename from testdata/fixtures/requirements/show.json rename to testdata/fixtures/address_requirements/show.json index 786d2cc..e7bbd42 100644 --- a/testdata/fixtures/requirements/show.json +++ b/testdata/fixtures/address_requirements/show.json @@ -1,12 +1,12 @@ { "data": { "id": "25d12afe-1ec6-4fe3-9621-b250dd1fb959", - "type": "requirements", + "type": "address_requirements", "attributes": { - "identity_type": "Any", - "personal_area_level": "WorldWide", - "business_area_level": "WorldWide", - "address_area_level": "WorldWide", + "identity_type": "any", + "personal_area_level": "world_wide", + "business_area_level": "world_wide", + "address_area_level": "world_wide", "personal_proof_qty": 1, "business_proof_qty": 1, "address_proof_qty": 1, diff --git a/testdata/fixtures/address_verifications/create.json b/testdata/fixtures/address_verifications/create.json index bd52a86..f17f0e1 100644 --- a/testdata/fixtures/address_verifications/create.json +++ b/testdata/fixtures/address_verifications/create.json @@ -5,8 +5,8 @@ "attributes": { "service_description": null, "callback_url": "http://example.com", - "callback_method": "GET", - "status": "Pending", + "callback_method": "get", + "status": "pending", "reject_reasons": null, "created_at": "2021-04-01T15:01:32.668Z" }, diff --git a/testdata/fixtures/address_verifications/create_request.json b/testdata/fixtures/address_verifications/create_request.json index ba198e1..cbc9b47 100644 --- a/testdata/fixtures/address_verifications/create_request.json +++ b/testdata/fixtures/address_verifications/create_request.json @@ -1 +1 @@ -{"data":{"type":"address_verifications","attributes":{"callback_url":"http://example.com","callback_method":"GET"},"relationships":{"address":{"data":{"type":"addresses","id":"d3414687-40f4-4346-a267-c2c65117d28c"}},"dids":{"data":[{"type":"dids","id":"a9d64c02-4486-4acb-a9a1-be4c81ff0659"}]}}}} \ No newline at end of file +{"data":{"type":"address_verifications","attributes":{"callback_url":"http://example.com","callback_method":"get"},"relationships":{"address":{"data":{"type":"addresses","id":"d3414687-40f4-4346-a267-c2c65117d28c"}},"dids":{"data":[{"type":"dids","id":"a9d64c02-4486-4acb-a9a1-be4c81ff0659"}]}}}} \ No newline at end of file diff --git a/testdata/fixtures/address_verifications/index.json b/testdata/fixtures/address_verifications/index.json index 25c23ac..18ff53b 100644 --- a/testdata/fixtures/address_verifications/index.json +++ b/testdata/fixtures/address_verifications/index.json @@ -6,8 +6,8 @@ "attributes": { "service_description": null, "callback_url": "http://example.com", - "callback_method": "GET", - "status": "Pending", + "callback_method": "get", + "status": "pending", "reject_reasons": null, "created_at": "2021-04-01T14:29:11.790Z" }, diff --git a/testdata/fixtures/address_verifications/show.json b/testdata/fixtures/address_verifications/show.json index 5363a28..382068a 100644 --- a/testdata/fixtures/address_verifications/show.json +++ b/testdata/fixtures/address_verifications/show.json @@ -6,7 +6,7 @@ "service_description": null, "callback_url": null, "callback_method": null, - "status": "Approved", + "status": "approved", "reject_reasons": null, "created_at": "2020-09-15T06:38:12.650Z", "reference": "SHB-485120" diff --git a/testdata/fixtures/address_verifications/show_rejected.json b/testdata/fixtures/address_verifications/show_rejected.json index 715b8f6..5f996a0 100644 --- a/testdata/fixtures/address_verifications/show_rejected.json +++ b/testdata/fixtures/address_verifications/show_rejected.json @@ -6,8 +6,8 @@ "service_description": null, "callback_url": null, "callback_method": null, - "status": "Rejected", - "reject_reasons": "Address cannot be validated; Proof of address should be not older than of 6 months", + "status": "rejected", + "reject_reasons": ["Address cannot be validated", "Proof of address should be not older than of 6 months"], "reference": "ODW-879912", "created_at": "2020-10-28T08:29:29.960Z" }, diff --git a/testdata/fixtures/address_verifications/update.json b/testdata/fixtures/address_verifications/update.json new file mode 100644 index 0000000..67f8bf0 --- /dev/null +++ b/testdata/fixtures/address_verifications/update.json @@ -0,0 +1,20 @@ +{ + "data": { + "id": "c8e004b0-87ec-4987-b4fb-ee89db099f0e", + "type": "address_verifications", + "attributes": { + "service_description": null, + "callback_url": null, + "callback_method": null, + "status": "approved", + "reject_reasons": null, + "reference": "SHB-485120", + "reject_comment": null, + "external_reference_id": "ext-ref-123", + "created_at": "2020-10-28T08:29:29.960Z" + } + }, + "meta": { + "api_version": "2026-04-16" + } +} diff --git a/testdata/fixtures/address_verifications/update_request.json b/testdata/fixtures/address_verifications/update_request.json new file mode 100644 index 0000000..7060668 --- /dev/null +++ b/testdata/fixtures/address_verifications/update_request.json @@ -0,0 +1 @@ +{"data":{"id":"c8e004b0-87ec-4987-b4fb-ee89db099f0e","type":"address_verifications","attributes":{"external_reference_id":"ext-ref-123"}}} diff --git a/testdata/fixtures/addresses/index.json b/testdata/fixtures/addresses/index.json index db5f6be..520267d 100644 --- a/testdata/fixtures/addresses/index.json +++ b/testdata/fixtures/addresses/index.json @@ -76,7 +76,7 @@ "vat_id": null, "description": null, "personal_tax_id": null, - "identity_type": "Personal", + "identity_type": "personal", "created_at": "2020-10-09T10:21:04.835Z", "external_reference_id": null, "verified": false diff --git a/testdata/fixtures/did_groups/show_with_requirement.json b/testdata/fixtures/did_groups/show_with_requirement.json index b8a4cf2..d4ad2d7 100644 --- a/testdata/fixtures/did_groups/show_with_requirement.json +++ b/testdata/fixtures/did_groups/show_with_requirement.json @@ -44,13 +44,13 @@ "related": "https://sandbox-api.didww.com/v3/did_groups/2187c36d-28fb-436f-8861-5a0f5b5a3ee1/stock_keeping_units" } }, - "requirement": { + "address_requirement": { "links": { - "self": "https://sandbox-api.didww.com/v3/did_groups/2187c36d-28fb-436f-8861-5a0f5b5a3ee1/relationships/requirement", - "related": "https://sandbox-api.didww.com/v3/did_groups/2187c36d-28fb-436f-8861-5a0f5b5a3ee1/requirement" + "self": "https://sandbox-api.didww.com/v3/did_groups/2187c36d-28fb-436f-8861-5a0f5b5a3ee1/relationships/address_requirement", + "related": "https://sandbox-api.didww.com/v3/did_groups/2187c36d-28fb-436f-8861-5a0f5b5a3ee1/address_requirement" }, "data": { - "type": "requirements", + "type": "address_requirements", "id": "8da1e0b2-047c-4baf-9c57-57143f09b9ce" } } @@ -64,12 +64,12 @@ "included": [ { "id": "8da1e0b2-047c-4baf-9c57-57143f09b9ce", - "type": "requirements", + "type": "address_requirements", "attributes": { - "identity_type": "Any", - "personal_area_level": "WorldWide", - "business_area_level": "Country", - "address_area_level": "City", + "identity_type": "any", + "personal_area_level": "world_wide", + "business_area_level": "country", + "address_area_level": "city", "personal_proof_qty": 1, "business_proof_qty": 1, "address_proof_qty": 1, diff --git a/testdata/fixtures/did_history/index.json b/testdata/fixtures/did_history/index.json new file mode 100644 index 0000000..7ed2744 --- /dev/null +++ b/testdata/fixtures/did_history/index.json @@ -0,0 +1,27 @@ +{ + "data": [ + { + "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "type": "did_history", + "attributes": { + "did_number": "12025551234", + "action": "assigned", + "method": "api3", + "created_at": "2026-04-10T12:00:00.000Z" + } + }, + { + "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901", + "type": "did_history", + "attributes": { + "did_number": "12025551234", + "action": "renewed", + "method": "system", + "created_at": "2026-04-11T12:00:00.000Z" + } + } + ], + "meta": { + "api_version": "2026-04-16" + } +} diff --git a/testdata/fixtures/did_history/show.json b/testdata/fixtures/did_history/show.json new file mode 100644 index 0000000..b790864 --- /dev/null +++ b/testdata/fixtures/did_history/show.json @@ -0,0 +1,15 @@ +{ + "data": { + "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "type": "did_history", + "attributes": { + "did_number": "12025551234", + "action": "assigned", + "method": "api3", + "created_at": "2026-04-10T12:00:00.000Z" + } + }, + "meta": { + "api_version": "2026-04-16" + } +} diff --git a/testdata/fixtures/did_history/show_billing_cycles_count_changed.json b/testdata/fixtures/did_history/show_billing_cycles_count_changed.json new file mode 100644 index 0000000..a7cac18 --- /dev/null +++ b/testdata/fixtures/did_history/show_billing_cycles_count_changed.json @@ -0,0 +1,19 @@ +{ + "data": { + "id": "c3d4e5f6-a7b8-9012-cdef-123456789012", + "type": "did_history", + "attributes": { + "did_number": "12025551234", + "action": "billing_cycles_count_changed", + "method": "system", + "created_at": "2026-04-12T12:00:00.000Z" + }, + "meta": { + "from": "2", + "to": "1" + } + }, + "meta": { + "api_version": "2026-04-16" + } +} diff --git a/testdata/fixtures/did_reservations/create.json b/testdata/fixtures/did_reservations/create.json index 07bd08b..bec7449 100644 --- a/testdata/fixtures/did_reservations/create.json +++ b/testdata/fixtures/did_reservations/create.json @@ -3,7 +3,7 @@ "id": "fd38d3ff-80cf-4e67-a605-609a2884a5c4", "type": "did_reservations", "attributes": { - "expire_at": "2018-12-28T16:22:00.417Z", + "expires_at": "2018-12-28T16:22:00.417Z", "created_at": "2018-12-28T16:12:00.440Z", "description": "DIDWW" }, diff --git a/testdata/fixtures/did_reservations/index.json b/testdata/fixtures/did_reservations/index.json index 2f97c50..753ae8a 100644 --- a/testdata/fixtures/did_reservations/index.json +++ b/testdata/fixtures/did_reservations/index.json @@ -4,7 +4,7 @@ "id": "fd38d3ff-80cf-4e67-a605-609a2884a5c4", "type": "did_reservations", "attributes": { - "expire_at": "2018-12-28T16:22:00.417Z", + "expires_at": "2018-12-28T16:22:00.417Z", "created_at": "2018-12-28T16:12:00.440Z", "description": "DIDWW" }, diff --git a/testdata/fixtures/did_reservations/show.json b/testdata/fixtures/did_reservations/show.json index 62d328c..4561e74 100644 --- a/testdata/fixtures/did_reservations/show.json +++ b/testdata/fixtures/did_reservations/show.json @@ -3,7 +3,7 @@ "id": "fd38d3ff-80cf-4e67-a605-609a2884a5c4", "type": "did_reservations", "attributes": { - "expire_at": "2018-12-28T16:22:00.417Z", + "expires_at": "2018-12-28T16:22:00.417Z", "created_at": "2018-12-28T16:12:00.440Z", "description": "DIDWW" }, diff --git a/testdata/fixtures/dids/index.json b/testdata/fixtures/dids/index.json index 46a5f07..9135136 100644 --- a/testdata/fixtures/dids/index.json +++ b/testdata/fixtures/dids/index.json @@ -184,7 +184,7 @@ "type": "orders", "attributes": { "amount": "0.37", - "status": "Completed", + "status": "completed", "created_at": "2018-12-27T09:59:54.892Z", "description": "DID", "reference": "TZO-560180", diff --git a/testdata/fixtures/dids/show_with_address_verification_and_did_group.json b/testdata/fixtures/dids/show_with_address_verification_and_did_group.json index c144421..f3c8124 100644 --- a/testdata/fixtures/dids/show_with_address_verification_and_did_group.json +++ b/testdata/fixtures/dids/show_with_address_verification_and_did_group.json @@ -78,7 +78,6 @@ "voice_in", "voice_out", "sms_in", - "sms_out", "t38" ], "is_metered": false, @@ -136,8 +135,8 @@ "attributes": { "service_description": "Address verification for registration 01kjan2paw2wfqndqdkd2m3hyc", "callback_url": "https://didwwwebhook.aurora-dev.sinchlab.com/AuroraDIDWWWebhookService/api/didww/verification/callback/119881627", - "callback_method": "POST", - "status": "Approved", + "callback_method": "post", + "status": "approved", "reject_reasons": null, "reference": "AHB-291174", "created_at": "2026-02-27T13:48:55.368Z" diff --git a/testdata/fixtures/dids/unassign_emergency_calling_service.json b/testdata/fixtures/dids/unassign_emergency_calling_service.json new file mode 100644 index 0000000..d16f9e2 --- /dev/null +++ b/testdata/fixtures/dids/unassign_emergency_calling_service.json @@ -0,0 +1,35 @@ +{ + "data": { + "id": "44957076-778a-4802-b60c-d22db0cda284", + "type": "dids", + "attributes": { + "blocked": false, + "capacity_limit": 1, + "description": "string", + "terminated": false, + "awaiting_registration": false, + "number": "437xxxxxxxxx", + "expires_at": "2026-06-25T08:21:41.795Z", + "channels_included_count": 2, + "created_at": "2026-06-25T08:21:41.795Z", + "billing_cycles_count": 1, + "dedicated_channels_count": 0, + "emergency_enabled": false + }, + "relationships": { + "did_group": { + "links": { + "self": "https://sandbox-api.didww.com/v3/dids/44957076-778a-4802-b60c-d22db0cda284/relationships/did_group", + "related": "https://sandbox-api.didww.com/v3/dids/44957076-778a-4802-b60c-d22db0cda284/did_group" + } + }, + "emergency_calling_service": { + "links": { + "self": "https://sandbox-api.didww.com/v3/dids/44957076-778a-4802-b60c-d22db0cda284/relationships/emergency_calling_service", + "related": "https://sandbox-api.didww.com/v3/dids/44957076-778a-4802-b60c-d22db0cda284/emergency_calling_service" + }, + "data": null + } + } + } +} diff --git a/testdata/fixtures/dids/unassign_emergency_calling_service_request.json b/testdata/fixtures/dids/unassign_emergency_calling_service_request.json new file mode 100644 index 0000000..244454a --- /dev/null +++ b/testdata/fixtures/dids/unassign_emergency_calling_service_request.json @@ -0,0 +1 @@ +{"data":{"id":"44957076-778a-4802-b60c-d22db0cda284","type":"dids","attributes":{},"relationships":{"emergency_calling_service":{"data":null}}}} \ No newline at end of file diff --git a/testdata/fixtures/emergency_calling_services/index.json b/testdata/fixtures/emergency_calling_services/index.json new file mode 100644 index 0000000..2370d6a --- /dev/null +++ b/testdata/fixtures/emergency_calling_services/index.json @@ -0,0 +1,32 @@ +{ + "data": [ + { + "id": "ecs-001-id", + "type": "emergency_calling_services", + "attributes": { + "name": "E911 Service US", + "reference": "ECS-12345", + "status": "active", + "activated_at": "2026-04-01T10:00:00.000Z", + "canceled_at": null, + "created_at": "2026-03-15T08:00:00.000Z", + "renew_date": "2026-05-01" + }, + "meta": { + "setup_price": "0.0", + "monthly_price": "1.5" + }, + "relationships": { + "country": { + "data": { "type": "countries", "id": "us-country" } + }, + "did_group_type": { + "data": { "type": "did_group_types", "id": "local-dgt" } + } + } + } + ], + "meta": { + "api_version": "2026-04-16" + } +} diff --git a/testdata/fixtures/emergency_calling_services/show_with_includes.json b/testdata/fixtures/emergency_calling_services/show_with_includes.json new file mode 100644 index 0000000..6c22f62 --- /dev/null +++ b/testdata/fixtures/emergency_calling_services/show_with_includes.json @@ -0,0 +1,81 @@ +{ + "data": { + "id": "ecs-001-id", + "type": "emergency_calling_services", + "attributes": { + "name": "E911 Service US", + "reference": "ECS-12345", + "status": "active", + "activated_at": "2026-04-01T10:00:00.000Z", + "canceled_at": null, + "created_at": "2026-03-15T08:00:00.000Z", + "renew_date": "2026-05-01" + }, + "meta": { + "setup_price": "0.0", + "monthly_price": "1.5" + }, + "relationships": { + "country": { + "data": { "type": "countries", "id": "us-country" } + }, + "did_group_type": { + "data": { "type": "did_group_types", "id": "local-dgt" } + }, + "emergency_requirement": { + "data": { "type": "emergency_requirements", "id": "ereq-001-id" } + }, + "emergency_verification": { + "data": { "type": "emergency_verifications", "id": "ever-001-id" } + } + } + }, + "included": [ + { + "id": "ereq-001-id", + "type": "emergency_requirements", + "attributes": { + "identity_type": "personal", + "address_area_level": "city", + "personal_area_level": "city", + "business_area_level": "country", + "address_mandatory_fields": ["city", "postal_code"], + "personal_mandatory_fields": ["first_name", "last_name"], + "business_mandatory_fields": ["company_name"] + }, + "relationships": { + "country": { + "data": { "type": "countries", "id": "us-country" } + }, + "did_group_type": { + "data": { "type": "did_group_types", "id": "local-dgt" } + } + } + }, + { + "id": "ever-001-id", + "type": "emergency_verifications", + "attributes": { + "reference": "EVR-54321", + "status": "approved", + "reject_reasons": null, + "reject_comment": "", + "callback_url": null, + "callback_method": null, + "external_reference_id": null, + "created_at": "2026-03-20T14:00:00.000Z" + }, + "relationships": { + "address": { + "data": { "type": "addresses", "id": "addr-001-id" } + }, + "emergency_calling_service": { + "data": { "type": "emergency_calling_services", "id": "ecs-001-id" } + } + } + } + ], + "meta": { + "api_version": "2026-04-16" + } +} diff --git a/testdata/fixtures/emergency_requirement_validations/create.json b/testdata/fixtures/emergency_requirement_validations/create.json new file mode 100644 index 0000000..10f603d --- /dev/null +++ b/testdata/fixtures/emergency_requirement_validations/create.json @@ -0,0 +1,9 @@ +{ + "data": { + "id": "c1d2e3f4-a5b6-7890-1234-567890abcdef", + "type": "emergency_requirement_validations" + }, + "meta": { + "api_version": "2026-04-16" + } +} diff --git a/testdata/fixtures/emergency_requirement_validations/create_request.json b/testdata/fixtures/emergency_requirement_validations/create_request.json new file mode 100644 index 0000000..7d55859 --- /dev/null +++ b/testdata/fixtures/emergency_requirement_validations/create_request.json @@ -0,0 +1,26 @@ +{ + "data": { + "type": "emergency_requirement_validations", + "attributes": {}, + "relationships": { + "emergency_requirement": { + "data": { + "type": "emergency_requirements", + "id": "c1d2e3f4-a5b6-7890-1234-567890abcdef" + } + }, + "address": { + "data": { + "type": "addresses", + "id": "d3414687-40f4-4346-a267-c2c65117d28c" + } + }, + "identity": { + "data": { + "type": "identities", + "id": "5e9df058-50d2-4e34-b0d4-d1746b86f41a" + } + } + } + } +} diff --git a/testdata/fixtures/emergency_requirements/index.json b/testdata/fixtures/emergency_requirements/index.json new file mode 100644 index 0000000..157df2f --- /dev/null +++ b/testdata/fixtures/emergency_requirements/index.json @@ -0,0 +1,34 @@ +{ + "data": [ + { + "id": "c1d2e3f4-a5b6-7890-1234-567890abcdef", + "type": "emergency_requirements", + "attributes": { + "identity_type": "any", + "address_area_level": "city", + "personal_area_level": "world_wide", + "business_area_level": "country", + "address_mandatory_fields": ["city", "postal_code"], + "personal_mandatory_fields": ["first_name", "last_name"], + "business_mandatory_fields": ["company_name"], + "estimate_setup_time": "7-14 days", + "requirement_restriction_message": null + }, + "meta": { + "setup_price": "0.0", + "monthly_price": "1.5" + }, + "relationships": { + "country": { + "data": { "type": "countries", "id": "abc-country" } + }, + "did_group_type": { + "data": { "type": "did_group_types", "id": "abc-dgt" } + } + } + } + ], + "meta": { + "api_version": "2026-04-16" + } +} diff --git a/testdata/fixtures/emergency_verifications/create.json b/testdata/fixtures/emergency_verifications/create.json new file mode 100644 index 0000000..b42df83 --- /dev/null +++ b/testdata/fixtures/emergency_verifications/create.json @@ -0,0 +1,19 @@ +{ + "data": { + "id": "ev-new-id", + "type": "emergency_verifications", + "attributes": { + "reference": "EV-NEW", + "status": "pending", + "reject_reasons": null, + "reject_comment": null, + "callback_url": null, + "callback_method": null, + "external_reference_id": null, + "created_at": "2026-04-16T10:00:00.000Z" + } + }, + "meta": { + "api_version": "2026-04-16" + } +} diff --git a/testdata/fixtures/emergency_verifications/create_request.json b/testdata/fixtures/emergency_verifications/create_request.json new file mode 100644 index 0000000..96d056b --- /dev/null +++ b/testdata/fixtures/emergency_verifications/create_request.json @@ -0,0 +1,20 @@ +{ + "data": { + "type": "emergency_verifications", + "attributes": {}, + "relationships": { + "address": { + "data": { + "type": "addresses", + "id": "d3414687-40f4-4346-a267-c2c65117d28c" + } + }, + "emergency_calling_service": { + "data": { + "type": "emergency_calling_services", + "id": "ecs-001-id" + } + } + } + } +} diff --git a/testdata/fixtures/emergency_verifications/index.json b/testdata/fixtures/emergency_verifications/index.json new file mode 100644 index 0000000..afa2d84 --- /dev/null +++ b/testdata/fixtures/emergency_verifications/index.json @@ -0,0 +1,29 @@ +{ + "data": [ + { + "id": "ev-001-id", + "type": "emergency_verifications", + "attributes": { + "reference": "EV-123", + "status": "pending", + "reject_reasons": null, + "reject_comment": null, + "callback_url": null, + "callback_method": null, + "external_reference_id": null, + "created_at": "2026-04-10T08:00:00.000Z" + }, + "relationships": { + "address": { + "data": { "type": "addresses", "id": "addr-001" } + }, + "emergency_calling_service": { + "data": { "type": "emergency_calling_services", "id": "ecs-001-id" } + } + } + } + ], + "meta": { + "api_version": "2026-04-16" + } +} diff --git a/testdata/fixtures/emergency_verifications/update.json b/testdata/fixtures/emergency_verifications/update.json new file mode 100644 index 0000000..3451f82 --- /dev/null +++ b/testdata/fixtures/emergency_verifications/update.json @@ -0,0 +1,19 @@ +{ + "data": { + "id": "ev-001-id", + "type": "emergency_verifications", + "attributes": { + "reference": "EV-123", + "status": "pending", + "reject_reasons": null, + "reject_comment": null, + "callback_url": null, + "callback_method": null, + "external_reference_id": "ev-ext-ref", + "created_at": "2026-04-10T08:00:00.000Z" + } + }, + "meta": { + "api_version": "2026-04-16" + } +} diff --git a/testdata/fixtures/emergency_verifications/update_request.json b/testdata/fixtures/emergency_verifications/update_request.json new file mode 100644 index 0000000..5b3059e --- /dev/null +++ b/testdata/fixtures/emergency_verifications/update_request.json @@ -0,0 +1 @@ +{"data":{"id":"ev-001-id","type":"emergency_verifications","attributes":{"external_reference_id":"ev-ext-ref"}}} diff --git a/testdata/fixtures/encrypted_files/create.json b/testdata/fixtures/encrypted_files/create.json index 36ea1cb..6ba7ce1 100644 --- a/testdata/fixtures/encrypted_files/create.json +++ b/testdata/fixtures/encrypted_files/create.json @@ -1,6 +1,13 @@ { - "ids": [ - "6eed102c-66a9-4a9b-a95f-4312d70ec12a", - "371eafbd-ac6a-485c-aadf-9e3c5da37eb4" - ] -} \ No newline at end of file + "data": { + "id": "6eed102c-66a9-4a9b-a95f-4312d70ec12a", + "type": "encrypted_files", + "attributes": { + "description": "sample.pdf", + "expires_at": "2026-04-22T10:00:00.000Z" + } + }, + "meta": { + "api_version": "2026-04-16" + } +} diff --git a/testdata/fixtures/encrypted_files/index.json b/testdata/fixtures/encrypted_files/index.json index 688fc5e..a4c8c51 100644 --- a/testdata/fixtures/encrypted_files/index.json +++ b/testdata/fixtures/encrypted_files/index.json @@ -5,7 +5,7 @@ "type": "encrypted_files", "attributes": { "description": "file.enc", - "expire_at": "2021-04-02T13:42:51.612Z" + "expires_at": "2021-04-02T13:42:51.612Z" } } ], diff --git a/testdata/fixtures/encrypted_files/show.json b/testdata/fixtures/encrypted_files/show.json index 1384321..a49cdf0 100644 --- a/testdata/fixtures/encrypted_files/show.json +++ b/testdata/fixtures/encrypted_files/show.json @@ -4,7 +4,7 @@ "type": "encrypted_files", "attributes": { "description": "some description", - "expire_at": "2021-04-06T16:38:34.396Z" + "expires_at": "2021-04-06T16:38:34.396Z" } }, "meta": { diff --git a/testdata/fixtures/encrypted_files/show_with_expiration.json b/testdata/fixtures/encrypted_files/show_with_expiration.json index 9fede27..df24135 100644 --- a/testdata/fixtures/encrypted_files/show_with_expiration.json +++ b/testdata/fixtures/encrypted_files/show_with_expiration.json @@ -4,7 +4,7 @@ "type": "encrypted_files", "attributes": { "description": null, - "expire_at": "2021-04-06T16:38:34.437Z" + "expires_at": "2021-04-06T16:38:34.437Z" } }, "meta": { diff --git a/testdata/fixtures/exports/create.json b/testdata/fixtures/exports/create.json index 0fb1e8e..362a27a 100644 --- a/testdata/fixtures/exports/create.json +++ b/testdata/fixtures/exports/create.json @@ -3,7 +3,7 @@ "id": "da15f006-5da4-45ca-b0df-735baeadf423", "type": "exports", "attributes": { - "status": "Pending", + "status": "pending", "created_at": "2019-01-02T10:23:00.897Z", "url": null, "callback_url": null, diff --git a/testdata/fixtures/exports/create_cdr_out.json b/testdata/fixtures/exports/create_cdr_out.json index 7fefa0f..51a4dfe 100644 --- a/testdata/fixtures/exports/create_cdr_out.json +++ b/testdata/fixtures/exports/create_cdr_out.json @@ -3,7 +3,7 @@ "id": "da15f006-5da4-45ca-b0df-735baeadf423", "type": "exports", "attributes": { - "status": "Pending", + "status": "pending", "created_at": "2019-01-02T10:23:00.897Z", "url": null, "callback_url": null, diff --git a/testdata/fixtures/exports/create_request.json b/testdata/fixtures/exports/create_request.json index 2966dcf..4c12a68 100644 --- a/testdata/fixtures/exports/create_request.json +++ b/testdata/fixtures/exports/create_request.json @@ -1 +1 @@ -{"data":{"type":"exports","attributes":{"export_type":"cdr_in","filters":{"did_number":"1234556789","year":"2019","month":"01"}}}} \ No newline at end of file +{"data":{"type":"exports","attributes":{"export_type":"cdr_in","filters":{"did_number":"1234556789","from":"2026-04-01 00:00:00","to":"2026-04-15 23:59:59"}}}} diff --git a/testdata/fixtures/exports/index.json b/testdata/fixtures/exports/index.json index b3729a9..675f16f 100644 --- a/testdata/fixtures/exports/index.json +++ b/testdata/fixtures/exports/index.json @@ -4,7 +4,7 @@ "id": "da15f006-5da4-45ca-b0df-735baeadf423", "type": "exports", "attributes": { - "status": "Completed", + "status": "completed", "created_at": "2019-01-02T10:23:00.897Z", "url": "https://sandbox-api.didww.com/v3/exports/e5352384-6f64-4132-bba1-cda18fbc5896.csv.gz", "callback_url": null, diff --git a/testdata/fixtures/exports/show.json b/testdata/fixtures/exports/show.json index 5ebbc18..041f22b 100644 --- a/testdata/fixtures/exports/show.json +++ b/testdata/fixtures/exports/show.json @@ -3,7 +3,7 @@ "id": "da15f006-5da4-45ca-b0df-735baeadf423", "type": "exports", "attributes": { - "status": "Completed", + "status": "completed", "created_at": "2019-01-02T10:23:00.897Z", "url": "https://sandbox-api.didww.com/v3/exports/e5352384-6f64-4132-bba1-cda18fbc5896.csv.gz", "callback_url": null, diff --git a/testdata/fixtures/exports/update.json b/testdata/fixtures/exports/update.json new file mode 100644 index 0000000..3216f0a --- /dev/null +++ b/testdata/fixtures/exports/update.json @@ -0,0 +1,19 @@ +{ + "data": { + "id": "da15f006-5da4-45ca-b0df-735baeadf423", + "type": "exports", + "attributes": { + "status": "completed", + "created_at": "2019-09-03T14:42:22.000Z", + "url": "https://sandbox-api.didww.com/v3/exports/02bf6df4.csv.gz", + "callback_url": null, + "callback_method": null, + "export_type": "cdr_in", + "filters": {"from": "2026-04-01 00:00:00", "to": "2026-04-15 23:59:59"}, + "external_reference_id": "export-ext-ref" + } + }, + "meta": { + "api_version": "2026-04-16" + } +} diff --git a/testdata/fixtures/exports/update_request.json b/testdata/fixtures/exports/update_request.json new file mode 100644 index 0000000..90b4e95 --- /dev/null +++ b/testdata/fixtures/exports/update_request.json @@ -0,0 +1 @@ +{"data":{"id":"da15f006-5da4-45ca-b0df-735baeadf423","type":"exports","attributes":{"external_reference_id":"export-ext-ref"}}} diff --git a/testdata/fixtures/identities/create.json b/testdata/fixtures/identities/create.json index f846f17..c58ac84 100644 --- a/testdata/fixtures/identities/create.json +++ b/testdata/fixtures/identities/create.json @@ -13,7 +13,7 @@ "vat_id": "GB1234", "description": "test identity", "personal_tax_id": "987654321", - "identity_type": "Business", + "identity_type": "business", "created_at": "2021-04-01T14:56:34.637Z", "external_reference_id": "111", "verified": false diff --git a/testdata/fixtures/identities/create_personal.json b/testdata/fixtures/identities/create_personal.json index 1980969..abcb43d 100644 --- a/testdata/fixtures/identities/create_personal.json +++ b/testdata/fixtures/identities/create_personal.json @@ -13,7 +13,7 @@ "vat_id": null, "description": "test identity", "personal_tax_id": "987654321", - "identity_type": "Personal", + "identity_type": "personal", "created_at": "2021-04-01T14:56:38.664Z", "external_reference_id": "111", "verified": false diff --git a/testdata/fixtures/identities/create_request.json b/testdata/fixtures/identities/create_request.json index ac561fc..99475db 100644 --- a/testdata/fixtures/identities/create_request.json +++ b/testdata/fixtures/identities/create_request.json @@ -1 +1 @@ -{"data":{"type":"identities","attributes":{"first_name":"John","last_name":"Doe","phone_number":"123456789","id_number":"ABC1234","birth_date":"1970-01-01","company_name":"Test Company Limited","company_reg_number":"543221","vat_id":"GB1234","description":"test identity","personal_tax_id":"987654321","identity_type":"Business","external_reference_id":"111","contact_email":"john.doe@example.com"},"relationships":{"country":{"data":{"type":"countries","id":"1f6fc2bd-f081-4202-9b1a-d9cb88d942b9"}}}}} \ No newline at end of file +{"data":{"type":"identities","attributes":{"first_name":"John","last_name":"Doe","phone_number":"123456789","id_number":"ABC1234","birth_date":"1970-01-01","company_name":"Test Company Limited","company_reg_number":"543221","vat_id":"GB1234","description":"test identity","personal_tax_id":"987654321","identity_type":"business","external_reference_id":"111","contact_email":"john.doe@example.com"},"relationships":{"country":{"data":{"type":"countries","id":"1f6fc2bd-f081-4202-9b1a-d9cb88d942b9"}}}}} \ No newline at end of file diff --git a/testdata/fixtures/identities/index.json b/testdata/fixtures/identities/index.json index a0a1725..5848ce0 100644 --- a/testdata/fixtures/identities/index.json +++ b/testdata/fixtures/identities/index.json @@ -14,7 +14,7 @@ "vat_id": null, "description": "test identity", "personal_tax_id": "987654321", - "identity_type": "Personal", + "identity_type": "personal", "created_at": "2021-04-01T12:57:24.025Z", "external_reference_id": "111", "verified": false @@ -72,7 +72,7 @@ "vat_id": null, "description": "test identity", "personal_tax_id": "987654321", - "identity_type": "Personal", + "identity_type": "personal", "created_at": "2021-04-01T14:56:38.664Z", "external_reference_id": "111", "verified": false diff --git a/testdata/fixtures/identities/show_with_birth_country.json b/testdata/fixtures/identities/show_with_birth_country.json new file mode 100644 index 0000000..40b8ac7 --- /dev/null +++ b/testdata/fixtures/identities/show_with_birth_country.json @@ -0,0 +1,78 @@ +{ + "data": { + "id": "e96ae7d1-11d5-42bc-a5c5-211f3c3788ae", + "type": "identities", + "attributes": { + "first_name": "John", + "last_name": "Doe", + "phone_number": "123456789", + "id_number": "ABC1234", + "birth_date": "1970-01-01", + "company_name": null, + "company_reg_number": null, + "vat_id": null, + "description": null, + "personal_tax_id": null, + "identity_type": "personal", + "created_at": "2021-04-01T14:56:34.637Z", + "external_reference_id": null, + "verified": false, + "contact_email": null + }, + "relationships": { + "country": { + "data": { + "type": "countries", + "id": "1f6fc2bd-f081-4202-9b1a-d9cb88d942b9" + } + }, + "birth_country": { + "data": { + "type": "countries", + "id": "a2b3c4d5-e6f7-8901-abcd-ef1234567890" + } + }, + "proofs": { + "links": { + "self": "https://sandbox-api.didww.com/v3/identities/e96ae7d1-11d5-42bc-a5c5-211f3c3788ae/relationships/proofs", + "related": "https://sandbox-api.didww.com/v3/identities/e96ae7d1-11d5-42bc-a5c5-211f3c3788ae/proofs" + } + }, + "addresses": { + "links": { + "self": "https://sandbox-api.didww.com/v3/identities/e96ae7d1-11d5-42bc-a5c5-211f3c3788ae/relationships/addresses", + "related": "https://sandbox-api.didww.com/v3/identities/e96ae7d1-11d5-42bc-a5c5-211f3c3788ae/addresses" + } + }, + "permanent_documents": { + "links": { + "self": "https://sandbox-api.didww.com/v3/identities/e96ae7d1-11d5-42bc-a5c5-211f3c3788ae/relationships/permanent_documents", + "related": "https://sandbox-api.didww.com/v3/identities/e96ae7d1-11d5-42bc-a5c5-211f3c3788ae/permanent_documents" + } + } + } + }, + "included": [ + { + "id": "1f6fc2bd-f081-4202-9b1a-d9cb88d942b9", + "type": "countries", + "attributes": { + "name": "United States", + "prefix": "1", + "iso": "US" + } + }, + { + "id": "a2b3c4d5-e6f7-8901-abcd-ef1234567890", + "type": "countries", + "attributes": { + "name": "Canada", + "prefix": "1", + "iso": "CA" + } + } + ], + "meta": { + "api_version": "2026-04-16" + } +} diff --git a/testdata/fixtures/identities/show_with_contact_email.json b/testdata/fixtures/identities/show_with_contact_email.json index 31b5f22..c135f12 100644 --- a/testdata/fixtures/identities/show_with_contact_email.json +++ b/testdata/fixtures/identities/show_with_contact_email.json @@ -13,7 +13,7 @@ "vat_id": "GB1234", "description": "test identity", "personal_tax_id": "987654321", - "identity_type": "Business", + "identity_type": "business", "created_at": "2021-04-01T14:56:34.637Z", "external_reference_id": "111", "verified": false, diff --git a/testdata/fixtures/identities/update.json b/testdata/fixtures/identities/update.json index 5cc6f7b..38d93f7 100644 --- a/testdata/fixtures/identities/update.json +++ b/testdata/fixtures/identities/update.json @@ -13,7 +13,7 @@ "vat_id": "GB1235", "description": "test", "personal_tax_id": "983217654", - "identity_type": "Business", + "identity_type": "business", "created_at": "2021-04-01T14:56:34.637Z", "external_reference_id": "112", "verified": false, diff --git a/testdata/fixtures/identities/update_request.json b/testdata/fixtures/identities/update_request.json index 63cda2b..d1a947b 100644 --- a/testdata/fixtures/identities/update_request.json +++ b/testdata/fixtures/identities/update_request.json @@ -1 +1 @@ -{"data":{"id":"e96ae7d1-11d5-42bc-a5c5-211f3c3788ae","type":"identities","attributes":{"first_name":"Jake","last_name":"Johnson","phone_number":"1111111","birth_date":"1979-01-01","company_name":"Some Company Limited","company_reg_number":"1222776","vat_id":"GB1235","description":"test","personal_tax_id":"983217654","identity_type":"Business","external_reference_id":"112","contact_email":"jake.johnson@example.com"}}} \ No newline at end of file +{"data":{"id":"e96ae7d1-11d5-42bc-a5c5-211f3c3788ae","type":"identities","attributes":{"first_name":"Jake","last_name":"Johnson","phone_number":"1111111","birth_date":"1979-01-01","company_name":"Some Company Limited","company_reg_number":"1222776","vat_id":"GB1235","description":"test","personal_tax_id":"983217654","identity_type":"business","external_reference_id":"112","contact_email":"jake.johnson@example.com"}}} \ No newline at end of file diff --git a/testdata/fixtures/orders/create.json b/testdata/fixtures/orders/create.json index 91cc4b0..490eb62 100644 --- a/testdata/fixtures/orders/create.json +++ b/testdata/fixtures/orders/create.json @@ -4,7 +4,7 @@ "type": "orders", "attributes": { "amount": "5.98", - "status": "Pending", + "status": "pending", "created_at": "2018-12-29T12:16:45.813Z", "description": "DID", "reference": "JXK-923618", diff --git a/testdata/fixtures/orders/create_available_did.json b/testdata/fixtures/orders/create_available_did.json index 92dddf5..3855033 100644 --- a/testdata/fixtures/orders/create_available_did.json +++ b/testdata/fixtures/orders/create_available_did.json @@ -4,7 +4,7 @@ "type": "orders", "attributes": { "amount": "0.19", - "status": "Pending", + "status": "pending", "created_at": "2018-12-28T10:40:48.355Z", "description": "DID", "reference": "UAM-764395", diff --git a/testdata/fixtures/orders/create_billing_cycles.json b/testdata/fixtures/orders/create_billing_cycles.json index aff07fb..c5aa308 100644 --- a/testdata/fixtures/orders/create_billing_cycles.json +++ b/testdata/fixtures/orders/create_billing_cycles.json @@ -4,7 +4,7 @@ "type": "orders", "attributes": { "amount": "0.19", - "status": "Pending", + "status": "pending", "created_at": "2018-12-27T16:44:05.511Z", "description": "DID", "reference": "YGA-665789", diff --git a/testdata/fixtures/orders/create_capacity.json b/testdata/fixtures/orders/create_capacity.json index 20b6c62..b23c117 100644 --- a/testdata/fixtures/orders/create_capacity.json +++ b/testdata/fixtures/orders/create_capacity.json @@ -4,7 +4,7 @@ "type": "orders", "attributes": { "amount": "44.35", - "status": "Completed", + "status": "completed", "created_at": "2018-12-28T14:08:38.304Z", "description": "Capacity", "reference": "RUG-725648", diff --git a/testdata/fixtures/orders/create_emergency.json b/testdata/fixtures/orders/create_emergency.json new file mode 100644 index 0000000..d045b56 --- /dev/null +++ b/testdata/fixtures/orders/create_emergency.json @@ -0,0 +1,29 @@ +{ + "data": { + "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "type": "orders", + "attributes": { + "amount": "30.0", + "status": "pending", + "created_at": "2026-04-16T10:00:00.000Z", + "description": "Emergency", + "reference": "EMG-100001", + "items": [ + { + "type": "emergency_order_items", + "attributes": { + "qty": 1, + "nrc": "5.0", + "mrc": "25.0", + "prorated_mrc": false, + "billed_from": null, + "billed_to": null, + "emergency_calling_service_id": "b6d9d793-578d-42d3-bc33-73dd8155e615" + } + } + ], + "callback_url": null, + "callback_method": null + } + } +} diff --git a/testdata/fixtures/orders/create_nanpa.json b/testdata/fixtures/orders/create_nanpa.json index 04fd995..23d1a13 100644 --- a/testdata/fixtures/orders/create_nanpa.json +++ b/testdata/fixtures/orders/create_nanpa.json @@ -4,7 +4,7 @@ "type": "orders", "attributes": { "amount": "4.0", - "status": "Pending", + "status": "pending", "created_at": "2022-06-22T22:02:23.333Z", "description": "DID", "reference": "WHT-994648", diff --git a/testdata/fixtures/orders/create_request_emergency.json b/testdata/fixtures/orders/create_request_emergency.json new file mode 100644 index 0000000..f214039 --- /dev/null +++ b/testdata/fixtures/orders/create_request_emergency.json @@ -0,0 +1 @@ +{"data":{"type":"orders","attributes":{"items":[{"type":"emergency_order_items","attributes":{"emergency_calling_service_id":"b6d9d793-578d-42d3-bc33-73dd8155e615","qty":1}}]}}} diff --git a/testdata/fixtures/orders/create_reservation.json b/testdata/fixtures/orders/create_reservation.json index 75864c1..b22bfe3 100644 --- a/testdata/fixtures/orders/create_reservation.json +++ b/testdata/fixtures/orders/create_reservation.json @@ -4,7 +4,7 @@ "type": "orders", "attributes": { "amount": "0.19", - "status": "Pending", + "status": "pending", "created_at": "2018-12-27T16:44:05.511Z", "description": "DID", "reference": "YGA-665789", diff --git a/testdata/fixtures/orders/show.json b/testdata/fixtures/orders/show.json index 23ca906..5ba88cb 100644 --- a/testdata/fixtures/orders/show.json +++ b/testdata/fixtures/orders/show.json @@ -4,7 +4,7 @@ "type": "orders", "attributes": { "amount": "25.07", - "status": "Completed", + "status": "completed", "created_at": "2018-08-17T09:48:48.440Z", "description": "Payment processing fee", "reference": "SPT-474057", diff --git a/testdata/fixtures/orders_with_callback/create.json b/testdata/fixtures/orders_with_callback/create.json index 78d35ff..2890e3b 100644 --- a/testdata/fixtures/orders_with_callback/create.json +++ b/testdata/fixtures/orders_with_callback/create.json @@ -4,7 +4,7 @@ "type": "orders", "attributes": { "amount": "5.98", - "status": "Pending", + "status": "pending", "created_at": "2018-12-29T12:16:45.813Z", "description": "DID", "reference": "JXK-923618", @@ -25,7 +25,7 @@ } ], "callback_url": "https://example.com/callback", - "callback_method": "POST" + "callback_method": "post" } } } \ No newline at end of file diff --git a/testdata/fixtures/orders_with_callback/create_request.json b/testdata/fixtures/orders_with_callback/create_request.json index 903660b..4545afd 100644 --- a/testdata/fixtures/orders_with_callback/create_request.json +++ b/testdata/fixtures/orders_with_callback/create_request.json @@ -1 +1 @@ -{"data":{"type":"orders","attributes":{"items":[{"type":"did_order_items","attributes":{"sku_id":"f36d2812-2195-4385-85e8-e59c3484a8bc","qty":1}}],"allow_back_ordering":true,"callback_url":"https://example.com/callback","callback_method":"POST"}}} \ No newline at end of file +{"data":{"type":"orders","attributes":{"items":[{"type":"did_order_items","attributes":{"sku_id":"f36d2812-2195-4385-85e8-e59c3484a8bc","qty":1}}],"allow_back_ordering":true,"callback_url":"https://example.com/callback","callback_method":"post"}}} \ No newline at end of file diff --git a/testdata/fixtures/voice_in_trunk_groups/create.json b/testdata/fixtures/voice_in_trunk_groups/create.json index ff43c71..3938986 100644 --- a/testdata/fixtures/voice_in_trunk_groups/create.json +++ b/testdata/fixtures/voice_in_trunk_groups/create.json @@ -46,7 +46,7 @@ "type": "sip_configurations", "attributes": { "username": "{CALL_DID}", - "host": "127.0.0.1", + "host": "203.0.113.1", "port": null, "codec_ids": [ 9, diff --git a/testdata/fixtures/voice_in_trunks/create_sip.json b/testdata/fixtures/voice_in_trunks/create_sip.json index 2217bf1..903f349 100644 --- a/testdata/fixtures/voice_in_trunks/create_sip.json +++ b/testdata/fixtures/voice_in_trunks/create_sip.json @@ -15,7 +15,7 @@ "type": "sip_configurations", "attributes": { "username": "username", - "host": "216.58.215.110", + "host": "203.0.113.110", "port": 5060, "codec_ids": [ 9, @@ -97,8 +97,9 @@ "media_encryption_mode": "zrtp", "stir_shaken_mode": "pai", "allowed_rtp_ips": [ - "127.0.0.1" - ] + "203.0.113.1" + ], + "diversion_relay_policy": "as_is" } }, "created_at": "2018-12-28T17:37:48.010Z" diff --git a/testdata/fixtures/voice_in_trunks/create_sip_request.json b/testdata/fixtures/voice_in_trunks/create_sip_request.json index 4a173fd..47dd0a5 100644 --- a/testdata/fixtures/voice_in_trunks/create_sip_request.json +++ b/testdata/fixtures/voice_in_trunks/create_sip_request.json @@ -1 +1 @@ -{"data":{"type":"voice_in_trunks","attributes":{"configuration":{"type":"sip_configurations","attributes":{"username":"username","host":"216.58.215.110","sst_refresh_method_id":1,"port":5060,"codec_ids":[9,10,8,7,6],"rerouting_disconnect_code_ids":[56,58,59,60,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,86,87,88,89,90,91,92,96,97,98,99,101,102,103,104,105,106,107,108,1505],"media_encryption_mode":"zrtp","stir_shaken_mode":"pai","allowed_rtp_ips":["127.0.0.1"]}},"name":"hello, test sip trunk"}}} +{"data":{"type":"voice_in_trunks","attributes":{"configuration":{"type":"sip_configurations","attributes":{"username":"username","host":"203.0.113.110","sst_refresh_method_id":1,"port":5060,"codec_ids":[9,10,8,7,6],"rerouting_disconnect_code_ids":[56,58,59,60,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,86,87,88,89,90,91,92,96,97,98,99,101,102,103,104,105,106,107,108,1505],"media_encryption_mode":"zrtp","stir_shaken_mode":"pai","allowed_rtp_ips":["203.0.113.1"]}},"name":"hello, test sip trunk"}}} diff --git a/testdata/fixtures/voice_in_trunks/index.json b/testdata/fixtures/voice_in_trunks/index.json index 018e164..069ab69 100644 --- a/testdata/fixtures/voice_in_trunks/index.json +++ b/testdata/fixtures/voice_in_trunks/index.json @@ -53,7 +53,7 @@ "type": "sip_configurations", "attributes": { "username": "username", - "host": "216.58.215.78", + "host": "203.0.113.78", "port": 8060, "codec_ids": [ 9, diff --git a/testdata/fixtures/voice_in_trunks/update_sip.json b/testdata/fixtures/voice_in_trunks/update_sip.json index e3c1cf9..3d6125f 100644 --- a/testdata/fixtures/voice_in_trunks/update_sip.json +++ b/testdata/fixtures/voice_in_trunks/update_sip.json @@ -15,7 +15,7 @@ "type": "sip_configurations", "attributes": { "username": "new-username", - "host": "216.58.215.110", + "host": "203.0.113.110", "port": 5060, "codec_ids": [ 9, @@ -97,7 +97,7 @@ "media_encryption_mode": "zrtp", "stir_shaken_mode": "pai", "allowed_rtp_ips": [ - "127.0.0.1" + "203.0.113.1" ] } }, diff --git a/testdata/fixtures/voice_out_trunks/create.json b/testdata/fixtures/voice_out_trunks/create.json index 44e2905..9afe475 100644 --- a/testdata/fixtures/voice_out_trunks/create.json +++ b/testdata/fixtures/voice_out_trunks/create.json @@ -3,17 +3,21 @@ "id": "b60201c1-21f0-4d9a-aafa-0e6d1e12f22e", "type": "voice_out_trunks", "attributes": { - "allowed_sip_ips": [ - "0.0.0.0/0" - ], + "authentication_method": { + "type": "credentials_and_ip", + "attributes": { + "allowed_sip_ips": ["203.0.113.0/24"], + "tech_prefix": "", + "username": "dLPa6JbLTeMjKjl5", + "password": "BZj1YvP45yWvX5Ic" + } + }, "allowed_rtp_ips": null, "allow_any_did_as_cli": false, "status": "active", "on_cli_mismatch_action": "replace_cli", "name": "java-test", "capacity_limit": null, - "username": "qkut5v4xwm", - "password": "np34mftrrq", "created_at": "2022-02-03T08:21:29.798Z", "threshold_reached": false, "threshold_amount": null, @@ -22,7 +26,10 @@ "dst_prefixes": [], "force_symmetric_rtp": false, "rtp_ping": false, - "callback_url": null + "callback_url": null, + "external_reference_id": null, + "emergency_enable_all": false, + "rtp_timeout": 30 }, "relationships": { "default_did": { @@ -43,6 +50,6 @@ } }, "meta": { - "api_version": "2021-12-15" + "api_version": "2026-04-16" } } \ No newline at end of file diff --git a/testdata/fixtures/voice_out_trunks/create_request.json b/testdata/fixtures/voice_out_trunks/create_request.json index 39721a1..c900c56 100644 --- a/testdata/fixtures/voice_out_trunks/create_request.json +++ b/testdata/fixtures/voice_out_trunks/create_request.json @@ -1 +1 @@ -{"data":{"type":"voice_out_trunks","attributes":{"name":"java-test","allowed_sip_ips":["0.0.0.0/0"],"on_cli_mismatch_action":"replace_cli"},"relationships":{"default_did":{"data":{"type":"dids","id":"7a028c32-e6b6-4c86-bf01-90f901b37012"}},"dids":{"data":[{"type":"dids","id":"7a028c32-e6b6-4c86-bf01-90f901b37012"}]}}}} \ No newline at end of file +{"data":{"type":"voice_out_trunks","attributes":{"name":"java-test","on_cli_mismatch_action":"replace_cli","authentication_method":{"type":"credentials_and_ip","attributes":{"allowed_sip_ips":["203.0.113.0/24"]}}},"relationships":{"default_did":{"data":{"type":"dids","id":"7a028c32-e6b6-4c86-bf01-90f901b37012"}},"dids":{"data":[{"type":"dids","id":"7a028c32-e6b6-4c86-bf01-90f901b37012"}]}}}} diff --git a/testdata/fixtures/voice_out_trunks/create_twilio.json b/testdata/fixtures/voice_out_trunks/create_twilio.json new file mode 100644 index 0000000..cbc1c7b --- /dev/null +++ b/testdata/fixtures/voice_out_trunks/create_twilio.json @@ -0,0 +1,52 @@ +{ + "data": { + "id": "507fa5a2-fd58-4c4d-a231-efba27f67c3a", + "type": "voice_out_trunks", + "attributes": { + "allowed_rtp_ips": null, + "allow_any_did_as_cli": false, + "status": "active", + "on_cli_mismatch_action": "reject_call", + "name": "SDK Test twilio create", + "capacity_limit": null, + "created_at": "2026-04-23T19:59:39.154Z", + "threshold_reached": false, + "threshold_amount": "3000.0", + "media_encryption_mode": "disabled", + "default_dst_action": "allow_all", + "dst_prefixes": [], + "force_symmetric_rtp": false, + "rtp_ping": false, + "callback_url": null, + "external_reference_id": null, + "authentication_method": { + "type": "twilio", + "attributes": { + "twilio_account_sid": "AC33333333333333333333333333333333" + } + }, + "emergency_enable_all": false, + "rtp_timeout": 30 + }, + "relationships": { + "dids": { + "links": { + "self": "https://sandbox-api.didww.com/v3/voice_out_trunks/507fa5a2-fd58-4c4d-a231-efba27f67c3a/relationships/dids", + "related": "https://sandbox-api.didww.com/v3/voice_out_trunks/507fa5a2-fd58-4c4d-a231-efba27f67c3a/dids" + } + }, + "emergency_dids": { + "links": { + "self": "https://sandbox-api.didww.com/v3/voice_out_trunks/507fa5a2-fd58-4c4d-a231-efba27f67c3a/relationships/emergency_dids", + "related": "https://sandbox-api.didww.com/v3/voice_out_trunks/507fa5a2-fd58-4c4d-a231-efba27f67c3a/emergency_dids" + } + } + }, + "meta": { + "spent_amount": "0.0" + } + }, + "meta": { + "api_version": "2026-04-16" + } +} diff --git a/testdata/fixtures/voice_out_trunks/create_twilio_request.json b/testdata/fixtures/voice_out_trunks/create_twilio_request.json new file mode 100644 index 0000000..3a3ae24 --- /dev/null +++ b/testdata/fixtures/voice_out_trunks/create_twilio_request.json @@ -0,0 +1 @@ +{"data":{"type":"voice_out_trunks","attributes":{"name":"SDK Test twilio create","on_cli_mismatch_action":"reject_call","authentication_method":{"type":"twilio","attributes":{"twilio_account_sid":"AC33333333333333333333333333333333"}}}}} diff --git a/testdata/fixtures/voice_out_trunks/index.json b/testdata/fixtures/voice_out_trunks/index.json index 9a1d768..26a57c4 100644 --- a/testdata/fixtures/voice_out_trunks/index.json +++ b/testdata/fixtures/voice_out_trunks/index.json @@ -4,17 +4,21 @@ "id": "425ce763-a3a9-49b4-af5b-ada1a65c8864", "type": "voice_out_trunks", "attributes": { - "allowed_sip_ips": [ - "10.11.12.13/32" - ], + "authentication_method": { + "type": "credentials_and_ip", + "attributes": { + "allowed_sip_ips": ["203.0.113.1/32"], + "tech_prefix": "", + "username": "dpjgwbbac9", + "password": "z0hshvbcy7" + } + }, "allowed_rtp_ips": null, "allow_any_did_as_cli": false, "status": "blocked", "on_cli_mismatch_action": "replace_cli", "name": "test", "capacity_limit": 123, - "username": "dpjgwbbac9", - "password": "z0hshvbcy7", "created_at": "2022-02-02T16:50:45.053Z", "threshold_reached": false, "threshold_amount": "200.0", @@ -50,7 +54,7 @@ "type": "voice_out_trunks", "attributes": { "allowed_sip_ips": [ - "0.0.0.0/0" + "203.0.113.0/24" ], "allowed_rtp_ips": null, "allow_any_did_as_cli": false, diff --git a/testdata/fixtures/voice_out_trunks/show.json b/testdata/fixtures/voice_out_trunks/show.json index b389ff3..a71dd7b 100644 --- a/testdata/fixtures/voice_out_trunks/show.json +++ b/testdata/fixtures/voice_out_trunks/show.json @@ -3,17 +3,21 @@ "id": "425ce763-a3a9-49b4-af5b-ada1a65c8864", "type": "voice_out_trunks", "attributes": { - "allowed_sip_ips": [ - "10.11.12.13/32" - ], + "authentication_method": { + "type": "credentials_and_ip", + "attributes": { + "allowed_sip_ips": ["203.0.113.1/32"], + "tech_prefix": "", + "username": "dpjgwbbac9", + "password": "z0hshvbcy7" + } + }, "allowed_rtp_ips": null, "allow_any_did_as_cli": false, "status": "blocked", "on_cli_mismatch_action": "replace_cli", "name": "test", "capacity_limit": 123, - "username": "dpjgwbbac9", - "password": "z0hshvbcy7", "created_at": "2022-02-02T16:50:45.053Z", "threshold_reached": false, "threshold_amount": "200.0", diff --git a/testdata/fixtures/voice_out_trunks/show_ip_only.json b/testdata/fixtures/voice_out_trunks/show_ip_only.json new file mode 100644 index 0000000..7fff18a --- /dev/null +++ b/testdata/fixtures/voice_out_trunks/show_ip_only.json @@ -0,0 +1,53 @@ +{ + "data": { + "id": "23fd58f9-9094-406c-bfd9-f4d25bda13c6", + "type": "voice_out_trunks", + "attributes": { + "allowed_rtp_ips": null, + "allow_any_did_as_cli": false, + "status": "active", + "on_cli_mismatch_action": "reject_call", + "name": "SDK Test credentials_and_ip", + "capacity_limit": null, + "created_at": "2026-04-23T19:52:20.859Z", + "threshold_reached": false, + "threshold_amount": "3000.0", + "media_encryption_mode": "disabled", + "default_dst_action": "allow_all", + "dst_prefixes": [], + "force_symmetric_rtp": false, + "rtp_ping": false, + "callback_url": null, + "external_reference_id": null, + "authentication_method": { + "type": "ip_only", + "attributes": { + "allowed_sip_ips": ["203.0.113.1/32"], + "tech_prefix": null + } + }, + "emergency_enable_all": false, + "rtp_timeout": 30 + }, + "relationships": { + "dids": { + "links": { + "self": "https://sandbox-api.didww.com/v3/voice_out_trunks/23fd58f9-9094-406c-bfd9-f4d25bda13c6/relationships/dids", + "related": "https://sandbox-api.didww.com/v3/voice_out_trunks/23fd58f9-9094-406c-bfd9-f4d25bda13c6/dids" + } + }, + "emergency_dids": { + "links": { + "self": "https://sandbox-api.didww.com/v3/voice_out_trunks/23fd58f9-9094-406c-bfd9-f4d25bda13c6/relationships/emergency_dids", + "related": "https://sandbox-api.didww.com/v3/voice_out_trunks/23fd58f9-9094-406c-bfd9-f4d25bda13c6/emergency_dids" + } + } + }, + "meta": { + "spent_amount": "0.0" + } + }, + "meta": { + "api_version": "2026-04-16" + } +} diff --git a/testdata/fixtures/voice_out_trunks/show_twilio.json b/testdata/fixtures/voice_out_trunks/show_twilio.json new file mode 100644 index 0000000..bf35b4d --- /dev/null +++ b/testdata/fixtures/voice_out_trunks/show_twilio.json @@ -0,0 +1,52 @@ +{ + "data": { + "id": "b5e701f4-ea15-4f9d-8f35-6a0bdce04385", + "type": "voice_out_trunks", + "attributes": { + "allowed_rtp_ips": null, + "allow_any_did_as_cli": false, + "status": "active", + "on_cli_mismatch_action": "reject_call", + "name": "SDK Test twilio", + "capacity_limit": null, + "created_at": "2026-04-23T19:52:32.708Z", + "threshold_reached": false, + "threshold_amount": "3000.0", + "media_encryption_mode": "disabled", + "default_dst_action": "allow_all", + "dst_prefixes": [], + "force_symmetric_rtp": false, + "rtp_ping": false, + "callback_url": null, + "external_reference_id": null, + "authentication_method": { + "type": "twilio", + "attributes": { + "twilio_account_sid": "AC22222222222222222222222222222222" + } + }, + "emergency_enable_all": false, + "rtp_timeout": 30 + }, + "relationships": { + "dids": { + "links": { + "self": "https://sandbox-api.didww.com/v3/voice_out_trunks/b5e701f4-ea15-4f9d-8f35-6a0bdce04385/relationships/dids", + "related": "https://sandbox-api.didww.com/v3/voice_out_trunks/b5e701f4-ea15-4f9d-8f35-6a0bdce04385/dids" + } + }, + "emergency_dids": { + "links": { + "self": "https://sandbox-api.didww.com/v3/voice_out_trunks/b5e701f4-ea15-4f9d-8f35-6a0bdce04385/relationships/emergency_dids", + "related": "https://sandbox-api.didww.com/v3/voice_out_trunks/b5e701f4-ea15-4f9d-8f35-6a0bdce04385/emergency_dids" + } + } + }, + "meta": { + "spent_amount": "0.0" + } + }, + "meta": { + "api_version": "2026-04-16" + } +} diff --git a/testdata/fixtures/voice_out_trunks/update.json b/testdata/fixtures/voice_out_trunks/update.json index d77a1b3..01ae760 100644 --- a/testdata/fixtures/voice_out_trunks/update.json +++ b/testdata/fixtures/voice_out_trunks/update.json @@ -3,17 +3,21 @@ "id": "425ce763-a3a9-49b4-af5b-ada1a65c8864", "type": "voice_out_trunks", "attributes": { - "allowed_sip_ips": [ - "10.11.12.13/32" - ], + "authentication_method": { + "type": "credentials_and_ip", + "attributes": { + "allowed_sip_ips": ["203.0.113.1/32"], + "tech_prefix": "", + "username": "dpjgwbbac9", + "password": "z0hshvbcy7" + } + }, "allowed_rtp_ips": null, "allow_any_did_as_cli": false, "status": "blocked", "on_cli_mismatch_action": "replace_cli", "name": "test", "capacity_limit": 123, - "username": "dpjgwbbac9", - "password": "z0hshvbcy7", "created_at": "2022-02-02T16:50:45.053Z", "threshold_reached": false, "threshold_amount": "200.0", diff --git a/testdata/fixtures/voice_out_trunks/update_auth_method_request.json b/testdata/fixtures/voice_out_trunks/update_auth_method_request.json new file mode 100644 index 0000000..4d48c05 --- /dev/null +++ b/testdata/fixtures/voice_out_trunks/update_auth_method_request.json @@ -0,0 +1 @@ +{"data":{"id":"425ce763-a3a9-49b4-af5b-ada1a65c8864","type":"voice_out_trunks","attributes":{"authentication_method":{"type":"credentials_and_ip","attributes":{"allowed_sip_ips":["192.0.2.10/32"],"tech_prefix":"99"}}}}} diff --git a/testdata/fixtures/voice_out_trunks/update_emergency_dids.json b/testdata/fixtures/voice_out_trunks/update_emergency_dids.json new file mode 100644 index 0000000..9a82c76 --- /dev/null +++ b/testdata/fixtures/voice_out_trunks/update_emergency_dids.json @@ -0,0 +1,53 @@ +{ + "data": { + "id": "01234567-89ab-cdef-0123-456789abcdef", + "type": "voice_out_trunks", + "attributes": { + "name": "Trunk with emergency DIDs", + "on_cli_mismatch_action": "Reject call", + "capacity_limit": 200, + "created_at": "2026-01-15T10:00:00.000Z", + "allow_any_did_as_cli": false, + "status": "Active", + "threshold_reached": false, + "threshold_amount": "100.0", + "default_dst_action": "Allow Calls", + "dst_prefixes": ["1"], + "media_encryption_mode": "Disable", + "callback_url": null, + "force_symmetric_rtp": false, + "allowed_rtp_ips": [], + "emergency_enable_all": false, + "rtp_timeout": 30, + "authentication_method": { + "type": "credentials_and_ip", + "attributes": { + "allowed_sip_ips": ["203.0.113.1/32"], + "tech_prefix": "", + "username": "user1", + "password": "pass1" + } + } + }, + "relationships": { + "default_did": { + "links": { + "self": "https://sandbox-api.didww.com/v3/voice_out_trunks/01234567-89ab-cdef-0123-456789abcdef/relationships/default_did", + "related": "https://sandbox-api.didww.com/v3/voice_out_trunks/01234567-89ab-cdef-0123-456789abcdef/default_did" + } + }, + "dids": { + "links": { + "self": "https://sandbox-api.didww.com/v3/voice_out_trunks/01234567-89ab-cdef-0123-456789abcdef/relationships/dids", + "related": "https://sandbox-api.didww.com/v3/voice_out_trunks/01234567-89ab-cdef-0123-456789abcdef/dids" + } + }, + "emergency_dids": { + "links": { + "self": "https://sandbox-api.didww.com/v3/voice_out_trunks/01234567-89ab-cdef-0123-456789abcdef/relationships/emergency_dids", + "related": "https://sandbox-api.didww.com/v3/voice_out_trunks/01234567-89ab-cdef-0123-456789abcdef/emergency_dids" + } + } + } + } +} diff --git a/testdata/fixtures/voice_out_trunks/update_emergency_dids_clear_request.json b/testdata/fixtures/voice_out_trunks/update_emergency_dids_clear_request.json new file mode 100644 index 0000000..ada4562 --- /dev/null +++ b/testdata/fixtures/voice_out_trunks/update_emergency_dids_clear_request.json @@ -0,0 +1 @@ +{"data":{"id":"01234567-89ab-cdef-0123-456789abcdef","type":"voice_out_trunks","attributes":{},"relationships":{"emergency_dids":{"data":[]}}}} \ No newline at end of file diff --git a/testdata/fixtures/voice_out_trunks/update_emergency_dids_request.json b/testdata/fixtures/voice_out_trunks/update_emergency_dids_request.json new file mode 100644 index 0000000..c361116 --- /dev/null +++ b/testdata/fixtures/voice_out_trunks/update_emergency_dids_request.json @@ -0,0 +1 @@ +{"data":{"id":"01234567-89ab-cdef-0123-456789abcdef","type":"voice_out_trunks","attributes":{},"relationships":{"emergency_dids":{"data":[{"type":"dids","id":"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"},{"type":"dids","id":"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"}]}}}} \ No newline at end of file diff --git a/testdata/fixtures/voice_out_trunks/update_emergency_enable_all_request.json b/testdata/fixtures/voice_out_trunks/update_emergency_enable_all_request.json new file mode 100644 index 0000000..d0f08b5 --- /dev/null +++ b/testdata/fixtures/voice_out_trunks/update_emergency_enable_all_request.json @@ -0,0 +1 @@ +{"data":{"id":"01234567-89ab-cdef-0123-456789abcdef","type":"voice_out_trunks","attributes":{"emergency_enable_all":true}}} \ No newline at end of file diff --git a/voice_in_trunks_test.go b/voice_in_trunks_test.go index 21ede55..5b985cc 100644 --- a/voice_in_trunks_test.go +++ b/voice_in_trunks_test.go @@ -37,7 +37,7 @@ func TestVoiceInTrunksList(t *testing.T) { assert.Equal(t, "Sip trunk sample", sip.Name) sipCfg, ok := sip.Configuration.(*trunkconfiguration.SIPConfiguration) require.True(t, ok, "expected SIP configuration") - assert.Equal(t, "216.58.215.78", sipCfg.Host) + assert.Equal(t, "203.0.113.78", sipCfg.Host) } func TestVoiceInTrunksCreate(t *testing.T) { @@ -67,7 +67,7 @@ func TestVoiceInTrunksCreateSipWithReroutingCodes(t *testing.T) { Name: "hello, test sip trunk", Configuration: &trunkconfiguration.SIPConfiguration{ Username: "username", - Host: "216.58.215.110", + Host: "203.0.113.110", SstRefreshMethodID: enums.SstRefreshMethodInvite, Port: 5060, CodecIDs: []enums.Codec{ @@ -93,7 +93,7 @@ func TestVoiceInTrunksCreateSipWithReroutingCodes(t *testing.T) { }, MediaEncryptionMode: enums.MediaEncryptionModeZrtp, StirShakenMode: enums.StirShakenModePai, - AllowedRtpIPs: []string{"127.0.0.1"}, + AllowedRtpIPs: []string{"203.0.113.1"}, }, }) require.NoError(t, err) @@ -110,7 +110,7 @@ func TestVoiceInTrunksCreateSip(t *testing.T) { Name: "hello, test sip trunk", Configuration: &trunkconfiguration.SIPConfiguration{ Username: "username", - Host: "216.58.215.110", + Host: "203.0.113.110", Port: 5060, }, }) @@ -121,7 +121,8 @@ func TestVoiceInTrunksCreateSip(t *testing.T) { sipCfg, ok := trunk.Configuration.(*trunkconfiguration.SIPConfiguration) require.True(t, ok, "expected SIP configuration") assert.Equal(t, "username", sipCfg.Username) - assert.Equal(t, "216.58.215.110", sipCfg.Host) + assert.Equal(t, "203.0.113.110", sipCfg.Host) + assert.Equal(t, enums.DiversionRelayPolicyAsIs, sipCfg.DiversionRelayPolicy) } func TestVoiceInTrunksUpdatePstn(t *testing.T) { @@ -157,7 +158,7 @@ func TestVoiceInTrunksUpdateSip(t *testing.T) { Description: &desc, Configuration: &trunkconfiguration.SIPConfiguration{ Username: "new-username", - Host: "216.58.215.110", + Host: "203.0.113.110", MaxTransfers: 5, }, }) diff --git a/voice_out_trunks_test.go b/voice_out_trunks_test.go index ae592e7..bcca169 100644 --- a/voice_out_trunks_test.go +++ b/voice_out_trunks_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/didww/didww-api-3-go-sdk/resource" + "github.com/didww/didww-api-3-go-sdk/resource/authenticationmethod" "github.com/didww/didww-api-3-go-sdk/resource/enums" "github.com/stretchr/testify/assert" @@ -26,6 +27,14 @@ func TestVoiceOutTrunksList(t *testing.T) { assert.Equal(t, "425ce763-a3a9-49b4-af5b-ada1a65c8864", trunk.ID) assert.Equal(t, "test", trunk.Name) assert.Equal(t, enums.VoiceOutTrunkStatusBlocked, trunk.Status) + + // Verify authentication_method is parsed as credentials_and_ip + require.NotNil(t, trunk.AuthenticationMethod) + credAM, ok := trunk.AuthenticationMethod.(*authenticationmethod.CredentialsAndIp) + require.True(t, ok, "expected CredentialsAndIp authentication method") + assert.Equal(t, "dpjgwbbac9", credAM.Username) + assert.Equal(t, "z0hshvbcy7", credAM.Password) + assert.Equal(t, []string{"203.0.113.1/32"}, credAM.AllowedSipIPs) } func TestVoiceOutTrunksFindWithIncludedDids(t *testing.T) { @@ -39,12 +48,17 @@ func TestVoiceOutTrunksFindWithIncludedDids(t *testing.T) { assert.Equal(t, "425ce763-a3a9-49b4-af5b-ada1a65c8864", trunk.ID) assert.Equal(t, "test", trunk.Name) - assert.Equal(t, "dpjgwbbac9", trunk.Username) - assert.Equal(t, "z0hshvbcy7", trunk.Password) assert.Equal(t, enums.MediaEncryptionModeSrtpSdes, trunk.MediaEncryptionMode) assert.True(t, trunk.ForceSymmetricRtp) assert.True(t, trunk.RtpPing) + // Verify authentication_method + require.NotNil(t, trunk.AuthenticationMethod) + credAM, ok := trunk.AuthenticationMethod.(*authenticationmethod.CredentialsAndIp) + require.True(t, ok, "expected CredentialsAndIp authentication method") + assert.Equal(t, "dpjgwbbac9", credAM.Username) + assert.Equal(t, "z0hshvbcy7", credAM.Password) + // Verify included default_did require.NotNil(t, trunk.DefaultDID) assert.Equal(t, "7de7f718-4042-4d74-9fe9-863fa1777520", trunk.DefaultDID.ID) @@ -54,6 +68,74 @@ func TestVoiceOutTrunksFindWithIncludedDids(t *testing.T) { require.Len(t, trunk.DIDs, 2) } +func TestVoiceOutTrunksFindIpOnly(t *testing.T) { + _, client := newTestServer(t, map[string]testRoute{ + "GET /v3/voice_out_trunks/23fd58f9-9094-406c-bfd9-f4d25bda13c6": {status: http.StatusOK, fixture: "voice_out_trunks/show_ip_only.json"}, + }) + + trunk, err := client.VoiceOutTrunks().Find(context.Background(), "23fd58f9-9094-406c-bfd9-f4d25bda13c6", nil) + require.NoError(t, err) + + assert.Equal(t, "23fd58f9-9094-406c-bfd9-f4d25bda13c6", trunk.ID) + assert.Equal(t, "SDK Test credentials_and_ip", trunk.Name) + assert.Equal(t, enums.VoiceOutTrunkStatusActive, trunk.Status) + + // Verify authentication_method is parsed as IpOnly, not CredentialsAndIp + require.NotNil(t, trunk.AuthenticationMethod) + ipAM, ok := trunk.AuthenticationMethod.(*authenticationmethod.IpOnly) + require.True(t, ok, "expected IpOnly authentication method, got %T", trunk.AuthenticationMethod) + assert.Equal(t, []string{"203.0.113.1/32"}, ipAM.AllowedSipIPs) + + // Must NOT be CredentialsAndIp + _, notCred := trunk.AuthenticationMethod.(*authenticationmethod.CredentialsAndIp) + assert.False(t, notCred, "authentication_method should not be CredentialsAndIp") +} + +func TestVoiceOutTrunksFindTwilio(t *testing.T) { + _, client := newTestServer(t, map[string]testRoute{ + "GET /v3/voice_out_trunks/b5e701f4-ea15-4f9d-8f35-6a0bdce04385": {status: http.StatusOK, fixture: "voice_out_trunks/show_twilio.json"}, + }) + + trunk, err := client.VoiceOutTrunks().Find(context.Background(), "b5e701f4-ea15-4f9d-8f35-6a0bdce04385", nil) + require.NoError(t, err) + + assert.Equal(t, "b5e701f4-ea15-4f9d-8f35-6a0bdce04385", trunk.ID) + assert.Equal(t, "SDK Test twilio", trunk.Name) + assert.Equal(t, enums.VoiceOutTrunkStatusActive, trunk.Status) + + // Verify authentication_method is parsed as Twilio + require.NotNil(t, trunk.AuthenticationMethod) + twilioAM, ok := trunk.AuthenticationMethod.(*authenticationmethod.Twilio) + require.True(t, ok, "expected Twilio authentication method, got %T", trunk.AuthenticationMethod) + assert.Equal(t, "AC22222222222222222222222222222222", twilioAM.TwilioAccountSid) +} + +func TestVoiceOutTrunksCreateTwilio(t *testing.T) { + server, capturedBodyPtr := captureRequestBody(t, map[string]testRoute{ + "POST /v3/voice_out_trunks": {status: http.StatusCreated, fixture: "voice_out_trunks/create_twilio.json"}, + }) + + trunk, err := server.client.VoiceOutTrunks().Create(context.Background(), &resource.VoiceOutTrunk{ + Name: "SDK Test twilio create", + OnCliMismatchAction: enums.OnCliMismatchActionRejectCall, + AuthenticationMethod: &authenticationmethod.Twilio{ + TwilioAccountSid: "AC33333333333333333333333333333333", + }, + }) + require.NoError(t, err) + + assert.Equal(t, "507fa5a2-fd58-4c4d-a231-efba27f67c3a", trunk.ID) + assert.Equal(t, "SDK Test twilio create", trunk.Name) + + // Verify authentication_method in response + require.NotNil(t, trunk.AuthenticationMethod) + twilioAM, ok := trunk.AuthenticationMethod.(*authenticationmethod.Twilio) + require.True(t, ok, "expected Twilio authentication method") + assert.Equal(t, "AC33333333333333333333333333333333", twilioAM.TwilioAccountSid) + + assertRequestJSON(t, *capturedBodyPtr, "voice_out_trunks/create_twilio_request.json") +} + func TestVoiceOutTrunksCreate(t *testing.T) { server, capturedBodyPtr := captureRequestBody(t, map[string]testRoute{ "POST /v3/voice_out_trunks": {status: http.StatusCreated, fixture: "voice_out_trunks/create.json"}, @@ -61,15 +143,25 @@ func TestVoiceOutTrunksCreate(t *testing.T) { trunk, err := server.client.VoiceOutTrunks().Create(context.Background(), &resource.VoiceOutTrunk{ Name: "java-test", - AllowedSipIPs: []string{"0.0.0.0/0"}, OnCliMismatchAction: enums.OnCliMismatchActionReplaceCli, - DefaultDIDID: "7a028c32-e6b6-4c86-bf01-90f901b37012", - DIDIDs: []string{"7a028c32-e6b6-4c86-bf01-90f901b37012"}, + AuthenticationMethod: &authenticationmethod.CredentialsAndIp{ + AllowedSipIPs: []string{"203.0.113.0/24"}, + }, + DefaultDIDID: "7a028c32-e6b6-4c86-bf01-90f901b37012", + DIDIDs: []string{"7a028c32-e6b6-4c86-bf01-90f901b37012"}, }) require.NoError(t, err) assert.Equal(t, "b60201c1-21f0-4d9a-aafa-0e6d1e12f22e", trunk.ID) + // Verify authentication_method in response + require.NotNil(t, trunk.AuthenticationMethod) + credAM, ok := trunk.AuthenticationMethod.(*authenticationmethod.CredentialsAndIp) + require.True(t, ok, "expected CredentialsAndIp authentication method") + assert.Equal(t, []string{"203.0.113.0/24"}, credAM.AllowedSipIPs) + assert.Equal(t, "dLPa6JbLTeMjKjl5", credAM.Username) + assert.Equal(t, "BZj1YvP45yWvX5Ic", credAM.Password) + assertRequestJSON(t, *capturedBodyPtr, "voice_out_trunks/create_request.json") } @@ -80,7 +172,6 @@ func TestVoiceOutTrunksUpdate(t *testing.T) { trunk, err := client.VoiceOutTrunks().Update(context.Background(), &resource.VoiceOutTrunk{ ID: "425ce763-a3a9-49b4-af5b-ada1a65c8864", - AllowedSipIPs: []string{"10.11.12.13/32"}, CapacityLimit: intPtr(123), }) require.NoError(t, err) @@ -89,11 +180,80 @@ func TestVoiceOutTrunksUpdate(t *testing.T) { assert.Equal(t, "test", trunk.Name) require.NotNil(t, trunk.CapacityLimit) assert.Equal(t, 123, *trunk.CapacityLimit) - assert.Equal(t, []string{"10.11.12.13/32"}, trunk.AllowedSipIPs) assert.True(t, trunk.ForceSymmetricRtp) assert.True(t, trunk.RtpPing) } +func TestVoiceOutTrunksUpdateAuthenticationMethod(t *testing.T) { + server, capturedBodyPtr := captureRequestBody(t, map[string]testRoute{ + "PATCH /v3/voice_out_trunks/425ce763-a3a9-49b4-af5b-ada1a65c8864": {status: http.StatusOK, fixture: "voice_out_trunks/update.json"}, + }) + + trunk, err := server.client.VoiceOutTrunks().Update(context.Background(), &resource.VoiceOutTrunk{ + ID: "425ce763-a3a9-49b4-af5b-ada1a65c8864", + AuthenticationMethod: &authenticationmethod.CredentialsAndIp{ + AllowedSipIPs: []string{"192.0.2.10/32"}, + TechPrefix: "99", + }, + }) + require.NoError(t, err) + + assert.Equal(t, "425ce763-a3a9-49b4-af5b-ada1a65c8864", trunk.ID) + + assertRequestJSON(t, *capturedBodyPtr, "voice_out_trunks/update_auth_method_request.json") +} + +func TestVoiceOutTrunksUpdateEmergencyEnableAll(t *testing.T) { + server, capturedBodyPtr := captureRequestBody(t, map[string]testRoute{ + "PATCH /v3/voice_out_trunks/01234567-89ab-cdef-0123-456789abcdef": {status: http.StatusOK, fixture: "voice_out_trunks/update_emergency_dids.json"}, + }) + + trunk, err := server.client.VoiceOutTrunks().Update(context.Background(), &resource.VoiceOutTrunk{ + ID: "01234567-89ab-cdef-0123-456789abcdef", + EmergencyEnableAll: true, + }) + require.NoError(t, err) + + assertRequestJSON(t, *capturedBodyPtr, "voice_out_trunks/update_emergency_enable_all_request.json") + + assert.Equal(t, "01234567-89ab-cdef-0123-456789abcdef", trunk.ID) +} + +func TestVoiceOutTrunksUpdateEmergencyDIDs(t *testing.T) { + server, capturedBodyPtr := captureRequestBody(t, map[string]testRoute{ + "PATCH /v3/voice_out_trunks/01234567-89ab-cdef-0123-456789abcdef": {status: http.StatusOK, fixture: "voice_out_trunks/update_emergency_dids.json"}, + }) + + trunk, err := server.client.VoiceOutTrunks().Update(context.Background(), &resource.VoiceOutTrunk{ + ID: "01234567-89ab-cdef-0123-456789abcdef", + EmergencyDIDIDs: []string{ + "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", + "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", + }, + }) + require.NoError(t, err) + + assertRequestJSON(t, *capturedBodyPtr, "voice_out_trunks/update_emergency_dids_request.json") + + assert.Equal(t, "01234567-89ab-cdef-0123-456789abcdef", trunk.ID) +} + +func TestVoiceOutTrunksUpdateClearEmergencyDIDs(t *testing.T) { + server, capturedBodyPtr := captureRequestBody(t, map[string]testRoute{ + "PATCH /v3/voice_out_trunks/01234567-89ab-cdef-0123-456789abcdef": {status: http.StatusOK, fixture: "voice_out_trunks/update_emergency_dids.json"}, + }) + + trunk, err := server.client.VoiceOutTrunks().Update(context.Background(), &resource.VoiceOutTrunk{ + ID: "01234567-89ab-cdef-0123-456789abcdef", + ClearEmergencyDIDs: true, + }) + require.NoError(t, err) + + assertRequestJSON(t, *capturedBodyPtr, "voice_out_trunks/update_emergency_dids_clear_request.json") + + assert.Equal(t, "01234567-89ab-cdef-0123-456789abcdef", trunk.ID) +} + func TestVoiceOutTrunksDelete(t *testing.T) { _, client := newTestServer(t, map[string]testRoute{ "DELETE /v3/voice_out_trunks/425ce763-a3a9-49b4-af5b-ada1a65c8864": {status: http.StatusNoContent},