This repository has no description
1import { Result, ok, err } from '../../../../shared/core/Result';
2import { Card } from '../Card';
3import { Collection } from '../Collection';
4import { CuratorId } from '../value-objects/CuratorId';
5import { CollectionId } from '../value-objects/CollectionId';
6import { ICollectionRepository } from '../ICollectionRepository';
7import { ICollectionPublisher } from '../../application/ports/ICollectionPublisher';
8import { AppError } from '../../../../shared/core/AppError';
9import { DomainService } from '../../../../shared/domain/DomainService';
10import { AuthenticationError } from '../../../../shared/core/AuthenticationError';
11
12export interface CardCollectionServiceOptions {
13 skipPublishing?: boolean;
14 publishedRecordIds?: Map<string, PublishedRecordId>; // collectionId -> publishedRecordId
15}
16
17export class CardCollectionValidationError extends Error {
18 constructor(message: string) {
19 super(message);
20 this.name = 'CardCollectionValidationError';
21 }
22}
23
24export class CardCollectionService implements DomainService {
25 constructor(
26 private collectionRepository: ICollectionRepository,
27 private collectionPublisher: ICollectionPublisher,
28 ) {}
29
30 async addCardToCollection(
31 card: Card,
32 collectionId: CollectionId,
33 curatorId: CuratorId,
34 options?: CardCollectionServiceOptions,
35 ): Promise<
36 Result<
37 Collection,
38 | CardCollectionValidationError
39 | AuthenticationError
40 | AppError.UnexpectedError
41 >
42 > {
43 try {
44 // Find the collection
45 const collectionResult =
46 await this.collectionRepository.findById(collectionId);
47 if (collectionResult.isErr()) {
48 return err(AppError.UnexpectedError.create(collectionResult.error));
49 }
50
51 const collection = collectionResult.value;
52 if (!collection) {
53 return err(
54 new CardCollectionValidationError(
55 `Collection not found: ${collectionId.getStringValue()}`,
56 ),
57 );
58 }
59
60 // Add card to collection
61 const addCardResult = collection.addCard(card.cardId, curatorId);
62 if (addCardResult.isErr()) {
63 return err(
64 new CardCollectionValidationError(
65 `Failed to add card to collection: ${addCardResult.error.message}`,
66 ),
67 );
68 }
69
70 // Handle publishing based on options
71 if (options?.skipPublishing && options?.publishedRecordIds) {
72 const publishedRecordId = options.publishedRecordIds.get(collectionId.getStringValue());
73 if (publishedRecordId) {
74 // Skip publishing and use provided record ID
75 collection.markCardLinkAsPublished(card.cardId, publishedRecordId);
76 }
77 } else {
78 // Publish the collection link normally
79 const publishLinkResult =
80 await this.collectionPublisher.publishCardAddedToCollection(
81 card,
82 collection,
83 curatorId,
84 );
85 if (publishLinkResult.isErr()) {
86 // Propagate authentication errors
87 if (publishLinkResult.error instanceof AuthenticationError) {
88 return err(publishLinkResult.error);
89 }
90 return err(
91 new CardCollectionValidationError(
92 `Failed to publish collection link: ${publishLinkResult.error.message}`,
93 ),
94 );
95 }
96
97 // Mark the card link as published in the collection
98 collection.markCardLinkAsPublished(card.cardId, publishLinkResult.value);
99 }
100
101 // Save the updated collection
102 const saveCollectionResult =
103 await this.collectionRepository.save(collection);
104 if (saveCollectionResult.isErr()) {
105 return err(AppError.UnexpectedError.create(saveCollectionResult.error));
106 }
107
108 return ok(collection);
109 } catch (error) {
110 return err(AppError.UnexpectedError.create(error));
111 }
112 }
113
114 async addCardToCollections(
115 card: Card,
116 collectionIds: CollectionId[],
117 curatorId: CuratorId,
118 options?: CardCollectionServiceOptions,
119 ): Promise<
120 Result<
121 Collection[],
122 | CardCollectionValidationError
123 | AuthenticationError
124 | AppError.UnexpectedError
125 >
126 > {
127 const updatedCollections: Collection[] = [];
128
129 for (const collectionId of collectionIds) {
130 const result = await this.addCardToCollection(
131 card,
132 collectionId,
133 curatorId,
134 options,
135 );
136 if (result.isErr()) {
137 return err(result.error);
138 }
139 updatedCollections.push(result.value);
140 }
141 return ok(updatedCollections);
142 }
143
144 async removeCardFromCollection(
145 card: Card,
146 collectionId: CollectionId,
147 curatorId: CuratorId,
148 options?: CardCollectionServiceOptions,
149 ): Promise<
150 Result<
151 Collection | null,
152 | CardCollectionValidationError
153 | AuthenticationError
154 | AppError.UnexpectedError
155 >
156 > {
157 try {
158 // Find the collection
159 const collectionResult =
160 await this.collectionRepository.findById(collectionId);
161 if (collectionResult.isErr()) {
162 return err(AppError.UnexpectedError.create(collectionResult.error));
163 }
164
165 const collection = collectionResult.value;
166 if (!collection) {
167 return err(
168 new CardCollectionValidationError(
169 `Collection not found: ${collectionId.getStringValue()}`,
170 ),
171 );
172 }
173
174 // Check if card is in collection
175 const cardLink = collection.cardLinks.find((link) =>
176 link.cardId.equals(card.cardId),
177 );
178 if (!cardLink) {
179 // Card is not in collection, nothing to do
180 return ok(null);
181 }
182
183 // Handle unpublishing based on options
184 if (!options?.skipPublishing && cardLink.publishedRecordId) {
185 const unpublishLinkResult =
186 await this.collectionPublisher.unpublishCardAddedToCollection(
187 cardLink.publishedRecordId,
188 );
189 if (unpublishLinkResult.isErr()) {
190 // Propagate authentication errors
191 if (unpublishLinkResult.error instanceof AuthenticationError) {
192 return err(unpublishLinkResult.error);
193 }
194 return err(
195 new CardCollectionValidationError(
196 `Failed to unpublish collection link: ${unpublishLinkResult.error.message}`,
197 ),
198 );
199 }
200 }
201
202 // Remove card from collection
203 const removeCardResult = collection.removeCard(card.cardId, curatorId);
204 if (removeCardResult.isErr()) {
205 return err(
206 new CardCollectionValidationError(
207 `Failed to remove card from collection: ${removeCardResult.error.message}`,
208 ),
209 );
210 }
211
212 // Save the updated collection
213 const saveCollectionResult =
214 await this.collectionRepository.save(collection);
215 if (saveCollectionResult.isErr()) {
216 return err(AppError.UnexpectedError.create(saveCollectionResult.error));
217 }
218
219 return ok(collection);
220 } catch (error) {
221 return err(AppError.UnexpectedError.create(error));
222 }
223 }
224
225 async removeCardFromCollections(
226 card: Card,
227 collectionIds: CollectionId[],
228 curatorId: CuratorId,
229 options?: CardCollectionServiceOptions,
230 ): Promise<
231 Result<
232 Collection[],
233 | CardCollectionValidationError
234 | AuthenticationError
235 | AppError.UnexpectedError
236 >
237 > {
238 const updatedCollections: Collection[] = [];
239
240 for (const collectionId of collectionIds) {
241 const result = await this.removeCardFromCollection(
242 card,
243 collectionId,
244 curatorId,
245 options,
246 );
247 if (result.isErr()) {
248 return err(result.error);
249 }
250 if (result.value !== null) {
251 updatedCollections.push(result.value);
252 }
253 }
254 return ok(updatedCollections);
255 }
256}