feat: Ipad app production readiness, Colony orchestration, Social posting
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
import XCTest
|
||||
@testable import velocity
|
||||
|
||||
final class VelocityEnterpriseFlowTests: XCTestCase {
|
||||
func testImportWorkbenchDuplicateMergePoliciesDecodeForReviewFlow() throws {
|
||||
let payload = Data(
|
||||
"""
|
||||
{
|
||||
"batch_id": "batch-1",
|
||||
"summary": {
|
||||
"proposal_count": 3,
|
||||
"duplicate_count": 2,
|
||||
"validation_error_count": 1,
|
||||
"validation_warning_count": 1
|
||||
},
|
||||
"rows": [
|
||||
{
|
||||
"proposal_id": "proposal-create",
|
||||
"row_number": 1,
|
||||
"status": "pending",
|
||||
"confidence": 0.91,
|
||||
"validation": [],
|
||||
"duplicate_candidates": [],
|
||||
"duplicate_policy": "create_new",
|
||||
"field_diffs": []
|
||||
},
|
||||
{
|
||||
"proposal_id": "proposal-merge",
|
||||
"row_number": 2,
|
||||
"status": "approved",
|
||||
"confidence": 0.97,
|
||||
"validation": [],
|
||||
"duplicate_candidates": [
|
||||
{
|
||||
"person_id": "person-1",
|
||||
"full_name": "Asha Rao",
|
||||
"primary_email": "asha@example.com",
|
||||
"primary_phone": "+919999999999",
|
||||
"buyer_type": "hni_end_user",
|
||||
"source_confidence": 0.94,
|
||||
"created_at": "2026-05-01T00:00:00Z",
|
||||
"updated_at": "2026-05-01T00:00:00Z",
|
||||
"match_reason": "phone",
|
||||
"match_score": 95
|
||||
}
|
||||
],
|
||||
"duplicate_policy": "update_existing",
|
||||
"field_diffs": [
|
||||
{"field":"budget_band","proposed":"10-15 Cr","existing":"8-10 Cr","changed":true}
|
||||
]
|
||||
},
|
||||
{
|
||||
"proposal_id": "proposal-skip",
|
||||
"row_number": 3,
|
||||
"status": "approved",
|
||||
"confidence": 0.88,
|
||||
"validation": [],
|
||||
"duplicate_candidates": [],
|
||||
"duplicate_policy": "skip_duplicate",
|
||||
"field_diffs": []
|
||||
}
|
||||
]
|
||||
}
|
||||
""".utf8
|
||||
)
|
||||
|
||||
let workbench = try JSONDecoder().decode(VelocityImportWorkbenchDTO.self, from: payload)
|
||||
XCTAssertEqual(workbench.summary.duplicateCount, 2)
|
||||
XCTAssertEqual(workbench.row(for: "proposal-create")?.duplicatePolicy, "create_new")
|
||||
XCTAssertEqual(workbench.row(for: "proposal-merge")?.duplicatePolicy, "update_existing")
|
||||
XCTAssertEqual(workbench.row(for: "proposal-skip")?.duplicatePolicy, "skip_duplicate")
|
||||
XCTAssertEqual(workbench.row(for: "proposal-merge")?.fieldDiffs.first?.field, "budget_band")
|
||||
}
|
||||
|
||||
func testEnterpriseIdentityContractsDecodeProviderObjectsAndSessionSwitch() throws {
|
||||
let providersPayload = Data(
|
||||
"""
|
||||
{
|
||||
"enabled": true,
|
||||
"tenantId": "tenant_velocity",
|
||||
"providers": [
|
||||
{
|
||||
"id": "azure_ad",
|
||||
"name": "Azure AD",
|
||||
"type": "oauth",
|
||||
"authorizationUrl": "https://login.microsoftonline.com/tenant/oauth2/v2.0/authorize",
|
||||
"metadataUrl": "https://login.microsoftonline.com/tenant/v2.0/.well-known/openid-configuration",
|
||||
"enabled": true
|
||||
}
|
||||
]
|
||||
}
|
||||
""".utf8
|
||||
)
|
||||
let providers = try JSONDecoder().decode(VelocitySSOProvidersDTO.self, from: providersPayload)
|
||||
XCTAssertTrue(providers.enabled)
|
||||
XCTAssertEqual(providers.providers.first?.id, "azure_ad")
|
||||
XCTAssertEqual(providers.providers.first?.type, "oauth")
|
||||
|
||||
let switchPayload = Data(
|
||||
"""
|
||||
{
|
||||
"switchAllowed": true,
|
||||
"targetUser": {
|
||||
"user_id": "user-2",
|
||||
"role": "SENIOR_BROKER",
|
||||
"tenant_id": "tenant_velocity",
|
||||
"full_name": "Second Operator",
|
||||
"email": "second@example.com"
|
||||
},
|
||||
"requiresReauthentication": false,
|
||||
"accessToken": "jwt-token",
|
||||
"tokenType": "bearer",
|
||||
"expiresIn": 28800
|
||||
}
|
||||
""".utf8
|
||||
)
|
||||
let switchResult = try JSONDecoder().decode(VelocitySessionSwitchDTO.self, from: switchPayload)
|
||||
XCTAssertTrue(switchResult.switchAllowed)
|
||||
XCTAssertEqual(switchResult.targetUser?.displayName, "Second Operator")
|
||||
XCTAssertEqual(switchResult.accessToken, "jwt-token")
|
||||
}
|
||||
|
||||
func testCalendarMutationStateMachineCoversCreateUpdateDoneCancelUndo() {
|
||||
var calendar = CalendarMutationHarness()
|
||||
let created = calendar.create(title: "Site visit")
|
||||
XCTAssertEqual(calendar.events[created]?.status, "confirmed")
|
||||
|
||||
calendar.update(id: created, title: "VIP site visit")
|
||||
XCTAssertEqual(calendar.events[created]?.title, "VIP site visit")
|
||||
|
||||
calendar.done(id: created)
|
||||
XCTAssertEqual(calendar.events[created]?.status, "done")
|
||||
|
||||
calendar.cancel(id: created)
|
||||
XCTAssertEqual(calendar.events[created]?.status, "cancelled")
|
||||
|
||||
calendar.undo()
|
||||
XCTAssertEqual(calendar.events[created]?.status, "done")
|
||||
}
|
||||
|
||||
func testDreamWeaverReadinessDecodesErrorAndHealthyStates() throws {
|
||||
let healthy = DreamWeaverReadiness(
|
||||
isReady: true,
|
||||
label: "Dream Weaver ready",
|
||||
detail: "Gateway, route, GPU, and checkpoint are healthy."
|
||||
)
|
||||
XCTAssertTrue(healthy.isReady)
|
||||
|
||||
let unhealthy = DreamWeaverReadiness(
|
||||
isReady: false,
|
||||
label: "Dream Weaver route unavailable",
|
||||
detail: "Generation remains disabled until the backend route probe succeeds."
|
||||
)
|
||||
XCTAssertFalse(unhealthy.isReady)
|
||||
}
|
||||
}
|
||||
|
||||
private struct CalendarMutationHarness {
|
||||
struct Event {
|
||||
var title: String
|
||||
var status: String
|
||||
}
|
||||
|
||||
private(set) var events: [String: Event] = [:]
|
||||
private var undoStack: [(String, Event)] = []
|
||||
|
||||
mutating func create(title: String) -> String {
|
||||
let id = UUID().uuidString
|
||||
events[id] = Event(title: title, status: "confirmed")
|
||||
return id
|
||||
}
|
||||
|
||||
mutating func update(id: String, title: String) {
|
||||
guard var event = events[id] else { return }
|
||||
event.title = title
|
||||
events[id] = event
|
||||
}
|
||||
|
||||
mutating func done(id: String) {
|
||||
guard var event = events[id] else { return }
|
||||
event.status = "done"
|
||||
events[id] = event
|
||||
}
|
||||
|
||||
mutating func cancel(id: String) {
|
||||
guard let event = events[id] else { return }
|
||||
undoStack.append((id, event))
|
||||
events[id]?.status = "cancelled"
|
||||
}
|
||||
|
||||
mutating func undo() {
|
||||
guard let (id, event) = undoStack.popLast() else { return }
|
||||
events[id] = event
|
||||
}
|
||||
}
|
||||
@@ -15,11 +15,8 @@ final class VelocitySmokeTests: XCTestCase {
|
||||
[
|
||||
"Dashboard",
|
||||
"Clients",
|
||||
"Imports",
|
||||
"Communications",
|
||||
"Calendar",
|
||||
"Oracle",
|
||||
"Sentinel",
|
||||
"Inventory",
|
||||
"Settings",
|
||||
]
|
||||
@@ -32,17 +29,22 @@ final class VelocitySmokeTests: XCTestCase {
|
||||
[
|
||||
"Dashboard",
|
||||
"Clients",
|
||||
"Imports",
|
||||
"Communications",
|
||||
"Calendar",
|
||||
"Oracle",
|
||||
"Operator Posture",
|
||||
"Inventory",
|
||||
"Settings",
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
func testShowroomDockExcludesAdministrativeWorkspaces() {
|
||||
let sectionNames = Set(AppSection.allCases.map(\.rawValue))
|
||||
XCTAssertFalse(sectionNames.contains("Imports"))
|
||||
XCTAssertFalse(sectionNames.contains("Sentinel"))
|
||||
XCTAssertFalse(sectionNames.contains("Oracle"))
|
||||
XCTAssertEqual(AppSection.communications.dockTitle, "Comms")
|
||||
}
|
||||
|
||||
func testAppConfigParsesExplicitValuesAndRejectsPlaceholders() {
|
||||
XCTAssertEqual(
|
||||
AppConfig.parsedValue(from: ["BASE_URL": " https://velocity.desineuron.in/api "], key: "BASE_URL"),
|
||||
@@ -182,7 +184,7 @@ final class VelocitySmokeTests: XCTestCase {
|
||||
email: nil,
|
||||
hasPassword: false,
|
||||
hasBearerToken: true,
|
||||
source: .buildConfiguration
|
||||
source: .secureDeviceStorage
|
||||
)
|
||||
XCTAssertEqual(open.dreamWeaverAuthenticationDescription, "No gateway key configured")
|
||||
}
|
||||
@@ -913,6 +915,64 @@ final class VelocitySmokeTests: XCTestCase {
|
||||
XCTAssertEqual(AppStoreRefreshPolicy.leadEventLimitPerLead, 4)
|
||||
}
|
||||
|
||||
func testMobileEdgeBulkRefreshContractDecodesCalendarAlertsAndLeadEvents() throws {
|
||||
let payload = Data(
|
||||
"""
|
||||
{
|
||||
"calendar_events": [
|
||||
{
|
||||
"calendar_event_id": "cal-1",
|
||||
"lead_id": "lead-1",
|
||||
"title": "Site visit",
|
||||
"description": "Walkthrough",
|
||||
"start_at": "2026-04-26T07:00:00Z",
|
||||
"end_at": "2026-04-26T08:00:00Z",
|
||||
"all_day": false,
|
||||
"status": "confirmed",
|
||||
"reminder_minutes": [15],
|
||||
"created_by": "user",
|
||||
"location": "Sales lounge",
|
||||
"created_at": "2026-04-26T06:30:00Z"
|
||||
}
|
||||
],
|
||||
"lead_events": {
|
||||
"lead-1": [
|
||||
{
|
||||
"event_id": "evt-1",
|
||||
"lead_id": "lead-1",
|
||||
"channel": "manual_note",
|
||||
"direction": "inbound",
|
||||
"provider": null,
|
||||
"capture_mode": "operator_note",
|
||||
"consent_state": "granted",
|
||||
"timestamp": "2026-04-26T06:00:00Z",
|
||||
"duration_seconds": null,
|
||||
"summary": "Client wants a larger balcony.",
|
||||
"raw_reference": null,
|
||||
"recording_ref": null,
|
||||
"provider_metadata": {},
|
||||
"created_at": "2026-04-26T06:00:00Z"
|
||||
}
|
||||
]
|
||||
},
|
||||
"alerts": {
|
||||
"pending_insights": 2,
|
||||
"upcoming_calendar_events_24h": 1,
|
||||
"pending_transcriptions": 3,
|
||||
"generated_at": "2026-04-26T06:35:00Z"
|
||||
},
|
||||
"generated_at": "2026-04-26T06:35:00Z"
|
||||
}
|
||||
""".utf8
|
||||
)
|
||||
|
||||
let bundle = try JSONDecoder().decode(VelocityMobileEdgeBulkDTO.self, from: payload)
|
||||
XCTAssertEqual(bundle.calendarEvents.first?.calendarEventId, "cal-1")
|
||||
XCTAssertEqual(bundle.leadEvents["lead-1"]?.first?.eventId, "evt-1")
|
||||
XCTAssertEqual(bundle.alerts.pendingInsights, 2)
|
||||
XCTAssertEqual(bundle.alerts.upcomingCalendarEvents24h, 1)
|
||||
}
|
||||
|
||||
func testAppStoreRefreshPolicyPrioritizesHighestScoreLeads() {
|
||||
let leads = [
|
||||
VelocityLeadDTO(
|
||||
@@ -970,4 +1030,280 @@ final class VelocitySmokeTests: XCTestCase {
|
||||
["lead-2", "lead-3"]
|
||||
)
|
||||
}
|
||||
|
||||
func testCanonicalDashboardMetricsIgnoreLocalDriftAndMatchBackendContracts() {
|
||||
let contacts = [
|
||||
VelocityCanonicalContactListItemDTO(
|
||||
personId: "person-1",
|
||||
fullName: "Whale Buyer",
|
||||
primaryPhone: nil,
|
||||
buyerType: "investor",
|
||||
leadId: "lead-1",
|
||||
leadStatus: "qualified",
|
||||
budgetBand: nil,
|
||||
urgency: "high",
|
||||
primaryInterest: nil,
|
||||
intentScore: 0.95,
|
||||
engagementScore: 0.70,
|
||||
urgencyScore: 0.80,
|
||||
interactionCount: 3,
|
||||
pendingTasks: 1,
|
||||
lastInteractionAt: nil,
|
||||
createdAt: nil
|
||||
),
|
||||
VelocityCanonicalContactListItemDTO(
|
||||
personId: "person-2",
|
||||
fullName: "Standard Buyer",
|
||||
primaryPhone: nil,
|
||||
buyerType: "end_user",
|
||||
leadId: "lead-2",
|
||||
leadStatus: "new",
|
||||
budgetBand: nil,
|
||||
urgency: nil,
|
||||
primaryInterest: nil,
|
||||
intentScore: 0.40,
|
||||
engagementScore: 0.30,
|
||||
urgencyScore: 0.20,
|
||||
interactionCount: 1,
|
||||
pendingTasks: 0,
|
||||
lastInteractionAt: nil,
|
||||
createdAt: nil
|
||||
),
|
||||
]
|
||||
let leads = VelocityLeadDTO.activeLeadSummaries(from: contacts)
|
||||
let board = [
|
||||
VelocityKanbanColumnDTO(status: "new", label: "New", count: 4, items: []),
|
||||
VelocityKanbanColumnDTO(status: "qualified", label: "Qualified", count: 3, items: []),
|
||||
]
|
||||
let taskRefresh = AppStore.CalendarTaskRefresh(
|
||||
tasks: [],
|
||||
pendingTaskCount: 5,
|
||||
pendingTaskIDs: ["task-1", "task-2", "task-3", "task-4", "task-5"],
|
||||
urgentTaskCount: 2
|
||||
)
|
||||
let today = ISO8601DateFormatter().string(from: Date())
|
||||
let metrics = AppStore.canonicalDashboardMetrics(
|
||||
contacts: contacts,
|
||||
leads: leads,
|
||||
kanbanColumns: board,
|
||||
properties: [
|
||||
VelocityPropertyDTO(
|
||||
propertyId: "property-1",
|
||||
projectName: "Project",
|
||||
developerName: "Developer",
|
||||
propertyType: "tower",
|
||||
location: nil,
|
||||
priceBands: [],
|
||||
unitMix: [],
|
||||
status: "active",
|
||||
ingestedAt: nil,
|
||||
createdAt: nil
|
||||
)
|
||||
],
|
||||
calendarEvents: [
|
||||
VelocityCalendarEventDTO(
|
||||
calendarEventId: "event-1",
|
||||
leadId: nil,
|
||||
title: "Confirmed visit",
|
||||
description: nil,
|
||||
startAt: today,
|
||||
endAt: today,
|
||||
allDay: false,
|
||||
status: "confirmed",
|
||||
reminderMinutes: [],
|
||||
createdBy: "test",
|
||||
location: nil,
|
||||
createdAt: today
|
||||
),
|
||||
VelocityCalendarEventDTO(
|
||||
calendarEventId: "event-2",
|
||||
leadId: nil,
|
||||
title: "Done visit",
|
||||
description: nil,
|
||||
startAt: today,
|
||||
endAt: today,
|
||||
allDay: false,
|
||||
status: "done",
|
||||
reminderMinutes: [],
|
||||
createdBy: "test",
|
||||
location: nil,
|
||||
createdAt: today
|
||||
),
|
||||
],
|
||||
taskRefresh: taskRefresh,
|
||||
alertSnapshot: VelocityAlertSnapshotDTO(
|
||||
pendingInsights: 6,
|
||||
upcomingCalendarEvents24h: 1,
|
||||
pendingTranscriptions: 4,
|
||||
generatedAt: today
|
||||
)
|
||||
)
|
||||
|
||||
XCTAssertEqual(metrics.leadCount, 7)
|
||||
XCTAssertEqual(metrics.whaleLeadCount, 1)
|
||||
XCTAssertEqual(metrics.propertyCount, 1)
|
||||
XCTAssertEqual(metrics.todayCalendarCount, 1)
|
||||
XCTAssertEqual(metrics.pendingTaskCount, 5)
|
||||
XCTAssertEqual(metrics.urgentTaskCount, 2)
|
||||
XCTAssertEqual(metrics.pendingInsights, 6)
|
||||
XCTAssertEqual(metrics.pendingTranscriptions, 4)
|
||||
}
|
||||
|
||||
func testDreamWeaverHealthDecodesCheckpointReadinessAliases() throws {
|
||||
let payload = Data(#"{"status":"ok","comfyui":true,"preferred_checkpoint_available":false}"#.utf8)
|
||||
let health = try JSONDecoder().decode(HealthResponse.self, from: payload)
|
||||
|
||||
XCTAssertEqual(health.status, "ok")
|
||||
XCTAssertEqual(health.comfyui, true)
|
||||
XCTAssertEqual(health.checkpointReady, false)
|
||||
}
|
||||
|
||||
func testCommunicationsThreadRequiresCanonicalCRMPersonLink() throws {
|
||||
let linked = try JSONDecoder().decode(
|
||||
VelocityCommsThreadDTO.self,
|
||||
from: Data(#"{"threadId":"thread-1","provider":"waha","personId":"person-1","phoneE164":"+910000000000","displayName":"Amina","channel":"whatsapp","status":"open","unreadCount":1,"lastMessageAt":null,"updatedAt":"2026-04-29T10:00:00+00:00","lastMessagePreview":"Hi","crmPerson":{"id":"person-1","fullName":"Amina","primaryPhone":"+910000000000","primaryEmail":null,"buyerType":"investor","leadStatus":"new","projectName":"Tower"}}"#.utf8)
|
||||
)
|
||||
let unlinked = try JSONDecoder().decode(
|
||||
VelocityCommsThreadDTO.self,
|
||||
from: Data(#"{"threadId":"thread-2","provider":"mock","personId":null,"phoneE164":"+910000000001","displayName":null,"channel":"whatsapp","status":"open","unreadCount":0,"lastMessageAt":null,"updatedAt":"2026-04-29T10:00:00+00:00","lastMessagePreview":null,"crmPerson":null}"#.utf8)
|
||||
)
|
||||
|
||||
XCTAssertTrue(linked.isLinkedToCanonicalPerson)
|
||||
XCTAssertEqual(linked.displayTitle, "Amina")
|
||||
XCTAssertFalse(unlinked.isLinkedToCanonicalPerson)
|
||||
XCTAssertEqual(unlinked.displayTitle, "+910000000001")
|
||||
}
|
||||
|
||||
func testCommunicationsMessageDetailContractDecodesThreadTimeline() throws {
|
||||
let payload = Data(
|
||||
#"""
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
"messageId": "message-1",
|
||||
"threadId": "thread-1",
|
||||
"provider": "waha",
|
||||
"externalMessageId": "external-1",
|
||||
"direction": "inbound",
|
||||
"messageType": "text",
|
||||
"body": "Can I visit tomorrow?",
|
||||
"mediaUrl": null,
|
||||
"mediaMimeType": null,
|
||||
"deliveryStatus": "delivered",
|
||||
"sentAt": "2026-04-29T10:00:00+00:00",
|
||||
"deliveredAt": null,
|
||||
"readAt": null,
|
||||
"rawPayload": {"source": "webhook"},
|
||||
"createdAt": "2026-04-29T10:00:01+00:00"
|
||||
},
|
||||
{
|
||||
"messageId": "message-2",
|
||||
"threadId": "thread-1",
|
||||
"provider": "waha",
|
||||
"externalMessageId": "external-2",
|
||||
"direction": "outbound",
|
||||
"messageType": "text",
|
||||
"body": "Yes, I can schedule it.",
|
||||
"mediaUrl": null,
|
||||
"mediaMimeType": null,
|
||||
"deliveryStatus": "sent",
|
||||
"sentAt": "2026-04-29T10:02:00+00:00",
|
||||
"deliveredAt": null,
|
||||
"readAt": null,
|
||||
"rawPayload": {},
|
||||
"createdAt": "2026-04-29T10:02:00+00:00"
|
||||
}
|
||||
],
|
||||
"thread": {
|
||||
"threadId": "thread-1",
|
||||
"provider": "waha",
|
||||
"personId": "person-1",
|
||||
"phoneE164": "+910000000000",
|
||||
"displayName": "Amina",
|
||||
"channel": "whatsapp",
|
||||
"status": "open",
|
||||
"unreadCount": 1,
|
||||
"lastMessageAt": "2026-04-29T10:02:00+00:00",
|
||||
"updatedAt": "2026-04-29T10:02:00+00:00",
|
||||
"lastMessagePreview": "Yes, I can schedule it.",
|
||||
"crmPerson": {
|
||||
"id": "person-1",
|
||||
"fullName": "Amina",
|
||||
"primaryPhone": "+910000000000",
|
||||
"primaryEmail": null,
|
||||
"buyerType": "investor",
|
||||
"leadStatus": "new",
|
||||
"projectName": "Tower"
|
||||
}
|
||||
}
|
||||
}
|
||||
"""#.utf8
|
||||
)
|
||||
|
||||
let detail = try JSONDecoder().decode(VelocityCommsThreadMessagesDTO.self, from: payload)
|
||||
|
||||
XCTAssertEqual(detail.messages.count, 2)
|
||||
XCTAssertEqual(detail.messages.first?.direction, "inbound")
|
||||
XCTAssertEqual(detail.messages.last?.deliveryStatus, "sent")
|
||||
XCTAssertEqual(detail.thread.threadId, "thread-1")
|
||||
XCTAssertTrue(detail.thread.isLinkedToCanonicalPerson)
|
||||
}
|
||||
|
||||
func testCommunicationsCallLogContractDecodesTranscriptState() throws {
|
||||
let payload = Data(
|
||||
#"""
|
||||
{
|
||||
"calls": [
|
||||
{
|
||||
"callId": "call-1",
|
||||
"threadId": "thread-1",
|
||||
"personId": "person-1",
|
||||
"provider": "waha",
|
||||
"externalCallId": "provider-call-1",
|
||||
"phoneE164": "+910000000000",
|
||||
"direction": "inbound",
|
||||
"status": "completed",
|
||||
"startedAt": "2026-04-29T10:00:00+00:00",
|
||||
"endedAt": "2026-04-29T10:05:00+00:00",
|
||||
"durationSeconds": 300,
|
||||
"recordingUrl": "https://example.test/recording.mp3",
|
||||
"transcriptId": null,
|
||||
"transcriptText": "Client asked for a Sunday visit.",
|
||||
"rawPayload": {"provider": "waha"},
|
||||
"createdAt": "2026-04-29T10:05:01+00:00"
|
||||
}
|
||||
],
|
||||
"thread": {
|
||||
"threadId": "thread-1",
|
||||
"provider": "waha",
|
||||
"personId": "person-1",
|
||||
"phoneE164": "+910000000000",
|
||||
"displayName": "Amina",
|
||||
"channel": "whatsapp",
|
||||
"status": "open",
|
||||
"unreadCount": 0,
|
||||
"lastMessageAt": "2026-04-29T10:02:00+00:00",
|
||||
"updatedAt": "2026-04-29T10:02:00+00:00",
|
||||
"lastMessagePreview": "Done",
|
||||
"crmPerson": {
|
||||
"id": "person-1",
|
||||
"fullName": "Amina",
|
||||
"primaryPhone": "+910000000000",
|
||||
"primaryEmail": null,
|
||||
"buyerType": "investor",
|
||||
"leadStatus": "new",
|
||||
"projectName": "Tower"
|
||||
}
|
||||
}
|
||||
}
|
||||
"""#.utf8
|
||||
)
|
||||
|
||||
let detail = try JSONDecoder().decode(VelocityCommsThreadCallsDTO.self, from: payload)
|
||||
|
||||
XCTAssertEqual(detail.calls.count, 1)
|
||||
XCTAssertEqual(detail.calls.first?.durationSeconds, 300)
|
||||
XCTAssertEqual(detail.calls.first?.transcriptText, "Client asked for a Sunday visit.")
|
||||
XCTAssertTrue(detail.thread.isLinkedToCanonicalPerson)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user