@nbt-dev/nbt 0.0.1 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +177 -21
- package/README.md +16 -13
- package/TRADEMARKS.md +49 -0
- package/dist/nbt.js +398 -19
- package/package.json +5 -4
- package/stdlib/auth/README.md +83 -0
- package/stdlib/auth/migrations/20260424144652_initial/migration.nbt +48 -0
- package/stdlib/auth/migrations/20260424144652_initial/schema_snapshot.nbt +58 -0
- package/stdlib/auth/migrations/20260521191014_update_user/migration.nbt +3 -0
- package/stdlib/auth/migrations/20260521191014_update_user/schema_snapshot.nbt +59 -0
- package/stdlib/auth/schema.nbt +142 -0
- package/stdlib/calendar/adapters/gohighlevel/tests/fixtures/v2_calendar_pilot.json +12 -0
- package/stdlib/calendar/adapters/gohighlevel/tests/fixtures/webhooks/appointment_changed.json +70 -0
- package/stdlib/calendar/adapters/gohighlevel/tests/fixtures/webhooks/appointment_created.json +72 -0
- package/stdlib/calendar/migrations/20260501210107_initial/migration.nbt +60 -0
- package/stdlib/calendar/migrations/20260501210107_initial/schema_snapshot.nbt +66 -0
- package/stdlib/calendar/migrations/20260513151050_schema_update/migration.nbt +17 -0
- package/stdlib/calendar/migrations/20260513151050_schema_update/schema_snapshot.nbt +83 -0
- package/stdlib/calendar/schema.nbt +86 -0
- package/stdlib/chat/migrations/20260429222411_initial/migration.nbt +59 -0
- package/stdlib/chat/migrations/20260429222411_initial/schema_snapshot.nbt +71 -0
- package/stdlib/chat/migrations/20260430185225_add_messagereaction/migration.nbt +9 -0
- package/stdlib/chat/migrations/20260430185225_add_messagereaction/schema_snapshot.nbt +78 -0
- package/stdlib/chat/migrations/20260518191152_update_message/migration.nbt +3 -0
- package/stdlib/chat/migrations/20260518191152_update_message/schema_snapshot.nbt +81 -0
- package/stdlib/chat/schema.nbt +130 -0
- package/stdlib/crm/adapters/gohighlevel/README.md +85 -0
- package/stdlib/crm/adapters/gohighlevel/tests/README.md +159 -0
- package/stdlib/crm/adapters/gohighlevel/tests/fixtures/webhooks/contact_138fields.json +222 -0
- package/stdlib/crm/adapters/gohighlevel/tests/fixtures/webhooks/contact_140fields.json +219 -0
- package/stdlib/crm/adapters/gohighlevel/tests/fixtures/webhooks/contact_alt.json +212 -0
- package/stdlib/crm/adapters/gohighlevel/tests/fixtures/webhooks/contact_changed.json +102 -0
- package/stdlib/crm/adapters/gohighlevel/tests/fixtures/webhooks/contact_created.json +95 -0
- package/stdlib/crm/adapters/gohighlevel/tests/fixtures/webhooks/contact_full.json +213 -0
- package/stdlib/crm/adapters/gohighlevel/tests/fixtures/webhooks/contact_sparse.json +161 -0
- package/stdlib/crm/adapters/gohighlevel/tests/fixtures/webhooks/contact_update_a.json +197 -0
- package/stdlib/crm/adapters/gohighlevel/tests/fixtures/webhooks/contact_update_b.json +197 -0
- package/stdlib/crm/adapters/gohighlevel/tests/fixtures/webhooks/opportunity_changed.json +85 -0
- package/stdlib/crm/adapters/gohighlevel/tests/fixtures/webhooks/opportunity_created.json +85 -0
- package/stdlib/crm/adapters/gohighlevel/tests/fixtures/webhooks/v2_contact_pilot.json +43 -0
- package/stdlib/crm/adapters/gohighlevel/tests/fixtures/webhooks/v2_contact_with_price_closed.json +7 -0
- package/stdlib/crm/adapters/gohighlevel/tests/fixtures/webhooks/v2_contact_with_price_open.json +7 -0
- package/stdlib/crm/adapters/gohighlevel/tests/fixtures/webhooks/v2_event_appointment_delete.json +1 -0
- package/stdlib/crm/adapters/gohighlevel/tests/fixtures/webhooks/v2_event_calendar_update.json +1 -0
- package/stdlib/crm/adapters/gohighlevel/tests/fixtures/webhooks/v2_event_contact_create.json +1 -0
- package/stdlib/crm/adapters/gohighlevel/tests/fixtures/webhooks/v2_event_opp_status_update.json +1 -0
- package/stdlib/crm/adapters/gohighlevel/tests/fixtures/webhooks/v2_opportunity_pilot.json +16 -0
- package/stdlib/crm/adapters/gohighlevel/tests/fixtures/webhooks/v2_pipelines_pilot.json +137 -0
- package/stdlib/crm/migrations/20260501210107_initial/migration.nbt +63 -0
- package/stdlib/crm/migrations/20260501210107_initial/schema_snapshot.nbt +73 -0
- package/stdlib/crm/migrations/20260513151050_schema_update/migration.nbt +13 -0
- package/stdlib/crm/migrations/20260513151050_schema_update/schema_snapshot.nbt +86 -0
- package/stdlib/crm/schema.nbt +148 -0
- package/stdlib/design/migrations/20260501210107_initial/migration.nbt +19 -0
- package/stdlib/design/migrations/20260501210107_initial/schema_snapshot.nbt +21 -0
- package/stdlib/design/migrations/20260610130000_design_system/migration.nbt +50 -0
- package/stdlib/design/migrations/20260610130000_design_system/schema_snapshot.nbt +80 -0
- package/stdlib/design/schema.nbt +140 -0
- package/stdlib/dns/migrations/20260501210107_initial/migration.nbt +32 -0
- package/stdlib/dns/migrations/20260501210107_initial/schema_snapshot.nbt +36 -0
- package/stdlib/dns/schema.nbt +68 -0
- package/stdlib/email/migrations/20260427235207_initial/migration.nbt +75 -0
- package/stdlib/email/migrations/20260427235207_initial/schema_snapshot.nbt +87 -0
- package/stdlib/email/schema.nbt +145 -0
- package/stdlib/ingest/README.md +29 -0
- package/stdlib/ingest/migrations/20260424144652_initial/migration.nbt +18 -0
- package/stdlib/ingest/migrations/20260424144652_initial/schema_snapshot.nbt +20 -0
- package/stdlib/ingest/migrations/20260429203747_schema_update/migration.nbt +3 -0
- package/stdlib/ingest/migrations/20260429203747_schema_update/schema_snapshot.nbt +21 -0
- package/stdlib/ingest/schema.nbt +37 -0
- package/stdlib/notifications/README.md +118 -0
- package/stdlib/notifications/migrations/20260430204408_initial/migration.nbt +42 -0
- package/stdlib/notifications/migrations/20260430204408_initial/schema_snapshot.nbt +46 -0
- package/stdlib/notifications/schema.nbt +67 -0
- package/stdlib/phone/migrations/20260605205722_initial/migration.nbt +50 -0
- package/stdlib/phone/migrations/20260605205722_initial/schema_snapshot.nbt +56 -0
- package/stdlib/phone/schema.nbt +95 -0
- package/stdlib/registry/migrations/20260602181932_initial/migration.nbt +8 -0
- package/stdlib/registry/migrations/20260602181932_initial/schema_snapshot.nbt +8 -0
- package/stdlib/registry/schema.nbt +20 -0
- package/stdlib/workflows/schema.nbt +44 -0
- package/vendor/linux-x64/cartridges/auth/migrations/20260424144652_initial/migration.nbt +48 -0
- package/vendor/linux-x64/cartridges/auth/migrations/20260424144652_initial/schema_snapshot.nbt +58 -0
- package/vendor/linux-x64/cartridges/auth/migrations/20260521191014_update_user/migration.nbt +3 -0
- package/vendor/linux-x64/cartridges/auth/migrations/20260521191014_update_user/schema_snapshot.nbt +59 -0
- package/vendor/linux-x64/cartridges/auth/schema.nbt +142 -0
- package/vendor/linux-x64/cartridges/calendar/migrations/20260501210107_initial/migration.nbt +60 -0
- package/vendor/linux-x64/cartridges/calendar/migrations/20260501210107_initial/schema_snapshot.nbt +66 -0
- package/vendor/linux-x64/cartridges/calendar/migrations/20260513151050_schema_update/migration.nbt +17 -0
- package/vendor/linux-x64/cartridges/calendar/migrations/20260513151050_schema_update/schema_snapshot.nbt +83 -0
- package/vendor/linux-x64/cartridges/calendar/schema.nbt +86 -0
- package/vendor/linux-x64/cartridges/chat/migrations/20260429222411_initial/migration.nbt +59 -0
- package/vendor/linux-x64/cartridges/chat/migrations/20260429222411_initial/schema_snapshot.nbt +71 -0
- package/vendor/linux-x64/cartridges/chat/migrations/20260430185225_add_messagereaction/migration.nbt +9 -0
- package/vendor/linux-x64/cartridges/chat/migrations/20260430185225_add_messagereaction/schema_snapshot.nbt +78 -0
- package/vendor/linux-x64/cartridges/chat/migrations/20260518191152_update_message/migration.nbt +3 -0
- package/vendor/linux-x64/cartridges/chat/migrations/20260518191152_update_message/schema_snapshot.nbt +81 -0
- package/vendor/linux-x64/cartridges/chat/schema.nbt +130 -0
- package/vendor/linux-x64/cartridges/crm/migrations/20260501210107_initial/migration.nbt +63 -0
- package/vendor/linux-x64/cartridges/crm/migrations/20260501210107_initial/schema_snapshot.nbt +73 -0
- package/vendor/linux-x64/cartridges/crm/migrations/20260513151050_schema_update/migration.nbt +13 -0
- package/vendor/linux-x64/cartridges/crm/migrations/20260513151050_schema_update/schema_snapshot.nbt +86 -0
- package/vendor/linux-x64/cartridges/crm/schema.nbt +148 -0
- package/vendor/linux-x64/cartridges/design/migrations/20260501210107_initial/migration.nbt +19 -0
- package/vendor/linux-x64/cartridges/design/migrations/20260501210107_initial/schema_snapshot.nbt +21 -0
- package/vendor/linux-x64/cartridges/design/migrations/20260610130000_design_system/migration.nbt +50 -0
- package/vendor/linux-x64/cartridges/design/migrations/20260610130000_design_system/schema_snapshot.nbt +80 -0
- package/vendor/linux-x64/cartridges/design/schema.nbt +140 -0
- package/vendor/linux-x64/cartridges/dns/migrations/20260501210107_initial/migration.nbt +32 -0
- package/vendor/linux-x64/cartridges/dns/migrations/20260501210107_initial/schema_snapshot.nbt +36 -0
- package/vendor/linux-x64/cartridges/dns/schema.nbt +68 -0
- package/vendor/linux-x64/cartridges/email/migrations/20260427235207_initial/migration.nbt +75 -0
- package/vendor/linux-x64/cartridges/email/migrations/20260427235207_initial/schema_snapshot.nbt +87 -0
- package/vendor/linux-x64/cartridges/email/schema.nbt +145 -0
- package/vendor/linux-x64/cartridges/ingest/migrations/20260424144652_initial/migration.nbt +18 -0
- package/vendor/linux-x64/cartridges/ingest/migrations/20260424144652_initial/schema_snapshot.nbt +20 -0
- package/vendor/linux-x64/cartridges/ingest/migrations/20260429203747_schema_update/migration.nbt +3 -0
- package/vendor/linux-x64/cartridges/ingest/migrations/20260429203747_schema_update/schema_snapshot.nbt +21 -0
- package/vendor/linux-x64/cartridges/ingest/schema.nbt +37 -0
- package/vendor/linux-x64/cartridges/notifications/migrations/20260430204408_initial/migration.nbt +42 -0
- package/vendor/linux-x64/cartridges/notifications/migrations/20260430204408_initial/schema_snapshot.nbt +46 -0
- package/vendor/linux-x64/cartridges/notifications/schema.nbt +67 -0
- package/vendor/linux-x64/cartridges/phone/migrations/20260605205722_initial/migration.nbt +50 -0
- package/vendor/linux-x64/cartridges/phone/migrations/20260605205722_initial/schema_snapshot.nbt +56 -0
- package/vendor/linux-x64/cartridges/phone/schema.nbt +95 -0
- package/vendor/linux-x64/cartridges/registry/migrations/20260602181932_initial/migration.nbt +8 -0
- package/vendor/linux-x64/cartridges/registry/migrations/20260602181932_initial/schema_snapshot.nbt +8 -0
- package/vendor/linux-x64/cartridges/registry/schema.nbt +20 -0
- package/vendor/linux-x64/cartridges/workflows/schema.nbt +44 -0
- package/vendor/linux-x64/console +0 -0
- package/vendor/linux-x64/nbt +0 -0
- package/contracts/audit/.dist/contract.json +0 -56
- package/contracts/auth/.dist/contract.json +0 -252
- package/contracts/calendar/.dist/contract.json +0 -141
- package/contracts/chat/.dist/contract.json +0 -229
- package/contracts/crm/.dist/contract.json +0 -239
- package/contracts/design/.dist/contract.json +0 -85
- package/contracts/dns/.dist/contract.json +0 -123
- package/contracts/email/.dist/contract.json +0 -267
- package/contracts/embed/.dist/contract.json +0 -137
- package/contracts/ingest/.dist/contract.json +0 -86
- package/contracts/notifications/.dist/contract.json +0 -133
- package/contracts/phone/.dist/contract.json +0 -168
- package/contracts/registry/.dist/contract.json +0 -49
- package/contracts/workflows/.dist/contract.json +0 -106
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
migration initial {
|
|
2
|
+
add_entity Channel
|
|
3
|
+
add_field Channel name string default("")
|
|
4
|
+
add_field Channel description string default("")
|
|
5
|
+
add_field Channel creatorId string default("")
|
|
6
|
+
add_field Channel isPrivate bool default(false)
|
|
7
|
+
add_field Channel isArchived bool default(false)
|
|
8
|
+
add_field Channel lastActivityAt DateTime default(0)
|
|
9
|
+
add_unique Channel [name]
|
|
10
|
+
add_entity ChannelMember
|
|
11
|
+
add_field ChannelMember channelId string default("")
|
|
12
|
+
add_field ChannelMember userId string default("")
|
|
13
|
+
add_field ChannelMember joinedAt DateTime default(0)
|
|
14
|
+
add_index ChannelMember [userId]
|
|
15
|
+
add_index ChannelMember [channelId]
|
|
16
|
+
add_unique ChannelMember [channelId, userId]
|
|
17
|
+
add_entity DirectMessageRoom
|
|
18
|
+
add_field DirectMessageRoom participantA string default("")
|
|
19
|
+
add_field DirectMessageRoom participantAType string default("USER")
|
|
20
|
+
add_field DirectMessageRoom participantB string default("")
|
|
21
|
+
add_field DirectMessageRoom participantBType string default("USER")
|
|
22
|
+
add_field DirectMessageRoom lastActivityAt DateTime default(0)
|
|
23
|
+
add_index DirectMessageRoom [participantA]
|
|
24
|
+
add_index DirectMessageRoom [participantB]
|
|
25
|
+
add_unique DirectMessageRoom [participantA, participantB]
|
|
26
|
+
add_entity Message
|
|
27
|
+
add_field Message channelId string default("")
|
|
28
|
+
add_field Message dmRoomId string default("")
|
|
29
|
+
add_field Message parentMessageId string default("")
|
|
30
|
+
add_field Message senderType string default("")
|
|
31
|
+
add_field Message senderId string default("")
|
|
32
|
+
add_field Message content string default("")
|
|
33
|
+
add_field Message editedAt DateTime default(0)
|
|
34
|
+
add_field Message deletedAt DateTime default(0)
|
|
35
|
+
add_field Message replyCount u32 default(0)
|
|
36
|
+
add_index Message [channelId]
|
|
37
|
+
add_index Message [dmRoomId]
|
|
38
|
+
add_index Message [parentMessageId]
|
|
39
|
+
add_index Message [senderId]
|
|
40
|
+
add_entity ChatReadState
|
|
41
|
+
add_field ChatReadState userId string default("")
|
|
42
|
+
add_field ChatReadState scopeKey string default("")
|
|
43
|
+
add_field ChatReadState scopeType string default("")
|
|
44
|
+
add_field ChatReadState scopeId string default("")
|
|
45
|
+
add_field ChatReadState lastReadMessageId string default("")
|
|
46
|
+
add_field ChatReadState lastReadAt DateTime default(0)
|
|
47
|
+
add_index ChatReadState [userId]
|
|
48
|
+
add_index ChatReadState [scopeKey]
|
|
49
|
+
add_unique ChatReadState [userId, scopeKey]
|
|
50
|
+
add_entity User
|
|
51
|
+
add_field User name string default("")
|
|
52
|
+
add_field User username string default("")
|
|
53
|
+
add_field User email string default("")
|
|
54
|
+
add_field User emailVerified bool default(false)
|
|
55
|
+
add_field User externalId string default("")
|
|
56
|
+
add_field User capsVersion u32 default(0)
|
|
57
|
+
add_index User [email]
|
|
58
|
+
add_index User [externalId]
|
|
59
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
entity Channel {
|
|
2
|
+
name: string
|
|
3
|
+
description?: string
|
|
4
|
+
creatorId: string
|
|
5
|
+
isPrivate: bool
|
|
6
|
+
isArchived: bool
|
|
7
|
+
lastActivityAt?: DateTime
|
|
8
|
+
@@unique([name])
|
|
9
|
+
@@collaborative([])
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
entity ChannelMember {
|
|
13
|
+
channelId: string
|
|
14
|
+
userId: string
|
|
15
|
+
joinedAt: DateTime
|
|
16
|
+
@@unique([channelId, userId])
|
|
17
|
+
@@index([userId])
|
|
18
|
+
@@index([channelId])
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
entity DirectMessageRoom {
|
|
22
|
+
participantA: string
|
|
23
|
+
participantAType: string
|
|
24
|
+
participantB: string
|
|
25
|
+
participantBType: string
|
|
26
|
+
lastActivityAt?: DateTime
|
|
27
|
+
@@unique([participantA, participantB])
|
|
28
|
+
@@index([participantA])
|
|
29
|
+
@@index([participantB])
|
|
30
|
+
@@collaborative([])
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
entity Message {
|
|
34
|
+
channelId?: string
|
|
35
|
+
dmRoomId?: string
|
|
36
|
+
parentMessageId?: string
|
|
37
|
+
senderType: string
|
|
38
|
+
senderId: string
|
|
39
|
+
content: string
|
|
40
|
+
editedAt?: DateTime
|
|
41
|
+
deletedAt?: DateTime
|
|
42
|
+
replyCount: u32
|
|
43
|
+
@@index([channelId])
|
|
44
|
+
@@index([dmRoomId])
|
|
45
|
+
@@index([parentMessageId])
|
|
46
|
+
@@index([senderId])
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
entity ChatReadState {
|
|
50
|
+
userId: string
|
|
51
|
+
scopeKey: string
|
|
52
|
+
scopeType: string
|
|
53
|
+
scopeId: string
|
|
54
|
+
lastReadMessageId: string
|
|
55
|
+
lastReadAt: DateTime
|
|
56
|
+
@@unique([userId, scopeKey])
|
|
57
|
+
@@index([userId])
|
|
58
|
+
@@index([scopeKey])
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
entity User {
|
|
62
|
+
name: string
|
|
63
|
+
username?: string
|
|
64
|
+
email?: string
|
|
65
|
+
emailVerified: bool
|
|
66
|
+
externalId?: string
|
|
67
|
+
capsVersion: u32
|
|
68
|
+
@@index([email])
|
|
69
|
+
@@index([externalId])
|
|
70
|
+
}
|
|
71
|
+
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
migration add_messagereaction {
|
|
2
|
+
add_entity MessageReaction
|
|
3
|
+
add_field MessageReaction messageId string default("")
|
|
4
|
+
add_field MessageReaction emoji string default("")
|
|
5
|
+
add_field MessageReaction userId string default("")
|
|
6
|
+
add_index MessageReaction [messageId]
|
|
7
|
+
add_index MessageReaction [userId]
|
|
8
|
+
add_unique MessageReaction [messageId, emoji, userId]
|
|
9
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
entity Channel {
|
|
2
|
+
name: string
|
|
3
|
+
description?: string
|
|
4
|
+
creatorId: string
|
|
5
|
+
isPrivate: bool
|
|
6
|
+
isArchived: bool
|
|
7
|
+
lastActivityAt?: DateTime
|
|
8
|
+
@@unique([name])
|
|
9
|
+
@@collaborative([])
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
entity ChannelMember {
|
|
13
|
+
channelId: string
|
|
14
|
+
userId: string
|
|
15
|
+
joinedAt: DateTime
|
|
16
|
+
@@unique([channelId, userId])
|
|
17
|
+
@@index([userId])
|
|
18
|
+
@@index([channelId])
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
entity DirectMessageRoom {
|
|
22
|
+
participantA: string
|
|
23
|
+
participantB: string
|
|
24
|
+
lastActivityAt?: DateTime
|
|
25
|
+
@@unique([participantA, participantB])
|
|
26
|
+
@@index([participantA])
|
|
27
|
+
@@index([participantB])
|
|
28
|
+
@@collaborative([])
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
entity Message {
|
|
32
|
+
channelId?: string
|
|
33
|
+
dmRoomId?: string
|
|
34
|
+
parentMessageId?: string
|
|
35
|
+
senderType: string
|
|
36
|
+
senderId: string
|
|
37
|
+
content: string
|
|
38
|
+
editedAt?: DateTime
|
|
39
|
+
deletedAt?: DateTime
|
|
40
|
+
replyCount: u32
|
|
41
|
+
@@index([channelId])
|
|
42
|
+
@@index([dmRoomId])
|
|
43
|
+
@@index([parentMessageId])
|
|
44
|
+
@@index([senderId])
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
entity MessageReaction {
|
|
48
|
+
messageId: string
|
|
49
|
+
emoji: string
|
|
50
|
+
userId: string
|
|
51
|
+
@@unique([messageId, emoji, userId])
|
|
52
|
+
@@index([messageId])
|
|
53
|
+
@@index([userId])
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
entity ChatReadState {
|
|
57
|
+
userId: string
|
|
58
|
+
scopeKey: string
|
|
59
|
+
scopeType: string
|
|
60
|
+
scopeId: string
|
|
61
|
+
lastReadMessageId: string
|
|
62
|
+
lastReadAt: DateTime
|
|
63
|
+
@@unique([userId, scopeKey])
|
|
64
|
+
@@index([userId])
|
|
65
|
+
@@index([scopeKey])
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
entity User {
|
|
69
|
+
name: string
|
|
70
|
+
username?: string
|
|
71
|
+
email?: string
|
|
72
|
+
emailVerified: bool
|
|
73
|
+
externalId?: string
|
|
74
|
+
capsVersion: u32
|
|
75
|
+
@@index([email])
|
|
76
|
+
@@index([externalId])
|
|
77
|
+
}
|
|
78
|
+
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
entity Channel {
|
|
2
|
+
name: string
|
|
3
|
+
description?: string
|
|
4
|
+
creatorId: string
|
|
5
|
+
isPrivate: bool
|
|
6
|
+
isArchived: bool
|
|
7
|
+
lastActivityAt?: DateTime
|
|
8
|
+
@@unique([name])
|
|
9
|
+
@@collaborative([])
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
entity ChannelMember {
|
|
13
|
+
channelId: string
|
|
14
|
+
userId: string
|
|
15
|
+
joinedAt: DateTime
|
|
16
|
+
@@unique([channelId, userId])
|
|
17
|
+
@@index([userId])
|
|
18
|
+
@@index([channelId])
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
entity DirectMessageRoom {
|
|
22
|
+
participantA: string
|
|
23
|
+
participantAType: string
|
|
24
|
+
participantB: string
|
|
25
|
+
participantBType: string
|
|
26
|
+
lastActivityAt?: DateTime
|
|
27
|
+
@@unique([participantA, participantB])
|
|
28
|
+
@@index([participantA])
|
|
29
|
+
@@index([participantB])
|
|
30
|
+
@@collaborative([])
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
entity Message {
|
|
34
|
+
channelId?: string
|
|
35
|
+
dmRoomId?: string
|
|
36
|
+
parentMessageId?: string
|
|
37
|
+
senderType: string
|
|
38
|
+
senderId: string
|
|
39
|
+
content: string
|
|
40
|
+
editedAt?: DateTime
|
|
41
|
+
deletedAt?: DateTime
|
|
42
|
+
replyCount: u32
|
|
43
|
+
@@index([channelId])
|
|
44
|
+
@@index([dmRoomId])
|
|
45
|
+
@@index([parentMessageId])
|
|
46
|
+
@@index([senderId])
|
|
47
|
+
@@search([content])
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
entity MessageReaction {
|
|
51
|
+
messageId: string
|
|
52
|
+
emoji: string
|
|
53
|
+
userId: string
|
|
54
|
+
@@unique([messageId, emoji, userId])
|
|
55
|
+
@@index([messageId])
|
|
56
|
+
@@index([userId])
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
entity ChatReadState {
|
|
60
|
+
userId: string
|
|
61
|
+
scopeKey: string
|
|
62
|
+
scopeType: string
|
|
63
|
+
scopeId: string
|
|
64
|
+
lastReadMessageId: string
|
|
65
|
+
lastReadAt: DateTime
|
|
66
|
+
@@unique([userId, scopeKey])
|
|
67
|
+
@@index([userId])
|
|
68
|
+
@@index([scopeKey])
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
entity User {
|
|
72
|
+
name: string
|
|
73
|
+
username?: string
|
|
74
|
+
email?: string
|
|
75
|
+
emailVerified: bool
|
|
76
|
+
externalId?: string
|
|
77
|
+
capsVersion: u32
|
|
78
|
+
@@index([email])
|
|
79
|
+
@@index([externalId])
|
|
80
|
+
}
|
|
81
|
+
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Chat cart — channels, DMs, threaded messages.
|
|
2
|
+
|
|
3
|
+
import {User} from "auth";
|
|
4
|
+
|
|
5
|
+
entity Channel {
|
|
6
|
+
id: ulid
|
|
7
|
+
createdAt: DateTime @default(now())
|
|
8
|
+
updatedAt: DateTime @updatedAt
|
|
9
|
+
name: string
|
|
10
|
+
description?: string
|
|
11
|
+
creatorId: string
|
|
12
|
+
isPrivate: bool
|
|
13
|
+
isArchived: bool
|
|
14
|
+
lastActivityAt?: DateTime
|
|
15
|
+
|
|
16
|
+
@@unique([name])
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
# Membership table for private channels. Public channels skip this — any
|
|
20
|
+
# authed user can post. Membership = read+post; admin gates write metadata.
|
|
21
|
+
entity ChannelMember {
|
|
22
|
+
id: ulid
|
|
23
|
+
createdAt: DateTime @default(now())
|
|
24
|
+
updatedAt: DateTime @updatedAt
|
|
25
|
+
channelId: string
|
|
26
|
+
userId: string
|
|
27
|
+
joinedAt: DateTime @default(now())
|
|
28
|
+
|
|
29
|
+
@@unique([channelId, userId])
|
|
30
|
+
@@index([userId])
|
|
31
|
+
@@index([channelId])
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# 1:1 DM room. Pair canonicalized in handler: participantA = lexicographic
|
|
35
|
+
# min(ULID), participantB = max. Lookups normalize both args before hitting
|
|
36
|
+
# the unique index.
|
|
37
|
+
#
|
|
38
|
+
# participantAType / participantBType ∈ "USER" | "AGENT". Required so DM
|
|
39
|
+
# triggers (`on dm(<agent>)`) can identify the peer at message send time
|
|
40
|
+
# without a cross-cart lookup. Existing rows default to USER/USER.
|
|
41
|
+
entity DirectMessageRoom {
|
|
42
|
+
id: ulid
|
|
43
|
+
createdAt: DateTime @default(now())
|
|
44
|
+
updatedAt: DateTime @updatedAt
|
|
45
|
+
participantA: string
|
|
46
|
+
participantAType: string
|
|
47
|
+
participantB: string
|
|
48
|
+
participantBType: string
|
|
49
|
+
lastActivityAt?: DateTime
|
|
50
|
+
|
|
51
|
+
@@unique([participantA, participantB])
|
|
52
|
+
@@index([participantA])
|
|
53
|
+
@@index([participantB])
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
# Single Message entity for both channel + DM scopes. Exactly one of
|
|
57
|
+
# channelId / dmRoomId is non-empty per row. parentMessageId set => thread
|
|
58
|
+
# reply (Google-Chat-style inline replies; only one level deep — replies
|
|
59
|
+
# of replies coalesce to the top-level parent).
|
|
60
|
+
#
|
|
61
|
+
# senderType ∈ "USER" | "AGENT" | "SYSTEM". senderId points at the
|
|
62
|
+
# corresponding entity in the appropriate cart (User/Agent/System). UI
|
|
63
|
+
# renders a bot badge for non-USER senders. No fake User rows for bots.
|
|
64
|
+
entity Message {
|
|
65
|
+
id: ulid
|
|
66
|
+
createdAt: DateTime @default(now())
|
|
67
|
+
updatedAt: DateTime @updatedAt
|
|
68
|
+
channelId?: string
|
|
69
|
+
dmRoomId?: string
|
|
70
|
+
parentMessageId?: string
|
|
71
|
+
|
|
72
|
+
senderType: string
|
|
73
|
+
senderId: string
|
|
74
|
+
|
|
75
|
+
content: string
|
|
76
|
+
|
|
77
|
+
editedAt?: DateTime
|
|
78
|
+
deletedAt?: DateTime
|
|
79
|
+
|
|
80
|
+
# Denormalized — bumped by send_message handler when a reply is created.
|
|
81
|
+
# Keeps the parent row paginatable without a per-message COUNT scan.
|
|
82
|
+
replyCount: u32
|
|
83
|
+
|
|
84
|
+
@@index([channelId])
|
|
85
|
+
@@index([dmRoomId])
|
|
86
|
+
@@index([parentMessageId])
|
|
87
|
+
@@index([senderId])
|
|
88
|
+
@@search([content])
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# Per-user emoji reaction on a message. Discord-style counts are computed from
|
|
92
|
+
# these rows at read time; one user can react once per emoji per message.
|
|
93
|
+
entity MessageReaction {
|
|
94
|
+
id: ulid
|
|
95
|
+
createdAt: DateTime @default(now())
|
|
96
|
+
updatedAt: DateTime @updatedAt
|
|
97
|
+
messageId: string
|
|
98
|
+
emoji: string
|
|
99
|
+
userId: string
|
|
100
|
+
|
|
101
|
+
@@unique([messageId, emoji, userId])
|
|
102
|
+
@@index([messageId])
|
|
103
|
+
@@index([userId])
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
# Per-(user, scope) read pointer.
|
|
107
|
+
#
|
|
108
|
+
# scopeKey is "channel:<channel_id>" or "dm:<dm_room_id>" — collapses the two
|
|
109
|
+
# parallel scope fields into a single composite-uniqueable key. Two parallel
|
|
110
|
+
# @@unique constraints on nullable cols aren't currently exercised by NBT
|
|
111
|
+
# codegen, so this is the safe shape.
|
|
112
|
+
#
|
|
113
|
+
# lastReadMessageId is a ULID; ULIDs sort lexicographically by creation time
|
|
114
|
+
# so "msg.id > lastReadMessageId" (string compare) cleanly answers "unread?".
|
|
115
|
+
entity ChatReadState {
|
|
116
|
+
id: ulid
|
|
117
|
+
createdAt: DateTime @default(now())
|
|
118
|
+
updatedAt: DateTime @updatedAt
|
|
119
|
+
userId: string
|
|
120
|
+
scopeKey: string
|
|
121
|
+
scopeType: string
|
|
122
|
+
scopeId: string
|
|
123
|
+
|
|
124
|
+
lastReadMessageId: string
|
|
125
|
+
lastReadAt: DateTime @default(now())
|
|
126
|
+
|
|
127
|
+
@@unique([userId, scopeKey])
|
|
128
|
+
@@index([userId])
|
|
129
|
+
@@index([scopeKey])
|
|
130
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# GHL Adapter
|
|
2
|
+
|
|
3
|
+
Webhook ingest → CRM persistence as composable tasks. The adapter classifies
|
|
4
|
+
inbound GHL Workflow webhook payloads and upserts the corresponding Contact
|
|
5
|
+
or Deal directly. Appointment persistence lives in the calendar cart's
|
|
6
|
+
parallel adapter (`cartridges/core/calendar/adapters/gohighlevel/`) because
|
|
7
|
+
Calendar/Appointment are calendar-cart entities.
|
|
8
|
+
|
|
9
|
+
No transformation, normalization, geocoding, AI scoring, or write-back to
|
|
10
|
+
GHL. Those are downstream client-cart concerns. Read-side tasks (backfill,
|
|
11
|
+
sync recovery) come back when there's a real consumer.
|
|
12
|
+
|
|
13
|
+
## Tasks (queue names = `crm.<task>`)
|
|
14
|
+
|
|
15
|
+
| Task | Inputs | Outputs |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| `crm.ghl_classify_event_type` | `body_json` | `event_type`, `tenant_org`, `tenant_project` |
|
|
18
|
+
| `crm.ghl_persist_contact` | `body_json` | `contact_id`, `created`, `ok`, `error` |
|
|
19
|
+
| `crm.ghl_persist_opportunity` | `body_json` | `deal_id`, `created`, `ok`, `error` |
|
|
20
|
+
|
|
21
|
+
Plus, in the calendar cart:
|
|
22
|
+
|
|
23
|
+
| Task | Inputs | Outputs |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| `calendar.ghl_persist_appointment` | `body_json` | `appointment_id`, `calendar_id`, `ok`, `error` |
|
|
26
|
+
|
|
27
|
+
`event_type` outputs from classify: `ContactCreate`, `ContactChange`,
|
|
28
|
+
`OpportunityCreate`, `OpportunityChange`, `AppointmentCreate`,
|
|
29
|
+
`AppointmentChange`, `Unknown`. The 7th GHL workflow "Pipeline Stage Changed
|
|
30
|
+
Sync" aliases onto `OpportunityChange` because the payload shape is
|
|
31
|
+
identical.
|
|
32
|
+
|
|
33
|
+
## Webhook entry
|
|
34
|
+
|
|
35
|
+
GHL POSTs to ingest:
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
POST /api/ingest/endpoint/receive?e=mylocalpro-ghl
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The `Endpoint` row for slug `mylocalpro-ghl` carries `systemId` pointing at
|
|
42
|
+
the per-client System graph. After Payload persistence, ingest fires
|
|
43
|
+
`queue_exec_start("system.<systemId>", { payloadId, endpointSlug, endpointId, body_json })`.
|
|
44
|
+
|
|
45
|
+
The first task in the per-client graph is `crm.ghl_classify_event_type`,
|
|
46
|
+
followed by a `branch` on `event_type` to one of the persist tasks
|
|
47
|
+
(`ghl_persist_contact`, `ghl_persist_opportunity`,
|
|
48
|
+
`calendar.ghl_persist_appointment`).
|
|
49
|
+
|
|
50
|
+
## Files
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
main.nbt # composes the persist tasks
|
|
54
|
+
native/persist.jai # ghl_apply_contact_payload, ghl_apply_opportunity_payload
|
|
55
|
+
tasks/classify_event_type.nbt
|
|
56
|
+
tasks/persist_contact.nbt
|
|
57
|
+
tasks/persist_opportunity.nbt
|
|
58
|
+
tests/fixtures/webhooks/ # 4 real-shape fixtures (contact_*/opportunity_*)
|
|
59
|
+
tests/README.md # fixture provenance + classifier expectations
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Native imports are declared at the cart root (`crm/cartridge.nbt`) because
|
|
63
|
+
the codegen's native-file copy step assumes paths are cart-root-relative.
|
|
64
|
+
|
|
65
|
+
The calendar cart mirrors this layout for appointment persistence:
|
|
66
|
+
`cartridges/core/calendar/adapters/gohighlevel/`.
|
|
67
|
+
|
|
68
|
+
## External-PK = local-PK
|
|
69
|
+
|
|
70
|
+
GHL ids are written into entity `id` directly: a Contact for
|
|
71
|
+
`contact_id="ctc_xyz"` lives at `Contact.id="ctc_xyz"`, an Opportunity for
|
|
72
|
+
`id="opp_001"` lives at `Deal.id="opp_001"`, an Appointment for
|
|
73
|
+
`calendar.appointmentId="apt_xyz"` lives at `Appointment.id="apt_xyz"`. No
|
|
74
|
+
`externalId` indirection — the next webhook update keys the same row.
|
|
75
|
+
|
|
76
|
+
## Adding a persist branch
|
|
77
|
+
|
|
78
|
+
1. Add the helper to `native/persist.jai` (or a calendar/agent/etc. cart's
|
|
79
|
+
`native/persist.jai` if the target entity lives there).
|
|
80
|
+
2. Create `tasks/persist_<name>.nbt` with `input body_json: string` and the
|
|
81
|
+
handler delegating to the helper.
|
|
82
|
+
3. Import the task in `main.nbt`.
|
|
83
|
+
4. Update `tasks/classify_event_type.nbt` if a new `event_type` is needed.
|
|
84
|
+
5. `jai first.jai` — task appears in the cart's `contract.json` and at
|
|
85
|
+
`/_console/task-catalog`.
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# GHL Adapter Tests
|
|
2
|
+
|
|
3
|
+
Two fixture sets, two purposes. Both describe the same GHL tenant
|
|
4
|
+
(`org=mlp, project=default, locationId=loc_test`) so a replay that uses
|
|
5
|
+
either source converges on the same CRM entity graph.
|
|
6
|
+
|
|
7
|
+
| Set | Role | Producer | Consumer | Discriminator |
|
|
8
|
+
| --- | --- | --- | --- | --- |
|
|
9
|
+
| `fixtures/webhooks/*.json` | Inbound delta stream | GHL workflow engine (or v2 native subscription) | `tasks/classify_event_type.nbt` + downstream user-cart systems | `body.workflow.name` (workflow shape) → fall back to `body.type` (v2 native) |
|
|
10
|
+
| `fixtures/api/*.json` | Backfill / sync recovery snapshot | Mocked GHL REST replies (when `GHL_MOCK_DIR` is set, `native/mock.jai` reads from disk instead of curl) | All `tasks/fetch_*` / `tasks/list_*` tasks | URL-derived filename (see "Mock key derivation" below) |
|
|
11
|
+
|
|
12
|
+
The two sets carry **different shapes** because GHL emits different shapes:
|
|
13
|
+
the workflow engine flattens custom fields onto the top-level payload (with
|
|
14
|
+
`customFields[]` redundantly nested), while the REST API returns them under
|
|
15
|
+
a typed envelope (`{contact: {...}}`, `{opportunities: [...], meta: {...}}`,
|
|
16
|
+
etc.). The flattening logic and field classification live in the portal at
|
|
17
|
+
`packages/api/src/services/ghl/field-classification.ts`.
|
|
18
|
+
|
|
19
|
+
## Source of truth for shapes
|
|
20
|
+
|
|
21
|
+
- **Webhook payload shapes** — derived from the portal handler at
|
|
22
|
+
`apps/portal/app/api/v1/[...route]/routes/webhooks/ghl.ts` (the consumer is
|
|
23
|
+
authoritative; GHL workflow webhook payloads are user-configurable and not
|
|
24
|
+
formally schema'd).
|
|
25
|
+
- **API response shapes** — the GHL marketplace API docs are authoritative;
|
|
26
|
+
cross-check each API fixture against the corresponding endpoint page:
|
|
27
|
+
- `GET /contacts/:id` — https://marketplace.gohighlevel.com/docs/ghl/contacts/get-contact
|
|
28
|
+
- `GET /contacts/` — https://marketplace.gohighlevel.com/docs/ghl/contacts/get-contacts
|
|
29
|
+
- `GET /opportunities/:id` — https://marketplace.gohighlevel.com/docs/ghl/opportunities/get-opportunity
|
|
30
|
+
- `GET /opportunities/search` — https://marketplace.gohighlevel.com/docs/ghl/opportunities/search-opportunity
|
|
31
|
+
- `GET /opportunities/pipelines` — https://marketplace.gohighlevel.com/docs/ghl/opportunities/get-pipelines
|
|
32
|
+
- `GET /calendars/` — https://marketplace.gohighlevel.com/docs/ghl/calendars/get-calendars
|
|
33
|
+
- `GET /calendars/events` — https://marketplace.gohighlevel.com/docs/ghl/calendars/get-calendar-events
|
|
34
|
+
- `GET /calendars/blocked-slots` — https://marketplace.gohighlevel.com/docs/ghl/calendars/get-blocked-slots
|
|
35
|
+
- `GET /users/` — https://marketplace.gohighlevel.com/docs/ghl/users/get-user-by-location
|
|
36
|
+
- `GET /locations/:id/customFields` — https://marketplace.gohighlevel.com/docs/ghl/custom-fields/get-custom-fields
|
|
37
|
+
|
|
38
|
+
## Shared CRM graph (referenced by both sets)
|
|
39
|
+
|
|
40
|
+
| Entity | ID | Notes |
|
|
41
|
+
| --- | --- | --- |
|
|
42
|
+
| Contact | `ctc_alice_001` | Alice Anderson; full address + property custom fields. After `contact_changed`, last name → `Anderson-Smith`, address → `456 Updated Ave`, email → `alice.smith@example.com`. |
|
|
43
|
+
| Contact | `ctc_bob_002` | Bob Builder; minimal — exists so `contacts__page1` is non-trivial. |
|
|
44
|
+
| Opportunity | `opp_001` | Alice's 8 kW solar deal. `Created` lands at `pip_solar / ps_qualified` @ $24 500. `Changed` advances to `ps_proposal_sent` @ $28 500 with an `Appointment Outcome=showed`. |
|
|
45
|
+
| Pipeline | `pip_solar` | 5 stages: `ps_lead → ps_qualified → ps_proposal_sent → ps_won / ps_lost`. |
|
|
46
|
+
| Calendar | `cal_solar_consult` | `America/Toronto`, team `usr_001`/`usr_002`. |
|
|
47
|
+
| Appointment | `apt_001` | Alice + cal_solar_consult. `Created` confirms for 2026-05-12 14:00 EDT. `Changed` reschedules to 2026-05-15 10:00 (timezone-naive) and transitions to `showed`. |
|
|
48
|
+
| Users | `usr_001..003` | Bob Brown / Charlie Chen / Diana Davis — match the `Setter`/`Closer` strings in webhook payloads. |
|
|
49
|
+
| Custom fields | `cf_utility`, `cf_avg_bill`, `cf_shade`, `cf_owner` (contact model); `cf_proposal`, `cf_inspection_date` (opportunity model); plus the deal-tracking projection (`cf_setter`, `cf_closer`, `cf_appt_outcome`, `cf_total_contract`, `cf_date_sold`). |
|
|
50
|
+
|
|
51
|
+
## Webhook fixtures (`fixtures/webhooks/`)
|
|
52
|
+
|
|
53
|
+
One fixture per real published GHL Workflow shape. Every payload mirrors
|
|
54
|
+
something GHL actually posts; synthetic probes (unknown event, wrong
|
|
55
|
+
tenant, v2 native subscriptions) have been dropped — they tested code
|
|
56
|
+
paths that don't fire for the real tenant configuration.
|
|
57
|
+
|
|
58
|
+
| File | `workflow.name` | `body.type` | Expected classifier output |
|
|
59
|
+
| --- | --- | --- | --- |
|
|
60
|
+
| `contact_created.json` | `Contact Created Sync` | `ContactCreate` | `event_type=ContactCreate, tenant_org=mlp, tenant_project=default` |
|
|
61
|
+
| `contact_changed.json` | `Contact Changed Sync` | `ContactUpdate` | `event_type=ContactChange` |
|
|
62
|
+
| `opportunity_created.json` | `Opportunity Created Sync` | `OpportunityCreate` | `event_type=OpportunityCreate` |
|
|
63
|
+
| `opportunity_changed.json` | `Opportunity Changed Sync` | `OpportunityUpdate` | `event_type=OpportunityChange` |
|
|
64
|
+
| `appointment_created.json` | `Appointment Created Sync` | `AppointmentCreate` | `event_type=AppointmentCreate` |
|
|
65
|
+
| `appointment_changed.json` | `Appointment Changed Sync` | `AppointmentUpdate` | `event_type=AppointmentChange` |
|
|
66
|
+
|
|
67
|
+
The 7th GHL workflow shown in the published list — **Pipeline Stage Changed
|
|
68
|
+
Sync** — carries the same opportunity payload shape as Opportunity Changed
|
|
69
|
+
Sync. The classifier aliases it onto `event_type=OpportunityChange`, so
|
|
70
|
+
`opportunity_changed.json` covers it (replay with `workflow.name` swapped to
|
|
71
|
+
verify).
|
|
72
|
+
|
|
73
|
+
### Real-payload shape and quirks
|
|
74
|
+
|
|
75
|
+
- **Custom fields are flat top-level keys, not a `customFields[]` array.**
|
|
76
|
+
Real GHL workflow webhooks emit every property field as a top-level body
|
|
77
|
+
key by display name (`"Closer": "..."`, `"Average Electric Bill": "245"`,
|
|
78
|
+
`"Shade?": "minimal"`). The `customFields[]` array shape only appears on
|
|
79
|
+
REST API responses (`fixtures/api/`), not on webhook bodies. Most
|
|
80
|
+
property keys come through as empty strings even when unset.
|
|
81
|
+
- **Cross-domain spillage is normal.** Contact webhooks carry deal-tracking
|
|
82
|
+
fields (`Setter`, `Closer`, `Total Contract Price`); opportunity webhooks
|
|
83
|
+
carry property fields (`Average Electric Bill`, `Shade?`); appointment
|
|
84
|
+
webhooks carry both. Don't assume Contact-side keys imply contact intent.
|
|
85
|
+
- **HVAC twin keys.** Many fields have `(HVAC)` variants
|
|
86
|
+
(`Closer` + `Closer (HVAC)`, `Setter Name` + `Setter Name (HVAC)`,
|
|
87
|
+
`# Touches` + `# Touches HVAC`). Both ship even when only one branch is
|
|
88
|
+
active.
|
|
89
|
+
- **Numeric values can be raw JSON numbers**, not always strings
|
|
90
|
+
(e.g. `"Outbound Dials": 8`, `"# Touches": 3`).
|
|
91
|
+
- `customData` carries `{org, project, type}` — `type` is one of
|
|
92
|
+
`"Contact"` / `"Opportunity"` / `"Appointment"`.
|
|
93
|
+
- Top-level `attributionSource: {}` is typically empty; real attribution
|
|
94
|
+
lives under `contact.attributionSource` and `contact.lastAttributionSource`.
|
|
95
|
+
- `appoinmentStatus` (GHL typo) is the canonical key on appointment
|
|
96
|
+
payloads. `appointment_changed.json` carries both `appoinmentStatus` and
|
|
97
|
+
`appointmentStatus` with conflicting values to exercise typo precedence.
|
|
98
|
+
- `pipleline_stage` (GHL typo) — `opportunity_changed.json` uses it
|
|
99
|
+
instead of `pipeline_stage` to exercise handler/cart fallback
|
|
100
|
+
(ghl.ts:1308, 1313, 1392).
|
|
101
|
+
- Naive vs offset timestamps — `appointment_created.json` uses
|
|
102
|
+
`-04:00`-offset times; `appointment_changed.json` uses naive
|
|
103
|
+
`"2026-05-15T10:00:00"` (no offset) so `parseGhlTime` falls into its
|
|
104
|
+
`fromZonedTime` branch.
|
|
105
|
+
- `tags` on workflow webhooks is a comma-separated CSV string (with
|
|
106
|
+
possible spaces, apostrophes, special chars); v2-native shape is a JSON
|
|
107
|
+
array.
|
|
108
|
+
|
|
109
|
+
API fixtures (`fixtures/api/`) keep the structured `customFields[]` array
|
|
110
|
+
shape — that's the format the GHL REST API actually returns. The webhook
|
|
111
|
+
flattening happens server-side inside GHL's workflow engine.
|
|
112
|
+
|
|
113
|
+
## API fixtures (`fixtures/api/`)
|
|
114
|
+
|
|
115
|
+
Filename is derived from URL path + selected query keys (see `native/mock.jai`).
|
|
116
|
+
|
|
117
|
+
### Mock key derivation
|
|
118
|
+
|
|
119
|
+
1. Strip leading `/`, replace remaining `/` with `_`, drop the query string.
|
|
120
|
+
2. If query has `skip` + `limit`, append `_pageN` where `N = skip/limit + 1`.
|
|
121
|
+
3. If query has `startTime` + `endTime`, append `_window`.
|
|
122
|
+
4. Other query params (e.g. `model=contact|opportunity` on
|
|
123
|
+
`customFields`) **do not** affect the key — a single fixture serves all
|
|
124
|
+
variants of those query shapes.
|
|
125
|
+
|
|
126
|
+
| Fixture | Endpoint | Notes |
|
|
127
|
+
| --- | --- | --- |
|
|
128
|
+
| `users_.json` | `GET /users/` | 3 users matching the `Setter`/`Closer` names in the webhook fixtures. |
|
|
129
|
+
| `opportunities_pipelines.json` | `GET /opportunities/pipelines` | One pipeline (`pip_solar`) with 5 stages. |
|
|
130
|
+
| `locations_loc_test_customFields.json` | `GET /locations/loc_test/customFields` | Union of contact-model and opportunity-model fields (mock dispatcher conflates the `?model=` query). |
|
|
131
|
+
| `contacts__page1.json` | `GET /contacts/?skip=0&limit=100` | Alice + Bob with full scalars, `attributionSource`, `customFields[]`. |
|
|
132
|
+
| `contacts__page2.json` | `GET /contacts/?skip=100&limit=100` | Empty terminator. |
|
|
133
|
+
| `contacts_ctc_alice_001.json` | `GET /contacts/ctc_alice_001` | Single-contact envelope `{contact: {...}}`. |
|
|
134
|
+
| `opportunities_search_page1.json` | `GET /opportunities/search?skip=0&limit=100` | One opp with custom fields and assignment metadata. |
|
|
135
|
+
| `opportunities_search_page2.json` | `GET /opportunities/search?skip=100&limit=100` | Empty terminator. |
|
|
136
|
+
| `opportunities_opp_001.json` | `GET /opportunities/opp_001` | Single-opp envelope `{opportunity: {...}}`. |
|
|
137
|
+
| `calendars_.json` | `GET /calendars/` | One calendar with team members. |
|
|
138
|
+
| `calendars_events_window.json` | `GET /calendars/events?calendarId=…&startTime=…&endTime=…` | One event matching `apt_001`'s **created** state, so an API-backfill replay sees the same starting point as the webhook stream's first appointment fixture. |
|
|
139
|
+
| `calendars_blocked-slots_window.json` | `GET /calendars/blocked-slots?userId=…&startTime=…&endTime=…` | Empty `{slots: []}` so the task has a fixture instead of `mock_missing:`. |
|
|
140
|
+
|
|
141
|
+
## Replay loop (smoke target — harness still pending)
|
|
142
|
+
|
|
143
|
+
```sh
|
|
144
|
+
GHL_MOCK_DIR=$(pwd)/cartridges/core/crm/adapters/gohighlevel/tests/fixtures \
|
|
145
|
+
GHL_PIT_TOKEN=test GHL_LOCATION_ID=loc_test \
|
|
146
|
+
./console &
|
|
147
|
+
for f in cartridges/core/crm/adapters/gohighlevel/tests/fixtures/webhooks/*.json; do
|
|
148
|
+
curl -fsS -X POST -H 'Content-Type: application/json' --data-binary "@$f" \
|
|
149
|
+
"http://localhost:8080/api/ingest/endpoint/receive?e=mylocalpro-ghl"
|
|
150
|
+
done
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Assertion targets:
|
|
154
|
+
- One `Payload` row per webhook fixture.
|
|
155
|
+
- One `Execution` per `Payload`.
|
|
156
|
+
- Classifier outputs match the table in "Webhook fixtures" above.
|
|
157
|
+
- After running the API-fetch path against the same `GHL_MOCK_DIR`, the
|
|
158
|
+
resulting CRM entity graph is congruent with the webhook-replay state
|
|
159
|
+
(Alice's contact + Alice's opp at `ps_proposal_sent` + `apt_001` showed).
|