@tellescope/sdk 1.253.0 → 1.254.0
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/lib/cjs/sdk.d.ts +4 -0
- package/lib/cjs/sdk.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/_tmp_bc_verify.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/_tmp_blank_status_verify.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/_tmp_followup_verify.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/_tmp_nonmatch_verify.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/beluga_manual_sync.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/beluga_manual_sync.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/beluga_manual_sync.test.js +256 -0
- package/lib/cjs/tests/api_tests/beluga_manual_sync.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/field_redaction.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/field_redaction.test.js +112 -99
- package/lib/cjs/tests/api_tests/field_redaction.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/gcal_sync_retry.test.d.ts +43 -0
- package/lib/cjs/tests/api_tests/gcal_sync_retry.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/gcal_sync_retry.test.js +168 -0
- package/lib/cjs/tests/api_tests/gcal_sync_retry.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/journey_error_branching.test.js +7 -2
- package/lib/cjs/tests/api_tests/journey_error_branching.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/resource_access_tags.test.d.ts +22 -0
- package/lib/cjs/tests/api_tests/resource_access_tags.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/resource_access_tags.test.js +488 -0
- package/lib/cjs/tests/api_tests/resource_access_tags.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/F-0106-F-0110-enduser-write-restrictions.test.d.ts +23 -0
- package/lib/cjs/tests/api_tests/security/F-0106-F-0110-enduser-write-restrictions.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/F-0106-F-0110-enduser-write-restrictions.test.js +325 -0
- package/lib/cjs/tests/api_tests/security/F-0106-F-0110-enduser-write-restrictions.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/time_tracks.test.d.ts +8 -0
- package/lib/cjs/tests/api_tests/time_tracks.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/time_tracks.test.js +638 -1
- package/lib/cjs/tests/api_tests/time_tracks.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/user_portal_settings.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/user_portal_settings.test.js +104 -28
- package/lib/cjs/tests/api_tests/user_portal_settings.test.js.map +1 -1
- package/lib/cjs/tests/tests.d.ts.map +1 -1
- package/lib/cjs/tests/tests.js +878 -200
- package/lib/cjs/tests/tests.js.map +1 -1
- package/lib/esm/sdk.d.ts +4 -0
- package/lib/esm/sdk.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/_tmp_bc_verify.test.d.ts +2 -0
- package/lib/esm/tests/api_tests/_tmp_bc_verify.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/_tmp_bc_verify.test.js +120 -0
- package/lib/esm/tests/api_tests/_tmp_bc_verify.test.js.map +1 -0
- package/lib/esm/tests/api_tests/_tmp_blank_status_verify.test.d.ts +2 -0
- package/lib/esm/tests/api_tests/_tmp_blank_status_verify.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/_tmp_blank_status_verify.test.js +200 -0
- package/lib/esm/tests/api_tests/_tmp_blank_status_verify.test.js.map +1 -0
- package/lib/esm/tests/api_tests/_tmp_followup_verify.test.d.ts +2 -0
- package/lib/esm/tests/api_tests/_tmp_followup_verify.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/_tmp_followup_verify.test.js +194 -0
- package/lib/esm/tests/api_tests/_tmp_followup_verify.test.js.map +1 -0
- package/lib/esm/tests/api_tests/_tmp_nonmatch_verify.test.d.ts +2 -0
- package/lib/esm/tests/api_tests/_tmp_nonmatch_verify.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/_tmp_nonmatch_verify.test.js +115 -0
- package/lib/esm/tests/api_tests/_tmp_nonmatch_verify.test.js.map +1 -0
- package/lib/esm/tests/api_tests/beluga_manual_sync.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/beluga_manual_sync.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/beluga_manual_sync.test.js +252 -0
- package/lib/esm/tests/api_tests/beluga_manual_sync.test.js.map +1 -0
- package/lib/esm/tests/api_tests/field_redaction.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/field_redaction.test.js +112 -99
- package/lib/esm/tests/api_tests/field_redaction.test.js.map +1 -1
- package/lib/esm/tests/api_tests/gcal_sync_retry.test.d.ts +43 -0
- package/lib/esm/tests/api_tests/gcal_sync_retry.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/gcal_sync_retry.test.js +164 -0
- package/lib/esm/tests/api_tests/gcal_sync_retry.test.js.map +1 -0
- package/lib/esm/tests/api_tests/journey_error_branching.test.js +7 -2
- package/lib/esm/tests/api_tests/journey_error_branching.test.js.map +1 -1
- package/lib/esm/tests/api_tests/resource_access_tags.test.d.ts +22 -0
- package/lib/esm/tests/api_tests/resource_access_tags.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/resource_access_tags.test.js +484 -0
- package/lib/esm/tests/api_tests/resource_access_tags.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/F-0106-F-0110-enduser-write-restrictions.test.d.ts +23 -0
- package/lib/esm/tests/api_tests/security/F-0106-F-0110-enduser-write-restrictions.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/F-0106-F-0110-enduser-write-restrictions.test.js +321 -0
- package/lib/esm/tests/api_tests/security/F-0106-F-0110-enduser-write-restrictions.test.js.map +1 -0
- package/lib/esm/tests/api_tests/time_tracks.test.d.ts +8 -0
- package/lib/esm/tests/api_tests/time_tracks.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/time_tracks.test.js +635 -0
- package/lib/esm/tests/api_tests/time_tracks.test.js.map +1 -1
- package/lib/esm/tests/api_tests/user_portal_settings.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/user_portal_settings.test.js +104 -28
- package/lib/esm/tests/api_tests/user_portal_settings.test.js.map +1 -1
- package/lib/esm/tests/tests.d.ts.map +1 -1
- package/lib/esm/tests/tests.js +879 -201
- package/lib/esm/tests/tests.js.map +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +10 -10
- package/src/tests/api_tests/beluga_manual_sync.test.ts +159 -0
- package/src/tests/api_tests/field_redaction.test.ts +13 -0
- package/src/tests/api_tests/gcal_sync_retry.test.ts +104 -0
- package/src/tests/api_tests/journey_error_branching.test.ts +5 -1
- package/src/tests/api_tests/resource_access_tags.test.ts +303 -0
- package/src/tests/api_tests/security/F-0106-F-0110-enduser-write-restrictions.test.ts +214 -0
- package/src/tests/api_tests/time_tracks.test.ts +420 -0
- package/src/tests/api_tests/user_portal_settings.test.ts +71 -1
- package/src/tests/tests.ts +520 -11
- package/test_generated.pdf +0 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
require('source-map-support').install();
|
|
2
|
+
|
|
3
|
+
import { Session, EnduserSession } from "../../../sdk"
|
|
4
|
+
import {
|
|
5
|
+
async_test,
|
|
6
|
+
log_header,
|
|
7
|
+
} from "@tellescope/testing"
|
|
8
|
+
import { setup_tests } from "../../setup"
|
|
9
|
+
|
|
10
|
+
const host = process.env.API_URL || 'http://localhost:8080' as const
|
|
11
|
+
const businessId = '60398b1131a295e64f084ff6'
|
|
12
|
+
|
|
13
|
+
const ENDUSER_PASSWORD = 'F0106TestPassword!123'
|
|
14
|
+
|
|
15
|
+
// Every endusers field marked enduserUpdatesDisabled in schema.ts, with a
|
|
16
|
+
// validator-passing sample value so the only rejection in play is the
|
|
17
|
+
// write-restriction (not input validation).
|
|
18
|
+
const restrictedEnduserUpdates = (staffId: string, mongoId: string): Record<string, any> => ({
|
|
19
|
+
externalId: 'f0106-attacker-external-id',
|
|
20
|
+
tags: ['f0106-attacker-tag'],
|
|
21
|
+
accessTags: ['f0106-attacker-access-tag'],
|
|
22
|
+
assignedTo: [staffId],
|
|
23
|
+
customTypeId: mongoId,
|
|
24
|
+
references: [{ type: 'Vital', id: 'f0106-attacker-reference' }],
|
|
25
|
+
sharedWithOrganizations: [],
|
|
26
|
+
journeys: { [mongoId]: 'Added' },
|
|
27
|
+
primaryAssignee: staffId,
|
|
28
|
+
fields: { f0106AttackerField: 'true' },
|
|
29
|
+
unread: true,
|
|
30
|
+
source: 'f0106-attacker-source',
|
|
31
|
+
note: 'f0106 attacker note',
|
|
32
|
+
insurance: { memberId: 'f0106-attacker-member' },
|
|
33
|
+
insuranceSecondary: { memberId: 'f0106-attacker-member-2' },
|
|
34
|
+
diagnoses: [{ code: 'I10' }],
|
|
35
|
+
lockedFromPortal: false, // false so a pre-fix run can't lock the test enduser out mid-suite
|
|
36
|
+
eligibleForAutoMerge: true,
|
|
37
|
+
athenaDepartmentId: 'f0106-dept',
|
|
38
|
+
athenaPracticeId: 'f0106-practice',
|
|
39
|
+
salesforceId: 'f0106-salesforce',
|
|
40
|
+
healthie_dietitian_id: 'f0106-healthie',
|
|
41
|
+
stripeCustomerId: 'f0106-stripe-customer',
|
|
42
|
+
stripeKey: 'f0106-stripe-key',
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
// Every form_responses field marked enduserUpdatesDisabled in schema.ts.
|
|
46
|
+
const restrictedFormResponseUpdates = (staffId: string): Record<string, any> => ({
|
|
47
|
+
markedAsSubmitted: true,
|
|
48
|
+
submittedBy: staffId,
|
|
49
|
+
submittedAt: new Date("2024-01-01T00:00:00Z"),
|
|
50
|
+
submittedByIsPlaceholder: true,
|
|
51
|
+
procedureCodes: [{ code: '99214', units: 1 }],
|
|
52
|
+
diagnosisCodes: [{ code: 'I10', description: 'Essential (primary) hypertension' }],
|
|
53
|
+
enduserAISummary: 'Patient is in excellent health, no follow-up needed.',
|
|
54
|
+
addenda: [{ text: 'forged staff addendum', timestamp: new Date(), userId: staffId }],
|
|
55
|
+
isInternalNote: true,
|
|
56
|
+
pinnedAt: new Date(),
|
|
57
|
+
hiddenFromTimeline: true,
|
|
58
|
+
lockedAt: new Date(),
|
|
59
|
+
tags: ['f0110-attacker-tag'],
|
|
60
|
+
formTitle: 'Spoofed Form Title',
|
|
61
|
+
userEmail: 'spoofed-staff@example.com',
|
|
62
|
+
logoURL: 'https://attacker.example.com/logo.png',
|
|
63
|
+
logoHeight: 100,
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Regression test for F-0106 and F-0110
|
|
68
|
+
* (security-audit/findings/F-0106-enduser-self-update-admin-only-fields.md,
|
|
69
|
+
* security-audit/findings/F-0110-form-responses-enduser-update-admin-only-fields.md).
|
|
70
|
+
*
|
|
71
|
+
* Endusers could PATCH admin-only / access-bearing / attribution-bearing fields on
|
|
72
|
+
* their own endusers record (assignedTo, tags, references, ...) and their own
|
|
73
|
+
* form_responses (procedureCodes, submittedBy, markedAsSubmitted, ...). The fix adds
|
|
74
|
+
* the `enduserUpdatesDisabled` field option (schema.ts ModelFieldInfo), enforced for
|
|
75
|
+
* enduser sessions in the generic update handler (routing.ts createDefaultEndpoints).
|
|
76
|
+
*
|
|
77
|
+
* This test asserts, for every flagged field on both models:
|
|
78
|
+
* - an enduser session updating its OWN record gets a 400 "<field> cannot be updated by endusers"
|
|
79
|
+
* - nothing persists (spot-checked on assignedTo, the highest-impact field)
|
|
80
|
+
* - enduser self-updates of allowed fields still work (fname, hideFromEnduserPortal)
|
|
81
|
+
* - staff sessions can still update the restricted fields
|
|
82
|
+
*/
|
|
83
|
+
export const enduser_write_restrictions_tests = async ({ sdk } : { sdk: Session, sdkNonAdmin: Session }) => {
|
|
84
|
+
log_header("F-0106/F-0110: enduser write restrictions (enduserUpdatesDisabled)")
|
|
85
|
+
|
|
86
|
+
let enduserId: string | undefined
|
|
87
|
+
let formId: string | undefined
|
|
88
|
+
let formResponseId: string | undefined
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
// Setup: throwaway enduser with portal credentials, plus a form_response they own
|
|
92
|
+
const enduserEmail = `f0106-enduser-${Date.now()}@example.com`
|
|
93
|
+
const enduser = await sdk.api.endusers.createOne({ email: enduserEmail, fname: 'F0106', lname: 'Restricted' })
|
|
94
|
+
enduserId = enduser.id
|
|
95
|
+
await sdk.api.endusers.set_password({ id: enduser.id, password: ENDUSER_PASSWORD })
|
|
96
|
+
|
|
97
|
+
const enduserSDK = new EnduserSession({ host, businessId })
|
|
98
|
+
await enduserSDK.authenticate(enduserEmail, ENDUSER_PASSWORD)
|
|
99
|
+
|
|
100
|
+
const form = await sdk.api.forms.createOne({ title: 'F-0106/F-0110 Write Restriction Test Form' })
|
|
101
|
+
formId = form.id
|
|
102
|
+
const { response: formResponse } = await sdk.api.form_responses.prepare_form_response({
|
|
103
|
+
formId: form.id,
|
|
104
|
+
enduserId: enduser.id,
|
|
105
|
+
})
|
|
106
|
+
formResponseId = formResponse.id
|
|
107
|
+
|
|
108
|
+
// ---- F-0106: enduser self-update of restricted endusers fields must 400 ----
|
|
109
|
+
for (const [field, value] of Object.entries(restrictedEnduserUpdates(sdk.userInfo.id, enduser.id))) {
|
|
110
|
+
await async_test(
|
|
111
|
+
`F-0106: enduser cannot self-update endusers.${field}`,
|
|
112
|
+
() => enduserSDK.api.endusers.updateOne(enduser.id, { [field]: value } as any),
|
|
113
|
+
{ shouldError: true, onError: e => e.message === `${field} cannot be updated by endusers` },
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// F-0106 Variant A persistence check: the rejected assignedTo write must not
|
|
118
|
+
// have granted the staff read-filter match.
|
|
119
|
+
await async_test(
|
|
120
|
+
'F-0106: rejected assignedTo write did not persist',
|
|
121
|
+
() => sdk.api.endusers.getOne(enduser.id),
|
|
122
|
+
{ onResult: e => !e.assignedTo?.length && !e.tags?.length && e.externalId === undefined },
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
// ---- F-0110: enduser self-update of restricted form_responses fields must 400 ----
|
|
126
|
+
for (const [field, value] of Object.entries(restrictedFormResponseUpdates(sdk.userInfo.id))) {
|
|
127
|
+
await async_test(
|
|
128
|
+
`F-0110: enduser cannot self-update form_responses.${field}`,
|
|
129
|
+
() => enduserSDK.api.form_responses.updateOne(formResponse.id, { [field]: value } as any),
|
|
130
|
+
{ shouldError: true, onError: e => e.message === `${field} cannot be updated by endusers` },
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
await async_test(
|
|
135
|
+
'F-0110: rejected writes did not persist (markedAsSubmitted, procedureCodes, submittedBy)',
|
|
136
|
+
() => sdk.api.form_responses.getOne(formResponse.id),
|
|
137
|
+
{ onResult: fr => !fr.markedAsSubmitted && !fr.procedureCodes?.length && fr.submittedBy === undefined },
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
// ---- Positive controls: intended enduser self-updates still work ----
|
|
141
|
+
await async_test(
|
|
142
|
+
'enduser can still update own profile fields (fname/lname)',
|
|
143
|
+
() => enduserSDK.api.endusers.updateOne(enduser.id, { fname: 'StillAllowed', lname: 'Patient' }),
|
|
144
|
+
{ onResult: e => e.fname === 'StillAllowed' && e.lname === 'Patient' },
|
|
145
|
+
)
|
|
146
|
+
await async_test(
|
|
147
|
+
'enduser can still update own form_response (hideFromEnduserPortal — intended per schema)',
|
|
148
|
+
() => enduserSDK.api.form_responses.updateOne(formResponse.id, { hideFromEnduserPortal: true }),
|
|
149
|
+
{ onResult: fr => fr.hideFromEnduserPortal === true },
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
// ---- Positive controls: staff sessions are unaffected by enduserUpdatesDisabled ----
|
|
153
|
+
await async_test(
|
|
154
|
+
'staff can still update restricted endusers fields (tags, assignedTo, externalId, note, source)',
|
|
155
|
+
() => sdk.api.endusers.updateOne(enduser.id, {
|
|
156
|
+
tags: ['staff-set-tag'],
|
|
157
|
+
assignedTo: [sdk.userInfo.id],
|
|
158
|
+
externalId: 'staff-set-external-id',
|
|
159
|
+
note: 'staff-set note',
|
|
160
|
+
source: 'staff-set-source',
|
|
161
|
+
}),
|
|
162
|
+
{ onResult: e => (
|
|
163
|
+
!!e.tags?.includes('staff-set-tag')
|
|
164
|
+
&& !!e.assignedTo?.includes(sdk.userInfo.id)
|
|
165
|
+
&& e.externalId === 'staff-set-external-id'
|
|
166
|
+
) },
|
|
167
|
+
)
|
|
168
|
+
await async_test(
|
|
169
|
+
'staff can still update restricted form_responses fields (procedureCodes, diagnosisCodes, tags)',
|
|
170
|
+
() => sdk.api.form_responses.updateOne(formResponse.id, {
|
|
171
|
+
procedureCodes: [{ code: 'G0019', units: 1 }],
|
|
172
|
+
diagnosisCodes: [{ code: 'Z71.3', description: 'Dietary counseling and surveillance' }],
|
|
173
|
+
tags: ['staff-set-tag'],
|
|
174
|
+
}),
|
|
175
|
+
{ onResult: fr => (
|
|
176
|
+
fr.procedureCodes?.[0]?.code === 'G0019'
|
|
177
|
+
&& fr.diagnosisCodes?.[0]?.code === 'Z71.3'
|
|
178
|
+
&& !!fr.tags?.includes('staff-set-tag')
|
|
179
|
+
) },
|
|
180
|
+
)
|
|
181
|
+
} finally {
|
|
182
|
+
if (formResponseId) {
|
|
183
|
+
try { await sdk.api.form_responses.deleteOne(formResponseId) } catch {}
|
|
184
|
+
}
|
|
185
|
+
if (formId) {
|
|
186
|
+
try { await sdk.api.forms.deleteOne(formId) } catch {}
|
|
187
|
+
}
|
|
188
|
+
if (enduserId) {
|
|
189
|
+
try { await sdk.api.endusers.deleteOne(enduserId) } catch {}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Allow running this test file independently
|
|
195
|
+
if (require.main === module) {
|
|
196
|
+
console.log(`🌐 Using API URL: ${host}`)
|
|
197
|
+
const sdk = new Session({ host })
|
|
198
|
+
const sdkNonAdmin = new Session({ host })
|
|
199
|
+
|
|
200
|
+
const runTests = async () => {
|
|
201
|
+
await setup_tests(sdk, sdkNonAdmin)
|
|
202
|
+
await enduser_write_restrictions_tests({ sdk, sdkNonAdmin })
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
runTests()
|
|
206
|
+
.then(() => {
|
|
207
|
+
console.log("✅ F-0106/F-0110 enduser write-restriction test suite completed successfully")
|
|
208
|
+
process.exit(0)
|
|
209
|
+
})
|
|
210
|
+
.catch((error) => {
|
|
211
|
+
console.error("❌ F-0106/F-0110 enduser write-restriction test suite failed:", error)
|
|
212
|
+
process.exit(1)
|
|
213
|
+
})
|
|
214
|
+
}
|
|
@@ -747,6 +747,424 @@ export const time_tracks_edge_case_tests = async ({ sdk, sdkNonAdmin }: { sdk: S
|
|
|
747
747
|
}
|
|
748
748
|
}
|
|
749
749
|
|
|
750
|
+
// ============================================================
|
|
751
|
+
// Group F: Rejection Notification + Correct & Resubmit
|
|
752
|
+
// ============================================================
|
|
753
|
+
export const time_tracks_resubmit_tests = async ({ sdk, sdkNonAdmin }: { sdk: Session, sdkNonAdmin: Session }) => {
|
|
754
|
+
log_header("Time Tracks - Rejection Notification + Resubmit Tests")
|
|
755
|
+
|
|
756
|
+
const trackIds: string[] = []
|
|
757
|
+
const nonAdminTrackIds: string[] = []
|
|
758
|
+
const notificationIds: string[] = []
|
|
759
|
+
|
|
760
|
+
// matches the webapp's Correct & Resubmit payload
|
|
761
|
+
const resubmit_payload = (track: { totalDurationInMS?: number }, byUserId: string, newDurationMS: number) => ({
|
|
762
|
+
correctedAt: new Date(),
|
|
763
|
+
correctedByUserId: byUserId,
|
|
764
|
+
correctionNote: "Corrected after rejection",
|
|
765
|
+
originalTotalDurationInMS: track.totalDurationInMS || 0,
|
|
766
|
+
totalDurationInMS: newDurationMS,
|
|
767
|
+
lockedAt: new Date(),
|
|
768
|
+
lockedByUserId: byUserId,
|
|
769
|
+
reviewedAt: '',
|
|
770
|
+
reviewedByUserId: '',
|
|
771
|
+
reviewNote: '',
|
|
772
|
+
})
|
|
773
|
+
|
|
774
|
+
try {
|
|
775
|
+
const now = new Date()
|
|
776
|
+
const oneHourAgo = new Date(now.getTime() - 3600000)
|
|
777
|
+
const twoHoursAgo = new Date(now.getTime() - 7200000)
|
|
778
|
+
|
|
779
|
+
const create_historical = (title: string) => sdk.api.time_tracks.createOne({
|
|
780
|
+
title,
|
|
781
|
+
isHistorical: true,
|
|
782
|
+
closedAt: now,
|
|
783
|
+
lockedAt: now,
|
|
784
|
+
lockedByUserId: sdk.userInfo.id,
|
|
785
|
+
totalDurationInMS: 3600000,
|
|
786
|
+
timestamps: [
|
|
787
|
+
{ type: 'start', timestamp: twoHoursAgo },
|
|
788
|
+
{ type: 'pause', timestamp: oneHourAgo },
|
|
789
|
+
],
|
|
790
|
+
} as any)
|
|
791
|
+
|
|
792
|
+
// F1: Rejection creates a notification for the owner
|
|
793
|
+
log("F1: Rejection creates timeTrackRejected notification for owner...")
|
|
794
|
+
const track = await create_historical("Track for Rejection Notification")
|
|
795
|
+
trackIds.push(track.id)
|
|
796
|
+
|
|
797
|
+
const rejectionNote = "Duration looks wrong, please correct"
|
|
798
|
+
await sdk.api.time_tracks.updateOne(track.id, {
|
|
799
|
+
reviewedAt: new Date(),
|
|
800
|
+
reviewedByUserId: sdkNonAdmin.userInfo.id,
|
|
801
|
+
reviewApproved: false,
|
|
802
|
+
reviewNote: rejectionNote,
|
|
803
|
+
} as any)
|
|
804
|
+
|
|
805
|
+
await wait(undefined, 2000) // allow side-effect handler to run
|
|
806
|
+
|
|
807
|
+
const notifications = await sdk.api.user_notifications.getSome({
|
|
808
|
+
filter: { userId: sdk.userInfo.id, type: 'timeTrackRejected' }
|
|
809
|
+
})
|
|
810
|
+
const rejectionNotification = notifications.find(n =>
|
|
811
|
+
n.relatedRecords?.find(r => r.type === 'time_track' && r.id === track.id)
|
|
812
|
+
)
|
|
813
|
+
assert(!!rejectionNotification, "F1: timeTrackRejected notification should exist for owner")
|
|
814
|
+
assert(rejectionNotification!.message.includes(rejectionNote), "F1: notification message should include rejection note")
|
|
815
|
+
notificationIds.push(rejectionNotification!.id)
|
|
816
|
+
log("F1: Rejection notification created for owner")
|
|
817
|
+
|
|
818
|
+
// F2: Resubmit happy path
|
|
819
|
+
log("F2: Owner resubmits rejected entry...")
|
|
820
|
+
const resubmitted = await sdk.api.time_tracks.updateOne(track.id,
|
|
821
|
+
resubmit_payload(track, sdk.userInfo.id, 1800000) as any
|
|
822
|
+
)
|
|
823
|
+
assert(!resubmitted.reviewedAt, "F2: reviewedAt should be cleared after resubmit")
|
|
824
|
+
|
|
825
|
+
const refetched = await sdk.api.time_tracks.getOne(track.id)
|
|
826
|
+
assert(!refetched.reviewedAt, "F2: reviewedAt should be falsy on refetch")
|
|
827
|
+
assert(refetched.reviewHistory?.length === 1, `F2: reviewHistory should have 1 entry, got ${refetched.reviewHistory?.length}`)
|
|
828
|
+
assert(refetched.reviewHistory?.[0].reviewApproved === false, "F2: reviewHistory should preserve reviewApproved false")
|
|
829
|
+
assert(refetched.reviewHistory?.[0].reviewNote === rejectionNote, "F2: reviewHistory should preserve original reviewNote")
|
|
830
|
+
assert(refetched.reviewHistory?.[0].reviewedByUserId === sdkNonAdmin.userInfo.id, "F2: reviewHistory should preserve reviewedByUserId")
|
|
831
|
+
assert(!!refetched.reviewHistory?.[0].resubmittedAt, "F2: reviewHistory should record resubmittedAt")
|
|
832
|
+
assert(refetched.reviewHistory?.[0].resubmittedByUserId === sdk.userInfo.id, "F2: reviewHistory should record resubmittedByUserId")
|
|
833
|
+
log("F2: Resubmit succeeded with reviewHistory audit trail")
|
|
834
|
+
|
|
835
|
+
// F3: Resubmit without clearing reviewedAt - expect 400
|
|
836
|
+
log("F3: Resubmit without reviewedAt: '' (should fail)...")
|
|
837
|
+
const track3 = await create_historical("Track for F3")
|
|
838
|
+
trackIds.push(track3.id)
|
|
839
|
+
await sdk.api.time_tracks.updateOne(track3.id, {
|
|
840
|
+
reviewedAt: new Date(),
|
|
841
|
+
reviewedByUserId: sdkNonAdmin.userInfo.id,
|
|
842
|
+
reviewApproved: false,
|
|
843
|
+
} as any)
|
|
844
|
+
|
|
845
|
+
const { reviewedAt: _omitted, ...payloadWithoutClear } = resubmit_payload(track3, sdk.userInfo.id, 1800000)
|
|
846
|
+
await assert_throws(
|
|
847
|
+
() => sdk.api.time_tracks.updateOne(track3.id, payloadWithoutClear as any),
|
|
848
|
+
"F3: resubmit without clearing reviewedAt"
|
|
849
|
+
)
|
|
850
|
+
log("F3: Correctly rejected resubmit without reviewedAt: ''")
|
|
851
|
+
|
|
852
|
+
// F4: Resubmit with self-approval - expect 400
|
|
853
|
+
log("F4: Resubmit with reviewApproved: true (should fail)...")
|
|
854
|
+
await assert_throws(
|
|
855
|
+
() => sdk.api.time_tracks.updateOne(track3.id, {
|
|
856
|
+
...resubmit_payload(track3, sdk.userInfo.id, 1800000),
|
|
857
|
+
reviewApproved: true,
|
|
858
|
+
} as any),
|
|
859
|
+
"F4: resubmit with self-approval"
|
|
860
|
+
)
|
|
861
|
+
log("F4: Correctly rejected self-approval during resubmit")
|
|
862
|
+
|
|
863
|
+
// F5: Correction payload on locked, non-rejected entry - expect 400
|
|
864
|
+
log("F5: Correction payload on locked non-rejected entry (should fail)...")
|
|
865
|
+
const track5 = await create_historical("Track for F5")
|
|
866
|
+
trackIds.push(track5.id)
|
|
867
|
+
await assert_throws(
|
|
868
|
+
() => sdk.api.time_tracks.updateOne(track5.id, resubmit_payload(track5, sdk.userInfo.id, 1800000) as any),
|
|
869
|
+
"F5: correction on locked non-rejected entry"
|
|
870
|
+
)
|
|
871
|
+
log("F5: Correctly rejected correction on locked non-rejected entry")
|
|
872
|
+
|
|
873
|
+
// F6: Non-owner resubmit attempt - expect 400
|
|
874
|
+
log("F6: Non-owner resubmit attempt (should fail)...")
|
|
875
|
+
const nonAdminTrack = await sdkNonAdmin.api.time_tracks.createOne({
|
|
876
|
+
title: "Non-Admin Track for F6",
|
|
877
|
+
isHistorical: true,
|
|
878
|
+
closedAt: now,
|
|
879
|
+
lockedAt: now,
|
|
880
|
+
lockedByUserId: sdkNonAdmin.userInfo.id,
|
|
881
|
+
totalDurationInMS: 3600000,
|
|
882
|
+
timestamps: [
|
|
883
|
+
{ type: 'start', timestamp: twoHoursAgo },
|
|
884
|
+
{ type: 'pause', timestamp: oneHourAgo },
|
|
885
|
+
],
|
|
886
|
+
} as any)
|
|
887
|
+
nonAdminTrackIds.push(nonAdminTrack.id)
|
|
888
|
+
|
|
889
|
+
await sdk.api.time_tracks.updateOne(nonAdminTrack.id, {
|
|
890
|
+
reviewedAt: new Date(),
|
|
891
|
+
reviewedByUserId: sdk.userInfo.id,
|
|
892
|
+
reviewApproved: false,
|
|
893
|
+
reviewNote: "Rejected for F6",
|
|
894
|
+
} as any)
|
|
895
|
+
|
|
896
|
+
await assert_throws(
|
|
897
|
+
() => sdk.api.time_tracks.updateOne(nonAdminTrack.id, resubmit_payload(nonAdminTrack, sdk.userInfo.id, 1800000) as any),
|
|
898
|
+
"F6: non-owner resubmit"
|
|
899
|
+
)
|
|
900
|
+
log("F6: Correctly rejected non-owner resubmit")
|
|
901
|
+
|
|
902
|
+
// F7: Second rejection + resubmit appends to reviewHistory
|
|
903
|
+
log("F7: Reject again after resubmit, resubmit again...")
|
|
904
|
+
await sdk.api.time_tracks.updateOne(track.id, {
|
|
905
|
+
reviewedAt: new Date(),
|
|
906
|
+
reviewedByUserId: sdkNonAdmin.userInfo.id,
|
|
907
|
+
reviewApproved: false,
|
|
908
|
+
reviewNote: "Still not right",
|
|
909
|
+
} as any)
|
|
910
|
+
|
|
911
|
+
const rejectedAgain = await sdk.api.time_tracks.getOne(track.id)
|
|
912
|
+
await sdk.api.time_tracks.updateOne(track.id,
|
|
913
|
+
resubmit_payload(rejectedAgain, sdk.userInfo.id, 2700000) as any
|
|
914
|
+
)
|
|
915
|
+
|
|
916
|
+
const afterSecondResubmit = await sdk.api.time_tracks.getOne(track.id)
|
|
917
|
+
assert(afterSecondResubmit.reviewHistory?.length === 2,
|
|
918
|
+
`F7: reviewHistory should have 2 entries, got ${afterSecondResubmit.reviewHistory?.length}`)
|
|
919
|
+
assert(afterSecondResubmit.reviewHistory?.[1].reviewNote === "Still not right",
|
|
920
|
+
"F7: second reviewHistory entry should preserve second rejection note")
|
|
921
|
+
log("F7: Second rejection/resubmit cycle appended to reviewHistory")
|
|
922
|
+
|
|
923
|
+
// F8: Status filters treat reviewedAt: '' as Pending
|
|
924
|
+
log("F8: Status mdbFilters handle reviewedAt: ''...")
|
|
925
|
+
const pendingMatches = await sdk.api.time_tracks.getSome({
|
|
926
|
+
mdbFilter: { $or: [{ reviewedAt: { $exists: false } }, { reviewedAt: '' }] }
|
|
927
|
+
})
|
|
928
|
+
assert(!!pendingMatches.find(t => t.id === track.id), "F8: Pending filter should match resubmitted track")
|
|
929
|
+
|
|
930
|
+
const rejectedMatches = await sdk.api.time_tracks.getSome({
|
|
931
|
+
mdbFilter: { reviewedAt: { $exists: true, $ne: '' }, reviewApproved: false }
|
|
932
|
+
})
|
|
933
|
+
assert(!rejectedMatches.find(t => t.id === track.id), "F8: Rejected filter should exclude resubmitted track")
|
|
934
|
+
log("F8: Status filters behave correctly with cleared reviewedAt")
|
|
935
|
+
|
|
936
|
+
log("All rejection notification + resubmit tests passed!")
|
|
937
|
+
|
|
938
|
+
} finally {
|
|
939
|
+
for (const id of trackIds) {
|
|
940
|
+
try { await sdk.api.time_tracks.deleteOne(id) } catch (e) {}
|
|
941
|
+
}
|
|
942
|
+
for (const id of nonAdminTrackIds) {
|
|
943
|
+
try { await sdkNonAdmin.api.time_tracks.deleteOne(id) } catch (e) {}
|
|
944
|
+
}
|
|
945
|
+
for (const id of notificationIds) {
|
|
946
|
+
try { await sdk.api.user_notifications.deleteOne(id) } catch (e) {}
|
|
947
|
+
}
|
|
948
|
+
// clean up any remaining timeTrackRejected notifications created by these tests
|
|
949
|
+
try {
|
|
950
|
+
const remaining = await sdk.api.user_notifications.getSome({
|
|
951
|
+
filter: { userId: sdk.userInfo.id, type: 'timeTrackRejected' }
|
|
952
|
+
})
|
|
953
|
+
for (const n of remaining) {
|
|
954
|
+
try { await sdk.api.user_notifications.deleteOne(n.id) } catch (e) {}
|
|
955
|
+
}
|
|
956
|
+
} catch (e) {}
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// ============================================================
|
|
961
|
+
// Group F: Appointment Duration from Tracked Time
|
|
962
|
+
// ============================================================
|
|
963
|
+
export const time_tracks_appointment_duration_tests = async ({ sdk, sdkNonAdmin }: { sdk: Session, sdkNonAdmin: Session }) => {
|
|
964
|
+
log_header("Time Tracks - Appointment Duration from Tracked Time Tests")
|
|
965
|
+
|
|
966
|
+
const businessId = sdk.userInfo.businessId
|
|
967
|
+
const trackIds: string[] = []
|
|
968
|
+
const formResponseIds: string[] = []
|
|
969
|
+
const eventIds: string[] = []
|
|
970
|
+
let formId: string | undefined
|
|
971
|
+
let enduserId: string | undefined
|
|
972
|
+
|
|
973
|
+
try {
|
|
974
|
+
// Enable time tracking + appointment duration setting
|
|
975
|
+
log("F0: Enabling timeTracking.setAppointmentDurationOnTimeTrackClose...")
|
|
976
|
+
await sdk.api.organizations.updateOne(businessId, {
|
|
977
|
+
settings: { timeTracking: { enabled: true, setAppointmentDurationOnTimeTrackClose: true } }
|
|
978
|
+
})
|
|
979
|
+
|
|
980
|
+
// Shared records
|
|
981
|
+
const enduser = await sdk.api.endusers.createOne({ email: `tt-duration-${Date.now()}@test.com` })
|
|
982
|
+
enduserId = enduser.id
|
|
983
|
+
const form = await sdk.api.forms.createOne({ title: 'Appointment Duration Form' })
|
|
984
|
+
formId = form.id
|
|
985
|
+
|
|
986
|
+
const event = await sdk.api.calendar_events.createOne({
|
|
987
|
+
title: 'Appointment for Tracked Time',
|
|
988
|
+
durationInMinutes: 30,
|
|
989
|
+
startTimeInMS: Date.now(),
|
|
990
|
+
attendees: [{ type: 'enduser', id: enduser.id }],
|
|
991
|
+
})
|
|
992
|
+
eventIds.push(event.id)
|
|
993
|
+
|
|
994
|
+
const response1 = await sdk.api.form_responses.createOne({
|
|
995
|
+
formId: form.id,
|
|
996
|
+
enduserId: enduser.id,
|
|
997
|
+
formTitle: form.title,
|
|
998
|
+
calendarEventId: event.id,
|
|
999
|
+
})
|
|
1000
|
+
formResponseIds.push(response1.id)
|
|
1001
|
+
|
|
1002
|
+
// Test F1: Close a linked time track -> actualDuration set from tracked time
|
|
1003
|
+
log("F1: Closing linked time track sets appointment actualDuration...")
|
|
1004
|
+
const track1 = await sdk.api.time_tracks.createOne({
|
|
1005
|
+
title: "Tracked Form Completion",
|
|
1006
|
+
activity: { type: 'form_response', id: response1.id },
|
|
1007
|
+
} as any)
|
|
1008
|
+
trackIds.push(track1.id)
|
|
1009
|
+
|
|
1010
|
+
await wait(undefined, 2000)
|
|
1011
|
+
await sdk.api.time_tracks.updateOne(track1.id, {
|
|
1012
|
+
closedAt: new Date(),
|
|
1013
|
+
totalDurationInMS: 1800000, // 30 minutes
|
|
1014
|
+
} as any)
|
|
1015
|
+
await wait(undefined, 1500) // allow side effects to run
|
|
1016
|
+
|
|
1017
|
+
const eventAfterClose = await sdk.api.calendar_events.getOne(event.id)
|
|
1018
|
+
assert(eventAfterClose.actualDuration === 30,
|
|
1019
|
+
`F1: actualDuration should be 30, got ${eventAfterClose.actualDuration}`)
|
|
1020
|
+
log("F1: actualDuration set from closed time track")
|
|
1021
|
+
|
|
1022
|
+
// Test F2: Second closed track on same appointment -> durations are summed
|
|
1023
|
+
log("F2: Second closed track sums durations...")
|
|
1024
|
+
const response2 = await sdk.api.form_responses.createOne({
|
|
1025
|
+
formId: form.id,
|
|
1026
|
+
enduserId: enduser.id,
|
|
1027
|
+
formTitle: form.title,
|
|
1028
|
+
calendarEventId: event.id,
|
|
1029
|
+
})
|
|
1030
|
+
formResponseIds.push(response2.id)
|
|
1031
|
+
|
|
1032
|
+
const track2 = await sdk.api.time_tracks.createOne({
|
|
1033
|
+
title: "Second Tracked Form Completion",
|
|
1034
|
+
activity: { type: 'form_response', id: response2.id },
|
|
1035
|
+
} as any)
|
|
1036
|
+
trackIds.push(track2.id)
|
|
1037
|
+
|
|
1038
|
+
await sdk.api.time_tracks.updateOne(track2.id, {
|
|
1039
|
+
closedAt: new Date(),
|
|
1040
|
+
totalDurationInMS: 600000, // 10 minutes
|
|
1041
|
+
} as any)
|
|
1042
|
+
await wait(undefined, 1500)
|
|
1043
|
+
|
|
1044
|
+
const eventAfterSecond = await sdk.api.calendar_events.getOne(event.id)
|
|
1045
|
+
assert(eventAfterSecond.actualDuration === 40,
|
|
1046
|
+
`F2: actualDuration should be 40 (30 + 10), got ${eventAfterSecond.actualDuration}`)
|
|
1047
|
+
log("F2: actualDuration correctly summed across tracks")
|
|
1048
|
+
|
|
1049
|
+
// Test F3: Linking activity after close triggers recompute
|
|
1050
|
+
log("F3: Post-close activity link triggers recompute...")
|
|
1051
|
+
const response3 = await sdk.api.form_responses.createOne({
|
|
1052
|
+
formId: form.id,
|
|
1053
|
+
enduserId: enduser.id,
|
|
1054
|
+
formTitle: form.title,
|
|
1055
|
+
calendarEventId: event.id,
|
|
1056
|
+
})
|
|
1057
|
+
formResponseIds.push(response3.id)
|
|
1058
|
+
|
|
1059
|
+
const track3 = await sdk.api.time_tracks.createOne({
|
|
1060
|
+
title: "Track Linked After Close",
|
|
1061
|
+
} as any)
|
|
1062
|
+
trackIds.push(track3.id)
|
|
1063
|
+
|
|
1064
|
+
await sdk.api.time_tracks.updateOne(track3.id, {
|
|
1065
|
+
closedAt: new Date(),
|
|
1066
|
+
totalDurationInMS: 300000, // 5 minutes
|
|
1067
|
+
} as any)
|
|
1068
|
+
await wait(undefined, 1500)
|
|
1069
|
+
|
|
1070
|
+
// no activity yet -> actualDuration unchanged
|
|
1071
|
+
const eventBeforeLink = await sdk.api.calendar_events.getOne(event.id)
|
|
1072
|
+
assert(eventBeforeLink.actualDuration === 40,
|
|
1073
|
+
`F3: actualDuration should still be 40 before activity link, got ${eventBeforeLink.actualDuration}`)
|
|
1074
|
+
|
|
1075
|
+
await sdk.api.time_tracks.updateOne(track3.id, {
|
|
1076
|
+
activity: { type: 'form_response', id: response3.id },
|
|
1077
|
+
} as any)
|
|
1078
|
+
await wait(undefined, 1500)
|
|
1079
|
+
|
|
1080
|
+
const eventAfterLink = await sdk.api.calendar_events.getOne(event.id)
|
|
1081
|
+
assert(eventAfterLink.actualDuration === 45,
|
|
1082
|
+
`F3: actualDuration should be 45 (30 + 10 + 5) after post-close link, got ${eventAfterLink.actualDuration}`)
|
|
1083
|
+
log("F3: Post-close activity link recomputed actualDuration")
|
|
1084
|
+
|
|
1085
|
+
// Test F4: Correction recomputes actualDuration
|
|
1086
|
+
log("F4: Correction recomputes actualDuration...")
|
|
1087
|
+
await sdk.api.time_tracks.updateOne(track2.id, {
|
|
1088
|
+
correctedAt: new Date(),
|
|
1089
|
+
correctedByUserId: sdk.userInfo.id,
|
|
1090
|
+
correctionNote: "Adjusting tracked time",
|
|
1091
|
+
originalTotalDurationInMS: 600000,
|
|
1092
|
+
totalDurationInMS: 1200000, // corrected to 20 minutes
|
|
1093
|
+
lockedAt: new Date(),
|
|
1094
|
+
lockedByUserId: sdk.userInfo.id,
|
|
1095
|
+
} as any)
|
|
1096
|
+
await wait(undefined, 1500)
|
|
1097
|
+
|
|
1098
|
+
const eventAfterCorrection = await sdk.api.calendar_events.getOne(event.id)
|
|
1099
|
+
assert(eventAfterCorrection.actualDuration === 55,
|
|
1100
|
+
`F4: actualDuration should be 55 (30 + 20 + 5) after correction, got ${eventAfterCorrection.actualDuration}`)
|
|
1101
|
+
log("F4: Correction recomputed actualDuration")
|
|
1102
|
+
|
|
1103
|
+
// Test F5: Setting disabled -> actualDuration not set
|
|
1104
|
+
log("F5: Setting disabled leaves actualDuration unset...")
|
|
1105
|
+
await sdk.api.organizations.updateOne(businessId, {
|
|
1106
|
+
settings: { timeTracking: { enabled: true, setAppointmentDurationOnTimeTrackClose: false } }
|
|
1107
|
+
})
|
|
1108
|
+
|
|
1109
|
+
const event2 = await sdk.api.calendar_events.createOne({
|
|
1110
|
+
title: 'Appointment Without Duration Sync',
|
|
1111
|
+
durationInMinutes: 30,
|
|
1112
|
+
startTimeInMS: Date.now(),
|
|
1113
|
+
attendees: [{ type: 'enduser', id: enduser.id }],
|
|
1114
|
+
})
|
|
1115
|
+
eventIds.push(event2.id)
|
|
1116
|
+
|
|
1117
|
+
const response4 = await sdk.api.form_responses.createOne({
|
|
1118
|
+
formId: form.id,
|
|
1119
|
+
enduserId: enduser.id,
|
|
1120
|
+
formTitle: form.title,
|
|
1121
|
+
calendarEventId: event2.id,
|
|
1122
|
+
})
|
|
1123
|
+
formResponseIds.push(response4.id)
|
|
1124
|
+
|
|
1125
|
+
const track4 = await sdk.api.time_tracks.createOne({
|
|
1126
|
+
title: "Track With Setting Disabled",
|
|
1127
|
+
activity: { type: 'form_response', id: response4.id },
|
|
1128
|
+
} as any)
|
|
1129
|
+
trackIds.push(track4.id)
|
|
1130
|
+
|
|
1131
|
+
await sdk.api.time_tracks.updateOne(track4.id, {
|
|
1132
|
+
closedAt: new Date(),
|
|
1133
|
+
totalDurationInMS: 1800000,
|
|
1134
|
+
} as any)
|
|
1135
|
+
await wait(undefined, 1500)
|
|
1136
|
+
|
|
1137
|
+
const event2AfterClose = await sdk.api.calendar_events.getOne(event2.id)
|
|
1138
|
+
assert(!event2AfterClose.actualDuration,
|
|
1139
|
+
`F5: actualDuration should stay unset when setting is disabled, got ${event2AfterClose.actualDuration}`)
|
|
1140
|
+
log("F5: actualDuration correctly unset with setting disabled")
|
|
1141
|
+
|
|
1142
|
+
log("All appointment duration tests passed!")
|
|
1143
|
+
|
|
1144
|
+
} finally {
|
|
1145
|
+
for (const id of trackIds) {
|
|
1146
|
+
try { await sdk.api.time_tracks.deleteOne(id) } catch (e) {}
|
|
1147
|
+
}
|
|
1148
|
+
for (const id of formResponseIds) {
|
|
1149
|
+
try { await sdk.api.form_responses.deleteOne(id) } catch (e) {}
|
|
1150
|
+
}
|
|
1151
|
+
for (const id of eventIds) {
|
|
1152
|
+
try { await sdk.api.calendar_events.deleteOne(id) } catch (e) {}
|
|
1153
|
+
}
|
|
1154
|
+
if (formId) {
|
|
1155
|
+
try { await sdk.api.forms.deleteOne(formId) } catch (e) {}
|
|
1156
|
+
}
|
|
1157
|
+
if (enduserId) {
|
|
1158
|
+
try { await sdk.api.endusers.deleteOne(enduserId) } catch (e) {}
|
|
1159
|
+
}
|
|
1160
|
+
try {
|
|
1161
|
+
await sdk.api.organizations.updateOne(businessId, {
|
|
1162
|
+
settings: { timeTracking: { setAppointmentDurationOnTimeTrackClose: false } }
|
|
1163
|
+
})
|
|
1164
|
+
} catch (e) {}
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
|
|
750
1168
|
// Allow running this test file independently
|
|
751
1169
|
if (require.main === module) {
|
|
752
1170
|
const sdk = new Session({ host })
|
|
@@ -764,6 +1182,8 @@ if (require.main === module) {
|
|
|
764
1182
|
await time_tracks_review_tests({ sdk, sdkNonAdmin })
|
|
765
1183
|
await time_tracks_lock_tests({ sdk, sdkNonAdmin })
|
|
766
1184
|
await time_tracks_edge_case_tests({ sdk, sdkNonAdmin })
|
|
1185
|
+
await time_tracks_resubmit_tests({ sdk, sdkNonAdmin })
|
|
1186
|
+
await time_tracks_appointment_duration_tests({ sdk, sdkNonAdmin })
|
|
767
1187
|
}
|
|
768
1188
|
|
|
769
1189
|
runTests()
|