From 89c37d26ae8c26bc3f41d3820d2000b7f4debd6d Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 09:48:52 +0200 Subject: [PATCH 01/89] chore: bump version to 3.0.0-dev --- client_test.go | 2 +- repository.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/repository.go b/repository.go index 7f67d2c..3362518 100644 --- a/repository.go +++ b/repository.go @@ -121,7 +121,7 @@ func (r *SingletonRepository[T]) Find(ctx context.Context) (*T, error) { const ( jsonapiMediaType = "application/vnd.api+json" apiVersion = "2022-05-10" - sdkVersion = "1.0.0" + sdkVersion = "3.0.0-dev" ) // doRequest executes an HTTP request and returns the response body. From b6e0f4a65105ba4c66875444c021d9b0449a0866 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 09:49:12 +0200 Subject: [PATCH 02/89] feat!: set default X-DIDWW-API-Version to 2026-04-16 --- repository.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/repository.go b/repository.go index 3362518..ba27fdf 100644 --- a/repository.go +++ b/repository.go @@ -120,7 +120,7 @@ func (r *SingletonRepository[T]) Find(ctx context.Context) (*T, error) { const ( jsonapiMediaType = "application/vnd.api+json" - apiVersion = "2022-05-10" + apiVersion = "2026-04-16" sdkVersion = "3.0.0-dev" ) From fcd2113758bf37f40be4e3797241778a11c26a83 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 09:51:25 +0200 Subject: [PATCH 03/89] feat!: rename requirement_validations resource to address_requirement_validations --- .claude/settings.local.json | 18 ++++++++++++++++++ client.go | 4 ++-- requirement_validations_test.go | 16 ++++++++-------- resource/requirement.go | 6 +++--- .../create.json | 2 +- .../create_error_validation.json | 0 .../create_request.json | 2 +- .../create_request_failed.json | 2 +- 8 files changed, 34 insertions(+), 16 deletions(-) create mode 100644 .claude/settings.local.json rename testdata/fixtures/{requirement_validations => address_requirement_validations}/create.json (71%) rename testdata/fixtures/{requirement_validations => address_requirement_validations}/create_error_validation.json (100%) rename testdata/fixtures/{requirement_validations => address_requirement_validations}/create_request.json (88%) rename testdata/fixtures/{requirement_validations => address_requirement_validations}/create_request_failed.json (91%) diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..5b061d6 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,18 @@ +{ + "permissions": { + "allow": [ + "Bash(gh api:*)", + "WebSearch", + "WebFetch(domain:github.com)", + "WebFetch(domain:raw.githubusercontent.com)", + "Bash(DIDWW_API_KEY=e6t010o1zug6x50bs84yvszzj0e5jhj2 go run:*)", + "Bash(go test:*)", + "Bash(gh pr:*)", + "Bash(go vet:*)", + "Bash(git:*)", + "Bash(go:*)", + "Bash(python3:*)", + "WebFetch(domain:didww.github.io)" + ] + } +} diff --git a/client.go b/client.go index 86052d3..06612b7 100644 --- a/client.go +++ b/client.go @@ -174,8 +174,8 @@ func (c *Client) ProofTypes() *Repository[resource.ProofType] { func (c *Client) Requirements() *Repository[resource.Requirement] { return NewRepository[resource.Requirement](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] { diff --git a/requirement_validations_test.go b/requirement_validations_test.go index 85b9f4d..5390595 100644 --- a/requirement_validations_test.go +++ b/requirement_validations_test.go @@ -11,12 +11,12 @@ 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{ + rv, err := server.client.AddressRequirementValidations().Create(context.Background(), &resource.AddressRequirementValidation{ AddressID: "d3414687-40f4-4346-a267-c2c65117d28c", RequirementID: "aea92b24-a044-4864-9740-89d3e15b65c7", }) @@ -24,15 +24,15 @@ func TestRequirementValidationsCreate(t *testing.T) { 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{ + _, err := server.client.AddressRequirementValidations().Create(context.Background(), &resource.AddressRequirementValidation{ IdentityID: "5e9df058-50d2-4e34-b0d4-d1746b86f41a", AddressID: "d3414687-40f4-4346-a267-c2c65117d28c", RequirementID: "2efc3427-8ba6-4d50-875d-f2de4a068de8", @@ -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/resource/requirement.go b/resource/requirement.go index 9c311c8..c2a7295 100644 --- a/resource/requirement.go +++ b/resource/requirement.go @@ -26,9 +26,9 @@ 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"` 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 88% rename from testdata/fixtures/requirement_validations/create_request.json rename to testdata/fixtures/address_requirement_validations/create_request.json index 2131aab..8c446cb 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": { diff --git a/testdata/fixtures/requirement_validations/create_request_failed.json b/testdata/fixtures/address_requirement_validations/create_request_failed.json similarity index 91% rename from testdata/fixtures/requirement_validations/create_request_failed.json rename to testdata/fixtures/address_requirement_validations/create_request_failed.json index bc7e8d4..c7460fc 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": { From 628ec02482a2a2dc31a2fcd29eee0871359b4599 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 09:51:48 +0200 Subject: [PATCH 04/89] chore: gitignore .claude/ and coverage artifacts --- .claude/settings.local.json | 18 ------------------ .gitignore | 5 +++++ 2 files changed, 5 insertions(+), 18 deletions(-) delete mode 100644 .claude/settings.local.json create mode 100644 .gitignore diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index 5b061d6..0000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(gh api:*)", - "WebSearch", - "WebFetch(domain:github.com)", - "WebFetch(domain:raw.githubusercontent.com)", - "Bash(DIDWW_API_KEY=e6t010o1zug6x50bs84yvszzj0e5jhj2 go run:*)", - "Bash(go test:*)", - "Bash(gh pr:*)", - "Bash(go vet:*)", - "Bash(git:*)", - "Bash(go:*)", - "Bash(python3:*)", - "WebFetch(domain:didww.github.io)" - ] - } -} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..862dbf4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +# Claude AI +.claude/ + +# Coverage +coverage.out From 718ee09affd716749c4d4990aa867debfcd55712 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 09:53:40 +0200 Subject: [PATCH 05/89] feat!: rename requirements resource to address_requirements --- client.go | 4 ++-- requirements_test.go | 12 ++++++------ resource/did_group.go | 2 +- resource/requirement.go | 8 ++++---- .../create_request.json | 2 +- .../create_request_failed.json | 2 +- .../index.json | 10 +++++----- .../{requirements => address_requirements}/show.json | 2 +- .../fixtures/did_groups/show_with_requirement.json | 4 ++-- 9 files changed, 23 insertions(+), 23 deletions(-) rename testdata/fixtures/{requirements => address_requirements}/index.json (99%) rename testdata/fixtures/{requirements => address_requirements}/show.json (99%) diff --git a/client.go b/client.go index 06612b7..378dfa4 100644 --- a/client.go +++ b/client.go @@ -171,8 +171,8 @@ 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) AddressRequirementValidations() *Repository[resource.AddressRequirementValidation] { return NewRepository[resource.AddressRequirementValidation](c) diff --git a/requirements_test.go b/requirements_test.go index 30825d9..442cd24 100644 --- a/requirements_test.go +++ b/requirements_test.go @@ -9,24 +9,24 @@ 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) diff --git a/resource/did_group.go b/resource/did_group.go index 9f111b6..6d0b529 100644 --- a/resource/did_group.go +++ b/resource/did_group.go @@ -16,7 +16,7 @@ type DIDGroup struct { Region *Region `json:"-" rel:"region"` DIDGroupType *DIDGroupType `json:"-" rel:"did_group_type"` StockKeepingUnits []*StockKeepingUnit `json:"-" rel:"stock_keeping_units"` - Requirement *Requirement `json:"-" rel:"requirement"` + Requirement *AddressRequirement `json:"-" rel:"requirement"` } // DIDGroupType represents a type of DID group. diff --git a/resource/requirement.go b/resource/requirement.go index c2a7295..da59dd4 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"` @@ -32,5 +32,5 @@ type AddressRequirementValidation struct { // Relationship IDs for create AddressID string `json:"-" rel:"address,addresses"` IdentityID string `json:"-" rel:"identity,identities"` - RequirementID string `json:"-" rel:"requirement,requirements"` + RequirementID string `json:"-" rel:"requirement,address_requirements"` } diff --git a/testdata/fixtures/address_requirement_validations/create_request.json b/testdata/fixtures/address_requirement_validations/create_request.json index 8c446cb..ee16711 100644 --- a/testdata/fixtures/address_requirement_validations/create_request.json +++ b/testdata/fixtures/address_requirement_validations/create_request.json @@ -11,7 +11,7 @@ }, "requirement": { "data": { - "type": "requirements", + "type": "address_requirements", "id": "aea92b24-a044-4864-9740-89d3e15b65c7" } } diff --git a/testdata/fixtures/address_requirement_validations/create_request_failed.json b/testdata/fixtures/address_requirement_validations/create_request_failed.json index c7460fc..6fa0b94 100644 --- a/testdata/fixtures/address_requirement_validations/create_request_failed.json +++ b/testdata/fixtures/address_requirement_validations/create_request_failed.json @@ -17,7 +17,7 @@ }, "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 99% rename from testdata/fixtures/requirements/index.json rename to testdata/fixtures/address_requirements/index.json index d4876b1..738f787 100644 --- a/testdata/fixtures/requirements/index.json +++ b/testdata/fixtures/address_requirements/index.json @@ -2,7 +2,7 @@ "data": [ { "id": "b6c80acb-3952-4d53-9e62-fe2348c0636b", - "type": "requirements", + "type": "address_requirements", "attributes": { "identity_type": "Any", "personal_area_level": "Country", @@ -77,7 +77,7 @@ }, { "id": "51b293af-a496-4bf2-9c68-5221b3b0dc1e", - "type": "requirements", + "type": "address_requirements", "attributes": { "identity_type": "Any", "personal_area_level": "Country", @@ -154,7 +154,7 @@ }, { "id": "f7c2a9a4-a40e-4f22-98ff-0f53c86052ac", - "type": "requirements", + "type": "address_requirements", "attributes": { "identity_type": "Any", "personal_area_level": "WorldWide", @@ -229,7 +229,7 @@ }, { "id": "edb71cff-d5e8-44ff-ba36-6f758066c175", - "type": "requirements", + "type": "address_requirements", "attributes": { "identity_type": "Any", "personal_area_level": "Country", @@ -306,7 +306,7 @@ }, { "id": "90b72a1a-1ebd-4771-b818-f7123f8ff7ec", - "type": "requirements", + "type": "address_requirements", "attributes": { "identity_type": "Any", "personal_area_level": "WorldWide", diff --git a/testdata/fixtures/requirements/show.json b/testdata/fixtures/address_requirements/show.json similarity index 99% rename from testdata/fixtures/requirements/show.json rename to testdata/fixtures/address_requirements/show.json index 786d2cc..9ad11ca 100644 --- a/testdata/fixtures/requirements/show.json +++ b/testdata/fixtures/address_requirements/show.json @@ -1,7 +1,7 @@ { "data": { "id": "25d12afe-1ec6-4fe3-9621-b250dd1fb959", - "type": "requirements", + "type": "address_requirements", "attributes": { "identity_type": "Any", "personal_area_level": "WorldWide", diff --git a/testdata/fixtures/did_groups/show_with_requirement.json b/testdata/fixtures/did_groups/show_with_requirement.json index b8a4cf2..3fbbb47 100644 --- a/testdata/fixtures/did_groups/show_with_requirement.json +++ b/testdata/fixtures/did_groups/show_with_requirement.json @@ -50,7 +50,7 @@ "related": "https://sandbox-api.didww.com/v3/did_groups/2187c36d-28fb-436f-8861-5a0f5b5a3ee1/requirement" }, "data": { - "type": "requirements", + "type": "address_requirements", "id": "8da1e0b2-047c-4baf-9c57-57143f09b9ce" } } @@ -64,7 +64,7 @@ "included": [ { "id": "8da1e0b2-047c-4baf-9c57-57143f09b9ce", - "type": "requirements", + "type": "address_requirements", "attributes": { "identity_type": "Any", "personal_area_level": "WorldWide", From d3d9a533792e77f3b16f8828568a56bb98a7e56a Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 09:54:30 +0200 Subject: [PATCH 06/89] feat!: rename AddressRequirementValidation#requirement relation to address_requirement --- requirement_validations_test.go | 4 ++-- resource/requirement.go | 2 +- .../address_requirement_validations/create_request.json | 2 +- .../create_request_failed.json | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/requirement_validations_test.go b/requirement_validations_test.go index 5390595..8b02d10 100644 --- a/requirement_validations_test.go +++ b/requirement_validations_test.go @@ -18,7 +18,7 @@ func TestAddressRequirementValidationsCreate(t *testing.T) { rv, err := server.client.AddressRequirementValidations().Create(context.Background(), &resource.AddressRequirementValidation{ AddressID: "d3414687-40f4-4346-a267-c2c65117d28c", - RequirementID: "aea92b24-a044-4864-9740-89d3e15b65c7", + AddressRequirementID: "aea92b24-a044-4864-9740-89d3e15b65c7", }) require.NoError(t, err) @@ -35,7 +35,7 @@ func TestAddressRequirementValidationsCreateError(t *testing.T) { _, err := server.client.AddressRequirementValidations().Create(context.Background(), &resource.AddressRequirementValidation{ IdentityID: "5e9df058-50d2-4e34-b0d4-d1746b86f41a", AddressID: "d3414687-40f4-4346-a267-c2c65117d28c", - RequirementID: "2efc3427-8ba6-4d50-875d-f2de4a068de8", + AddressRequirementID: "2efc3427-8ba6-4d50-875d-f2de4a068de8", }) require.Error(t, err) diff --git a/resource/requirement.go b/resource/requirement.go index da59dd4..100fbea 100644 --- a/resource/requirement.go +++ b/resource/requirement.go @@ -32,5 +32,5 @@ type AddressRequirementValidation struct { // Relationship IDs for create AddressID string `json:"-" rel:"address,addresses"` IdentityID string `json:"-" rel:"identity,identities"` - RequirementID string `json:"-" rel:"requirement,address_requirements"` + AddressRequirementID string `json:"-" rel:"address_requirement,address_requirements"` } diff --git a/testdata/fixtures/address_requirement_validations/create_request.json b/testdata/fixtures/address_requirement_validations/create_request.json index ee16711..dcae3a4 100644 --- a/testdata/fixtures/address_requirement_validations/create_request.json +++ b/testdata/fixtures/address_requirement_validations/create_request.json @@ -9,7 +9,7 @@ "id": "d3414687-40f4-4346-a267-c2c65117d28c" } }, - "requirement": { + "address_requirement": { "data": { "type": "address_requirements", "id": "aea92b24-a044-4864-9740-89d3e15b65c7" diff --git a/testdata/fixtures/address_requirement_validations/create_request_failed.json b/testdata/fixtures/address_requirement_validations/create_request_failed.json index 6fa0b94..aa7ee15 100644 --- a/testdata/fixtures/address_requirement_validations/create_request_failed.json +++ b/testdata/fixtures/address_requirement_validations/create_request_failed.json @@ -15,7 +15,7 @@ "id": "d3414687-40f4-4346-a267-c2c65117d28c" } }, - "requirement": { + "address_requirement": { "data": { "type": "address_requirements", "id": "2efc3427-8ba6-4d50-875d-f2de4a068de8" From 7a4e494e5b41f75cc7dc399684cda2ec9668d9bb Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 09:55:12 +0200 Subject: [PATCH 07/89] feat!: address_verifications#reject_reasons is now an array of strings --- resource/address_verification.go | 28 ------------------- .../address_verifications/show_rejected.json | 2 +- 2 files changed, 1 insertion(+), 29 deletions(-) diff --git a/resource/address_verification.go b/resource/address_verification.go index 3447740..7cfedf1 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" @@ -24,29 +22,3 @@ type AddressVerification struct { // Resolved relationships 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 -} diff --git a/testdata/fixtures/address_verifications/show_rejected.json b/testdata/fixtures/address_verifications/show_rejected.json index 715b8f6..a41da32 100644 --- a/testdata/fixtures/address_verifications/show_rejected.json +++ b/testdata/fixtures/address_verifications/show_rejected.json @@ -7,7 +7,7 @@ "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", + "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" }, From a37572b797677c274f947bdc2514b4c3c87daf8e Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 09:56:14 +0200 Subject: [PATCH 08/89] feat!: rename DidGroup#requirement relation to address_requirement --- did_groups_test.go | 24 +++++++++---------- resource/did_group.go | 2 +- .../did_groups/show_with_requirement.json | 6 ++--- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/did_groups_test.go b/did_groups_test.go index 5567dd7..6246ccc 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, "WorldWide", 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/resource/did_group.go b/resource/did_group.go index 6d0b529..ee7482f 100644 --- a/resource/did_group.go +++ b/resource/did_group.go @@ -16,7 +16,7 @@ type DIDGroup struct { Region *Region `json:"-" rel:"region"` DIDGroupType *DIDGroupType `json:"-" rel:"did_group_type"` StockKeepingUnits []*StockKeepingUnit `json:"-" rel:"stock_keeping_units"` - Requirement *AddressRequirement `json:"-" rel:"requirement"` + AddressRequirement *AddressRequirement `json:"-" rel:"address_requirement"` } // DIDGroupType represents a type of DID group. diff --git a/testdata/fixtures/did_groups/show_with_requirement.json b/testdata/fixtures/did_groups/show_with_requirement.json index 3fbbb47..a641bb7 100644 --- a/testdata/fixtures/did_groups/show_with_requirement.json +++ b/testdata/fixtures/did_groups/show_with_requirement.json @@ -44,10 +44,10 @@ "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": "address_requirements", From ab9c28a52735fcefb3905b7cbe74853cd560a5d6 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 09:57:17 +0200 Subject: [PATCH 09/89] feat!: drop sms_out from did_groups features --- resource/enums/feature.go | 1 - resource/enums/feature_test.go | 1 - .../dids/show_with_address_verification_and_did_group.json | 1 - 3 files changed, 3 deletions(-) diff --git a/resource/enums/feature.go b/resource/enums/feature.go index 17a6bc8..9cb22bc 100644 --- a/resource/enums/feature.go +++ b/resource/enums/feature.go @@ -8,5 +8,4 @@ const ( FeatureVoiceOut Feature = "voice_out" FeatureT38 Feature = "t38" FeatureSmsIn Feature = "sms_in" - FeatureSmsOut Feature = "sms_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/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..7117e0e 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, From 1cbcb4e12f6e0ace7f1b751a9e2161dc5b71389d Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 09:58:36 +0200 Subject: [PATCH 10/89] feat!: rename DidReservation#expire_at to expires_at --- examples/did_reservations/main.go | 2 +- examples/orders_reservation_dids/main.go | 2 +- resource/available_did.go | 2 +- testdata/fixtures/did_reservations/create.json | 2 +- testdata/fixtures/did_reservations/index.json | 2 +- testdata/fixtures/did_reservations/show.json | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) 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/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/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/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" }, From 68e693103246cf0d4774d158de9c341bd6b0e0b1 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 09:59:22 +0200 Subject: [PATCH 11/89] feat!: rename EncryptedFile#expire_at to expires_at --- encrypted_files_test.go | 4 ++-- resource/encrypted_file.go | 2 +- testdata/fixtures/encrypted_files/index.json | 2 +- testdata/fixtures/encrypted_files/show.json | 2 +- testdata/fixtures/encrypted_files/show_with_expiration.json | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) 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/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/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": { From 861c44856fe7a5647c23004bc7a848e311e1d2d0 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 10:00:44 +0200 Subject: [PATCH 12/89] feat!: encrypted_files POST accepts a single file per request --- encrypt.go | 40 ++++++++++--------- encrypt_test.go | 6 +-- examples/upload_encrypted_file/main.go | 4 +- testdata/fixtures/encrypted_files/create.json | 17 +++++--- 4 files changed, 37 insertions(+), 30 deletions(-) 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/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/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" + } +} From 74c2bf3b6526de0bd41ba98f1fb8833dc5ffa86a Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 10:01:56 +0200 Subject: [PATCH 13/89] feat!: replace exports year/month filters with from/to datetime range --- examples/exports/main.go | 2 +- exports_test.go | 8 ++++---- testdata/fixtures/exports/create_request.json | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/exports/main.go b/examples/exports/main.go index c92c6e4..3d7f21b 100644 --- a/examples/exports/main.go +++ b/examples/exports/main.go @@ -19,7 +19,7 @@ func main() { // Create an export export := &resource.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-15 23:59:59"}, } created, err := client.Exports().Create(ctx, export) if err != nil { diff --git a/exports_test.go b/exports_test.go index b54a755..c24e627 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) 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"}}}} From ce79f0f508feac6a280ee5598aaf46471a13869d Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 10:03:43 +0200 Subject: [PATCH 14/89] feat!: add diversion_relay_policy to VoiceInTrunk SIP configuration --- resource/enums/diversion_relay_policy.go | 11 +++++++++++ resource/trunkconfiguration/sip_configuration.go | 1 + testdata/fixtures/voice_in_trunks/create_sip.json | 3 ++- voice_in_trunks_test.go | 1 + 4 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 resource/enums/diversion_relay_policy.go 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/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/testdata/fixtures/voice_in_trunks/create_sip.json b/testdata/fixtures/voice_in_trunks/create_sip.json index 2217bf1..b2a441a 100644 --- a/testdata/fixtures/voice_in_trunks/create_sip.json +++ b/testdata/fixtures/voice_in_trunks/create_sip.json @@ -98,7 +98,8 @@ "stir_shaken_mode": "pai", "allowed_rtp_ips": [ "127.0.0.1" - ] + ], + "diversion_relay_policy": "as_is" } }, "created_at": "2018-12-28T17:37:48.010Z" diff --git a/voice_in_trunks_test.go b/voice_in_trunks_test.go index 21ede55..72e677a 100644 --- a/voice_in_trunks_test.go +++ b/voice_in_trunks_test.go @@ -122,6 +122,7 @@ func TestVoiceInTrunksCreateSip(t *testing.T) { 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, enums.DiversionRelayPolicyAsIs, sipCfg.DiversionRelayPolicy) } func TestVoiceInTrunksUpdatePstn(t *testing.T) { From fe40a763964b5328372b6cd7ee1cbc8ddfc6cf45 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 10:07:18 +0200 Subject: [PATCH 15/89] feat!: replace flat VoiceOutTrunk credentials with polymorphic authentication_method --- examples/voice_out_trunks/main.go | 15 +-- .../authentication_method.go | 110 ++++++++++++++++++ resource/voice_out_trunk.go | 79 ++++++++++--- .../fixtures/voice_out_trunks/create.json | 12 +- .../voice_out_trunks/create_request.json | 2 +- testdata/fixtures/voice_out_trunks/index.json | 14 ++- testdata/fixtures/voice_out_trunks/show.json | 14 ++- .../fixtures/voice_out_trunks/update.json | 14 ++- voice_out_trunks_test.go | 34 ++++-- 9 files changed, 240 insertions(+), 54 deletions(-) create mode 100644 resource/authenticationmethod/authentication_method.go diff --git a/examples/voice_out_trunks/main.go b/examples/voice_out_trunks/main.go index b0e9daf..a24710d 100644 --- a/examples/voice_out_trunks/main.go +++ b/examples/voice_out_trunks/main.go @@ -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,11 +21,13 @@ func main() { client := examples.ClientFromEnv() ctx := context.Background() - // Create a voice out trunk + // Create a voice out trunk with ip_only authentication 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 %d", time.Now().UnixMilli()), + AuthenticationMethod: &authenticationmethod.IpOnly{ + AllowedSipIPs: []string{"203.0.113.1"}, + }, + AllowedRtpIPs: []string{"203.0.113.1"}, DstPrefixes: []string{}, DefaultDstAction: enums.DefaultDstActionAllowAll, OnCliMismatchAction: enums.OnCliMismatchActionRejectCall, @@ -37,8 +40,7 @@ func main() { } fmt.Println("Created voice out trunk:", created.ID) fmt.Println(" name:", created.Name) - fmt.Println(" username:", created.Username) - fmt.Println(" password:", created.Password) + fmt.Println(" auth type:", created.AuthenticationMethod.AuthenticationType()) fmt.Println(" status:", created.Status) // List voice out trunks @@ -53,7 +55,6 @@ func main() { // Update created.Name = "Updated Outbound Trunk" - created.AllowedSipIPs = []string{"10.0.0.0/8"} updated, err := client.VoiceOutTrunks().Update(ctx, created) if err != nil { panic(err) diff --git a/resource/authenticationmethod/authentication_method.go b/resource/authenticationmethod/authentication_method.go new file mode 100644 index 0000000..5b6c1ba --- /dev/null +++ b/resource/authenticationmethod/authentication_method.go @@ -0,0 +1,110 @@ +package authenticationmethod + +import ( + "encoding/json" + "fmt" +) + +// AuthenticationMethod is the interface for polymorphic authentication methods +// on VoiceOutTrunk resources. +type AuthenticationMethod interface { + AuthenticationType() string +} + +// IpOnly uses IP-based authentication only. +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/voice_out_trunk.go b/resource/voice_out_trunk.go index ad9f1d1..b51c625 100644 --- a/resource/voice_out_trunk.go +++ b/resource/voice_out_trunk.go @@ -1,32 +1,32 @@ package resource import ( + "encoding/json" "time" + "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"` + AuthenticationMethod authenticationmethod.AuthenticationMethod `json:"-"` // Relationship IDs for create/update DefaultDIDID string `json:"-" rel:"default_did,dids"` DIDIDs []string `json:"-" rel:"dids,dids"` @@ -35,6 +35,47 @@ type VoiceOutTrunk struct { DIDs []*DID `json:"-" rel:"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 +} + // 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/voice_out_trunks/create.json b/testdata/fixtures/voice_out_trunks/create.json index 44e2905..266aab9 100644 --- a/testdata/fixtures/voice_out_trunks/create.json +++ b/testdata/fixtures/voice_out_trunks/create.json @@ -3,17 +3,19 @@ "id": "b60201c1-21f0-4d9a-aafa-0e6d1e12f22e", "type": "voice_out_trunks", "attributes": { - "allowed_sip_ips": [ - "0.0.0.0/0" - ], + "authentication_method": { + "type": "ip_only", + "attributes": { + "allowed_sip_ips": ["203.0.113.0/24"], + "tech_prefix": "" + } + }, "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, diff --git a/testdata/fixtures/voice_out_trunks/create_request.json b/testdata/fixtures/voice_out_trunks/create_request.json index 39721a1..609192f 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":"ip_only","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/index.json b/testdata/fixtures/voice_out_trunks/index.json index 9a1d768..21434d6 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": ["10.11.12.13/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.json b/testdata/fixtures/voice_out_trunks/show.json index b389ff3..cad9c63 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": ["10.11.12.13/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.json b/testdata/fixtures/voice_out_trunks/update.json index d77a1b3..8aa4e36 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": ["10.11.12.13/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/voice_out_trunks_test.go b/voice_out_trunks_test.go index ae592e7..39ed9db 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{"10.11.12.13/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) @@ -61,15 +75,23 @@ 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.IpOnly{ + 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) + ipAM, ok := trunk.AuthenticationMethod.(*authenticationmethod.IpOnly) + require.True(t, ok, "expected IpOnly authentication method") + assert.Equal(t, []string{"203.0.113.0/24"}, ipAM.AllowedSipIPs) + assertRequestJSON(t, *capturedBodyPtr, "voice_out_trunks/create_request.json") } @@ -80,7 +102,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,7 +110,6 @@ 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) } From d202850c23adbb1aace4a8a5b7db965d3c4ac04e Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 10:08:33 +0200 Subject: [PATCH 16/89] feat: add DidHistory resource (/v3/did_history) for 2026-04-16 --- client.go | 3 ++ did_history_test.go | 42 ++++++++++++++++++++++++ resource/did_history.go | 13 ++++++++ testdata/fixtures/did_history/index.json | 27 +++++++++++++++ testdata/fixtures/did_history/show.json | 15 +++++++++ 5 files changed, 100 insertions(+) create mode 100644 did_history_test.go create mode 100644 resource/did_history.go create mode 100644 testdata/fixtures/did_history/index.json create mode 100644 testdata/fixtures/did_history/show.json diff --git a/client.go b/client.go index 378dfa4..7573477 100644 --- a/client.go +++ b/client.go @@ -199,6 +199,9 @@ func (c *Client) PermanentSupportingDocuments() *Repository[resource.PermanentSu func (c *Client) NanpaPrefixes() *Repository[resource.NanpaPrefix] { return NewRepository[resource.NanpaPrefix](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/did_history_test.go b/did_history_test.go new file mode 100644 index 0000000..fb90663 --- /dev/null +++ b/did_history_test.go @@ -0,0 +1,42 @@ +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()) +} diff --git a/resource/did_history.go b/resource/did_history.go new file mode 100644 index 0000000..dbb2850 --- /dev/null +++ b/resource/did_history.go @@ -0,0 +1,13 @@ +package resource + +import "time" + +// DIDHistory represents a DID ownership history record. +// Introduced in API 2026-04-16. Records are retained for the last 90 days. +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"` +} 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" + } +} From 2a7768ca5ec8dd90f594f0ade65937e32151ca1f Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 10:09:23 +0200 Subject: [PATCH 17/89] feat: add EmergencyRequirement resource (/v3/emergency_requirements) for 2026-04-16 --- client.go | 3 ++ emergency_requirements_test.go | 27 +++++++++++++++++ resource/emergency_requirement.go | 21 +++++++++++++ .../emergency_requirements/index.json | 30 +++++++++++++++++++ 4 files changed, 81 insertions(+) create mode 100644 emergency_requirements_test.go create mode 100644 resource/emergency_requirement.go create mode 100644 testdata/fixtures/emergency_requirements/index.json diff --git a/client.go b/client.go index 7573477..f25a198 100644 --- a/client.go +++ b/client.go @@ -199,6 +199,9 @@ func (c *Client) PermanentSupportingDocuments() *Repository[resource.PermanentSu func (c *Client) NanpaPrefixes() *Repository[resource.NanpaPrefix] { return NewRepository[resource.NanpaPrefix](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) } diff --git a/emergency_requirements_test.go b/emergency_requirements_test.go new file mode 100644 index 0000000..9f47d66 --- /dev/null +++ b/emergency_requirements_test.go @@ -0,0 +1,27 @@ +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) +} diff --git a/resource/emergency_requirement.go b/resource/emergency_requirement.go new file mode 100644 index 0000000..215d98a --- /dev/null +++ b/resource/emergency_requirement.go @@ -0,0 +1,21 @@ +package resource + +// 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 string `json:"identity_type" api:"readonly"` + AddressAreaLevel string `json:"address_area_level" api:"readonly"` + PersonalAreaLevel string `json:"personal_area_level" api:"readonly"` + BusinessAreaLevel string `json:"business_area_level" api:"readonly"` + AddressMandatoryFields []string `json:"address_mandatory_fields" api:"readonly"` + PersonalMandatoryFields []string `json:"personal_mandatory_fields" api:"readonly"` + 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"` + // Resolved relationships + Country *Country `json:"-" rel:"country"` + DIDGroupType *DIDGroupType `json:"-" rel:"did_group_type"` +} diff --git a/testdata/fixtures/emergency_requirements/index.json b/testdata/fixtures/emergency_requirements/index.json new file mode 100644 index 0000000..c611224 --- /dev/null +++ b/testdata/fixtures/emergency_requirements/index.json @@ -0,0 +1,30 @@ +{ + "data": [ + { + "id": "c1d2e3f4-a5b6-7890-1234-567890abcdef", + "type": "emergency_requirements", + "attributes": { + "identity_type": "Any", + "address_area_level": "City", + "personal_area_level": "WorldWide", + "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 + }, + "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" + } +} From 6a6eba9833e416e968a09bce864733f208a34817 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 10:10:19 +0200 Subject: [PATCH 18/89] feat: add EmergencyRequirementValidation resource (/v3/emergency_requirement_validations) for 2026-04-16 --- client.go | 3 ++ emergency_requirement_validations_test.go | 28 +++++++++++++++++++ resource/emergency_requirement_validation.go | 13 +++++++++ .../create.json | 9 ++++++ .../create_request.json | 26 +++++++++++++++++ 5 files changed, 79 insertions(+) create mode 100644 emergency_requirement_validations_test.go create mode 100644 resource/emergency_requirement_validation.go create mode 100644 testdata/fixtures/emergency_requirement_validations/create.json create mode 100644 testdata/fixtures/emergency_requirement_validations/create_request.json diff --git a/client.go b/client.go index f25a198..f6009a7 100644 --- a/client.go +++ b/client.go @@ -199,6 +199,9 @@ func (c *Client) PermanentSupportingDocuments() *Repository[resource.PermanentSu func (c *Client) NanpaPrefixes() *Repository[resource.NanpaPrefix] { return NewRepository[resource.NanpaPrefix](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) } 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/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/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" + } + } + } + } +} From 5a5f0e2b3c5922fa0924dbb915580a12bc2a0805 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 10:11:39 +0200 Subject: [PATCH 19/89] feat: add EmergencyCallingService resource (/v3/emergency_calling_services) for 2026-04-16 --- client.go | 6 ++ emergency_calling_services_test.go | 56 ++++++++++++++ resource/emergency_calling_service.go | 23 ++++++ resource/emergency_verification.go | 25 ++++++ .../emergency_calling_services/index.json | 28 +++++++ .../show_with_includes.json | 77 +++++++++++++++++++ 6 files changed, 215 insertions(+) create mode 100644 emergency_calling_services_test.go create mode 100644 resource/emergency_calling_service.go create mode 100644 resource/emergency_verification.go create mode 100644 testdata/fixtures/emergency_calling_services/index.json create mode 100644 testdata/fixtures/emergency_calling_services/show_with_includes.json diff --git a/client.go b/client.go index f6009a7..6bbeaa5 100644 --- a/client.go +++ b/client.go @@ -199,6 +199,12 @@ 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) } diff --git a/emergency_calling_services_test.go b/emergency_calling_services_test.go new file mode 100644 index 0000000..d6b930c --- /dev/null +++ b/emergency_calling_services_test.go @@ -0,0 +1,56 @@ +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) +} + +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) + + // 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/resource/emergency_calling_service.go b/resource/emergency_calling_service.go new file mode 100644 index 0000000..6a0613f --- /dev/null +++ b/resource/emergency_calling_service.go @@ -0,0 +1,23 @@ +package resource + +import "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 string `json:"name" api:"readonly"` + Reference string `json:"reference" api:"readonly"` + Status string `json:"status" api:"readonly"` + ActivatedAt *time.Time `json:"activated_at" api:"readonly"` + CanceledAt *time.Time `json:"canceled_at" api:"readonly"` + CreatedAt time.Time `json:"created_at" api:"readonly"` + RenewDate string `json:"renew_date" api:"readonly"` + // Resolved relationships + Country *Country `json:"-" rel:"country"` + DIDGroupType *DIDGroupType `json:"-" rel:"did_group_type"` + Order *Order `json:"-" rel:"order"` + EmergencyRequirement *EmergencyRequirement `json:"-" rel:"emergency_requirement"` + EmergencyVerification *EmergencyVerification `json:"-" rel:"emergency_verification"` + DIDs []*DID `json:"-" rel:"dids"` +} diff --git a/resource/emergency_verification.go b/resource/emergency_verification.go new file mode 100644 index 0000000..bef7846 --- /dev/null +++ b/resource/emergency_verification.go @@ -0,0 +1,25 @@ +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"` +} diff --git a/testdata/fixtures/emergency_calling_services/index.json b/testdata/fixtures/emergency_calling_services/index.json new file mode 100644 index 0000000..5a8bb3b --- /dev/null +++ b/testdata/fixtures/emergency_calling_services/index.json @@ -0,0 +1,28 @@ +{ + "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" + }, + "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..04a0b2f --- /dev/null +++ b/testdata/fixtures/emergency_calling_services/show_with_includes.json @@ -0,0 +1,77 @@ +{ + "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" + }, + "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" + } +} From 99287b73aba215aa6a6d2754eef71522d45aeab8 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 10:12:30 +0200 Subject: [PATCH 20/89] feat: add EmergencyVerification resource (/v3/emergency_verifications) for 2026-04-16 --- emergency_verifications_test.go | 44 +++++++++++++++++++ .../emergency_verifications/create.json | 19 ++++++++ .../create_request.json | 20 +++++++++ .../emergency_verifications/index.json | 29 ++++++++++++ 4 files changed, 112 insertions(+) create mode 100644 emergency_verifications_test.go create mode 100644 testdata/fixtures/emergency_verifications/create.json create mode 100644 testdata/fixtures/emergency_verifications/create_request.json create mode 100644 testdata/fixtures/emergency_verifications/index.json diff --git a/emergency_verifications_test.go b/emergency_verifications_test.go new file mode 100644 index 0000000..6a3e146 --- /dev/null +++ b/emergency_verifications_test.go @@ -0,0 +1,44 @@ +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 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/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" + } +} From ed1754c8af01eb4f6c2974d771aad4a66b741f76 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 10:12:58 +0200 Subject: [PATCH 21/89] feat: add external_reference_id to Address for 2026-04-16 --- resource/address.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/resource/address.go b/resource/address.go index b9a542e..e48b5df 100644 --- a/resource/address.go +++ b/resource/address.go @@ -10,7 +10,8 @@ type Address struct { Address string `json:"address"` Description string `json:"description"` CreatedAt time.Time `json:"created_at" api:"readonly"` - Verified bool `json:"verified" 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"` From aa610862917bd194d39f1bb201ec6bd4bde80481 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 10:13:21 +0200 Subject: [PATCH 22/89] feat: add reject_comment and external_reference_id to AddressVerification for 2026-04-16 --- resource/address_verification.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/resource/address_verification.go b/resource/address_verification.go index 7cfedf1..356d0e1 100644 --- a/resource/address_verification.go +++ b/resource/address_verification.go @@ -15,7 +15,9 @@ type AddressVerification struct { 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"` + 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"` From d107e64f46b2ee90e3cac18373d0a07994a31216 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 10:13:58 +0200 Subject: [PATCH 23/89] feat: support PATCH /address_verifications/:id to update external_reference_id (2026-04-16) --- address_verifications_test.go | 41 +++++++++++++++++++ .../address_verifications/update.json | 20 +++++++++ .../address_verifications/update_request.json | 1 + 3 files changed, 62 insertions(+) create mode 100644 testdata/fixtures/address_verifications/update.json create mode 100644 testdata/fixtures/address_verifications/update_request.json diff --git a/address_verifications_test.go b/address_verifications_test.go index 3ba9cd6..b400ffa 100644 --- a/address_verifications_test.go +++ b/address_verifications_test.go @@ -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/testdata/fixtures/address_verifications/update.json b/testdata/fixtures/address_verifications/update.json new file mode 100644 index 0000000..db23afa --- /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"}}} From fd8991b8427e097c1ec4043f10ffc58f287505a1 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 10:14:18 +0200 Subject: [PATCH 24/89] feat: add external_reference_id to Export for 2026-04-16 --- resource/export.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/resource/export.go b/resource/export.go index e8d6ec0..406f94f 100644 --- a/resource/export.go +++ b/resource/export.go @@ -15,5 +15,6 @@ type Export struct { 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"` + Filters map[string]interface{} `json:"filters,omitempty"` + ExternalReferenceID *string `json:"external_reference_id,omitempty"` } From 780d4f66dbd1d6cf71524bc678244b6cfe781b60 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 10:14:51 +0200 Subject: [PATCH 25/89] feat: support PATCH /exports/:id to update external_reference_id (2026-04-16) --- exports_test.go | 41 +++++++++++++++++++ testdata/fixtures/exports/update.json | 19 +++++++++ testdata/fixtures/exports/update_request.json | 1 + 3 files changed, 61 insertions(+) create mode 100644 testdata/fixtures/exports/update.json create mode 100644 testdata/fixtures/exports/update_request.json diff --git a/exports_test.go b/exports_test.go index c24e627..1e19b0e 100644 --- a/exports_test.go +++ b/exports_test.go @@ -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/testdata/fixtures/exports/update.json b/testdata/fixtures/exports/update.json new file mode 100644 index 0000000..e16938f --- /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"}}} From 98b02c70f0ab81270e21bc31c8566c76002e3891 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 10:15:24 +0200 Subject: [PATCH 26/89] feat: support PATCH /emergency_verifications/:id to update external_reference_id (2026-04-16) --- emergency_verifications_test.go | 19 +++++++++++++++++++ .../emergency_verifications/update.json | 19 +++++++++++++++++++ .../update_request.json | 1 + 3 files changed, 39 insertions(+) create mode 100644 testdata/fixtures/emergency_verifications/update.json create mode 100644 testdata/fixtures/emergency_verifications/update_request.json diff --git a/emergency_verifications_test.go b/emergency_verifications_test.go index 6a3e146..3d84ef6 100644 --- a/emergency_verifications_test.go +++ b/emergency_verifications_test.go @@ -26,6 +26,25 @@ func TestEmergencyVerificationsList(t *testing.T) { 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"}, 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"}}} From 39dda5480c9886009f2120d92ed45ef10588e862 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 10:15:50 +0200 Subject: [PATCH 27/89] feat: add external_reference_id to Order for 2026-04-16 --- resource/order.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/resource/order.go b/resource/order.go index a3f07ea..7b73b46 100644 --- a/resource/order.go +++ b/resource/order.go @@ -19,7 +19,8 @@ type Order struct { Items []orderitem.OrderItem `json:"items"` AllowBackOrdering bool `json:"allow_back_ordering,omitempty"` CallbackURL *string `json:"callback_url,omitempty"` - CallbackMethod *string `json:"callback_method,omitempty"` + CallbackMethod *string `json:"callback_method,omitempty"` + ExternalReferenceID *string `json:"external_reference_id,omitempty"` } // UnmarshalJSON implements custom unmarshaling for Order. From dc7af11f546f5a8d5892ae4f1cb801008769acb3 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 10:19:43 +0200 Subject: [PATCH 28/89] feat: add external_reference_id to PermanentSupportingDocument for 2026-04-16 --- resource/supporting_document.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/resource/supporting_document.go b/resource/supporting_document.go index 2b098e5..1ad3706 100644 --- a/resource/supporting_document.go +++ b/resource/supporting_document.go @@ -13,7 +13,8 @@ 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"` + 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"` From c94a9f4e703e2b7acc347a72a555c2802f6505f2 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 10:19:56 +0200 Subject: [PATCH 29/89] feat: add external_reference_id to Proof for 2026-04-16 --- resource/proof.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/resource/proof.go b/resource/proof.go index 0fbe25f..ec8fc4f 100644 --- a/resource/proof.go +++ b/resource/proof.go @@ -11,7 +11,8 @@ import ( type Proof struct { ID string `json:"-" jsonapi:"proofs"` CreatedAt time.Time `json:"created_at" api:"readonly"` - ExpiresAt *time.Time `json:"expires_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:"-"` From 67861a255befc0356af43d608e332f47ac40e2cb Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 10:20:08 +0200 Subject: [PATCH 30/89] feat: add external_reference_id to SharedCapacityGroup for 2026-04-16 --- resource/capacity_pool.go | 1 + 1 file changed, 1 insertion(+) 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 From 4f418a759808f07a06a2a3c6311e5557506556a4 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 10:20:31 +0200 Subject: [PATCH 31/89] feat: add external_reference_id to VoiceInTrunkGroup for 2026-04-16 --- resource/voice_in_trunk.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/resource/voice_in_trunk.go b/resource/voice_in_trunk.go index cc81d7f..265b6fc 100644 --- a/resource/voice_in_trunk.go +++ b/resource/voice_in_trunk.go @@ -77,10 +77,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 From 38fa917e9e7a32b16fd7f0783e7b6a1af1ebaa41 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 10:20:49 +0200 Subject: [PATCH 32/89] feat: add external_reference_id to VoiceInTrunk for 2026-04-16 --- resource/voice_in_trunk.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/resource/voice_in_trunk.go b/resource/voice_in_trunk.go index 265b6fc..9b4bd35 100644 --- a/resource/voice_in_trunk.go +++ b/resource/voice_in_trunk.go @@ -19,8 +19,9 @@ type VoiceInTrunk struct { 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"` + 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"` From 1c0c59c60b84019fbad5fd63cba0d2b2ced64299 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 10:21:06 +0200 Subject: [PATCH 33/89] feat: add external_reference_id to VoiceOutTrunk for 2026-04-16 --- resource/voice_out_trunk.go | 1 + 1 file changed, 1 insertion(+) diff --git a/resource/voice_out_trunk.go b/resource/voice_out_trunk.go index b51c625..dedf115 100644 --- a/resource/voice_out_trunk.go +++ b/resource/voice_out_trunk.go @@ -26,6 +26,7 @@ type VoiceOutTrunk struct { 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"` AuthenticationMethod authenticationmethod.AuthenticationMethod `json:"-"` // Relationship IDs for create/update DefaultDIDID string `json:"-" rel:"default_did,dids"` From 7e4c5f69fcb218bf83b3ca4e56ab316d855a9373 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 10:21:36 +0200 Subject: [PATCH 34/89] feat: add emergency_enable_all to VoiceOutTrunk for 2026-04-16 --- resource/voice_out_trunk.go | 1 + 1 file changed, 1 insertion(+) diff --git a/resource/voice_out_trunk.go b/resource/voice_out_trunk.go index dedf115..7d89e84 100644 --- a/resource/voice_out_trunk.go +++ b/resource/voice_out_trunk.go @@ -27,6 +27,7 @@ type VoiceOutTrunk struct { 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"` AuthenticationMethod authenticationmethod.AuthenticationMethod `json:"-"` // Relationship IDs for create/update DefaultDIDID string `json:"-" rel:"default_did,dids"` From 17144cef3440455ba7c9a8a5a6e62c282a5b639e Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 10:21:49 +0200 Subject: [PATCH 35/89] feat: add rtp_timeout to VoiceOutTrunk for 2026-04-16 --- resource/voice_out_trunk.go | 1 + 1 file changed, 1 insertion(+) diff --git a/resource/voice_out_trunk.go b/resource/voice_out_trunk.go index 7d89e84..8cf8bdb 100644 --- a/resource/voice_out_trunk.go +++ b/resource/voice_out_trunk.go @@ -28,6 +28,7 @@ type VoiceOutTrunk struct { 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"` From c1c765d5e898dd1621e3c807412a645b8831757e Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 10:22:13 +0200 Subject: [PATCH 36/89] feat: add emergency_dids has_many relationship to VoiceOutTrunk for 2026-04-16 --- resource/voice_out_trunk.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/resource/voice_out_trunk.go b/resource/voice_out_trunk.go index 8cf8bdb..7c2cc1e 100644 --- a/resource/voice_out_trunk.go +++ b/resource/voice_out_trunk.go @@ -31,11 +31,13 @@ type VoiceOutTrunk struct { 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"` // 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. From 161385f56ab3fc48b9df7fda0f484945272f705e Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 10:22:36 +0200 Subject: [PATCH 37/89] feat: add emergency_enabled to Did for 2026-04-16 --- resource/did.go | 1 + 1 file changed, 1 insertion(+) diff --git a/resource/did.go b/resource/did.go index 6a2fe68..bdef198 100644 --- a/resource/did.go +++ b/resource/did.go @@ -20,6 +20,7 @@ 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"` From e9a2057d97c00ee04e1a48146860334037b0ab3a Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 10:22:58 +0200 Subject: [PATCH 38/89] feat: add emergency_calling_service has_one relationship to Did for 2026-04-16 --- resource/did.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/resource/did.go b/resource/did.go index bdef198..ab0e61f 100644 --- a/resource/did.go +++ b/resource/did.go @@ -24,16 +24,18 @@ type DID struct { // 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"` + 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"` // 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"` + CapacityPool *CapacityPool `json:"-" rel:"capacity_pool"` + SharedCapacityGroup *SharedCapacityGroup `json:"-" rel:"shared_capacity_group"` + EmergencyCallingService *EmergencyCallingService `json:"-" rel:"emergency_calling_service"` } // MarshalRelationships implements RelationshipMarshaler for DID. From a83681d097049ad23a4ee0813e23635df32a98d1 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 10:23:30 +0200 Subject: [PATCH 39/89] feat: add emergency_verification has_one relationship to Did for 2026-04-16 --- resource/did.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/resource/did.go b/resource/did.go index ab0e61f..2864071 100644 --- a/resource/did.go +++ b/resource/did.go @@ -27,6 +27,7 @@ type DID struct { 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"` // Resolved relationships Order *Order `json:"-" rel:"order"` AddressVerification *AddressVerification `json:"-" rel:"address_verification"` @@ -36,6 +37,7 @@ type DID struct { 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"` } // MarshalRelationships implements RelationshipMarshaler for DID. From a180e33a3432529b88483c69e6a56aec4568daa4 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 10:23:50 +0200 Subject: [PATCH 40/89] feat: add identity has_one relationship to Did for 2026-04-16 --- resource/did.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/resource/did.go b/resource/did.go index 2864071..988c489 100644 --- a/resource/did.go +++ b/resource/did.go @@ -28,6 +28,7 @@ type DID struct { 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"` // Resolved relationships Order *Order `json:"-" rel:"order"` AddressVerification *AddressVerification `json:"-" rel:"address_verification"` @@ -38,6 +39,7 @@ type DID struct { 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. From a534c96d613dcd0dea676c0703907a95b6750fe3 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 10:24:16 +0200 Subject: [PATCH 41/89] feat: add birth_country has_one relationship to Identity for 2026-04-16 --- identities_test.go | 24 ++++++ resource/identity.go | 6 +- .../identities/show_with_birth_country.json | 78 +++++++++++++++++++ 3 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 testdata/fixtures/identities/show_with_birth_country.json 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/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/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" + } +} From 24fb10bf34d91582ae3ee9ade9f44859aa726fd8 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 10:25:06 +0200 Subject: [PATCH 42/89] feat: add p2p/a2p/emergency/cnam_out features to DidGroup for 2026-04-16 --- resource/enums/feature.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/resource/enums/feature.go b/resource/enums/feature.go index 9cb22bc..2406b61 100644 --- a/resource/enums/feature.go +++ b/resource/enums/feature.go @@ -4,8 +4,12 @@ package enums type Feature string const ( - FeatureVoiceIn Feature = "voice_in" - FeatureVoiceOut Feature = "voice_out" - FeatureT38 Feature = "t38" - FeatureSmsIn Feature = "sms_in" + 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" ) From d8562a978268f118500ec13e015bae44b8aa4b19 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 10:25:35 +0200 Subject: [PATCH 43/89] feat: add service_restrictions attribute to DidGroup for 2026-04-16 --- resource/did_group.go | 1 + 1 file changed, 1 insertion(+) diff --git a/resource/did_group.go b/resource/did_group.go index ee7482f..894f1da 100644 --- a/resource/did_group.go +++ b/resource/did_group.go @@ -10,6 +10,7 @@ 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"` From 4fca06721e1ed8e766c8e6501ee98d2b7e510b76 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 10:27:04 +0200 Subject: [PATCH 44/89] feat: add EmergencyOrderItem complex object for 2026-04-16 Order support --- orders_test.go | 33 +++++++++++++++++++ resource/orderitem/emergency_order_item.go | 10 ++++++ resource/orderitem/order_item.go | 13 ++++++-- .../fixtures/orders/create_emergency.json | 29 ++++++++++++++++ .../orders/create_request_emergency.json | 1 + 5 files changed, 83 insertions(+), 3 deletions(-) create mode 100644 resource/orderitem/emergency_order_item.go create mode 100644 testdata/fixtures/orders/create_emergency.json create mode 100644 testdata/fixtures/orders/create_request_emergency.json diff --git a/orders_test.go b/orders_test.go index 83110d3..f499bc0 100644 --- a/orders_test.go +++ b/orders_test.go @@ -167,6 +167,39 @@ 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"}, 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/testdata/fixtures/orders/create_emergency.json b/testdata/fixtures/orders/create_emergency.json new file mode 100644 index 0000000..9cf447c --- /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_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}}]}}} From 8f1ce0cb17b3fd81a4f740971e570397bcbe4801 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 10:27:58 +0200 Subject: [PATCH 45/89] examples: update voice_out_trunks to use 2026-04-16 polymorphic authentication_method --- examples/voice_out_trunks/main.go | 74 ++++++++++++++----- .../authentication_method.go | 6 +- 2 files changed, 61 insertions(+), 19 deletions(-) diff --git a/examples/voice_out_trunks/main.go b/examples/voice_out_trunks/main.go index a24710d..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. @@ -21,11 +21,45 @@ func main() { client := examples.ClientFromEnv() ctx := context.Background() - // Create a voice out trunk with ip_only authentication + // 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()), - AuthenticationMethod: &authenticationmethod.IpOnly{ - AllowedSipIPs: []string{"203.0.113.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{}, @@ -33,37 +67,41 @@ func main() { 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(" auth type:", created.AuthenticationMethod.AuthenticationType()) - fmt.Println(" status:", created.Status) - - // List voice out trunks - trunks, err := client.VoiceOutTrunks().List(ctx, nil) - if err != nil { - panic(err) + 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.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/resource/authenticationmethod/authentication_method.go b/resource/authenticationmethod/authentication_method.go index 5b6c1ba..eb80129 100644 --- a/resource/authenticationmethod/authentication_method.go +++ b/resource/authenticationmethod/authentication_method.go @@ -11,7 +11,11 @@ type AuthenticationMethod interface { AuthenticationType() string } -// IpOnly uses IP-based authentication only. +// 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"` From 295bc22480b7065c63687615e046a0a04eaf1424 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 10:28:46 +0200 Subject: [PATCH 46/89] examples: add did_history example for 2026-04-16 --- examples/did_history/main.go | 71 ++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 examples/did_history/main.go 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, + ) + } + } +} From 106e3cc70c6784e1a34825b6bc30b260cf86ca37 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 10:29:17 +0200 Subject: [PATCH 47/89] examples: add emergency_requirements example for 2026-04-16 --- examples/emergency_requirements/main.go | 49 +++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 examples/emergency_requirements/main.go diff --git a/examples/emergency_requirements/main.go b/examples/emergency_requirements/main.go new file mode 100644 index 0000000..2ead685 --- /dev/null +++ b/examples/emergency_requirements/main.go @@ -0,0 +1,49 @@ +// 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, ", ")) + } + } +} From 46cf88fc64177ac8cd8b82d3bf536b092e63e62c Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 10:29:43 +0200 Subject: [PATCH 48/89] complex_objects: clarify ExportFilters from/to semantics (from=inclusive, to=exclusive) --- resource/export.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/resource/export.go b/resource/export.go index 406f94f..7e7bfd5 100644 --- a/resource/export.go +++ b/resource/export.go @@ -7,6 +7,13 @@ 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"` From 09e35925a617d5f09e380420e25be9b483ae2f38 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 10:30:18 +0200 Subject: [PATCH 49/89] examples: enhance exports example for 2026-04-16 --- examples/exports/main.go | 81 +++++++++++++++++++++++++++++++++------- 1 file changed, 67 insertions(+), 14 deletions(-) diff --git a/examples/exports/main.go b/examples/exports/main.go index 3d7f21b..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{}{"from": "2026-04-01 00:00:00", "to": "2026-04-15 23:59:59"}, + 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) + } } } From 984cd9989eb2ed28daa6e4670bc4a6ea3fec4482 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 10:30:47 +0200 Subject: [PATCH 50/89] examples: add emergency_calling_services example for 2026-04-16 --- examples/emergency_calling_services/main.go | 57 +++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 examples/emergency_calling_services/main.go diff --git a/examples/emergency_calling_services/main.go b/examples/emergency_calling_services/main.go new file mode 100644 index 0000000..5201cb7 --- /dev/null +++ b/examples/emergency_calling_services/main.go @@ -0,0 +1,57 @@ +// 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) + } + } + + // 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) + } +} From 69c7b72f37dd1fe03dd2e161b471d904ff49a35b Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 10:31:05 +0200 Subject: [PATCH 51/89] examples: add emergency_verifications example for 2026-04-16 --- examples/emergency_verifications/main.go | 40 ++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 examples/emergency_verifications/main.go 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) + } + } +} From f6341a012f203cbae8bccfd004feceeb168d10f3 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 10:31:24 +0200 Subject: [PATCH 52/89] examples: add emergency_requirement_validations example for 2026-04-16 --- .../emergency_requirement_validations/main.go | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 examples/emergency_requirement_validations/main.go 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)") +} From fbb219a74a34a4d3e18c3f2cc4d9ae0ce1634a84 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 10:32:15 +0200 Subject: [PATCH 53/89] examples: add address_verifications example for 2026-04-16 --- examples/address_verifications/main.go | 75 ++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 examples/address_verifications/main.go diff --git a/examples/address_verifications/main.go b/examples/address_verifications/main.go new file mode 100644 index 0000000..50e66be --- /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) + } +} From ae16559080fa3ffa0384d2b56fc0e6976d7349b3 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 10:32:39 +0200 Subject: [PATCH 54/89] examples: add orders_emergency example for 2026-04-16 --- examples/orders_emergency/main.go | 66 +++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 examples/orders_emergency/main.go 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) + } + } +} From e01a8e65fc8feff0b0a797d686ec6b1d9095558b Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 10:33:33 +0200 Subject: [PATCH 55/89] examples: identities_and_proofs - include birth_country for 2026-04-16 --- examples/identities/main.go | 47 +++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 examples/identities/main.go 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) + } +} From d98df869cce56caa0089f8d058a5e71c7520145d Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 10:33:57 +0200 Subject: [PATCH 56/89] examples: document new 2026-04-16 examples in README --- examples/README.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/examples/README.md b/examples/README.md index f8ba202..9a80447 100644 --- a/examples/README.md +++ b/examples/README.md @@ -35,10 +35,18 @@ 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). | +| [`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 From d01beacf6915cae71e78153f489aa3f6d9d1dfca Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 10:34:20 +0200 Subject: [PATCH 57/89] examples: voice_in_trunk_groups - demonstrate external_reference_id --- examples/voice_in_trunk_groups/main.go | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) 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 From 292e104386eb893f9fdcbda40a791baba31e7711 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 10:34:50 +0200 Subject: [PATCH 58/89] examples: shared_capacity_groups - display external_reference_id --- examples/shared_capacity_groups/main.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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) + } } From 5e98578d03ac94f89b85f26eef28657476fcc5ac Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 10:35:11 +0200 Subject: [PATCH 59/89] examples: orders - demonstrate external_reference_id --- examples/orders/main.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/examples/orders/main.go b/examples/orders/main.go index 8fc7b7c..20d1fbe 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, From bd62bfa11aaa1f78daa3d621fa1785a4f95b7585 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 10:35:33 +0200 Subject: [PATCH 60/89] examples: dids - display 2026-04-16 emergency fields and identity --- examples/dids/main.go | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) 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(). From 345ebaf3ee1d41bf7328c89cd8efddda5258e77f Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 10:35:49 +0200 Subject: [PATCH 61/89] examples: did_groups - display allow_additional_channels / service_restrictions + new features --- examples/did_groups/main.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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 From 896e95b15bb7c7cf8c0673130bfd2d37ca01212d Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 10:36:31 +0200 Subject: [PATCH 62/89] fix: declare has_one :address on EmergencyCallingService to mirror server --- resource/emergency_calling_service.go | 1 + 1 file changed, 1 insertion(+) diff --git a/resource/emergency_calling_service.go b/resource/emergency_calling_service.go index 6a0613f..975454d 100644 --- a/resource/emergency_calling_service.go +++ b/resource/emergency_calling_service.go @@ -17,6 +17,7 @@ type EmergencyCallingService struct { 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"` From 254b432618a8de9c8327c6b21879a3cf9e054f56 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 10:38:17 +0200 Subject: [PATCH 63/89] docs(readme): refresh to API version 2026-04-16 --- README.md | 51 ++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 36 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index c88e175..8c3d81f 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 @@ -161,8 +164,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) @@ -233,12 +242,17 @@ created, _ := client.VoiceInTrunkGroups().Create(ctx, group) > The `replace_cli` and `randomize_cli` values of `OnCliMismatchAction` also require account configuration. ```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" +) trunk := &didww.VoiceOutTrunk{ - Name: "My Outbound Trunk", - AllowedSipIPs: []string{"0.0.0.0/0"}, - AllowedRtpIPs: []string{"0.0.0.0/0"}, + Name: "My Outbound Trunk", + AuthenticationMethod: &authenticationmethod.IpOnly{ + AllowedSipIPs: []string{"203.0.113.0/24"}, + }, + AllowedRtpIPs: []string{"203.0.113.1"}, DstPrefixes: []string{}, DefaultDstAction: enums.DefaultDstActionAllowAll, OnCliMismatchAction: enums.OnCliMismatchActionRejectCall, @@ -342,7 +356,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 +453,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 +493,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 +507,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` | — | @@ -510,7 +530,7 @@ 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) + - Expiry fields — `*time.Time`: `DID.ExpiresAt`, `Proof.ExpiresAt`, `EncryptedFile.ExpiresAt`; `DIDReservation.ExpiresAt` 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. ```go @@ -529,7 +549,8 @@ 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`, `TransportProtocol`, `Codec`, `RxDtmfFormat`, `TxDtmfFormat`, `SstRefreshMethod`, -`ReroutingDisconnectCode`, `Feature`, `AreaLevel`, `AddressVerificationStatus`, `StirShakenMode` +`ReroutingDisconnectCode`, `Feature`, `AreaLevel`, `AddressVerificationStatus`, `StirShakenMode`, +`DiversionRelayPolicy` \* `replace_cli` and `randomize_cli` require account configuration. From baeaa0ad64aa85bd4e9d6c348e33769108a69e17 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 10:39:26 +0200 Subject: [PATCH 64/89] test: lock in PATCH dirty-tracking for polymorphic authentication_method --- dirty_serialization_test.go | 32 +++++++++++++++++++ .../update_auth_method_request.json | 1 + voice_out_trunks_test.go | 19 +++++++++++ 3 files changed, 52 insertions(+) create mode 100644 testdata/fixtures/voice_out_trunks/update_auth_method_request.json diff --git a/dirty_serialization_test.go b/dirty_serialization_test.go index 8d9d8bf..33255a6 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,37 @@ 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) +} + // --- test helpers --- type patchBodyDoc struct { 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/voice_out_trunks_test.go b/voice_out_trunks_test.go index 39ed9db..6e3c4ea 100644 --- a/voice_out_trunks_test.go +++ b/voice_out_trunks_test.go @@ -114,6 +114,25 @@ func TestVoiceOutTrunksUpdate(t *testing.T) { 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 TestVoiceOutTrunksDelete(t *testing.T) { _, client := newTestServer(t, map[string]testRoute{ "DELETE /v3/voice_out_trunks/425ce763-a3a9-49b4-af5b-ada1a65c8864": {status: http.StatusNoContent}, From 8bc8e47b3c1cee9322df1322f8111cae1a8f0a85 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 10:40:12 +0200 Subject: [PATCH 65/89] docs: add property docblocks to new Emergency resources --- resource/emergency_calling_service.go | 21 ++++++++++++++------- resource/emergency_requirement.go | 19 +++++++++++++------ 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/resource/emergency_calling_service.go b/resource/emergency_calling_service.go index 975454d..3d17973 100644 --- a/resource/emergency_calling_service.go +++ b/resource/emergency_calling_service.go @@ -5,14 +5,21 @@ import "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 string `json:"name" api:"readonly"` - Reference string `json:"reference" api:"readonly"` - Status string `json:"status" api:"readonly"` + 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 ("pending", "active", "canceled", etc.). + 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 *time.Time `json:"canceled_at" api:"readonly"` - CreatedAt time.Time `json:"created_at" api:"readonly"` - RenewDate string `json:"renew_date" 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"` // Resolved relationships Country *Country `json:"-" rel:"country"` DIDGroupType *DIDGroupType `json:"-" rel:"did_group_type"` diff --git a/resource/emergency_requirement.go b/resource/emergency_requirement.go index 215d98a..29e7343 100644 --- a/resource/emergency_requirement.go +++ b/resource/emergency_requirement.go @@ -3,13 +3,20 @@ package resource // 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 string `json:"identity_type" api:"readonly"` - AddressAreaLevel string `json:"address_area_level" api:"readonly"` - PersonalAreaLevel string `json:"personal_area_level" api:"readonly"` - BusinessAreaLevel string `json:"business_area_level" api:"readonly"` - AddressMandatoryFields []string `json:"address_mandatory_fields" api:"readonly"` + 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"` From 1df8eff4a3060ca1638856df0422b506155fc8a1 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 10:40:52 +0200 Subject: [PATCH 66/89] feat: add status predicate helpers to VoiceOutTrunk --- resource/voice_out_trunk.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/resource/voice_out_trunk.go b/resource/voice_out_trunk.go index 7c2cc1e..7eb4af3 100644 --- a/resource/voice_out_trunk.go +++ b/resource/voice_out_trunk.go @@ -81,6 +81,12 @@ func (v *VoiceOutTrunk) UnmarshalJSON(data []byte) error { return 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"` From 591d678aad88853928b93acb478ecca14733f7be Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 10:41:20 +0200 Subject: [PATCH 67/89] feat: surface STATUS_PENDING and STATUS_PROCESSING on Export --- resource/export.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/resource/export.go b/resource/export.go index 7e7bfd5..6538304 100644 --- a/resource/export.go +++ b/resource/export.go @@ -25,3 +25,12 @@ type Export struct { 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 } From 2173ebaf6dc39a39fda8a2aa8cefc4b3ef1fe6b2 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 10:42:03 +0200 Subject: [PATCH 68/89] feat: add status predicate helpers to EmergencyCallingService --- resource/emergency_calling_service.go | 32 ++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/resource/emergency_calling_service.go b/resource/emergency_calling_service.go index 3d17973..265fc57 100644 --- a/resource/emergency_calling_service.go +++ b/resource/emergency_calling_service.go @@ -10,7 +10,7 @@ type EmergencyCallingService struct { 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 ("pending", "active", "canceled", etc.). + // Status is the current lifecycle status ("active", "canceled", "new", etc.). 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"` @@ -29,3 +29,33 @@ type EmergencyCallingService struct { EmergencyVerification *EmergencyVerification `json:"-" rel:"emergency_verification"` DIDs []*DID `json:"-" rel:"dids"` } + +// 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 } From 4816509fc5cb631476ddd87f3dcb4de4a13ac1a6 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 10:43:38 +0200 Subject: [PATCH 69/89] docs: document DidHistory#meta fields for billing_cycles_count_changed --- did_history_test.go | 21 +++++++++++++ jsonapi/jsonapi.go | 15 ++++++++++ resource/did_history.go | 30 +++++++++++++++---- .../show_billing_cycles_count_changed.json | 19 ++++++++++++ 4 files changed, 79 insertions(+), 6 deletions(-) create mode 100644 testdata/fixtures/did_history/show_billing_cycles_count_changed.json diff --git a/did_history_test.go b/did_history_test.go index fb90663..db81a0f 100644 --- a/did_history_test.go +++ b/did_history_test.go @@ -39,4 +39,25 @@ func TestDIDHistoryFind(t *testing.T) { 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/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/resource/did_history.go b/resource/did_history.go index dbb2850..d7479d3 100644 --- a/resource/did_history.go +++ b/resource/did_history.go @@ -1,13 +1,31 @@ package resource -import "time" +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"` + 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/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" + } +} From eabd2d2880162f3dfa6ea37799ee8a8fcc0fe9ff Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 10:48:14 +0200 Subject: [PATCH 70/89] test: cover PATCH /dids/:id unassigning emergency_calling_service Add NullifyEmergencyCallingService bool field to DID and handle it in MarshalRelationships to emit {"data": null} for the emergency_calling_service relationship. Wire-level test asserts the full request body and verifies the response shows emergency_enabled=false with emergency_calling_service data null. --- dids_test.go | 18 ++++++++++ resource/did.go | 6 ++++ .../unassign_emergency_calling_service.json | 35 +++++++++++++++++++ ...ign_emergency_calling_service_request.json | 1 + 4 files changed, 60 insertions(+) create mode 100644 testdata/fixtures/dids/unassign_emergency_calling_service.json create mode 100644 testdata/fixtures/dids/unassign_emergency_calling_service_request.json diff --git a/dids_test.go b/dids_test.go index 986587f..49ddefd 100644 --- a/dids_test.go +++ b/dids_test.go @@ -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/resource/did.go b/resource/did.go index 988c489..28a62cf 100644 --- a/resource/did.go +++ b/resource/did.go @@ -29,6 +29,9 @@ type DID struct { 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"` @@ -58,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/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 From 46e05d8eb77a009105eb5dc98eb4a2450058f1ae Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 10:50:04 +0200 Subject: [PATCH 71/89] test: cover PATCH /voice_out_trunks/:id emergency_dids + emergency_enable_all Three wire-level specs for the 2026-04-16 emergency trunk attributes: * toggle emergency_enable_all (Boolean attribute) * replace emergency_dids to-many with a specific two-DID set * clear emergency_dids with data: [] Add ClearEmergencyDIDs bool field and MarshalRelationships to VoiceOutTrunk so the SDK can express an empty to-many relationship payload. --- resource/voice_out_trunk.go | 14 +++++ .../update_emergency_dids.json | 53 +++++++++++++++++++ .../update_emergency_dids_clear_request.json | 1 + .../update_emergency_dids_request.json | 1 + .../update_emergency_enable_all_request.json | 1 + voice_out_trunks_test.go | 51 ++++++++++++++++++ 6 files changed, 121 insertions(+) create mode 100644 testdata/fixtures/voice_out_trunks/update_emergency_dids.json create mode 100644 testdata/fixtures/voice_out_trunks/update_emergency_dids_clear_request.json create mode 100644 testdata/fixtures/voice_out_trunks/update_emergency_dids_request.json create mode 100644 testdata/fixtures/voice_out_trunks/update_emergency_enable_all_request.json diff --git a/resource/voice_out_trunk.go b/resource/voice_out_trunk.go index 7eb4af3..3073520 100644 --- a/resource/voice_out_trunk.go +++ b/resource/voice_out_trunk.go @@ -4,6 +4,7 @@ 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" ) @@ -34,6 +35,9 @@ type VoiceOutTrunk struct { 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"` @@ -81,6 +85,16 @@ func (v *VoiceOutTrunk) UnmarshalJSON(data []byte) error { 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 } 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..8fc1900 --- /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": ["10.0.0.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_out_trunks_test.go b/voice_out_trunks_test.go index 6e3c4ea..baeaa40 100644 --- a/voice_out_trunks_test.go +++ b/voice_out_trunks_test.go @@ -133,6 +133,57 @@ func TestVoiceOutTrunksUpdateAuthenticationMethod(t *testing.T) { 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}, From 66f7b2447b58f3ff377e32391ac9913ac3f1e183 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 12:24:27 +0200 Subject: [PATCH 72/89] feat: add status predicate helpers to AddressVerification, EmergencyVerification, and Order --- resource/address_verification.go | 15 ++++++ resource/emergency_verification.go | 22 +++++++++ resource/order.go | 9 ++++ resource/status_predicates_test.go | 76 ++++++++++++++++++++++++++++++ 4 files changed, 122 insertions(+) create mode 100644 resource/status_predicates_test.go diff --git a/resource/address_verification.go b/resource/address_verification.go index 356d0e1..de69492 100644 --- a/resource/address_verification.go +++ b/resource/address_verification.go @@ -24,3 +24,18 @@ type AddressVerification struct { // Resolved relationships AddressRel *Address `json:"-" rel:"address"` } + +// 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/emergency_verification.go b/resource/emergency_verification.go index bef7846..bfa3ccc 100644 --- a/resource/emergency_verification.go +++ b/resource/emergency_verification.go @@ -23,3 +23,25 @@ type EmergencyVerification struct { 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/order.go b/resource/order.go index 7b73b46..c6136cd 100644 --- a/resource/order.go +++ b/resource/order.go @@ -23,6 +23,15 @@ type Order struct { 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 } + +// IsCancelled returns true when the order status is "Canceled". +func (o *Order) IsCancelled() 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/status_predicates_test.go b/resource/status_predicates_test.go new file mode 100644 index 0000000..43b9d05 --- /dev/null +++ b/resource/status_predicates_test.go @@ -0,0 +1,76 @@ +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 TestOrderStatusPredicates(t *testing.T) { + tests := []struct { + status enums.OrderStatus + expectPending, expectCompleted, expectCancelled 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.IsCancelled(); got != tt.expectCancelled { + t.Errorf("IsCancelled() for %q = %v, want %v", tt.status, got, tt.expectCancelled) + } + } +} From 48768709a072b1df59a62f81b33bbbb09a5b06c1 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 12:24:44 +0200 Subject: [PATCH 73/89] docs(readme): document module path decision (no /v3 suffix) --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 8c3d81f..6de54ca 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,11 @@ Version **2.x** (branch `release-2`) targets API version `2022-05-10`. 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 From 8188030cb0f666309d5627d21093947972aa351a Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 12:25:10 +0200 Subject: [PATCH 74/89] fix(fixtures): replace 0.0.0.0/0 with RFC 5737 TEST-NET-3 range (203.0.113.0/24) --- testdata/fixtures/voice_out_trunks/index.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testdata/fixtures/voice_out_trunks/index.json b/testdata/fixtures/voice_out_trunks/index.json index 21434d6..f103c14 100644 --- a/testdata/fixtures/voice_out_trunks/index.json +++ b/testdata/fixtures/voice_out_trunks/index.json @@ -54,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, From 9106ce0b9de47e3ccd0cb323b143e2bf2b396fb3 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 16:52:04 +0200 Subject: [PATCH 75/89] feat!: standardize attribute values to lowercase snake_case --- address_verifications_test.go | 2 +- did_groups_test.go | 2 +- dids_test.go | 2 +- emergency_requirements_test.go | 2 +- examples/address_verifications/main.go | 2 +- orders_test.go | 2 +- requirements_test.go | 2 +- resource/address_verification.go | 6 +-- resource/emergency_calling_service.go | 14 +++---- resource/emergency_requirement.go | 2 +- resource/enums/address.go | 14 +++---- resource/enums/address_test.go | 6 +-- resource/enums/export.go | 10 ++--- resource/enums/export_test.go | 10 ++--- resource/enums/identity.go | 6 +-- resource/enums/identity_test.go | 6 +-- resource/enums/order.go | 6 +-- resource/enums/order_test.go | 6 +-- resource/export.go | 6 +-- resource/order.go | 6 +-- resource/status_predicates_test.go | 35 ++++++++++++++++ .../fixtures/address_requirements/index.json | 40 +++++++++---------- .../fixtures/address_requirements/show.json | 8 ++-- .../address_verifications/create.json | 4 +- .../address_verifications/create_request.json | 2 +- .../fixtures/address_verifications/index.json | 4 +- .../fixtures/address_verifications/show.json | 2 +- .../address_verifications/show_rejected.json | 2 +- .../address_verifications/update.json | 2 +- testdata/fixtures/addresses/index.json | 2 +- .../did_groups/show_with_requirement.json | 8 ++-- testdata/fixtures/dids/index.json | 2 +- ...th_address_verification_and_did_group.json | 4 +- .../emergency_requirements/index.json | 8 ++-- testdata/fixtures/exports/create.json | 2 +- testdata/fixtures/exports/create_cdr_out.json | 2 +- testdata/fixtures/exports/index.json | 2 +- testdata/fixtures/exports/show.json | 2 +- testdata/fixtures/exports/update.json | 2 +- testdata/fixtures/identities/create.json | 2 +- .../fixtures/identities/create_personal.json | 2 +- .../fixtures/identities/create_request.json | 2 +- testdata/fixtures/identities/index.json | 4 +- .../identities/show_with_contact_email.json | 2 +- testdata/fixtures/identities/update.json | 2 +- .../fixtures/identities/update_request.json | 2 +- testdata/fixtures/orders/create.json | 2 +- .../fixtures/orders/create_available_did.json | 2 +- .../orders/create_billing_cycles.json | 2 +- testdata/fixtures/orders/create_capacity.json | 2 +- .../fixtures/orders/create_emergency.json | 2 +- testdata/fixtures/orders/create_nanpa.json | 2 +- .../fixtures/orders/create_reservation.json | 2 +- testdata/fixtures/orders/show.json | 2 +- .../fixtures/orders_with_callback/create.json | 4 +- .../orders_with_callback/create_request.json | 2 +- 56 files changed, 159 insertions(+), 124 deletions(-) diff --git a/address_verifications_test.go b/address_verifications_test.go index b400ffa..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, diff --git a/did_groups_test.go b/did_groups_test.go index 6246ccc..49717c6 100644 --- a/did_groups_test.go +++ b/did_groups_test.go @@ -73,7 +73,7 @@ func TestDIDGroupsFindWithIncludedRequirement(t *testing.T) { // 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, "any", group.AddressRequirement.IdentityType) assert.Equal(t, "WorldWide", group.AddressRequirement.PersonalAreaLevel) assert.Equal(t, "Country", group.AddressRequirement.BusinessAreaLevel) assert.Equal(t, "City", group.AddressRequirement.AddressAreaLevel) diff --git a/dids_test.go b/dids_test.go index 49ddefd..42bbc3f 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) diff --git a/emergency_requirements_test.go b/emergency_requirements_test.go index 9f47d66..8150ea7 100644 --- a/emergency_requirements_test.go +++ b/emergency_requirements_test.go @@ -19,7 +19,7 @@ func TestEmergencyRequirementsList(t *testing.T) { 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, "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) diff --git a/examples/address_verifications/main.go b/examples/address_verifications/main.go index 50e66be..e565bd9 100644 --- a/examples/address_verifications/main.go +++ b/examples/address_verifications/main.go @@ -54,7 +54,7 @@ func main() { // Filter: only rejected verifications fmt.Println("\n=== Rejected verifications ===") - params = didww.NewQueryParams().Filter("status", "Rejected") + params = didww.NewQueryParams().Filter("status", "rejected") rejected, err := client.AddressVerifications().List(ctx, params) if err != nil { _ = rejected // silence unused diff --git a/orders_test.go b/orders_test.go index f499bc0..6850ca3 100644 --- a/orders_test.go +++ b/orders_test.go @@ -206,7 +206,7 @@ func TestOrdersCreateWithCallback(t *testing.T) { }) 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/requirements_test.go b/requirements_test.go index 442cd24..be69f2d 100644 --- a/requirements_test.go +++ b/requirements_test.go @@ -30,7 +30,7 @@ func TestAddressRequirementsFindWithIncludes(t *testing.T) { 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_verification.go b/resource/address_verification.go index de69492..f013713 100644 --- a/resource/address_verification.go +++ b/resource/address_verification.go @@ -25,17 +25,17 @@ type AddressVerification struct { AddressRel *Address `json:"-" rel:"address"` } -// IsPending returns true when the verification status is "Pending". +// 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". +// 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". +// IsRejected returns true when the verification status is "rejected". func (a *AddressVerification) IsRejected() bool { return a.Status == enums.AddressVerificationStatusRejected } diff --git a/resource/emergency_calling_service.go b/resource/emergency_calling_service.go index 265fc57..01b32a1 100644 --- a/resource/emergency_calling_service.go +++ b/resource/emergency_calling_service.go @@ -10,7 +10,7 @@ type EmergencyCallingService struct { 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", etc.). + // 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"` @@ -34,10 +34,10 @@ type EmergencyCallingService struct { const ( ECSStatusActive = "active" ECSStatusCanceled = "canceled" - ECSStatusChangesRequired = "changes required" - ECSStatusInProcess = "in process" + ECSStatusChangesRequired = "changes_required" + ECSStatusInProcess = "in_process" ECSStatusNew = "new" - ECSStatusPendingUpdate = "pending update" + ECSStatusPendingUpdate = "pending_update" ) // IsActive returns true when the service status is "active". @@ -46,16 +46,16 @@ func (e *EmergencyCallingService) IsActive() bool { return e.Status == ECSStatus // 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". +// 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". +// 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". +// 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 index 29e7343..0f4325b 100644 --- a/resource/emergency_requirement.go +++ b/resource/emergency_requirement.go @@ -4,7 +4,7 @@ package resource // 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 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"` 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..ddd192e 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) { 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/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 6538304..11f2acd 100644 --- a/resource/export.go +++ b/resource/export.go @@ -26,11 +26,11 @@ type Export struct { ExternalReferenceID *string `json:"external_reference_id,omitempty"` } -// IsPending returns true when the export status is "Pending". +// 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". +// 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". +// IsCompleted returns true when the export status is "completed". func (e *Export) IsCompleted() bool { return e.Status == enums.ExportStatusCompleted } diff --git a/resource/order.go b/resource/order.go index c6136cd..e373963 100644 --- a/resource/order.go +++ b/resource/order.go @@ -23,13 +23,13 @@ type Order struct { ExternalReferenceID *string `json:"external_reference_id,omitempty"` } -// IsPending returns true when the order status is "Pending". +// 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". +// IsCompleted returns true when the order status is "completed". func (o *Order) IsCompleted() bool { return o.Status == enums.OrderStatusCompleted } -// IsCancelled returns true when the order status is "Canceled". +// IsCancelled returns true when the order status is "canceled". func (o *Order) IsCancelled() bool { return o.Status == enums.OrderStatusCanceled } // UnmarshalJSON implements custom unmarshaling for Order. diff --git a/resource/status_predicates_test.go b/resource/status_predicates_test.go index 43b9d05..3effc21 100644 --- a/resource/status_predicates_test.go +++ b/resource/status_predicates_test.go @@ -52,6 +52,41 @@ func TestEmergencyVerificationStatusPredicates(t *testing.T) { } } +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 diff --git a/testdata/fixtures/address_requirements/index.json b/testdata/fixtures/address_requirements/index.json index 738f787..237991b 100644 --- a/testdata/fixtures/address_requirements/index.json +++ b/testdata/fixtures/address_requirements/index.json @@ -4,10 +4,10 @@ "id": "b6c80acb-3952-4d53-9e62-fe2348c0636b", "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, @@ -79,10 +79,10 @@ "id": "51b293af-a496-4bf2-9c68-5221b3b0dc1e", "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, @@ -156,10 +156,10 @@ "id": "f7c2a9a4-a40e-4f22-98ff-0f53c86052ac", "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, @@ -231,10 +231,10 @@ "id": "edb71cff-d5e8-44ff-ba36-6f758066c175", "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, @@ -308,10 +308,10 @@ "id": "90b72a1a-1ebd-4771-b818-f7123f8ff7ec", "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/address_requirements/show.json b/testdata/fixtures/address_requirements/show.json index 9ad11ca..e7bbd42 100644 --- a/testdata/fixtures/address_requirements/show.json +++ b/testdata/fixtures/address_requirements/show.json @@ -3,10 +3,10 @@ "id": "25d12afe-1ec6-4fe3-9621-b250dd1fb959", "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 a41da32..5f996a0 100644 --- a/testdata/fixtures/address_verifications/show_rejected.json +++ b/testdata/fixtures/address_verifications/show_rejected.json @@ -6,7 +6,7 @@ "service_description": null, "callback_url": null, "callback_method": null, - "status": "Rejected", + "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 index db23afa..67f8bf0 100644 --- a/testdata/fixtures/address_verifications/update.json +++ b/testdata/fixtures/address_verifications/update.json @@ -6,7 +6,7 @@ "service_description": null, "callback_url": null, "callback_method": null, - "status": "Approved", + "status": "approved", "reject_reasons": null, "reference": "SHB-485120", "reject_comment": null, 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 a641bb7..d4ad2d7 100644 --- a/testdata/fixtures/did_groups/show_with_requirement.json +++ b/testdata/fixtures/did_groups/show_with_requirement.json @@ -66,10 +66,10 @@ "id": "8da1e0b2-047c-4baf-9c57-57143f09b9ce", "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/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 7117e0e..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 @@ -135,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/emergency_requirements/index.json b/testdata/fixtures/emergency_requirements/index.json index c611224..548e992 100644 --- a/testdata/fixtures/emergency_requirements/index.json +++ b/testdata/fixtures/emergency_requirements/index.json @@ -4,10 +4,10 @@ "id": "c1d2e3f4-a5b6-7890-1234-567890abcdef", "type": "emergency_requirements", "attributes": { - "identity_type": "Any", - "address_area_level": "City", - "personal_area_level": "WorldWide", - "business_area_level": "Country", + "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"], 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/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 index e16938f..3216f0a 100644 --- a/testdata/fixtures/exports/update.json +++ b/testdata/fixtures/exports/update.json @@ -3,7 +3,7 @@ "id": "da15f006-5da4-45ca-b0df-735baeadf423", "type": "exports", "attributes": { - "status": "Completed", + "status": "completed", "created_at": "2019-09-03T14:42:22.000Z", "url": "https://sandbox-api.didww.com/v3/exports/02bf6df4.csv.gz", "callback_url": null, 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_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 index 9cf447c..d045b56 100644 --- a/testdata/fixtures/orders/create_emergency.json +++ b/testdata/fixtures/orders/create_emergency.json @@ -4,7 +4,7 @@ "type": "orders", "attributes": { "amount": "30.0", - "status": "Pending", + "status": "pending", "created_at": "2026-04-16T10:00:00.000Z", "description": "Emergency", "reference": "EMG-100001", 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_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 From 039b664500701c288a290094de8af3ac8a7e7d35 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 16:52:04 +0200 Subject: [PATCH 76/89] chore: remove compiled binaries from tracking; add to .gitignore --- .gitignore | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.gitignore b/.gitignore index 862dbf4..c81b04a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,20 @@ # 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 From d946adda205e6e5a9661b85c34a41941c3e3da37 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 21:15:23 +0200 Subject: [PATCH 77/89] test: use RFC 5737 documentation IPs (203.0.113.0/24) in fixtures and tests --- testdata/fixtures/voice_in_trunk_groups/create.json | 2 +- testdata/fixtures/voice_in_trunks/create_sip.json | 4 ++-- .../fixtures/voice_in_trunks/create_sip_request.json | 2 +- testdata/fixtures/voice_in_trunks/index.json | 2 +- testdata/fixtures/voice_in_trunks/update_sip.json | 4 ++-- testdata/fixtures/voice_out_trunks/index.json | 2 +- testdata/fixtures/voice_out_trunks/show.json | 2 +- testdata/fixtures/voice_out_trunks/update.json | 2 +- .../voice_out_trunks/update_emergency_dids.json | 2 +- voice_in_trunks_test.go | 12 ++++++------ voice_out_trunks_test.go | 2 +- 11 files changed, 18 insertions(+), 18 deletions(-) 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 b2a441a..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,7 +97,7 @@ "media_encryption_mode": "zrtp", "stir_shaken_mode": "pai", "allowed_rtp_ips": [ - "127.0.0.1" + "203.0.113.1" ], "diversion_relay_policy": "as_is" } 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/index.json b/testdata/fixtures/voice_out_trunks/index.json index f103c14..26a57c4 100644 --- a/testdata/fixtures/voice_out_trunks/index.json +++ b/testdata/fixtures/voice_out_trunks/index.json @@ -7,7 +7,7 @@ "authentication_method": { "type": "credentials_and_ip", "attributes": { - "allowed_sip_ips": ["10.11.12.13/32"], + "allowed_sip_ips": ["203.0.113.1/32"], "tech_prefix": "", "username": "dpjgwbbac9", "password": "z0hshvbcy7" diff --git a/testdata/fixtures/voice_out_trunks/show.json b/testdata/fixtures/voice_out_trunks/show.json index cad9c63..a71dd7b 100644 --- a/testdata/fixtures/voice_out_trunks/show.json +++ b/testdata/fixtures/voice_out_trunks/show.json @@ -6,7 +6,7 @@ "authentication_method": { "type": "credentials_and_ip", "attributes": { - "allowed_sip_ips": ["10.11.12.13/32"], + "allowed_sip_ips": ["203.0.113.1/32"], "tech_prefix": "", "username": "dpjgwbbac9", "password": "z0hshvbcy7" diff --git a/testdata/fixtures/voice_out_trunks/update.json b/testdata/fixtures/voice_out_trunks/update.json index 8aa4e36..01ae760 100644 --- a/testdata/fixtures/voice_out_trunks/update.json +++ b/testdata/fixtures/voice_out_trunks/update.json @@ -6,7 +6,7 @@ "authentication_method": { "type": "credentials_and_ip", "attributes": { - "allowed_sip_ips": ["10.11.12.13/32"], + "allowed_sip_ips": ["203.0.113.1/32"], "tech_prefix": "", "username": "dpjgwbbac9", "password": "z0hshvbcy7" diff --git a/testdata/fixtures/voice_out_trunks/update_emergency_dids.json b/testdata/fixtures/voice_out_trunks/update_emergency_dids.json index 8fc1900..9a82c76 100644 --- a/testdata/fixtures/voice_out_trunks/update_emergency_dids.json +++ b/testdata/fixtures/voice_out_trunks/update_emergency_dids.json @@ -22,7 +22,7 @@ "authentication_method": { "type": "credentials_and_ip", "attributes": { - "allowed_sip_ips": ["10.0.0.1/32"], + "allowed_sip_ips": ["203.0.113.1/32"], "tech_prefix": "", "username": "user1", "password": "pass1" diff --git a/voice_in_trunks_test.go b/voice_in_trunks_test.go index 72e677a..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,7 @@ 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) } @@ -158,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 baeaa40..6131c00 100644 --- a/voice_out_trunks_test.go +++ b/voice_out_trunks_test.go @@ -34,7 +34,7 @@ func TestVoiceOutTrunksList(t *testing.T) { require.True(t, ok, "expected CredentialsAndIp authentication method") assert.Equal(t, "dpjgwbbac9", credAM.Username) assert.Equal(t, "z0hshvbcy7", credAM.Password) - assert.Equal(t, []string{"10.11.12.13/32"}, credAM.AllowedSipIPs) + assert.Equal(t, []string{"203.0.113.1/32"}, credAM.AllowedSipIPs) } func TestVoiceOutTrunksFindWithIncludedDids(t *testing.T) { From 94b72e80df5b80052e8356e1c465df977d0a99c7 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 23:31:52 +0200 Subject: [PATCH 78/89] fix(tests): update area_level assertions to match lowercase snake_case values --- did_groups_test.go | 6 +++--- emergency_requirements_test.go | 2 +- resource/enums/address_test.go | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/did_groups_test.go b/did_groups_test.go index 49717c6..8cd922b 100644 --- a/did_groups_test.go +++ b/did_groups_test.go @@ -74,9 +74,9 @@ func TestDIDGroupsFindWithIncludedRequirement(t *testing.T) { 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, "WorldWide", group.AddressRequirement.PersonalAreaLevel) - assert.Equal(t, "Country", group.AddressRequirement.BusinessAreaLevel) - assert.Equal(t, "City", group.AddressRequirement.AddressAreaLevel) + 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) diff --git a/emergency_requirements_test.go b/emergency_requirements_test.go index 8150ea7..9f6f101 100644 --- a/emergency_requirements_test.go +++ b/emergency_requirements_test.go @@ -20,7 +20,7 @@ func TestEmergencyRequirementsList(t *testing.T) { 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, "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) diff --git a/resource/enums/address_test.go b/resource/enums/address_test.go index ddd192e..0ac8b71 100644 --- a/resource/enums/address_test.go +++ b/resource/enums/address_test.go @@ -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) { From f97d0c7eb96cc10b037f1190febdd83e093c9a9d Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Fri, 17 Apr 2026 23:36:28 +0200 Subject: [PATCH 79/89] test: add dirty-tracking tests for VoiceOutTrunk emergency_dids replace and clear --- dids_test.go | 2 +- dirty_serialization_test.go | 37 +++++++++++++++++++++ requirement_validations_test.go | 6 ++-- resource/address.go | 12 +++---- resource/address_verification.go | 14 ++++---- resource/did.go | 14 ++++---- resource/did_group.go | 12 +++---- resource/emergency_calling_service.go | 12 +++---- resource/emergency_verification.go | 20 +++++------ resource/export.go | 14 ++++---- resource/order.go | 18 +++++----- resource/proof.go | 4 +-- resource/requirement.go | 4 +-- resource/status_predicates_test.go | 6 ++-- resource/supporting_document.go | 2 +- resource/voice_in_trunk.go | 18 +++++----- resource/voice_out_trunk.go | 48 +++++++++++++-------------- voice_out_trunks_test.go | 4 +-- 18 files changed, 142 insertions(+), 105 deletions(-) diff --git a/dids_test.go b/dids_test.go index 42bbc3f..3ee9369 100644 --- a/dids_test.go +++ b/dids_test.go @@ -217,7 +217,7 @@ func TestDIDsUpdateUnassignEmergencyCallingService(t *testing.T) { }) did, err := server.client.DIDs().Update(context.Background(), &resource.DID{ - ID: "44957076-778a-4802-b60c-d22db0cda284", + ID: "44957076-778a-4802-b60c-d22db0cda284", NullifyEmergencyCallingService: true, }) require.NoError(t, err) diff --git a/dirty_serialization_test.go b/dirty_serialization_test.go index 33255a6..4f38b17 100644 --- a/dirty_serialization_test.go +++ b/dirty_serialization_test.go @@ -527,6 +527,43 @@ func TestDirtyPatch_VoiceOutTrunk_ReassignAuthenticationMethod(t *testing.T) { 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/requirement_validations_test.go b/requirement_validations_test.go index 8b02d10..450c850 100644 --- a/requirement_validations_test.go +++ b/requirement_validations_test.go @@ -17,7 +17,7 @@ func TestAddressRequirementValidationsCreate(t *testing.T) { }) rv, err := server.client.AddressRequirementValidations().Create(context.Background(), &resource.AddressRequirementValidation{ - AddressID: "d3414687-40f4-4346-a267-c2c65117d28c", + AddressID: "d3414687-40f4-4346-a267-c2c65117d28c", AddressRequirementID: "aea92b24-a044-4864-9740-89d3e15b65c7", }) require.NoError(t, err) @@ -33,8 +33,8 @@ func TestAddressRequirementValidationsCreateError(t *testing.T) { }) _, err := server.client.AddressRequirementValidations().Create(context.Background(), &resource.AddressRequirementValidation{ - IdentityID: "5e9df058-50d2-4e34-b0d4-d1746b86f41a", - AddressID: "d3414687-40f4-4346-a267-c2c65117d28c", + IdentityID: "5e9df058-50d2-4e34-b0d4-d1746b86f41a", + AddressID: "d3414687-40f4-4346-a267-c2c65117d28c", AddressRequirementID: "2efc3427-8ba6-4d50-875d-f2de4a068de8", }) require.Error(t, err) diff --git a/resource/address.go b/resource/address.go index e48b5df..f37b960 100644 --- a/resource/address.go +++ b/resource/address.go @@ -4,12 +4,12 @@ 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"` + 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 diff --git a/resource/address_verification.go b/resource/address_verification.go index f013713..dbdec98 100644 --- a/resource/address_verification.go +++ b/resource/address_verification.go @@ -8,13 +8,13 @@ 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"` + 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"` diff --git a/resource/did.go b/resource/did.go index 28a62cf..a138da0 100644 --- a/resource/did.go +++ b/resource/did.go @@ -22,8 +22,8 @@ type DID struct { 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"` + 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"` @@ -33,11 +33,11 @@ type DID struct { // 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"` + 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"` diff --git a/resource/did_group.go b/resource/did_group.go index 894f1da..61983a6 100644 --- a/resource/did_group.go +++ b/resource/did_group.go @@ -12,12 +12,12 @@ type DIDGroup struct { 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"` - AddressRequirement *AddressRequirement `json:"-" rel:"address_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/emergency_calling_service.go b/resource/emergency_calling_service.go index 01b32a1..a8b4d1a 100644 --- a/resource/emergency_calling_service.go +++ b/resource/emergency_calling_service.go @@ -22,12 +22,12 @@ type EmergencyCallingService struct { RenewDate string `json:"renew_date" api:"readonly"` // 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"` + 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"` } // EmergencyCallingService status constants. diff --git a/resource/emergency_verification.go b/resource/emergency_verification.go index bfa3ccc..a0f08a7 100644 --- a/resource/emergency_verification.go +++ b/resource/emergency_verification.go @@ -5,22 +5,22 @@ 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"` + 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"` + EmergencyCallingService *EmergencyCallingService `json:"-" rel:"emergency_calling_service"` DIDs []*DID `json:"-" rel:"dids"` } diff --git a/resource/export.go b/resource/export.go index 11f2acd..e936de1 100644 --- a/resource/export.go +++ b/resource/export.go @@ -15,13 +15,13 @@ import ( // - "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"` + 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"` } diff --git a/resource/order.go b/resource/order.go index e373963..0e66bc2 100644 --- a/resource/order.go +++ b/resource/order.go @@ -10,15 +10,15 @@ 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"` + 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"` } diff --git a/resource/proof.go b/resource/proof.go index ec8fc4f..7d67ab0 100644 --- a/resource/proof.go +++ b/resource/proof.go @@ -9,8 +9,8 @@ import ( // Proof represents a proof document. type Proof struct { - ID string `json:"-" jsonapi:"proofs"` - CreatedAt time.Time `json:"created_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") diff --git a/resource/requirement.go b/resource/requirement.go index 100fbea..d7cabe1 100644 --- a/resource/requirement.go +++ b/resource/requirement.go @@ -30,7 +30,7 @@ type AddressRequirement struct { 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"` + 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 index 3effc21..616564e 100644 --- a/resource/status_predicates_test.go +++ b/resource/status_predicates_test.go @@ -8,7 +8,7 @@ import ( func TestAddressVerificationStatusPredicates(t *testing.T) { tests := []struct { - status enums.AddressVerificationStatus + status enums.AddressVerificationStatus expectPending, expectApproved, expectRejected bool }{ {enums.AddressVerificationStatusPending, true, false, false}, @@ -54,7 +54,7 @@ func TestEmergencyVerificationStatusPredicates(t *testing.T) { func TestEmergencyCallingServiceStatusPredicates(t *testing.T) { tests := []struct { - status string + status string active, canceled, changesRequired, inProcess, newStatus, pendingUpdate bool }{ {ECSStatusActive, true, false, false, false, false, false}, @@ -89,7 +89,7 @@ func TestEmergencyCallingServiceStatusPredicates(t *testing.T) { func TestOrderStatusPredicates(t *testing.T) { tests := []struct { - status enums.OrderStatus + status enums.OrderStatus expectPending, expectCompleted, expectCancelled bool }{ {enums.OrderStatusPending, true, false, false}, diff --git a/resource/supporting_document.go b/resource/supporting_document.go index 1ad3706..e6cdc9a 100644 --- a/resource/supporting_document.go +++ b/resource/supporting_document.go @@ -12,7 +12,7 @@ type SupportingDocumentTemplate struct { // PermanentSupportingDocument represents a permanent supporting document. type PermanentSupportingDocument struct { - ID string `json:"-" jsonapi:"permanent_supporting_documents"` + 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 diff --git a/resource/voice_in_trunk.go b/resource/voice_in_trunk.go index 9b4bd35..7ab8d27 100644 --- a/resource/voice_in_trunk.go +++ b/resource/voice_in_trunk.go @@ -10,15 +10,15 @@ 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"` + 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"` diff --git a/resource/voice_out_trunk.go b/resource/voice_out_trunk.go index 3073520..ac99b9e 100644 --- a/resource/voice_out_trunk.go +++ b/resource/voice_out_trunk.go @@ -11,29 +11,29 @@ import ( // VoiceOutTrunk represents a voice outbound trunk. type VoiceOutTrunk struct { - 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:"-"` + 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). @@ -45,13 +45,13 @@ type VoiceOutTrunk struct { } // MarshalJSON handles custom serialization for VoiceOutTrunk. -func (v VoiceOutTrunk) MarshalJSON() ([]byte, error) { +func (v *VoiceOutTrunk) MarshalJSON() ([]byte, error) { type Alias VoiceOutTrunk aux := struct { Alias AuthenticationMethod json.RawMessage `json:"authentication_method,omitempty"` }{ - Alias: Alias(v), + Alias: Alias(*v), } if v.AuthenticationMethod != nil { am, err := authenticationmethod.MarshalJSON(v.AuthenticationMethod) diff --git a/voice_out_trunks_test.go b/voice_out_trunks_test.go index 6131c00..7470680 100644 --- a/voice_out_trunks_test.go +++ b/voice_out_trunks_test.go @@ -139,7 +139,7 @@ func TestVoiceOutTrunksUpdateEmergencyEnableAll(t *testing.T) { }) trunk, err := server.client.VoiceOutTrunks().Update(context.Background(), &resource.VoiceOutTrunk{ - ID: "01234567-89ab-cdef-0123-456789abcdef", + ID: "01234567-89ab-cdef-0123-456789abcdef", EmergencyEnableAll: true, }) require.NoError(t, err) @@ -174,7 +174,7 @@ func TestVoiceOutTrunksUpdateClearEmergencyDIDs(t *testing.T) { }) trunk, err := server.client.VoiceOutTrunks().Update(context.Background(), &resource.VoiceOutTrunk{ - ID: "01234567-89ab-cdef-0123-456789abcdef", + ID: "01234567-89ab-cdef-0123-456789abcdef", ClearEmergencyDIDs: true, }) require.NoError(t, err) From c0febd3aadb315cdfd1f3aafa55c2b65bedb2183 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Wed, 22 Apr 2026 12:59:22 +0200 Subject: [PATCH 80/89] examples: add emergency_scenario end-to-end example --- .gitignore | 2 + examples/README.md | 1 + examples/emergency_scenario/main.go | 297 ++++++++++++++++++++++++++++ 3 files changed, 300 insertions(+) create mode 100644 examples/emergency_scenario/main.go diff --git a/.gitignore b/.gitignore index c81b04a..aa17052 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,5 @@ orders_emergency shared_capacity_groups voice_in_trunk_groups voice_out_trunks +/emergency_scenario +/did_trunk_assignment diff --git a/examples/README.md b/examples/README.md index 9a80447..f8b124e 100644 --- a/examples/README.md +++ b/examples/README.md @@ -45,6 +45,7 @@ DIDWW_API_KEY=your_api_key go run ./examples/balance/ | [`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). | 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.") +} From 4a6e06f1d2c8e5f743a8dbec59dca7677a0925c9 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Wed, 22 Apr 2026 21:29:07 +0200 Subject: [PATCH 81/89] examples: add did_trunk_assignment example --- examples/README.md | 1 + examples/did_trunk_assignment/main.go | 130 ++++++++++++++++++++++++++ 2 files changed, 131 insertions(+) create mode 100644 examples/did_trunk_assignment/main.go diff --git a/examples/README.md b/examples/README.md index f8b124e..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. | 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!") +} From b223450fa23adf2c74fa7fa4c9616a5bd91a1cc4 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Wed, 22 Apr 2026 23:00:11 +0200 Subject: [PATCH 82/89] docs(readme): update date and datetime field documentation --- README.md | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6de54ca..e69d49e 100644 --- a/README.md +++ b/README.md @@ -534,9 +534,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.ExpiresAt`; `DIDReservation.ExpiresAt` 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") From d16c5d109a100b670cb9c424638bd0c270f98f8d Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Wed, 22 Apr 2026 23:07:29 +0200 Subject: [PATCH 83/89] docs(readme): add emergency and diversion enums to documentation --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e69d49e..073c6da 100644 --- a/README.md +++ b/README.md @@ -566,9 +566,9 @@ 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`, -`DiversionRelayPolicy` +`ReroutingDisconnectCode`, `Feature`, `AreaLevel`, `AddressVerificationStatus`, `StirShakenMode` \* `replace_cli` and `randomize_cli` require account configuration. From ec5585876c658a028ec1ea792720bd96b7154587 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Wed, 22 Apr 2026 23:27:42 +0200 Subject: [PATCH 84/89] docs(readme): add emergency and did_history resource examples --- README.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/README.md b/README.md index 073c6da..f555895 100644 --- a/README.md +++ b/README.md @@ -354,6 +354,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 From 20d2cc1bea2124b3e41aff10f5822a6e7e415d3a Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Thu, 23 Apr 2026 17:35:03 +0200 Subject: [PATCH 85/89] feat: add resource-level meta support for EmergencyCallingService and EmergencyRequirement --- emergency_calling_services_test.go | 10 ++++++++++ emergency_requirements_test.go | 5 +++++ examples/emergency_calling_services/main.go | 4 ++++ examples/emergency_requirements/main.go | 4 ++++ resource/emergency_calling_service.go | 17 ++++++++++++++++- resource/emergency_requirement.go | 14 ++++++++++++++ .../emergency_calling_services/index.json | 4 ++++ .../show_with_includes.json | 4 ++++ .../fixtures/emergency_requirements/index.json | 4 ++++ 9 files changed, 65 insertions(+), 1 deletion(-) diff --git a/emergency_calling_services_test.go b/emergency_calling_services_test.go index d6b930c..60f5840 100644 --- a/emergency_calling_services_test.go +++ b/emergency_calling_services_test.go @@ -26,6 +26,11 @@ func TestEmergencyCallingServicesList(t *testing.T) { 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) { @@ -41,6 +46,11 @@ func TestEmergencyCallingServicesFindWithIncludes(t *testing.T) { 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) diff --git a/emergency_requirements_test.go b/emergency_requirements_test.go index 9f6f101..e1b4d39 100644 --- a/emergency_requirements_test.go +++ b/emergency_requirements_test.go @@ -24,4 +24,9 @@ func TestEmergencyRequirementsList(t *testing.T) { 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/examples/emergency_calling_services/main.go b/examples/emergency_calling_services/main.go index 5201cb7..fb0a3ae 100644 --- a/examples/emergency_calling_services/main.go +++ b/examples/emergency_calling_services/main.go @@ -42,6 +42,10 @@ func main() { 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 diff --git a/examples/emergency_requirements/main.go b/examples/emergency_requirements/main.go index 2ead685..3755a0f 100644 --- a/examples/emergency_requirements/main.go +++ b/examples/emergency_requirements/main.go @@ -45,5 +45,9 @@ func main() { 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/resource/emergency_calling_service.go b/resource/emergency_calling_service.go index a8b4d1a..15d42de 100644 --- a/resource/emergency_calling_service.go +++ b/resource/emergency_calling_service.go @@ -1,6 +1,9 @@ package resource -import "time" +import ( + "encoding/json" + "time" +) // EmergencyCallingService represents a customer-owned subscription to emergency calling. // Supported operations: index, show, destroy. Introduced in API 2026-04-16. @@ -20,6 +23,8 @@ type EmergencyCallingService struct { 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"` @@ -30,6 +35,16 @@ type EmergencyCallingService struct { 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" diff --git a/resource/emergency_requirement.go b/resource/emergency_requirement.go index 0f4325b..8999397 100644 --- a/resource/emergency_requirement.go +++ b/resource/emergency_requirement.go @@ -1,5 +1,7 @@ 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 { @@ -22,7 +24,19 @@ type EmergencyRequirement struct { 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/testdata/fixtures/emergency_calling_services/index.json b/testdata/fixtures/emergency_calling_services/index.json index 5a8bb3b..2370d6a 100644 --- a/testdata/fixtures/emergency_calling_services/index.json +++ b/testdata/fixtures/emergency_calling_services/index.json @@ -12,6 +12,10 @@ "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" } diff --git a/testdata/fixtures/emergency_calling_services/show_with_includes.json b/testdata/fixtures/emergency_calling_services/show_with_includes.json index 04a0b2f..6c22f62 100644 --- a/testdata/fixtures/emergency_calling_services/show_with_includes.json +++ b/testdata/fixtures/emergency_calling_services/show_with_includes.json @@ -11,6 +11,10 @@ "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" } diff --git a/testdata/fixtures/emergency_requirements/index.json b/testdata/fixtures/emergency_requirements/index.json index 548e992..157df2f 100644 --- a/testdata/fixtures/emergency_requirements/index.json +++ b/testdata/fixtures/emergency_requirements/index.json @@ -14,6 +14,10 @@ "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" } From b9273f97558942b6ba0fef600ae65d643ad01ff6 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Thu, 23 Apr 2026 21:52:05 +0200 Subject: [PATCH 86/89] fix: use credentials_and_ip for VoiceOutTrunk create (ip_only is not allowed) --- testdata/fixtures/voice_out_trunks/create.json | 13 +++++++++---- .../fixtures/voice_out_trunks/create_request.json | 2 +- voice_out_trunks_test.go | 10 ++++++---- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/testdata/fixtures/voice_out_trunks/create.json b/testdata/fixtures/voice_out_trunks/create.json index 266aab9..9afe475 100644 --- a/testdata/fixtures/voice_out_trunks/create.json +++ b/testdata/fixtures/voice_out_trunks/create.json @@ -4,10 +4,12 @@ "type": "voice_out_trunks", "attributes": { "authentication_method": { - "type": "ip_only", + "type": "credentials_and_ip", "attributes": { "allowed_sip_ips": ["203.0.113.0/24"], - "tech_prefix": "" + "tech_prefix": "", + "username": "dLPa6JbLTeMjKjl5", + "password": "BZj1YvP45yWvX5Ic" } }, "allowed_rtp_ips": null, @@ -24,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": { @@ -45,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 609192f..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","on_cli_mismatch_action":"replace_cli","authentication_method":{"type":"ip_only","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"}]}}}} +{"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/voice_out_trunks_test.go b/voice_out_trunks_test.go index 7470680..647305c 100644 --- a/voice_out_trunks_test.go +++ b/voice_out_trunks_test.go @@ -76,7 +76,7 @@ func TestVoiceOutTrunksCreate(t *testing.T) { trunk, err := server.client.VoiceOutTrunks().Create(context.Background(), &resource.VoiceOutTrunk{ Name: "java-test", OnCliMismatchAction: enums.OnCliMismatchActionReplaceCli, - AuthenticationMethod: &authenticationmethod.IpOnly{ + AuthenticationMethod: &authenticationmethod.CredentialsAndIp{ AllowedSipIPs: []string{"203.0.113.0/24"}, }, DefaultDIDID: "7a028c32-e6b6-4c86-bf01-90f901b37012", @@ -88,9 +88,11 @@ func TestVoiceOutTrunksCreate(t *testing.T) { // Verify authentication_method in response require.NotNil(t, trunk.AuthenticationMethod) - ipAM, ok := trunk.AuthenticationMethod.(*authenticationmethod.IpOnly) - require.True(t, ok, "expected IpOnly authentication method") - assert.Equal(t, []string{"203.0.113.0/24"}, ipAM.AllowedSipIPs) + 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") } From 4edd2189edc0e9b3bd8335ade2b5e752e40cd7ce Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Thu, 23 Apr 2026 21:59:25 +0200 Subject: [PATCH 87/89] test: add fixture and test for reading VoiceOutTrunk with ip_only authentication --- .../voice_out_trunks/show_ip_only.json | 53 +++++++++++++++++++ voice_out_trunks_test.go | 23 ++++++++ 2 files changed, 76 insertions(+) create mode 100644 testdata/fixtures/voice_out_trunks/show_ip_only.json 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/voice_out_trunks_test.go b/voice_out_trunks_test.go index 647305c..0520aeb 100644 --- a/voice_out_trunks_test.go +++ b/voice_out_trunks_test.go @@ -68,6 +68,29 @@ 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 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"}, From 6ed3bdc1a241ba1f0f9e99c1574ac86e9a5b0b4c Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Thu, 23 Apr 2026 22:10:50 +0200 Subject: [PATCH 88/89] feat: add twilio auth fixtures/tests + update Voice Out Trunks documentation --- README.md | 14 +++-- .../voice_out_trunks/create_twilio.json | 52 +++++++++++++++++++ .../create_twilio_request.json | 1 + .../voice_out_trunks/show_twilio.json | 52 +++++++++++++++++++ voice_out_trunks_test.go | 45 ++++++++++++++++ 5 files changed, 161 insertions(+), 3 deletions(-) create mode 100644 testdata/fixtures/voice_out_trunks/create_twilio.json create mode 100644 testdata/fixtures/voice_out_trunks/create_twilio_request.json create mode 100644 testdata/fixtures/voice_out_trunks/show_twilio.json diff --git a/README.md b/README.md index f555895..1381a30 100644 --- a/README.md +++ b/README.md @@ -246,24 +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/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", - AuthenticationMethod: &authenticationmethod.IpOnly{ + 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, } created, _ := client.VoiceOutTrunks().Create(ctx, trunk) +// created.AuthenticationMethod.(*authenticationmethod.CredentialsAndIp).Username -- server-generated +// created.AuthenticationMethod.(*authenticationmethod.CredentialsAndIp).Password -- server-generated ``` ### Orders 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/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/voice_out_trunks_test.go b/voice_out_trunks_test.go index 0520aeb..bcca169 100644 --- a/voice_out_trunks_test.go +++ b/voice_out_trunks_test.go @@ -91,6 +91,51 @@ func TestVoiceOutTrunksFindIpOnly(t *testing.T) { 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"}, From 0213ff783c7cf1fc1ddb3c4e717dbc60ab4d2271 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Thu, 23 Apr 2026 23:34:31 +0200 Subject: [PATCH 89/89] refactor!: rename IsCancelled to IsCanceled for wire-format consistency The server wire value is 'canceled' (single L). Rename the helper method to match, aligning with how the Order status is serialized on the wire. Sibling helpers already use the single-L form (e.g. IsEcsCanceled). --- examples/orders/main.go | 2 +- resource/order.go | 4 ++-- resource/status_predicates_test.go | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/examples/orders/main.go b/examples/orders/main.go index 20d1fbe..9bb721e 100644 --- a/examples/orders/main.go +++ b/examples/orders/main.go @@ -66,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/resource/order.go b/resource/order.go index 0e66bc2..995f83b 100644 --- a/resource/order.go +++ b/resource/order.go @@ -29,8 +29,8 @@ 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 } -// IsCancelled returns true when the order status is "canceled". -func (o *Order) IsCancelled() bool { return o.Status == enums.OrderStatusCanceled } +// 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 { diff --git a/resource/status_predicates_test.go b/resource/status_predicates_test.go index 616564e..ef7c4c1 100644 --- a/resource/status_predicates_test.go +++ b/resource/status_predicates_test.go @@ -89,8 +89,8 @@ func TestEmergencyCallingServiceStatusPredicates(t *testing.T) { func TestOrderStatusPredicates(t *testing.T) { tests := []struct { - status enums.OrderStatus - expectPending, expectCompleted, expectCancelled bool + status enums.OrderStatus + expectPending, expectCompleted, expectCanceled bool }{ {enums.OrderStatusPending, true, false, false}, {enums.OrderStatusCompleted, false, true, false}, @@ -104,8 +104,8 @@ func TestOrderStatusPredicates(t *testing.T) { if got := o.IsCompleted(); got != tt.expectCompleted { t.Errorf("IsCompleted() for %q = %v, want %v", tt.status, got, tt.expectCompleted) } - if got := o.IsCancelled(); got != tt.expectCancelled { - t.Errorf("IsCancelled() for %q = %v, want %v", tt.status, got, tt.expectCancelled) + if got := o.IsCanceled(); got != tt.expectCanceled { + t.Errorf("IsCanceled() for %q = %v, want %v", tt.status, got, tt.expectCanceled) } } }