@remit/backend 0.0.135 → 0.0.137

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remit/backend",
3
- "version": "0.0.135",
3
+ "version": "0.0.137",
4
4
  "description": "Remit Mail Inspector API backend",
5
5
  "license": "MIT",
6
6
  "author": "",
@@ -29,6 +29,7 @@ import {
29
29
  assertSettleable,
30
30
  CalendarSuggestionActionOperations,
31
31
  CalendarSuggestionOperations,
32
+ MessageCalendarSuggestionOperations,
32
33
  type MuteSenderDeps,
33
34
  muteSender,
34
35
  settleSuggestion,
@@ -58,6 +59,7 @@ const suggestion = (
58
59
  zoneCertainty: "Explicit",
59
60
  icalData: "BEGIN:VCALENDAR\r\nEND:VCALENDAR\r\n",
60
61
  acceptedCalendarObjectId: "",
62
+ supersededByMessageId: "",
61
63
  createdAt: 1,
62
64
  updatedAt: 1,
63
65
  ...overrides,
@@ -308,12 +310,38 @@ const acceptSuggestion =
308
310
  CalendarSuggestionActionOperations.CalendarSuggestionActionOperations_acceptCalendarSuggestion as Handler;
309
311
  const dismissSuggestion =
310
312
  CalendarSuggestionActionOperations.CalendarSuggestionActionOperations_dismissCalendarSuggestion as Handler;
313
+ const listMessageSuggestions =
314
+ MessageCalendarSuggestionOperations.MessageCalendarSuggestionOperations_listMessageCalendarSuggestions as Handler;
311
315
 
312
316
  interface Card {
313
317
  suggestionId: string;
314
318
  state: string;
319
+ supersededByMessageId: string;
320
+ supersededByThreadId: string;
315
321
  }
316
322
 
323
+ const fileInThread = async (
324
+ accountConfigId: string,
325
+ messageId: string,
326
+ threadId: string,
327
+ ): Promise<void> => {
328
+ await client.threadMessage.create({
329
+ accountConfigId,
330
+ threadId,
331
+ messageId,
332
+ mailboxId: "mbx-inbox",
333
+ uid: 1,
334
+ referenceOrder: 0,
335
+ internalDate: 1,
336
+ sentDate: 1,
337
+ subject: "Invitation: Quarterly review",
338
+ isRead: false,
339
+ isDeleted: false,
340
+ hasAttachment: false,
341
+ hasStars: false,
342
+ });
343
+ };
344
+
317
345
  let client: RemitClient;
318
346
  let cleanup: () => void;
319
347
  let mintedSubs = 0;
@@ -473,6 +501,74 @@ describe("GET /calendar-suggestions", () => {
473
501
  });
474
502
  });
475
503
 
