@remit/backend 0.0.136 → 0.0.138
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/package.json
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
type CalendarInstance,
|
|
14
14
|
type CalendarWindow,
|
|
15
15
|
deleteCalendarObject,
|
|
16
|
+
followingUidOf,
|
|
16
17
|
listBusySpans,
|
|
17
18
|
listCalendarInstances,
|
|
18
19
|
listObjectInstances,
|
|
@@ -445,7 +446,10 @@ export const updateCalendarEventFor = async (
|
|
|
445
446
|
{
|
|
446
447
|
scope: request.scope,
|
|
447
448
|
recurrenceId: request.recurrenceId,
|
|
448
|
-
followingUid:
|
|
449
|
+
followingUid: followingUidOf(
|
|
450
|
+
object.icalUid,
|
|
451
|
+
`${deps.newId()}@reader.remit`,
|
|
452
|
+
),
|
|
449
453
|
},
|
|
450
454
|
patch,
|
|
451
455
|
);
|
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { after, before, describe, test } from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
acceptCalendarSuggestion,
|
|
5
|
+
reopenCalendarSuggestion,
|
|
6
|
+
} from "@remit/calendar-service";
|
|
3
7
|
import type {
|
|
4
8
|
CalendarSuggestionItem,
|
|
5
9
|
CreateFilterInput,
|
|
6
10
|
FilterItem,
|
|
7
11
|
ICalendarSuggestionRepository,
|
|
12
|
+
ICalendarUnitOfWork,
|
|
8
13
|
MessageData,
|
|
9
14
|
PutCalendarSuggestionInput,
|
|
10
15
|
SettleCalendarSuggestionInput,
|
|
11
16
|
} from "@remit/data-ports";
|
|
12
17
|
import {
|
|
18
|
+
CalendarAnswerOvertaken,
|
|
13
19
|
CalendarInviteAnswer,
|
|
14
20
|
CalendarInviteMethod,
|
|
15
21
|
CalendarSuggestionSource,
|
|
16
22
|
CalendarSuggestionState,
|
|
17
23
|
FilterState,
|
|
24
|
+
RecurrenceScope,
|
|
18
25
|
} from "@remit/domain-enums";
|
|
19
26
|
import type { APIGatewayProxyEvent } from "aws-lambda";
|
|
20
27
|
import type { Context } from "openapi-backend";
|
|
@@ -24,11 +31,17 @@ import {
|
|
|
24
31
|
type RemitClient,
|
|
25
32
|
setClient,
|
|
26
33
|
} from "../service/data-client.js";
|
|
34
|
+
import { calendarDepsOf } from "./calendar.js";
|
|
35
|
+
import {
|
|
36
|
+
calendarEventDepsOf,
|
|
37
|
+
updateCalendarEventFor,
|
|
38
|
+
} from "./calendar-event.js";
|
|
27
39
|
import { createCalendarSqliteClient } from "./calendar-sqlite-fixture.js";
|
|
28
40
|
import {
|
|
29
41
|
assertSettleable,
|
|
30
42
|
CalendarSuggestionActionOperations,
|
|
31
43
|
CalendarSuggestionOperations,
|
|
44
|
+
MessageCalendarSuggestionOperations,
|
|
32
45
|
type MuteSenderDeps,
|
|
33
46
|
muteSender,
|
|
34
47
|
settleSuggestion,
|
|
@@ -58,6 +71,7 @@ const suggestion = (
|
|
|
58
71
|
zoneCertainty: "Explicit",
|
|
59
72
|
icalData: "BEGIN:VCALENDAR\r\nEND:VCALENDAR\r\n",
|
|
60
73
|
acceptedCalendarObjectId: "",
|
|
74
|
+
supersededByMessageId: "",
|
|
61
75
|
createdAt: 1,
|
|
62
76
|
updatedAt: 1,
|
|
63
77
|
...overrides,
|
|
@@ -133,7 +147,10 @@ const muteRule = (sender: string): FilterItem =>
|
|
|
133
147
|
|
|
134
148
|
describe("toCalendarSuggestionResponse", () => {
|
|
135
149
|
test("keeps the raw invitation bytes on the server", async () => {
|
|
136
|
-
const response = toCalendarSuggestionResponse(
|
|
150
|
+
const response = toCalendarSuggestionResponse(
|
|
151
|
+
suggestion(),
|
|
152
|
+
CalendarAnswerOvertaken.None,
|
|
153
|
+
);
|
|
137
154
|
|
|
138
155
|
assert.equal("icalData" in response, false);
|
|
139
156
|
assert.equal(response.summary, "Quarterly review");
|
|
@@ -308,12 +325,40 @@ const acceptSuggestion =
|
|
|
308
325
|
CalendarSuggestionActionOperations.CalendarSuggestionActionOperations_acceptCalendarSuggestion as Handler;
|
|
309
326
|
const dismissSuggestion =
|
|
310
327
|
CalendarSuggestionActionOperations.CalendarSuggestionActionOperations_dismissCalendarSuggestion as Handler;
|
|
328
|
+
const listMessageSuggestions =
|
|
329
|
+
MessageCalendarSuggestionOperations.MessageCalendarSuggestionOperations_listMessageCalendarSuggestions as Handler;
|
|
330
|
+
const reopenSuggestion =
|
|
331
|
+
CalendarSuggestionActionOperations.CalendarSuggestionActionOperations_reopenCalendarSuggestion as Handler;
|
|
311
332
|
|
|
312
333
|
interface Card {
|
|
313
334
|
suggestionId: string;
|
|
314
335
|
state: string;
|
|
336
|
+
supersededByMessageId: string;
|
|
337
|
+
supersededByThreadId: string;
|
|
315
338
|
}
|
|
316
339
|
|
|
340
|
+
const fileInThread = async (
|
|
341
|
+
accountConfigId: string,
|
|
342
|
+
messageId: string,
|
|
343
|
+
threadId: string,
|
|
344
|
+
): Promise<void> => {
|
|
345
|
+
await client.threadMessage.create({
|
|
346
|
+
accountConfigId,
|
|
347
|
+
threadId,
|
|
348
|
+
messageId,
|
|
349
|
+
mailboxId: "mbx-inbox",
|
|
350
|
+
uid: 1,
|
|
351
|
+
referenceOrder: 0,
|
|
352
|
+
internalDate: 1,
|
|
353
|
+
sentDate: 1,
|
|
354
|
+
subject: "Invitation: Quarterly review",
|
|
355
|
+
isRead: false,
|
|
356
|
+
isDeleted: false,
|
|
357
|
+
hasAttachment: false,
|
|
358
|
+
hasStars: false,
|
|
359
|
+
});
|
|
360
|
+
};
|
|
361
|
+
|
|
317
362
|
let client: RemitClient;
|
|
318
363
|
let cleanup: () => void;
|
|
319
364
|
let mintedSubs = 0;
|
|
@@ -473,6 +518,74 @@ describe("GET /calendar-suggestions", () => {
|
|
|
473
518
|
});
|
|
474
519
|
});
|
|
475
520
|
|
|
521
|
+
describe("GET /messages/{messageId}/calendar-suggestions", () => {
|
|
522
|
+
test("names the conversation of the revision that retired a card", async () => {
|
|
523
|
+
const { accountConfigId, event } = anAccount();
|
|
524
|
+
const older = await putSuggestion(accountConfigId, "msg-revision-0");
|
|
525
|
+
await client.calendarSuggestion.supersedeIfPending(
|
|
526
|
+
accountConfigId,
|
|
527
|
+
older.suggestionId,
|
|
528
|
+
"msg-revision-1",
|
|
529
|
+
);
|
|
530
|
+
await fileInThread(accountConfigId, "msg-revision-1", "thread-revision-1");
|
|
531
|
+
|
|
532
|
+
const listed = (await listMessageSuggestions(
|
|
533
|
+
contextOf({ params: { messageId: "msg-revision-0" } }),
|
|
534
|
+
event,
|
|
535
|
+
)) as unknown as { items: Card[] };
|
|
536
|
+
|
|
537
|
+
assert.deepEqual(
|
|
538
|
+
listed.items.map((card) => [
|
|
539
|
+
card.state,
|
|
540
|
+
card.supersededByMessageId,
|
|
541
|
+
card.supersededByThreadId,
|
|
542
|
+
]),
|
|
543
|
+
[
|
|
544
|
+
[
|
|
545
|
+
CalendarSuggestionState.Superseded,
|
|
546
|
+
"msg-revision-1",
|
|
547
|
+
"thread-revision-1",
|
|
548
|
+
],
|
|
549
|
+
],
|
|
550
|
+
);
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
test("names no conversation when the newer message sits in none", async () => {
|
|
554
|
+
const { accountConfigId, event } = anAccount();
|
|
555
|
+
const older = await putSuggestion(accountConfigId, "msg-orphan-0");
|
|
556
|
+
await client.calendarSuggestion.supersedeIfPending(
|
|
557
|
+
accountConfigId,
|
|
558
|
+
older.suggestionId,
|
|
559
|
+
"msg-orphan-1",
|
|
560
|
+
);
|
|
561
|
+
|
|
562
|
+
const listed = (await listMessageSuggestions(
|
|
563
|
+
contextOf({ params: { messageId: "msg-orphan-0" } }),
|
|
564
|
+
event,
|
|
565
|
+
)) as unknown as { items: Card[] };
|
|
566
|
+
|
|
567
|
+
assert.equal(listed.items[0]?.supersededByThreadId, "");
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
test("names no conversation for a card nothing retired", async () => {
|
|
571
|
+
const { accountConfigId, event } = anAccount();
|
|
572
|
+
await putSuggestion(accountConfigId, "msg-current");
|
|
573
|
+
|
|
574
|
+
const listed = (await listMessageSuggestions(
|
|
575
|
+
contextOf({ params: { messageId: "msg-current" } }),
|
|
576
|
+
event,
|
|
577
|
+
)) as unknown as { items: Card[] };
|
|
578
|
+
|
|
579
|
+
assert.deepEqual(
|
|
580
|
+
listed.items.map((card) => [
|
|
581
|
+
card.supersededByMessageId,
|
|
582
|
+
card.supersededByThreadId,
|
|
583
|
+
]),
|
|
584
|
+
[["", ""]],
|
|
585
|
+
);
|
|
586
|
+
});
|
|
587
|
+
});
|
|
588
|
+
|
|
476
589
|
describe("POST /calendar-suggestions/{suggestionId}/accept", () => {
|
|
477
590
|
test("answers not-found for a calendar on another account, before writing anything", async () => {
|
|
478
591
|
const stranger = anAccount();
|
|
@@ -668,3 +781,382 @@ describe("POST /calendar-suggestions/{suggestionId}/accept with corrections", ()
|
|
|
668
781
|
);
|
|
669
782
|
});
|
|
670
783
|
});
|
|
784
|
+
|
|
785
|
+
describe("POST /calendar-suggestions/{suggestionId}/reopen", () => {
|
|
786
|
+
const answeredInto = async (
|
|
787
|
+
accountConfigId: string,
|
|
788
|
+
messageId: string,
|
|
789
|
+
answer: (typeof CalendarInviteAnswer)[keyof typeof CalendarInviteAnswer],
|
|
790
|
+
): Promise<{ calendarId: string; card: CalendarSuggestionItem }> => {
|
|
791
|
+
const calendar = await client.calendarCollection.create({
|
|
792
|
+
accountConfigId,
|
|
793
|
+
urlSegment: "default",
|
|
794
|
+
displayName: "Calendar",
|
|
795
|
+
});
|
|
796
|
+
const accepted = await acceptCalendarSuggestion(client.calendarUnitOfWork, {
|
|
797
|
+
accountConfigId,
|
|
798
|
+
calendarId: calendar.calendarId,
|
|
799
|
+
suggestion: await putSuggestion(accountConfigId, messageId),
|
|
800
|
+
attendee: "user@example.test",
|
|
801
|
+
answer,
|
|
802
|
+
});
|
|
803
|
+
assert.ok(accepted.ok);
|
|
804
|
+
return { calendarId: calendar.calendarId, card: accepted.value.suggestion };
|
|
805
|
+
};
|
|
806
|
+
|
|
807
|
+
for (const answer of [
|
|
808
|
+
CalendarInviteAnswer.Accepted,
|
|
809
|
+
CalendarInviteAnswer.Tentative,
|
|
810
|
+
]) {
|
|
811
|
+
test(`takes a ${answer} answer back and removes the event it wrote`, async () => {
|
|
812
|
+
const { accountConfigId, event } = anAccount();
|
|
813
|
+
const { calendarId, card } = await answeredInto(
|
|
814
|
+
accountConfigId,
|
|
815
|
+
`msg-reopen-${answer}`,
|
|
816
|
+
answer,
|
|
817
|
+
);
|
|
818
|
+
assert.equal(
|
|
819
|
+
(await client.calendarObject.listByCalendar(calendarId)).length,
|
|
820
|
+
1,
|
|
821
|
+
);
|
|
822
|
+
|
|
823
|
+
const reopened = (await reopenSuggestion(
|
|
824
|
+
contextOf({ params: { suggestionId: card.suggestionId } }),
|
|
825
|
+
event,
|
|
826
|
+
)) as unknown as CalendarSuggestionItem;
|
|
827
|
+
|
|
828
|
+
assert.equal(reopened.state, CalendarSuggestionState.Pending);
|
|
829
|
+
assert.equal(reopened.acceptedCalendarObjectId, "");
|
|
830
|
+
assert.deepEqual(
|
|
831
|
+
await client.calendarObject.listByCalendar(calendarId),
|
|
832
|
+
[],
|
|
833
|
+
);
|
|
834
|
+
const stored = await client.calendarSuggestion.get(
|
|
835
|
+
accountConfigId,
|
|
836
|
+
card.suggestionId,
|
|
837
|
+
);
|
|
838
|
+
assert.equal(stored.state, CalendarSuggestionState.Pending);
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
for (const state of [
|
|
843
|
+
CalendarSuggestionState.Declined,
|
|
844
|
+
CalendarSuggestionState.Dismissed,
|
|
845
|
+
]) {
|
|
846
|
+
test(`returns a ${state} card to Pending`, async () => {
|
|
847
|
+
const { accountConfigId, event } = anAccount();
|
|
848
|
+
const card = await putSuggestion(accountConfigId, `msg-reopen-${state}`);
|
|
849
|
+
await client.calendarSuggestion.settle(
|
|
850
|
+
accountConfigId,
|
|
851
|
+
card.suggestionId,
|
|
852
|
+
{ state, acceptedCalendarObjectId: "" },
|
|
853
|
+
);
|
|
854
|
+
|
|
855
|
+
const reopened = (await reopenSuggestion(
|
|
856
|
+
contextOf({ params: { suggestionId: card.suggestionId } }),
|
|
857
|
+
event,
|
|
858
|
+
)) as unknown as Card;
|
|
859
|
+
|
|
860
|
+
assert.equal(reopened.state, CalendarSuggestionState.Pending);
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
test("refuses a superseded card and leaves it as it is", async () => {
|
|
865
|
+
const { accountConfigId, event } = anAccount();
|
|
866
|
+
const card = await putSuggestion(accountConfigId, "msg-reopen-superseded");
|
|
867
|
+
await client.calendarSuggestion.supersedeIfPending(
|
|
868
|
+
accountConfigId,
|
|
869
|
+
card.suggestionId,
|
|
870
|
+
"msg-reopen-newer",
|
|
871
|
+
);
|
|
872
|
+
|
|
873
|
+
await assert.rejects(
|
|
874
|
+
() =>
|
|
875
|
+
reopenSuggestion(
|
|
876
|
+
contextOf({ params: { suggestionId: card.suggestionId } }),
|
|
877
|
+
event,
|
|
878
|
+
),
|
|
879
|
+
(error: unknown) => (error as { statusCode?: number }).statusCode === 400,
|
|
880
|
+
);
|
|
881
|
+
|
|
882
|
+
const untouched = await client.calendarSuggestion.get(
|
|
883
|
+
accountConfigId,
|
|
884
|
+
card.suggestionId,
|
|
885
|
+
);
|
|
886
|
+
assert.equal(untouched.state, CalendarSuggestionState.Superseded);
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
test("refuses a cancellation, whose answer it cannot undo", async () => {
|
|
890
|
+
const { accountConfigId, event } = anAccount();
|
|
891
|
+
const card = await putSuggestion(
|
|
892
|
+
accountConfigId,
|
|
893
|
+
"msg-reopen-cancel",
|
|
894
|
+
CalendarInviteMethod.Cancel,
|
|
895
|
+
);
|
|
896
|
+
await client.calendarSuggestion.settle(accountConfigId, card.suggestionId, {
|
|
897
|
+
state: CalendarSuggestionState.Dismissed,
|
|
898
|
+
acceptedCalendarObjectId: "",
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
await assert.rejects(
|
|
902
|
+
() =>
|
|
903
|
+
reopenSuggestion(
|
|
904
|
+
contextOf({ params: { suggestionId: card.suggestionId } }),
|
|
905
|
+
event,
|
|
906
|
+
),
|
|
907
|
+
(error: unknown) => (error as { statusCode?: number }).statusCode === 400,
|
|
908
|
+
);
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
test("answers not-found for a card on another account", async () => {
|
|
912
|
+
const stranger = anAccount();
|
|
913
|
+
const { card } = await answeredInto(
|
|
914
|
+
stranger.accountConfigId,
|
|
915
|
+
"msg-reopen-stranger",
|
|
916
|
+
CalendarInviteAnswer.Accepted,
|
|
917
|
+
);
|
|
918
|
+
const { event } = anAccount();
|
|
919
|
+
|
|
920
|
+
await assert.rejects(
|
|
921
|
+
() =>
|
|
922
|
+
reopenSuggestion(
|
|
923
|
+
contextOf({ params: { suggestionId: card.suggestionId } }),
|
|
924
|
+
event,
|
|
925
|
+
),
|
|
926
|
+
(error: unknown) => (error as { statusCode?: number }).statusCode === 404,
|
|
927
|
+
);
|
|
928
|
+
const untouched = await client.calendarSuggestion.get(
|
|
929
|
+
stranger.accountConfigId,
|
|
930
|
+
card.suggestionId,
|
|
931
|
+
);
|
|
932
|
+
assert.equal(untouched.state, CalendarSuggestionState.Accepted);
|
|
933
|
+
});
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
describe("POST /calendar-suggestions/{suggestionId}/reopen, against what else holds the event", () => {
|
|
937
|
+
const recurring = (sequence: number): string =>
|
|
938
|
+
INVITATION.replace(
|
|
939
|
+
"SUMMARY:Quarterly review",
|
|
940
|
+
`SEQUENCE:${sequence}\r\nRRULE:FREQ=WEEKLY;COUNT=4\r\nSUMMARY:Quarterly review`,
|
|
941
|
+
);
|
|
942
|
+
|
|
943
|
+
const putInvite = (
|
|
944
|
+
accountConfigId: string,
|
|
945
|
+
messageId: string,
|
|
946
|
+
sequence: number,
|
|
947
|
+
): Promise<CalendarSuggestionItem> =>
|
|
948
|
+
client.calendarSuggestion.put({
|
|
949
|
+
accountConfigId,
|
|
950
|
+
messageId,
|
|
951
|
+
bodyPartId: "part-1",
|
|
952
|
+
icalUid: "invite@example.test",
|
|
953
|
+
sequence,
|
|
954
|
+
method: CalendarInviteMethod.Request,
|
|
955
|
+
source: CalendarSuggestionSource.IcalendarPart,
|
|
956
|
+
summary: "Quarterly review",
|
|
957
|
+
dtStart: "2026-09-01T10:00:00+02:00",
|
|
958
|
+
dtEnd: "2026-09-01T11:00:00+02:00",
|
|
959
|
+
allDay: false,
|
|
960
|
+
location: "Room 4",
|
|
961
|
+
organizer: "organizer@example.test",
|
|
962
|
+
zoneCertainty: "Explicit",
|
|
963
|
+
icalData: recurring(sequence),
|
|
964
|
+
} as PutCalendarSuggestionInput);
|
|
965
|
+
|
|
966
|
+
const acceptInto = async (
|
|
967
|
+
accountConfigId: string,
|
|
968
|
+
calendarId: string,
|
|
969
|
+
suggestion: CalendarSuggestionItem,
|
|
970
|
+
): Promise<CalendarSuggestionItem> => {
|
|
971
|
+
const accepted = await acceptCalendarSuggestion(client.calendarUnitOfWork, {
|
|
972
|
+
accountConfigId,
|
|
973
|
+
calendarId,
|
|
974
|
+
suggestion,
|
|
975
|
+
attendee: "user@example.test",
|
|
976
|
+
answer: CalendarInviteAnswer.Accepted,
|
|
977
|
+
});
|
|
978
|
+
assert.ok(accepted.ok);
|
|
979
|
+
return accepted.value.suggestion;
|
|
980
|
+
};
|
|
981
|
+
|
|
982
|
+
const calendarOf = (accountConfigId: string) =>
|
|
983
|
+
client.calendarCollection.create({
|
|
984
|
+
accountConfigId,
|
|
985
|
+
urlSegment: "default",
|
|
986
|
+
displayName: "Calendar",
|
|
987
|
+
});
|
|
988
|
+
|
|
989
|
+
test("refuses a request's answer once its cancellation was answered into the event, and says so on the card", async () => {
|
|
990
|
+
const { accountConfigId, event } = anAccount();
|
|
991
|
+
const calendar = await calendarOf(accountConfigId);
|
|
992
|
+
const first = await acceptInto(
|
|
993
|
+
accountConfigId,
|
|
994
|
+
calendar.calendarId,
|
|
995
|
+
await putInvite(accountConfigId, "msg-overtaken-0", 0),
|
|
996
|
+
);
|
|
997
|
+
await acceptInto(
|
|
998
|
+
accountConfigId,
|
|
999
|
+
calendar.calendarId,
|
|
1000
|
+
await putSuggestion(
|
|
1001
|
+
accountConfigId,
|
|
1002
|
+
"msg-overtaken-cancel",
|
|
1003
|
+
CalendarInviteMethod.Cancel,
|
|
1004
|
+
),
|
|
1005
|
+
);
|
|
1006
|
+
|
|
1007
|
+
await assert.rejects(
|
|
1008
|
+
() =>
|
|
1009
|
+
reopenSuggestion(
|
|
1010
|
+
contextOf({ params: { suggestionId: first.suggestionId } }),
|
|
1011
|
+
event,
|
|
1012
|
+
),
|
|
1013
|
+
(error: unknown) => (error as { statusCode?: number }).statusCode === 400,
|
|
1014
|
+
);
|
|
1015
|
+
|
|
1016
|
+
assert.equal(
|
|
1017
|
+
(await client.calendarObject.listByCalendar(calendar.calendarId)).length,
|
|
1018
|
+
1,
|
|
1019
|
+
);
|
|
1020
|
+
const listed = (await listMessageSuggestions(
|
|
1021
|
+
contextOf({ params: { messageId: "msg-overtaken-0" } }),
|
|
1022
|
+
event,
|
|
1023
|
+
)) as unknown as { items: { answerOvertakenBy: string }[] };
|
|
1024
|
+
assert.deepEqual(
|
|
1025
|
+
listed.items.map((item) => item.answerOvertakenBy),
|
|
1026
|
+
[CalendarAnswerOvertaken.Cancellation],
|
|
1027
|
+
);
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
test("takes back both halves of a this-and-following split, and a re-add writes one event", async () => {
|
|
1031
|
+
const { accountConfigId, event } = anAccount();
|
|
1032
|
+
const calendar = await calendarOf(accountConfigId);
|
|
1033
|
+
const card = await acceptInto(
|
|
1034
|
+
accountConfigId,
|
|
1035
|
+
calendar.calendarId,
|
|
1036
|
+
await putInvite(accountConfigId, "msg-split", 0),
|
|
1037
|
+
);
|
|
1038
|
+
const split = await updateCalendarEventFor(
|
|
1039
|
+
calendarEventDepsOf(calendarDepsOf(client)),
|
|
1040
|
+
accountConfigId,
|
|
1041
|
+
{
|
|
1042
|
+
calendarId: calendar.calendarId,
|
|
1043
|
+
calendarObjectId: card.acceptedCalendarObjectId,
|
|
1044
|
+
scope: RecurrenceScope.Following,
|
|
1045
|
+
recurrenceId: "2026-09-15T08:00:00Z",
|
|
1046
|
+
ifMatch: undefined,
|
|
1047
|
+
},
|
|
1048
|
+
{ summary: "Quarterly review (moved)" },
|
|
1049
|
+
);
|
|
1050
|
+
assert.ok(split.ok, JSON.stringify(split));
|
|
1051
|
+
assert.equal(
|
|
1052
|
+
(await client.calendarObject.listByCalendar(calendar.calendarId)).length,
|
|
1053
|
+
2,
|
|
1054
|
+
);
|
|
1055
|
+
|
|
1056
|
+
const reopened = (await reopenSuggestion(
|
|
1057
|
+
contextOf({ params: { suggestionId: card.suggestionId } }),
|
|
1058
|
+
event,
|
|
1059
|
+
)) as unknown as CalendarSuggestionItem;
|
|
1060
|
+
|
|
1061
|
+
assert.equal(reopened.state, CalendarSuggestionState.Pending);
|
|
1062
|
+
assert.deepEqual(
|
|
1063
|
+
await client.calendarObject.listByCalendar(calendar.calendarId),
|
|
1064
|
+
[],
|
|
1065
|
+
);
|
|
1066
|
+
await acceptInto(accountConfigId, calendar.calendarId, {
|
|
1067
|
+
...card,
|
|
1068
|
+
...reopened,
|
|
1069
|
+
});
|
|
1070
|
+
assert.equal(
|
|
1071
|
+
(await client.calendarObject.listByCalendar(calendar.calendarId)).length,
|
|
1072
|
+
1,
|
|
1073
|
+
);
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
test("keeps the sender rule a mute wrote", async () => {
|
|
1077
|
+
const { accountConfigId, event } = anAccount();
|
|
1078
|
+
const card = await putSuggestion(accountConfigId, "msg-mute-reopen");
|
|
1079
|
+
await seedMessageFrom("msg-mute-reopen", "organizer@example.test");
|
|
1080
|
+
await dismissSuggestion(
|
|
1081
|
+
contextOf({
|
|
1082
|
+
params: { suggestionId: card.suggestionId },
|
|
1083
|
+
requestBody: { muteSender: true },
|
|
1084
|
+
}),
|
|
1085
|
+
event,
|
|
1086
|
+
);
|
|
1087
|
+
|
|
1088
|
+
const reopened = (await reopenSuggestion(
|
|
1089
|
+
contextOf({ params: { suggestionId: card.suggestionId } }),
|
|
1090
|
+
event,
|
|
1091
|
+
)) as unknown as Card;
|
|
1092
|
+
|
|
1093
|
+
assert.equal(reopened.state, CalendarSuggestionState.Pending);
|
|
1094
|
+
const rules = await client.filter.listByAccountAndState(
|
|
1095
|
+
accountConfigId,
|
|
1096
|
+
FilterState.Active,
|
|
1097
|
+
);
|
|
1098
|
+
assert.deepEqual(
|
|
1099
|
+
rules.map((rule) => rule.literalClauses),
|
|
1100
|
+
[[{ field: "From", value: "organizer@example.test" }]],
|
|
1101
|
+
);
|
|
1102
|
+
});
|
|
1103
|
+
|
|
1104
|
+
test("a delete that fails part-way leaves the event and the answer as they were", async () => {
|
|
1105
|
+
const { accountConfigId } = anAccount();
|
|
1106
|
+
const calendar = await calendarOf(accountConfigId);
|
|
1107
|
+
const card = await acceptInto(
|
|
1108
|
+
accountConfigId,
|
|
1109
|
+
calendar.calendarId,
|
|
1110
|
+
await putInvite(accountConfigId, "msg-rollback", 0),
|
|
1111
|
+
);
|
|
1112
|
+
const failing: ICalendarUnitOfWork = {
|
|
1113
|
+
transaction: (fn) =>
|
|
1114
|
+
client.calendarUnitOfWork.transaction((repos) =>
|
|
1115
|
+
fn({
|
|
1116
|
+
...repos,
|
|
1117
|
+
calendarCollection: Object.assign(
|
|
1118
|
+
Object.create(repos.calendarCollection),
|
|
1119
|
+
{
|
|
1120
|
+
bumpSyncSequence: async () => {
|
|
1121
|
+
throw new Error("disk full");
|
|
1122
|
+
},
|
|
1123
|
+
},
|
|
1124
|
+
),
|
|
1125
|
+
}),
|
|
1126
|
+
),
|
|
1127
|
+
};
|
|
1128
|
+
|
|
1129
|
+
await assert.rejects(
|
|
1130
|
+
() =>
|
|
1131
|
+
reopenCalendarSuggestion(failing, {
|
|
1132
|
+
accountConfigId,
|
|
1133
|
+
suggestion: card,
|
|
1134
|
+
}),
|
|
1135
|
+
/disk full/,
|
|
1136
|
+
);
|
|
1137
|
+
|
|
1138
|
+
const object = await client.calendarObject.find(
|
|
1139
|
+
calendar.calendarId,
|
|
1140
|
+
card.acceptedCalendarObjectId,
|
|
1141
|
+
);
|
|
1142
|
+
assert.ok(object, "the event is still on the calendar");
|
|
1143
|
+
assert.ok(
|
|
1144
|
+
(
|
|
1145
|
+
await client.calendarEventIndex.listForObject(
|
|
1146
|
+
calendar.calendarId,
|
|
1147
|
+
card.acceptedCalendarObjectId,
|
|
1148
|
+
)
|
|
1149
|
+
).length > 0,
|
|
1150
|
+
"and so are its occurrences",
|
|
1151
|
+
);
|
|
1152
|
+
const untouched = await client.calendarSuggestion.get(
|
|
1153
|
+
accountConfigId,
|
|
1154
|
+
card.suggestionId,
|
|
1155
|
+
);
|
|
1156
|
+
assert.equal(untouched.state, CalendarSuggestionState.Accepted);
|
|
1157
|
+
assert.equal(
|
|
1158
|
+
untouched.acceptedCalendarObjectId,
|
|
1159
|
+
card.acceptedCalendarObjectId,
|
|
1160
|
+
);
|
|
1161
|
+
});
|
|
1162
|
+
});
|
|
@@ -5,13 +5,16 @@ import type {
|
|
|
5
5
|
} from "@remit/api-openapi-types";
|
|
6
6
|
import {
|
|
7
7
|
acceptCalendarSuggestion,
|
|
8
|
+
answersOvertakenBy,
|
|
8
9
|
correctCalendarSuggestion,
|
|
10
|
+
reopenCalendarSuggestion,
|
|
9
11
|
} from "@remit/calendar-service";
|
|
10
12
|
import {
|
|
11
13
|
type CalendarSuggestionItem,
|
|
12
14
|
type ICalendarSuggestionRepository,
|
|
13
15
|
type IEnvelopeRepository,
|
|
14
16
|
type IFilterRepository,
|
|
17
|
+
type IThreadMessageRepository,
|
|
15
18
|
isSenderMuted,
|
|
16
19
|
} from "@remit/data-ports";
|
|
17
20
|
import { BadRequestError } from "@remit/data-ports/errors";
|
|
@@ -43,8 +46,51 @@ import { pickEventUpdate } from "./calendar-event.js";
|
|
|
43
46
|
*/
|
|
44
47
|
export const toCalendarSuggestionResponse = (
|
|
45
48
|
item: CalendarSuggestionItem,
|
|
49
|
+
answerOvertakenBy: CalendarSuggestionResponse["answerOvertakenBy"],
|
|
46
50
|
): CalendarSuggestionResponse => {
|
|
47
51
|
const { icalData: _icalData, ...response } = item;
|
|
52
|
+
return { ...response, supersededByThreadId: "", answerOvertakenBy };
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const toCalendarSuggestionResponses = async (
|
|
56
|
+
deps: {
|
|
57
|
+
threadMessage: Pick<IThreadMessageRepository, "findByMessageId">;
|
|
58
|
+
calendarSuggestion: Pick<
|
|
59
|
+
ICalendarSuggestionRepository,
|
|
60
|
+
"listByAcceptedCalendarObjects"
|
|
61
|
+
>;
|
|
62
|
+
},
|
|
63
|
+
accountConfigId: string,
|
|
64
|
+
items: CalendarSuggestionItem[],
|
|
65
|
+
): Promise<CalendarSuggestionResponse[]> =>
|
|
66
|
+
Promise.all(
|
|
67
|
+
(
|
|
68
|
+
await answersOvertakenBy(deps.calendarSuggestion, accountConfigId, items)
|
|
69
|
+
).map(async ({ suggestion, answerOvertakenBy }) => {
|
|
70
|
+
const response = toCalendarSuggestionResponse(
|
|
71
|
+
suggestion,
|
|
72
|
+
answerOvertakenBy,
|
|
73
|
+
);
|
|
74
|
+
if (suggestion.supersededByMessageId === "") return response;
|
|
75
|
+
const newer = await deps.threadMessage.findByMessageId(
|
|
76
|
+
accountConfigId,
|
|
77
|
+
suggestion.supersededByMessageId,
|
|
78
|
+
);
|
|
79
|
+
return { ...response, supersededByThreadId: newer?.threadId ?? "" };
|
|
80
|
+
}),
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const respondOne = async (
|
|
84
|
+
client: RemitClient,
|
|
85
|
+
accountConfigId: string,
|
|
86
|
+
item: CalendarSuggestionItem,
|
|
87
|
+
): Promise<CalendarSuggestionResponse> => {
|
|
88
|
+
const [response] = await toCalendarSuggestionResponses(
|
|
89
|
+
client,
|
|
90
|
+
accountConfigId,
|
|
91
|
+
[item],
|
|
92
|
+
);
|
|
93
|
+
if (!response) throw new Error("unreachable: one item in, one response out");
|
|
48
94
|
return response;
|
|
49
95
|
};
|
|
50
96
|
|
|
@@ -177,7 +223,11 @@ export const CalendarSuggestionOperations: Record<
|
|
|
177
223
|
);
|
|
178
224
|
|
|
179
225
|
return {
|
|
180
|
-
items:
|
|
226
|
+
items: await toCalendarSuggestionResponses(
|
|
227
|
+
client,
|
|
228
|
+
accountConfigId,
|
|
229
|
+
page.items,
|
|
230
|
+
),
|
|
181
231
|
continuationToken: page.continuationToken,
|
|
182
232
|
};
|
|
183
233
|
},
|
|
@@ -202,7 +252,11 @@ export const MessageCalendarSuggestionOperations: Record<
|
|
|
202
252
|
);
|
|
203
253
|
|
|
204
254
|
return {
|
|
205
|
-
items:
|
|
255
|
+
items: await toCalendarSuggestionResponses(
|
|
256
|
+
client,
|
|
257
|
+
accountConfigId,
|
|
258
|
+
items,
|
|
259
|
+
),
|
|
206
260
|
continuationToken: undefined,
|
|
207
261
|
};
|
|
208
262
|
},
|
|
@@ -286,7 +340,7 @@ export const CalendarSuggestionActionOperations: Record<
|
|
|
286
340
|
throw new BadRequestError(accepted.error.message);
|
|
287
341
|
}
|
|
288
342
|
|
|
289
|
-
return
|
|
343
|
+
return respondOne(client, accountConfigId, accepted.value.suggestion);
|
|
290
344
|
},
|
|
291
345
|
|
|
292
346
|
CalendarSuggestionActionOperations_declineCalendarSuggestion: async (
|
|
@@ -307,7 +361,7 @@ export const CalendarSuggestionActionOperations: Record<
|
|
|
307
361
|
CalendarSuggestionState.Declined,
|
|
308
362
|
);
|
|
309
363
|
|
|
310
|
-
return
|
|
364
|
+
return respondOne(client, accountConfigId, declined);
|
|
311
365
|
},
|
|
312
366
|
|
|
313
367
|
CalendarSuggestionActionOperations_dismissCalendarSuggestion: async (
|
|
@@ -334,6 +388,32 @@ export const CalendarSuggestionActionOperations: Record<
|
|
|
334
388
|
await muteSender(client, accountConfigId, dismissed.messageId);
|
|
335
389
|
}
|
|
336
390
|
|
|
337
|
-
return
|
|
391
|
+
return respondOne(client, accountConfigId, dismissed);
|
|
392
|
+
},
|
|
393
|
+
|
|
394
|
+
CalendarSuggestionActionOperations_reopenCalendarSuggestion: async (
|
|
395
|
+
context,
|
|
396
|
+
...args: unknown[]
|
|
397
|
+
) => {
|
|
398
|
+
const event = args[0] as APIGatewayProxyEvent;
|
|
399
|
+
const accountConfigId = getAccountConfigIdFromEvent(event);
|
|
400
|
+
const { suggestionId } = context.request.params as {
|
|
401
|
+
suggestionId: string;
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
const client = await getClient();
|
|
405
|
+
const suggestion = await client.calendarSuggestion.get(
|
|
406
|
+
accountConfigId,
|
|
407
|
+
suggestionId,
|
|
408
|
+
);
|
|
409
|
+
const reopened = await reopenCalendarSuggestion(client.calendarUnitOfWork, {
|
|
410
|
+
accountConfigId,
|
|
411
|
+
suggestion,
|
|
412
|
+
});
|
|
413
|
+
if (!reopened.ok) {
|
|
414
|
+
throw new BadRequestError(reopened.error.message);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return respondOne(client, accountConfigId, reopened.value);
|
|
338
418
|
},
|
|
339
419
|
};
|
package/src/types.ts
CHANGED
|
@@ -88,7 +88,8 @@ export type OperationIds =
|
|
|
88
88
|
| "MessageCalendarSuggestionOperations_listMessageCalendarSuggestions"
|
|
89
89
|
| "CalendarSuggestionActionOperations_acceptCalendarSuggestion"
|
|
90
90
|
| "CalendarSuggestionActionOperations_declineCalendarSuggestion"
|
|
91
|
-
| "CalendarSuggestionActionOperations_dismissCalendarSuggestion"
|
|
91
|
+
| "CalendarSuggestionActionOperations_dismissCalendarSuggestion"
|
|
92
|
+
| "CalendarSuggestionActionOperations_reopenCalendarSuggestion";
|
|
92
93
|
|
|
93
94
|
export type MeOperationIds = MatchPrefix<"MeOperations_", OperationIds>;
|
|
94
95
|
|