@tellescope/sdk 1.253.1 → 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/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/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/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/tests.d.ts.map +1 -1
- package/lib/cjs/tests/tests.js +586 -178
- 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/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/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/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/tests.d.ts.map +1 -1
- package/lib/esm/tests/tests.js +587 -179
- 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/field_redaction.test.ts +13 -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/time_tracks.test.ts +420 -0
- package/src/tests/tests.ts +298 -5
- package/test_generated.pdf +0 -0
|
@@ -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()
|