504
+ describe("GET /messages/{messageId}/calendar-suggestions", () => {
505
+ test("names the conversation of the revision that retired a card", async () => {
506
+ const { accountConfigId, event } = anAccount();
507
+ const older = await putSuggestion(accountConfigId, "msg-revision-0");
508
+ await client.calendarSuggestion.supersedeIfPending(
509
+ accountConfigId,
510
+ older.suggestionId,
511
+ "msg-revision-1",
512
+ );
513
+ await fileInThread(accountConfigId, "msg-revision-1", "thread-revision-1");
514
+
515
+ const listed = (await listMessageSuggestions(
516
+ contextOf({ params: { messageId: "msg-revision-0" } }),
517
+ event,
518
+ )) as unknown as { items: Card[] };
519
+
520
+ assert.deepEqual(
521
+ listed.items.map((card) => [
522
+ card.state,
523
+ card.supersededByMessageId,
524
+ card.supersededByThreadId,
525
+ ]),
526
+ [
527
+ [
528
+ CalendarSuggestionState.Superseded,
529
+ "msg-revision-1",
530
+ "thread-revision-1",
531
+ ],
532
+ ],
533
+ );
534
+ });
535
+
536
+ test("names no conversation when the newer message sits in none", async () => {
537
+ const { accountConfigId, event } = anAccount();
538
+ const older = await putSuggestion(accountConfigId, "msg-orphan-0");
539
+ await client.calendarSuggestion.supersedeIfPending(
540
+ accountConfigId,
541
+ older.suggestionId,
542
+ "msg-orphan-1",
543
+ );
544
+
545
+ const listed = (await listMessageSuggestions(
546
+ contextOf({ params: { messageId: "msg-orphan-0" } }),
547
+ event,
548
+ )) as unknown as { items: Card[] };
549
+
550
+ assert.equal(listed.items[0]?.supersededByThreadId, "");
551
+ });
552
+
553
+ test("names no conversation for a card nothing retired", async () => {
554
+ const { accountConfigId, event } = anAccount();
555
+ await putSuggestion(accountConfigId, "msg-current");
556
+
557
+ const listed = (await listMessageSuggestions(
558
+ contextOf({ params: { messageId: "msg-current" } }),
559
+ event,
560
+ )) as unknown as { items: Card[] };
561
+
562
+ assert.deepEqual(
563
+ listed.items.map((card) => [
564
+ card.supersededByMessageId,
565
+ card.supersededByThreadId,
566
+ ]),
567
+ [["", ""]],
568
+ );
569
+ });
570
+ });
571
+
476
572
  describe("POST /calendar-suggestions/{suggestionId}/accept", () => {
477
573
  test("answers not-found for a calendar on another account, before writing anything", async () => {
478
574
  const stranger = anAccount();
@@ -597,3 +693,74 @@ describe("POST /calendar-suggestions/{suggestionId}/dismiss", () => {
597
693
  );
598
694
  });
599
695
  });
696
+
697
+ describe("POST /calendar-suggestions/{suggestionId}/accept with corrections", () => {
698
+ test("answers not-found for a calendar on another account, before writing anything", async () => {
699
+ const stranger = anAccount();
700
+ const strangersCalendar = await client.calendarCollection.create({
701
+ accountConfigId: stranger.accountConfigId,
702
+ urlSegment: "default",
703
+ displayName: "Calendar",
704
+ });
705
+ const { accountConfigId, event } = anAccount();
706
+ const card = await putSuggestion(accountConfigId, "msg-edited-cross");
707
+
708
+ await assert.rejects(
709
+ () =>
710
+ acceptSuggestion(
711
+ contextOf({
712
+ params: { suggestionId: card.suggestionId },
713
+ requestBody: {
714
+ calendarId: strangersCalendar.calendarId,
715
+ event: { summary: "Quarterly review, moved" },
716
+ },
717
+ }),
718
+ event,
719
+ ),
720
+ (error: unknown) => (error as { statusCode?: number }).statusCode === 404,
721
+ );
722
+
723
+ assert.deepEqual(
724
+ await client.calendarObject.listByCalendar(strangersCalendar.calendarId),
725
+ [],
726
+ );
727
+ const untouched = await client.calendarSuggestion.get(
728
+ accountConfigId,
729
+ card.suggestionId,
730
+ );
731
+ assert.equal(untouched.state, CalendarSuggestionState.Pending);
732
+ });
733
+
734
+ test("refuses a card the reader already declined", async () => {
735
+ const { accountConfigId, event } = anAccount();
736
+ const calendar = await client.calendarCollection.create({
737
+ accountConfigId,
738
+ urlSegment: "default",
739
+ displayName: "Calendar",
740
+ });
741
+ const card = await putSuggestion(accountConfigId, "msg-edited-declined");
742
+ await client.calendarSuggestion.settle(accountConfigId, card.suggestionId, {
743
+ state: CalendarSuggestionState.Declined,
744
+ acceptedCalendarObjectId: "",
745
+ });
746
+
747
+ await assert.rejects(
748
+ () =>
749
+ acceptSuggestion(
750
+ contextOf({
751
+ params: { suggestionId: card.suggestionId },
752
+ requestBody: {
753
+ calendarId: calendar.calendarId,
754
+ event: { summary: "Quarterly review, moved" },
755
+ },
756
+ }),
757
+ event,
758
+ ),
759
+ (error: unknown) => (error as { statusCode?: number }).statusCode === 400,
760
+ );
761
+ assert.deepEqual(
762
+ await client.calendarObject.listByCalendar(calendar.calendarId),
763
+ [],
764
+ );
765
+ });
766
+ });
@@ -1,13 +1,18 @@
1
1
  import type {
2
2
  CalendarInviteAnswer as CalendarInviteAnswerValue,
3
3
  CalendarSuggestionResponse,
4
+ UpdateCalendarEventInput,
4
5
  } from "@remit/api-openapi-types";
5
- import { acceptCalendarSuggestion } from "@remit/calendar-service";
6
+ import {
7
+ acceptCalendarSuggestion,
8
+ correctCalendarSuggestion,
9
+ } from "@remit/calendar-service";
6
10
  import {
7
11
  type CalendarSuggestionItem,
8
12
  type ICalendarSuggestionRepository,
9
13
  type IEnvelopeRepository,
10
14
  type IFilterRepository,
15
+ type IThreadMessageRepository,
11
16
  isSenderMuted,
12
17
  } from "@remit/data-ports";
13
18
  import { BadRequestError } from "@remit/data-ports/errors";
@@ -30,6 +35,7 @@ import type {
30
35
  MessageCalendarSuggestionOperationIds,
31
36
  OperationHandler,
32
37
  } from "../types.js";
38
+ import { pickEventUpdate } from "./calendar-event.js";
33
39
 
34
40
  /**
35
41
  * The raw invitation bytes stay on the server. A client renders the projected
@@ -40,9 +46,26 @@ export const toCalendarSuggestionResponse = (
40
46
  item: CalendarSuggestionItem,
41
47
  ): CalendarSuggestionResponse => {
42
48
  const { icalData: _icalData, ...response } = item;
43
- return response;
49
+ return { ...response, supersededByThreadId: "" };
44
50
  };
45
51
 
52
+ export const toCalendarSuggestionResponses = (
53
+ threads: Pick<IThreadMessageRepository, "findByMessageId">,
54
+ accountConfigId: string,
55
+ items: CalendarSuggestionItem[],
56
+ ): Promise<CalendarSuggestionResponse[]> =>
57
+ Promise.all(
58
+ items.map(async (item) => {
59
+ const response = toCalendarSuggestionResponse(item);
60
+ if (item.supersededByMessageId === "") return response;
61
+ const newer = await threads.findByMessageId(
62
+ accountConfigId,
63
+ item.supersededByMessageId,
64
+ );
65
+ return { ...response, supersededByThreadId: newer?.threadId ?? "" };
66
+ }),
67
+ );
68
+
46
69
  /**
47
70
  * The states a person can move a pending card into. `Superseded` is the
48
71
  * producer's alone — a revision retires a card, a person never does — and the
@@ -172,7 +195,11 @@ export const CalendarSuggestionOperations: Record<
172
195
  );
173
196
 
174
197
  return {
175
- items: page.items.map(toCalendarSuggestionResponse),
198
+ items: await toCalendarSuggestionResponses(
199
+ client.threadMessage,
200
+ accountConfigId,
201
+ page.items,
202
+ ),
176
203
  continuationToken: page.continuationToken,
177
204
  };
178
205
  },
@@ -197,7 +224,11 @@ export const MessageCalendarSuggestionOperations: Record<
197
224
  );
198
225
 
199
226
  return {
200
- items: items.map(toCalendarSuggestionResponse),
227
+ items: await toCalendarSuggestionResponses(
228
+ client.threadMessage,
229
+ accountConfigId,
230
+ items,
231
+ ),
201
232
  continuationToken: undefined,
202
233
  };
203
234
  },
@@ -255,10 +286,25 @@ export const CalendarSuggestionActionOperations: Record<
255
286
  );
256
287
  }
257
288
 
289
+ const { event: corrections } = context.request.requestBody as {
290
+ event?: UpdateCalendarEventInput;
291
+ };
292
+ const corrected =
293
+ corrections === undefined
294
+ ? { ok: true as const, value: suggestion }
295
+ : await correctCalendarSuggestion(
296
+ suggestion,
297
+ pickEventUpdate(corrections),
298
+ collection.timezone,
299
+ );
300
+ if (!corrected.ok) {
301
+ throw new BadRequestError(corrected.error.message);
302
+ }
303
+
258
304
  const accepted = await acceptCalendarSuggestion(client.calendarUnitOfWork, {
259
305
  accountConfigId,
260
306
  calendarId,
261
- suggestion,
307
+ suggestion: corrected.value,
262
308
  attendee: await accountEmailForMessage(client, suggestion.messageId),
263
309
  answer,
264
310
  });