yapi-seller-service 0.0.0-alpha1
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/README.md +26 -0
- package/index.js +1621 -0
- package/package.json +1 -0
package/index.js
ADDED
@@ -0,0 +1,1621 @@
|
|
1
|
+
// index.js
|
2
|
+
/**
|
3
|
+
* @file index.js
|
4
|
+
* @description A simulated service layer for managing seller operations within the Yapi platform.
|
5
|
+
* This package provides functionalities for seller profile management, product listing and management,
|
6
|
+
* order viewing, and basic inventory updates, simulating interactions with a backend API.
|
7
|
+
* All data storage and API interactions are simulated in-memory using standard JavaScript features.
|
8
|
+
* This code is intended to mimic the structure and complexity of a real service package,
|
9
|
+
* including validation, error handling, and asynchronous operations.
|
10
|
+
*/
|
11
|
+
|
12
|
+
// --- Simulated Data Storage (In-memory) ---
|
13
|
+
// These variables simulate a backend database for a specific seller.
|
14
|
+
// In a real application, these would be API calls to a server.
|
15
|
+
|
16
|
+
/** @type {SellerProfile|null} */
|
17
|
+
let _sellerProfile = null;
|
18
|
+
|
19
|
+
/** @type {Product[]} */
|
20
|
+
let _products = [];
|
21
|
+
|
22
|
+
/** @type {Order[]} */
|
23
|
+
let _orders = [];
|
24
|
+
|
25
|
+
// --- Constants and Configuration (Simulated) ---
|
26
|
+
|
27
|
+
const SIMULATED_API_MIN_DELAY_MS = 100;
|
28
|
+
const SIMULATED_API_MAX_DELAY_MS = 500;
|
29
|
+
const SIMULATED_API_ERROR_CHANCE = 0.1; // 10% chance of simulated network/connection error
|
30
|
+
|
31
|
+
const PRODUCT_STATUSES = ['draft', 'active', 'archived'];
|
32
|
+
const ORDER_STATUSES = ['pending', 'processing', 'shipped', 'delivered', 'cancelled', 'returned'];
|
33
|
+
|
34
|
+
// --- Data Structure Classes (Simulated Models) ---
|
35
|
+
|
36
|
+
/**
|
37
|
+
* Represents a Seller Profile on the Yapi platform.
|
38
|
+
* @typedef {object} SellerProfile
|
39
|
+
* @property {string} id - Unique identifier for the seller.
|
40
|
+
* @property {string} name - The seller's name or business name.
|
41
|
+
* @property {string} email - Seller's contact email.
|
42
|
+
* @property {string} phone - Seller's contact phone number.
|
43
|
+
* @property {string} address - Seller's primary address.
|
44
|
+
* @property {string} [description] - Optional description of the seller or business.
|
45
|
+
* @property {Date} createdAt - Timestamp when the profile was created.
|
46
|
+
* @property {Date} updatedAt - Timestamp when the profile was last updated.
|
47
|
+
*/
|
48
|
+
class SellerProfile {
|
49
|
+
/**
|
50
|
+
* @param {object} data - Initial data for the seller profile.
|
51
|
+
* @param {string} data.id - Unique identifier.
|
52
|
+
* @param {string} data.name - Seller name.
|
53
|
+
* @param {string} data.email - Seller email.
|
54
|
+
* @param {string} data.phone - Seller phone.
|
55
|
+
* @param {string} data.address - Seller address.
|
56
|
+
* @param {string} [data.description] - Optional description.
|
57
|
+
* @param {Date} [data.createdAt=new Date()] - Creation timestamp.
|
58
|
+
* @param {Date} [data.updatedAt=new Date()] - Update timestamp.
|
59
|
+
*/
|
60
|
+
constructor({ id, name, email, phone, address, description = '', createdAt = new Date(), updatedAt = new Date() }) {
|
61
|
+
if (!id || !name || !email || !phone || !address) {
|
62
|
+
throw new Error("SellerProfile requires id, name, email, phone, and address.");
|
63
|
+
}
|
64
|
+
this.id = id;
|
65
|
+
this.name = name;
|
66
|
+
this.email = email;
|
67
|
+
this.phone = phone;
|
68
|
+
this.address = address;
|
69
|
+
this.description = description;
|
70
|
+
this.createdAt = new Date(createdAt); // Ensure Date object
|
71
|
+
this.updatedAt = new Date(updatedAt); // Ensure Date object
|
72
|
+
}
|
73
|
+
|
74
|
+
/**
|
75
|
+
* Updates profile data.
|
76
|
+
* @param {object} updateData - Data to update.
|
77
|
+
*/
|
78
|
+
update(updateData) {
|
79
|
+
Object.assign(this, updateData);
|
80
|
+
this.updatedAt = new Date();
|
81
|
+
}
|
82
|
+
|
83
|
+
/**
|
84
|
+
* Returns a plain object representation.
|
85
|
+
* @returns {object}
|
86
|
+
*/
|
87
|
+
toObject() {
|
88
|
+
return {
|
89
|
+
id: this.id,
|
90
|
+
name: this.name,
|
91
|
+
email: this.email,
|
92
|
+
phone: this.phone,
|
93
|
+
address: this.address,
|
94
|
+
description: this.description,
|
95
|
+
createdAt: this.createdAt,
|
96
|
+
updatedAt: this.updatedAt,
|
97
|
+
};
|
98
|
+
}
|
99
|
+
}
|
100
|
+
|
101
|
+
/**
|
102
|
+
* Represents a Product listed by the seller.
|
103
|
+
* @typedef {object} Product
|
104
|
+
* @property {string} id - Unique identifier for the product.
|
105
|
+
* @property {string} sellerId - The ID of the seller who owns this product.
|
106
|
+
* @property {string} title - Product title.
|
107
|
+
* @property {string} [description] - Optional product description.
|
108
|
+
* @property {number} price - Product price (e.g., in currency units).
|
109
|
+
* @property {number} stock - Current stock quantity.
|
110
|
+
* @property {string} [imageUrl] - URL of the product image.
|
111
|
+
* @property {string} [category] - Product category.
|
112
|
+
* @property {string} status - Product status ('draft', 'active', 'archived').
|
113
|
+
* @property {Date} createdAt - Timestamp when the product was created.
|
114
|
+
* @property {Date} updatedAt - Timestamp when the product was last updated.
|
115
|
+
*/
|
116
|
+
class Product {
|
117
|
+
/**
|
118
|
+
* @param {object} data - Initial data for the product.
|
119
|
+
* @param {string} data.id - Unique identifier.
|
120
|
+
* @param {string} data.sellerId - Seller ID.
|
121
|
+
* @param {string} data.title - Product title.
|
122
|
+
* @param {number} data.price - Product price.
|
123
|
+
* @param {number} data.stock - Stock quantity.
|
124
|
+
* @param {string} [data.description] - Description.
|
125
|
+
* @param {string} [data.imageUrl] - Image URL.
|
126
|
+
* @param {string} [data.category] - Category.
|
127
|
+
* @param {string} [data.status='draft'] - Product status.
|
128
|
+
* @param {Date} [data.createdAt=new Date()] - Creation timestamp.
|
129
|
+
* @param {Date} [data.updatedAt=new Date()] - Update timestamp.
|
130
|
+
*/
|
131
|
+
constructor({
|
132
|
+
id,
|
133
|
+
sellerId,
|
134
|
+
title,
|
135
|
+
description = '',
|
136
|
+
price,
|
137
|
+
stock,
|
138
|
+
imageUrl = '',
|
139
|
+
category = 'uncategorized',
|
140
|
+
status = 'draft',
|
141
|
+
createdAt = new Date(),
|
142
|
+
updatedAt = new Date()
|
143
|
+
}) {
|
144
|
+
if (!id || !sellerId || !title || price === undefined || stock === undefined) {
|
145
|
+
throw new Error("Product requires id, sellerId, title, price, and stock.");
|
146
|
+
}
|
147
|
+
if (!PRODUCT_STATUSES.includes(status)) {
|
148
|
+
throw new Error(`Invalid product status: ${status}. Must be one of ${PRODUCT_STATUSES.join(', ')}.`);
|
149
|
+
}
|
150
|
+
|
151
|
+
this.id = id;
|
152
|
+
this.sellerId = sellerId;
|
153
|
+
this.title = title;
|
154
|
+
this.description = description;
|
155
|
+
this.price = price;
|
156
|
+
this.stock = stock;
|
157
|
+
this.imageUrl = imageUrl;
|
158
|
+
this.category = category;
|
159
|
+
this.status = status;
|
160
|
+
this.createdAt = new Date(createdAt);
|
161
|
+
this.updatedAt = new Date(updatedAt);
|
162
|
+
}
|
163
|
+
|
164
|
+
/**
|
165
|
+
* Updates product data.
|
166
|
+
* @param {object} updateData - Data to update.
|
167
|
+
*/
|
168
|
+
update(updateData) {
|
169
|
+
// Basic validation for status update
|
170
|
+
if (updateData.status !== undefined && !PRODUCT_STATUSES.includes(updateData.status)) {
|
171
|
+
throw new Error(`Invalid product status update: ${updateData.status}. Must be one of ${PRODUCT_STATUSES.join(', ')}.`);
|
172
|
+
}
|
173
|
+
Object.assign(this, updateData);
|
174
|
+
this.updatedAt = new Date();
|
175
|
+
}
|
176
|
+
|
177
|
+
/**
|
178
|
+
* Returns a plain object representation.
|
179
|
+
* @returns {object}
|
180
|
+
*/
|
181
|
+
toObject() {
|
182
|
+
return {
|
183
|
+
id: this.id,
|
184
|
+
sellerId: this.sellerId,
|
185
|
+
title: this.title,
|
186
|
+
description: this.description,
|
187
|
+
price: this.price,
|
188
|
+
stock: this.stock,
|
189
|
+
imageUrl: this.imageUrl,
|
190
|
+
category: this.category,
|
191
|
+
status: this.status,
|
192
|
+
createdAt: this.createdAt,
|
193
|
+
updatedAt: this.updatedAt,
|
194
|
+
};
|
195
|
+
}
|
196
|
+
}
|
197
|
+
|
198
|
+
|
199
|
+
/**
|
200
|
+
* Represents an Order placed by a buyer for the seller's products.
|
201
|
+
* Note: This simplified model represents an order *item* or a single product order.
|
202
|
+
* A real system would likely have an Order header and multiple OrderItems.
|
203
|
+
* @typedef {object} Order
|
204
|
+
* @property {string} id - Unique identifier for the order item.
|
205
|
+
* @property {string} orderId - Identifier for the overall order (if multiple items).
|
206
|
+
* @property {string} sellerId - The ID of the seller fulfilling this item.
|
207
|
+
* @property {string} productId - The ID of the product ordered.
|
208
|
+
* @property {string} productName - The name of the product ordered (snapshot at time of order).
|
209
|
+
* @property {number} quantity - Quantity of the product ordered.
|
210
|
+
* @property {number} itemPrice - Price of one unit of the product at the time of order.
|
211
|
+
* @property {number} totalPrice - Total price for this item (quantity * itemPrice).
|
212
|
+
* @property {string} buyerId - The ID of the buyer.
|
213
|
+
* @property {object} buyerInfo - Snapshot of buyer's info (e.g., { name, address }).
|
214
|
+
* @property {string} status - Order item status ('pending', 'processing', 'shipped', 'delivered', 'cancelled', 'returned').
|
215
|
+
* @property {Date} orderDate - Timestamp when the order was placed.
|
216
|
+
* @property {Date} [updatedAt] - Timestamp when the order item status was last updated.
|
217
|
+
*/
|
218
|
+
class Order {
|
219
|
+
/**
|
220
|
+
* @param {object} data - Initial data for the order item.
|
221
|
+
* @param {string} data.id - Unique identifier.
|
222
|
+
* @param {string} data.orderId - Overall order ID.
|
223
|
+
* @param {string} data.sellerId - Seller ID.
|
224
|
+
* @param {string} data.productId - Product ID.
|
225
|
+
* @param {string} data.productName - Product name snapshot.
|
226
|
+
* @param {number} data.quantity - Quantity.
|
227
|
+
* @param {number} data.itemPrice - Price per item at order time.
|
228
|
+
* @param {object} data.buyerInfo - Buyer info snapshot.
|
229
|
+
* @param {string} [data.status='pending'] - Order status.
|
230
|
+
* @param {Date} [data.orderDate=new Date()] - Order placement timestamp.
|
231
|
+
* @param {Date} [data.updatedAt=new Date()] - Update timestamp.
|
232
|
+
*/
|
233
|
+
constructor({
|
234
|
+
id,
|
235
|
+
orderId,
|
236
|
+
sellerId,
|
237
|
+
productId,
|
238
|
+
productName,
|
239
|
+
quantity,
|
240
|
+
itemPrice,
|
241
|
+
buyerInfo,
|
242
|
+
status = 'pending',
|
243
|
+
orderDate = new Date(),
|
244
|
+
updatedAt = new Date()
|
245
|
+
}) {
|
246
|
+
if (!id || !orderId || !sellerId || !productId || !productName || quantity === undefined || itemPrice === undefined || !buyerInfo) {
|
247
|
+
throw new Error("Order requires id, orderId, sellerId, productId, productName, quantity, itemPrice, and buyerInfo.");
|
248
|
+
}
|
249
|
+
if (!ORDER_STATUSES.includes(status)) {
|
250
|
+
throw new Error(`Invalid order status: ${status}. Must be one of ${ORDER_STATUSES.join(', ')}.`);
|
251
|
+
}
|
252
|
+
|
253
|
+
this.id = id;
|
254
|
+
this.orderId = orderId;
|
255
|
+
this.sellerId = sellerId;
|
256
|
+
this.productId = productId;
|
257
|
+
this.productName = productName;
|
258
|
+
this.quantity = quantity;
|
259
|
+
this.itemPrice = itemPrice;
|
260
|
+
this.totalPrice = quantity * itemPrice;
|
261
|
+
this.buyerInfo = buyerInfo; // Simple object copy
|
262
|
+
this.status = status;
|
263
|
+
this.orderDate = new Date(orderDate);
|
264
|
+
this.updatedAt = new Date(updatedAt);
|
265
|
+
}
|
266
|
+
|
267
|
+
/**
|
268
|
+
* Updates the order item status.
|
269
|
+
* @param {string} newStatus - The new status for the order item.
|
270
|
+
*/
|
271
|
+
updateStatus(newStatus) {
|
272
|
+
if (!ORDER_STATUSES.includes(newStatus)) {
|
273
|
+
throw new Error(`Invalid order status update: ${newStatus}. Must be one of ${ORDER_STATUSES.join(', ')}.`);
|
274
|
+
}
|
275
|
+
this.status = newStatus;
|
276
|
+
this.updatedAt = new Date();
|
277
|
+
}
|
278
|
+
|
279
|
+
/**
|
280
|
+
* Returns a plain object representation.
|
281
|
+
* @returns {object}
|
282
|
+
*/
|
283
|
+
toObject() {
|
284
|
+
return {
|
285
|
+
id: this.id,
|
286
|
+
orderId: this.orderId,
|
287
|
+
sellerId: this.sellerId,
|
288
|
+
productId: this.productId,
|
289
|
+
productName: this.productName,
|
290
|
+
quantity: this.quantity,
|
291
|
+
itemPrice: this.itemPrice,
|
292
|
+
totalPrice: this.totalPrice,
|
293
|
+
buyerInfo: { ...this.buyerInfo }, // Return a copy
|
294
|
+
status: this.status,
|
295
|
+
orderDate: this.orderDate,
|
296
|
+
updatedAt: this.updatedAt,
|
297
|
+
};
|
298
|
+
}
|
299
|
+
}
|
300
|
+
|
301
|
+
|
302
|
+
// --- Utility Functions (Helper Methods) ---
|
303
|
+
|
304
|
+
/**
|
305
|
+
* Generates a simple unique ID string.
|
306
|
+
* Not a true UUID, but sufficient for simulation.
|
307
|
+
* @returns {string} A unique identifier string.
|
308
|
+
* @private
|
309
|
+
*/
|
310
|
+
function _generateUniqueId() {
|
311
|
+
// Simple simulation of ID generation
|
312
|
+
// Combination of random string and timestamp for uniqueness likelihood
|
313
|
+
return 'yapi_' + Math.random().toString(36).substring(2, 15) + Date.now().toString(36);
|
314
|
+
}
|
315
|
+
|
316
|
+
/**
|
317
|
+
* Basic validation for a non-empty string.
|
318
|
+
* @param {*} value - The value to validate.
|
319
|
+
* @param {string} name - The name of the parameter being validated (for error messages).
|
320
|
+
* @param {boolean} [allowEmptyString=false] - If true, an empty string is considered valid.
|
321
|
+
* @private
|
322
|
+
* @throws {Error} If validation fails.
|
323
|
+
*/
|
324
|
+
function _validateString(value, name, allowEmptyString = false) {
|
325
|
+
if (value === null || value === undefined) {
|
326
|
+
throw new Error(`${name} cannot be null or undefined.`);
|
327
|
+
}
|
328
|
+
if (typeof value !== 'string') {
|
329
|
+
throw new Error(`${name} must be a string.`);
|
330
|
+
}
|
331
|
+
if (!allowEmptyString && value.trim().length === 0) {
|
332
|
+
throw new Error(`${name} cannot be an empty string.`);
|
333
|
+
}
|
334
|
+
}
|
335
|
+
|
336
|
+
/**
|
337
|
+
* Basic validation for a non-negative number (integer or float).
|
338
|
+
* @param {*} value - The value to validate.
|
339
|
+
* @param {string} name - The name of the parameter being validated.
|
340
|
+
* @private
|
341
|
+
* @throws {Error} If validation fails.
|
342
|
+
*/
|
343
|
+
function _validatePositiveNumber(value, name) {
|
344
|
+
if (value === null || value === undefined) {
|
345
|
+
throw new Error(`${name} cannot be null or undefined.`);
|
346
|
+
}
|
347
|
+
if (typeof value !== 'number' || isNaN(value) || !isFinite(value)) {
|
348
|
+
throw new Error(`${name} must be a valid number.`);
|
349
|
+
}
|
350
|
+
if (value < 0) {
|
351
|
+
throw new Error(`${name} cannot be negative.`);
|
352
|
+
}
|
353
|
+
}
|
354
|
+
|
355
|
+
/**
|
356
|
+
* Basic validation for a non-negative integer.
|
357
|
+
* @param {*} value - The value to validate.
|
358
|
+
* @param {string} name - The name of the parameter being validated.
|
359
|
+
* @private
|
360
|
+
* @throws {Error} If validation fails.
|
361
|
+
*/
|
362
|
+
function _validateNonNegativeInteger(value, name) {
|
363
|
+
_validatePositiveNumber(value, name); // Must be non-negative number first
|
364
|
+
if (!Number.isInteger(value)) {
|
365
|
+
throw new Error(`${name} must be an integer.`);
|
366
|
+
}
|
367
|
+
}
|
368
|
+
|
369
|
+
|
370
|
+
/**
|
371
|
+
* Validates that a value is a valid status from a given list.
|
372
|
+
* @param {string} status - The status string to validate.
|
373
|
+
* @param {string[]} validStatuses - An array of valid status strings.
|
374
|
+
* @param {string} name - The name of the status being validated (e.g., 'product status').
|
375
|
+
* @private
|
376
|
+
* @throws {Error} If validation fails.
|
377
|
+
*/
|
378
|
+
function _validateStatus(status, validStatuses, name) {
|
379
|
+
_validateString(status, name);
|
380
|
+
if (!validStatuses.includes(status)) {
|
381
|
+
throw new Error(`Invalid ${name}: "${status}". Must be one of ${validStatuses.join(', ')}.`);
|
382
|
+
}
|
383
|
+
}
|
384
|
+
|
385
|
+
/**
|
386
|
+
* Validates data required for creating a new product.
|
387
|
+
* @param {object} productData - The data object for creating a product.
|
388
|
+
* @private
|
389
|
+
* @throws {Error} If validation fails.
|
390
|
+
*/
|
391
|
+
function _validateNewProductData(productData) {
|
392
|
+
if (!productData || typeof productData !== 'object') {
|
393
|
+
throw new Error('Invalid product data: Must be an object.');
|
394
|
+
}
|
395
|
+
_validateString(productData.title, 'product title');
|
396
|
+
if (productData.title.length < 3 || productData.title.length > 100) {
|
397
|
+
throw new Error('Invalid product title: Must be between 3 and 100 characters.');
|
398
|
+
}
|
399
|
+
// Description is optional but validate if provided
|
400
|
+
if (productData.description !== undefined && productData.description !== null) {
|
401
|
+
_validateString(productData.description, 'product description', true); // Allow empty string
|
402
|
+
if (productData.description.length > 500) {
|
403
|
+
throw new Error('Invalid product description: Cannot exceed 500 characters.');
|
404
|
+
}
|
405
|
+
} else {
|
406
|
+
productData.description = ''; // Default if not provided
|
407
|
+
}
|
408
|
+
_validatePositiveNumber(productData.price, 'product price');
|
409
|
+
if (productData.price > 100000) { // Arbitrary max price
|
410
|
+
throw new Error('Invalid product price: Price exceeds maximum allowed value.');
|
411
|
+
}
|
412
|
+
_validateNonNegativeInteger(productData.stock, 'product stock');
|
413
|
+
if (productData.stock > 1000000) { // Arbitrary max stock
|
414
|
+
throw new Error('Invalid product stock: Stock exceeds maximum allowed value.');
|
415
|
+
}
|
416
|
+
// Image URL is optional but validate if provided
|
417
|
+
if (productData.imageUrl !== undefined && productData.imageUrl !== null) {
|
418
|
+
_validateString(productData.imageUrl, 'product imageUrl', true);
|
419
|
+
if (productData.imageUrl.length > 0 && !productData.imageUrl.startsWith('http')) {
|
420
|
+
throw new Error('Invalid product imageUrl: Must be a valid URL starting with http.');
|
421
|
+
}
|
422
|
+
} else {
|
423
|
+
productData.imageUrl = ''; // Default if not provided
|
424
|
+
}
|
425
|
+
// Category is optional but validate if provided
|
426
|
+
if (productData.category !== undefined && productData.category !== null) {
|
427
|
+
_validateString(productData.category, 'product category', true); // Allow empty string
|
428
|
+
if (productData.category.length > 50) {
|
429
|
+
throw new Error('Invalid product category: Cannot exceed 50 characters.');
|
430
|
+
}
|
431
|
+
} else {
|
432
|
+
productData.category = 'uncategorized'; // Default if not provided
|
433
|
+
}
|
434
|
+
// Status is optional but validate if provided, default to 'draft'
|
435
|
+
if (productData.status !== undefined && productData.status !== null) {
|
436
|
+
_validateStatus(productData.status, PRODUCT_STATUSES, 'product status');
|
437
|
+
} else {
|
438
|
+
productData.status = 'draft'; // Default if not provided
|
439
|
+
}
|
440
|
+
}
|
441
|
+
|
442
|
+
/**
|
443
|
+
* Validates data provided for updating an existing product.
|
444
|
+
* @param {object} updateData - The data object for updating a product.
|
445
|
+
* @private
|
446
|
+
* @throws {Error} If validation fails.
|
447
|
+
*/
|
448
|
+
function _validateProductUpdateData(updateData) {
|
449
|
+
if (!updateData || typeof updateData !== 'object') {
|
450
|
+
throw new Error('Invalid product update data: Must be an object.');
|
451
|
+
}
|
452
|
+
// Allow partial updates, but validate fields if they exist
|
453
|
+
let hasUpdateFields = false;
|
454
|
+
if (updateData.title !== undefined) {
|
455
|
+
_validateString(updateData.title, 'title');
|
456
|
+
if (updateData.title.length < 3 || updateData.title.length > 100) {
|
457
|
+
throw new Error('Invalid product title: Must be between 3 and 100 characters.');
|
458
|
+
}
|
459
|
+
hasUpdateFields = true;
|
460
|
+
}
|
461
|
+
if (updateData.description !== undefined) {
|
462
|
+
if (updateData.description !== null) { // Allow null/undefined to potentially clear
|
463
|
+
_validateString(updateData.description, 'description', true);
|
464
|
+
if (updateData.description.length > 500) {
|
465
|
+
throw new Error('Invalid product description: Cannot exceed 500 characters.');
|
466
|
+
}
|
467
|
+
}
|
468
|
+
hasUpdateFields = true;
|
469
|
+
}
|
470
|
+
if (updateData.price !== undefined) {
|
471
|
+
_validatePositiveNumber(updateData.price, 'price');
|
472
|
+
if (updateData.price > 100000) {
|
473
|
+
throw new Error('Invalid product price: Price exceeds maximum allowed value.');
|
474
|
+
}
|
475
|
+
hasUpdateFields = true;
|
476
|
+
}
|
477
|
+
if (updateData.stock !== undefined) {
|
478
|
+
_validateNonNegativeInteger(updateData.stock, 'stock');
|
479
|
+
if (updateData.stock > 1000000) {
|
480
|
+
throw new Error('Invalid product stock: Stock exceeds maximum allowed value.');
|
481
|
+
}
|
482
|
+
hasUpdateFields = true;
|
483
|
+
}
|
484
|
+
if (updateData.imageUrl !== undefined) {
|
485
|
+
if (updateData.imageUrl !== null) {
|
486
|
+
_validateString(updateData.imageUrl, 'imageUrl', true);
|
487
|
+
if (updateData.imageUrl.length > 0 && !updateData.imageUrl.startsWith('http')) {
|
488
|
+
throw new Error('Invalid product imageUrl: Must be a valid URL starting with http.');
|
489
|
+
}
|
490
|
+
}
|
491
|
+
hasUpdateFields = true;
|
492
|
+
}
|
493
|
+
if (updateData.category !== undefined) {
|
494
|
+
if (updateData.category !== null) {
|
495
|
+
_validateString(updateData.category, 'category', true);
|
496
|
+
if (updateData.category.length > 50) {
|
497
|
+
throw new Error('Invalid product category: Cannot exceed 50 characters.');
|
498
|
+
}
|
499
|
+
}
|
500
|
+
hasUpdateFields = true;
|
501
|
+
}
|
502
|
+
if (updateData.status !== undefined) {
|
503
|
+
_validateStatus(updateData.status, PRODUCT_STATUSES, 'status');
|
504
|
+
hasUpdateFields = true;
|
505
|
+
}
|
506
|
+
|
507
|
+
if (!hasUpdateFields) {
|
508
|
+
throw new Error('Invalid product update data: No valid fields provided for update.');
|
509
|
+
}
|
510
|
+
}
|
511
|
+
|
512
|
+
|
513
|
+
/**
|
514
|
+
* Validates data provided for updating the seller profile.
|
515
|
+
* @param {object} updateData - The data object for updating the profile.
|
516
|
+
* @private
|
517
|
+
* @throws {Error} If validation fails.
|
518
|
+
*/
|
519
|
+
function _validateSellerProfileUpdateData(updateData) {
|
520
|
+
if (!updateData || typeof updateData !== 'object') {
|
521
|
+
throw new Error('Invalid profile update data: Must be an object.');
|
522
|
+
}
|
523
|
+
let hasUpdateFields = false;
|
524
|
+
if (updateData.name !== undefined) {
|
525
|
+
_validateString(updateData.name, 'name');
|
526
|
+
hasUpdateFields = true;
|
527
|
+
}
|
528
|
+
if (updateData.email !== undefined) {
|
529
|
+
_validateString(updateData.email, 'email');
|
530
|
+
// Basic email format check (can be improved)
|
531
|
+
if (!/\S+@\S+\.\S+/.test(updateData.email)) {
|
532
|
+
throw new Error('Invalid email format.');
|
533
|
+
}
|
534
|
+
hasUpdateFields = true;
|
535
|
+
}
|
536
|
+
if (updateData.phone !== undefined) {
|
537
|
+
_validateString(updateData.phone, 'phone');
|
538
|
+
// Basic phone format check (can be improved, e.g., regex)
|
539
|
+
if (updateData.phone.length < 5) {
|
540
|
+
throw new Error('Invalid phone number format.');
|
541
|
+
}
|
542
|
+
hasUpdateFields = true;
|
543
|
+
}
|
544
|
+
if (updateData.address !== undefined) {
|
545
|
+
_validateString(updateData.address, 'address');
|
546
|
+
hasUpdateFields = true;
|
547
|
+
}
|
548
|
+
if (updateData.description !== undefined) {
|
549
|
+
if (updateData.description !== null) {
|
550
|
+
_validateString(updateData.description, 'description', true);
|
551
|
+
if (updateData.description.length > 1000) {
|
552
|
+
throw new Error('Invalid description: Cannot exceed 1000 characters.');
|
553
|
+
}
|
554
|
+
}
|
555
|
+
hasUpdateFields = true;
|
556
|
+
}
|
557
|
+
|
558
|
+
if (!hasUpdateFields) {
|
559
|
+
throw new Error('Invalid profile update data: No valid fields provided for update.');
|
560
|
+
}
|
561
|
+
}
|
562
|
+
|
563
|
+
|
564
|
+
/**
|
565
|
+
* Simulates an asynchronous API call with potential network delay and error chance.
|
566
|
+
* Wraps a given operation function.
|
567
|
+
* @param {function(): any} operation - The synchronous function containing the core logic to simulate.
|
568
|
+
* @param {number} [minDelay=SIMULATED_API_MIN_DELAY_MS] - Minimum delay in milliseconds.
|
569
|
+
* @param {number} [maxDelay=SIMULATED_API_MAX_DELAY_MS] - Maximum delay in milliseconds.
|
570
|
+
* @param {number} [errorChance=SIMULATED_API_ERROR_CHANCE] - Probability of a simulated API network/connection error (0 to 1).
|
571
|
+
* @returns {Promise<any>} A promise that resolves with the result of the operation or rejects with an error.
|
572
|
+
* @private
|
573
|
+
*/
|
574
|
+
function _simulateApiCall(operation, minDelay = SIMULATED_API_MIN_DELAY_MS, maxDelay = SIMULATED_API_MAX_DELAY_MS, errorChance = SIMULATED_API_ERROR_CHANCE) {
|
575
|
+
const delay = Math.random() * (maxDelay - minDelay) + minDelay;
|
576
|
+
const simulateNetworkError = Math.random() < errorChance;
|
577
|
+
|
578
|
+
return new Promise((resolve, reject) => {
|
579
|
+
setTimeout(() => {
|
580
|
+
if (simulateNetworkError) {
|
581
|
+
console.warn(`[YapiSellerService - Simulation] Simulated API network error after ${delay.toFixed(0)}ms delay.`);
|
582
|
+
// Use a generic network error message
|
583
|
+
reject(new Error(`Network Error: Could not connect to the service.`));
|
584
|
+
return;
|
585
|
+
}
|
586
|
+
try {
|
587
|
+
// Execute the actual operation
|
588
|
+
const result = operation();
|
589
|
+
console.debug(`[YapiSellerService - Simulation] Operation successful after ${delay.toFixed(0)}ms delay.`);
|
590
|
+
resolve(result);
|
591
|
+
} catch (error) {
|
592
|
+
// Catch and propagate errors thrown by the operation itself
|
593
|
+
console.error(`[YapiSellerService - Simulation] Operation failed after ${delay.toFixed(0)}ms delay:`, error.message);
|
594
|
+
// Wrap known errors or propagate them
|
595
|
+
if (error instanceof Error) {
|
596
|
+
reject(error);
|
597
|
+
} else {
|
598
|
+
reject(new Error(`An unexpected error occurred during the operation: ${error}`));
|
599
|
+
}
|
600
|
+
}
|
601
|
+
}, delay);
|
602
|
+
});
|
603
|
+
}
|
604
|
+
|
605
|
+
|
606
|
+
// --- Simulate Initial Data Population (for demonstration) ---
|
607
|
+
// In a real scenario, this data would come from a backend API.
|
608
|
+
function _populateSimulatedData(sellerId) {
|
609
|
+
if (_sellerProfile === null) {
|
610
|
+
_sellerProfile = new SellerProfile({
|
611
|
+
id: sellerId,
|
612
|
+
name: 'Acme Supplies Inc.',
|
613
|
+
email: 'seller@acmesupplies.com',
|
614
|
+
phone: '+1-555-0101',
|
615
|
+
address: '123 Main St, Anytown, CA 91234',
|
616
|
+
description: 'Your one-stop shop for quality goods.',
|
617
|
+
createdAt: new Date(Date.now() - 86400000 * 365), // 1 year ago
|
618
|
+
updatedAt: new Date(Date.now() - 86400000 * 30) // 30 days ago
|
619
|
+
});
|
620
|
+
|
621
|
+
// Simulate some products
|
622
|
+
const product1Id = _generateUniqueId();
|
623
|
+
const product2Id = _generateUniqueId();
|
624
|
+
const product3Id = _generateUniqueId();
|
625
|
+
const product4Id = _generateUniqueId();
|
626
|
+
|
627
|
+
_products = [
|
628
|
+
new Product({
|
629
|
+
id: product1Id,
|
630
|
+
sellerId: sellerId,
|
631
|
+
title: 'Wireless Mouse',
|
632
|
+
description: 'Ergonomic wireless mouse with long battery life.',
|
633
|
+
price: 25.99,
|
634
|
+
stock: 150,
|
635
|
+
imageUrl: 'https://example.com/images/mouse.jpg',
|
636
|
+
category: 'electronics',
|
637
|
+
status: 'active',
|
638
|
+
createdAt: new Date(Date.now() - 86400000 * 100),
|
639
|
+
updatedAt: new Date(Date.now() - 86400000 * 5)
|
640
|
+
}),
|
641
|
+
new Product({
|
642
|
+
id: product2Id,
|
643
|
+
sellerId: sellerId,
|
644
|
+
title: 'Mechanical Keyboard',
|
645
|
+
description: 'Clicky mechanical keyboard for typing enthusiasts.',
|
646
|
+
price: 75.00,
|
647
|
+
stock: 50,
|
648
|
+
imageUrl: 'https://example.com/images/keyboard.jpg',
|
649
|
+
category: 'electronics',
|
650
|
+
status: 'active',
|
651
|
+
createdAt: new Date(Date.now() - 86400000 * 90),
|
652
|
+
updatedAt: new Date(Date.now() - 86400000 * 2)
|
653
|
+
}),
|
654
|
+
new Product({
|
655
|
+
id: product3Id,
|
656
|
+
sellerId: sellerId,
|
657
|
+
title: 'Desk Lamp',
|
658
|
+
description: 'Adjustable LED desk lamp with dimmer.',
|
659
|
+
price: 35.50,
|
660
|
+
stock: 10, // Low stock
|
661
|
+
imageUrl: 'https://example.com/images/lamp.jpg',
|
662
|
+
category: 'home goods',
|
663
|
+
status: 'active',
|
664
|
+
createdAt: new Date(Date.now() - 86400000 * 80),
|
665
|
+
updatedAt: new Date(Date.now() - 86400000 * 1)
|
666
|
+
}),
|
667
|
+
new Product({
|
668
|
+
id: product4Id,
|
669
|
+
sellerId: sellerId,
|
670
|
+
title: 'Archived Item',
|
671
|
+
description: 'This product is no longer available.',
|
672
|
+
price: 9.99,
|
673
|
+
stock: 0,
|
674
|
+
imageUrl: 'https://example.com/images/archived.jpg',
|
675
|
+
category: 'misc',
|
676
|
+
status: 'archived',
|
677
|
+
createdAt: new Date(Date.now() - 86400000 * 200),
|
678
|
+
updatedAt: new Date(Date.now() - 86400000 * 60)
|
679
|
+
}),
|
680
|
+
];
|
681
|
+
|
682
|
+
// Simulate some orders for these products
|
683
|
+
const order1Id = _generateUniqueId();
|
684
|
+
const order2Id = _generateUniqueId();
|
685
|
+
const order3Id = _generateUniqueId();
|
686
|
+
const order4Id = _generateUniqueId();
|
687
|
+
|
688
|
+
|
689
|
+
_orders = [
|
690
|
+
new Order({
|
691
|
+
id: _generateUniqueId(),
|
692
|
+
orderId: order1Id,
|
693
|
+
sellerId: sellerId,
|
694
|
+
productId: product1Id,
|
695
|
+
productName: 'Wireless Mouse',
|
696
|
+
quantity: 2,
|
697
|
+
itemPrice: 25.99,
|
698
|
+
buyerInfo: { name: 'Alice Smith', address: '456 Oak Ave' },
|
699
|
+
status: 'delivered',
|
700
|
+
orderDate: new Date(Date.now() - 86400000 * 20),
|
701
|
+
updatedAt: new Date(Date.now() - 86400000 * 15)
|
702
|
+
}),
|
703
|
+
new Order({
|
704
|
+
id: _generateUniqueId(),
|
705
|
+
orderId: order2Id,
|
706
|
+
sellerId: sellerId,
|
707
|
+
productId: product2Id,
|
708
|
+
productName: 'Mechanical Keyboard',
|
709
|
+
quantity: 1,
|
710
|
+
itemPrice: 75.00,
|
711
|
+
buyerInfo: { name: 'Bob Johnson', address: '789 Pine Ln' },
|
712
|
+
status: 'shipped',
|
713
|
+
orderDate: new Date(Date.now() - 86400000 * 10),
|
714
|
+
updatedAt: new Date(Date.now() - 86400000 * 8)
|
715
|
+
}),
|
716
|
+
new Order({
|
717
|
+
id: _generateUniqueId(),
|
718
|
+
orderId: order3Id,
|
719
|
+
sellerId: sellerId,
|
720
|
+
productId: product1Id,
|
721
|
+
productName: 'Wireless Mouse',
|
722
|
+
quantity: 1,
|
723
|
+
itemPrice: 25.99,
|
724
|
+
buyerInfo: { name: 'Charlie Brown', address: '1011 Maple Dr' },
|
725
|
+
status: 'pending',
|
726
|
+
orderDate: new Date(Date.now() - 86400000 * 2),
|
727
|
+
updatedAt: new Date(Date.now() - 86400000 * 1)
|
728
|
+
}),
|
729
|
+
new Order({
|
730
|
+
id: _generateUniqueId(),
|
731
|
+
orderId: order4Id,
|
732
|
+
sellerId: sellerId,
|
733
|
+
productId: product3Id,
|
734
|
+
productName: 'Desk Lamp',
|
735
|
+
quantity: 1,
|
736
|
+
itemPrice: 35.50,
|
737
|
+
buyerInfo: { name: 'Diana Prince', address: '1213 Elm St' },
|
738
|
+
status: 'processing',
|
739
|
+
orderDate: new Date(Date.now() - 86400000 * 0.5),
|
740
|
+
updatedAt: new Date(Date.now() - 86400000 * 0.1)
|
741
|
+
}),
|
742
|
+
];
|
743
|
+
console.log('[YapiSellerService - Simulation] Simulated data populated.');
|
744
|
+
}
|
745
|
+
}
|
746
|
+
|
747
|
+
|
748
|
+
// --- Main Service Class ---
|
749
|
+
|
750
|
+
/**
|
751
|
+
* @class YapiSellerService
|
752
|
+
* @description Provides methods for interacting with seller-specific data on the Yapi platform.
|
753
|
+
* All operations are simulated and asynchronous, mimicking real API calls.
|
754
|
+
*/
|
755
|
+
class YapiSellerService {
|
756
|
+
|
757
|
+
/**
|
758
|
+
* Creates an instance of YapiSellerService.
|
759
|
+
* @param {string} sellerId - The unique identifier for the seller.
|
760
|
+
* @throws {Error} If sellerId is not provided or is invalid.
|
761
|
+
*/
|
762
|
+
constructor(sellerId) {
|
763
|
+
_validateString(sellerId, 'sellerId');
|
764
|
+
this._sellerId = sellerId;
|
765
|
+
_populateSimulatedData(this._sellerId); // Populate data specific to this seller instance (in simulation)
|
766
|
+
console.log(`[YapiSellerService] Initialized for seller: ${this._sellerId}`);
|
767
|
+
}
|
768
|
+
|
769
|
+
/**
|
770
|
+
* Get the seller's unique ID.
|
771
|
+
* @returns {string} The seller ID this service instance is associated with.
|
772
|
+
*/
|
773
|
+
getSellerId() {
|
774
|
+
return this._sellerId;
|
775
|
+
}
|
776
|
+
|
777
|
+
// --- Seller Profile Methods ---
|
778
|
+
|
779
|
+
/**
|
780
|
+
* Retrieves the profile information for the current seller.
|
781
|
+
* @returns {Promise<SellerProfile>} A promise that resolves with the seller's profile object.
|
782
|
+
* @throws {Error} If the seller profile is not found (in simulation, this means it wasn't populated).
|
783
|
+
*/
|
784
|
+
async getProfile() {
|
785
|
+
console.log(`[YapiSellerService] Attempting to get profile for seller: ${this._sellerId}`);
|
786
|
+
return this._simulateApiCall(() => {
|
787
|
+
if (!_sellerProfile || _sellerProfile.id !== this._sellerId) {
|
788
|
+
// This case should ideally not happen with _populateSimulatedData,
|
789
|
+
// but included for robust simulation logic.
|
790
|
+
console.error(`[YapiSellerService] Profile not found for seller: ${this._sellerId}`);
|
791
|
+
throw new Error(`Seller profile not found.`);
|
792
|
+
}
|
793
|
+
// Return a copy to prevent external modification of the internal state
|
794
|
+
return new SellerProfile(_sellerProfile.toObject());
|
795
|
+
});
|
796
|
+
}
|
797
|
+
|
798
|
+
/**
|
799
|
+
* Updates the profile information for the current seller.
|
800
|
+
* @param {object} updateData - An object containing the fields to update (e.g., { name, email, address }).
|
801
|
+
* @returns {Promise<SellerProfile>} A promise that resolves with the updated seller's profile object.
|
802
|
+
* @throws {Error} If the updateData is invalid or the profile is not found.
|
803
|
+
*/
|
804
|
+
async updateProfile(updateData) {
|
805
|
+
console.log(`[YapiSellerService] Attempting to update profile for seller: ${this._sellerId}`);
|
806
|
+
return this._simulateApiCall(() => {
|
807
|
+
_validateSellerProfileUpdateData(updateData);
|
808
|
+
|
809
|
+
if (!_sellerProfile || _sellerProfile.id !== this._sellerId) {
|
810
|
+
console.error(`[YapiSellerService] Profile not found for update for seller: ${this._sellerId}`);
|
811
|
+
throw new Error(`Seller profile not found.`);
|
812
|
+
}
|
813
|
+
|
814
|
+
// Apply updates to the internal simulated profile object
|
815
|
+
_sellerProfile.update(updateData);
|
816
|
+
|
817
|
+
console.log(`[YapiSellerService] Profile updated successfully for seller: ${this._sellerId}`);
|
818
|
+
// Return a copy of the updated profile
|
819
|
+
return new SellerProfile(_sellerProfile.toObject());
|
820
|
+
});
|
821
|
+
}
|
822
|
+
|
823
|
+
// --- Product Management Methods ---
|
824
|
+
|
825
|
+
/**
|
826
|
+
* Creates a new product for the seller.
|
827
|
+
* @param {object} productData - The data for the new product (excluding id and sellerId).
|
828
|
+
* @param {string} productData.title - The title of the product.
|
829
|
+
* @param {string} [productData.description] - The description of the product.
|
830
|
+
* @param {number} productData.price - The price of the product.
|
831
|
+
* @param {number} productData.stock - The initial stock quantity.
|
832
|
+
* @param {string} [productData.imageUrl] - The URL of the main product image.
|
833
|
+
* @param {string} [productData.category] - The product category.
|
834
|
+
* @param {string} [productData.status='draft'] - The initial status of the product.
|
835
|
+
* @returns {Promise<Product>} A promise that resolves with the newly created Product object.
|
836
|
+
* @throws {Error} If the productData is invalid.
|
837
|
+
*/
|
838
|
+
async createProduct(productData) {
|
839
|
+
console.log(`[YapiSellerService] Attempting to create product for seller: ${this._sellerId}`);
|
840
|
+
return this._simulateApiCall(() => {
|
841
|
+
_validateNewProductData(productData);
|
842
|
+
|
843
|
+
const newProductId = _generateUniqueId();
|
844
|
+
const product = new Product({
|
845
|
+
id: newProductId,
|
846
|
+
sellerId: this._sellerId,
|
847
|
+
...productData,
|
848
|
+
createdAt: new Date(), // Set creation timestamp
|
849
|
+
updatedAt: new Date() // Set update timestamp
|
850
|
+
});
|
851
|
+
|
852
|
+
_products.push(product);
|
853
|
+
console.log(`[YapiSellerService] Product created successfully with ID: ${newProductId} for seller: ${this._sellerId}`);
|
854
|
+
// Return a copy
|
855
|
+
return new Product(product.toObject());
|
856
|
+
}, SIMULATED_API_MIN_DELAY_MS, SIMULATED_API_MAX_DELAY_MS, SIMULATED_API_ERROR_CHANCE); // Potentially higher delay/error for creation
|
857
|
+
|
858
|
+
}
|
859
|
+
|
860
|
+
/**
|
861
|
+
* Retrieves a specific product by its ID.
|
862
|
+
* @param {string} productId - The unique identifier of the product.
|
863
|
+
* @returns {Promise<Product>} A promise that resolves with the Product object.
|
864
|
+
* @throws {Error} If the productId is invalid or the product is not found for this seller.
|
865
|
+
*/
|
866
|
+
async getProductById(productId) {
|
867
|
+
console.log(`[YapiSellerService] Attempting to get product by ID: ${productId} for seller: ${this._sellerId}`);
|
868
|
+
return this._simulateApiCall(() => {
|
869
|
+
_validateString(productId, 'productId');
|
870
|
+
|
871
|
+
const product = _products.find(p => p.id === productId && p.sellerId === this._sellerId);
|
872
|
+
|
873
|
+
if (!product) {
|
874
|
+
console.error(`[YapiSellerService] Product with ID ${productId} not found for seller: ${this._sellerId}.`);
|
875
|
+
throw new Error(`Product with ID ${productId} not found.`);
|
876
|
+
}
|
877
|
+
|
878
|
+
console.log(`[YapiSellerService] Successfully retrieved product with ID: ${productId}`);
|
879
|
+
// Return a copy
|
880
|
+
return new Product(product.toObject());
|
881
|
+
});
|
882
|
+
}
|
883
|
+
|
884
|
+
/**
|
885
|
+
* Updates an existing product for the seller.
|
886
|
+
* Allows partial updates. Fields not included in updateData will not be changed.
|
887
|
+
* @param {string} productId - The unique identifier of the product to update.
|
888
|
+
* @param {object} updateData - An object containing the fields to update.
|
889
|
+
* @returns {Promise<Product>} A promise that resolves with the updated Product object.
|
890
|
+
* @throws {Error} If the productId is invalid, updateData is invalid, or the product is not found.
|
891
|
+
*/
|
892
|
+
async updateProduct(productId, updateData) {
|
893
|
+
console.log(`[YapiSellerService] Attempting to update product ID: ${productId} for seller: ${this._sellerId}`);
|
894
|
+
return this._simulateApiCall(() => {
|
895
|
+
_validateString(productId, 'productId');
|
896
|
+
_validateProductUpdateData(updateData); // Validates the update data object structure and fields
|
897
|
+
|
898
|
+
const productIndex = _products.findIndex(p => p.id === productId && p.sellerId === this._sellerId);
|
899
|
+
|
900
|
+
if (productIndex === -1) {
|
901
|
+
console.error(`[YapiSellerService] Product with ID ${productId} not found for update for seller: ${this._sellerId}.`);
|
902
|
+
throw new Error(`Product with ID ${productId} not found.`);
|
903
|
+
}
|
904
|
+
|
905
|
+
// Apply updates to the found product instance
|
906
|
+
_products[productIndex].update(updateData);
|
907
|
+
|
908
|
+
console.log(`[YapiSellerService] Product ID: ${productId} updated successfully for seller: ${this._sellerId}`);
|
909
|
+
// Return a copy of the updated product
|
910
|
+
return new Product(_products[productIndex].toObject());
|
911
|
+
});
|
912
|
+
}
|
913
|
+
|
914
|
+
/**
|
915
|
+
* Deletes a product permanently.
|
916
|
+
* This simulates a hard delete.
|
917
|
+
* @param {string} productId - The unique identifier of the product to delete.
|
918
|
+
* @returns {Promise<void>} A promise that resolves when the product is deleted.
|
919
|
+
* @throws {Error} If the productId is invalid or the product is not found.
|
920
|
+
*/
|
921
|
+
async deleteProduct(productId) {
|
922
|
+
console.log(`[YapiSellerService] Attempting to delete product ID: ${productId} for seller: ${this._sellerId}`);
|
923
|
+
return this._simulateApiCall(() => {
|
924
|
+
_validateString(productId, 'productId');
|
925
|
+
|
926
|
+
const initialLength = _products.length;
|
927
|
+
// Filter out the product. Assumes product IDs are unique to the seller.
|
928
|
+
_products = _products.filter(p => !(p.id === productId && p.sellerId === this._sellerId));
|
929
|
+
|
930
|
+
if (_products.length === initialLength) {
|
931
|
+
console.error(`[YapiSellerService] Product with ID ${productId} not found for delete for seller: ${this._sellerId}.`);
|
932
|
+
throw new Error(`Product with ID ${productId} not found or could not be deleted.`);
|
933
|
+
}
|
934
|
+
|
935
|
+
console.log(`[YapiSellerService] Product ID: ${productId} deleted successfully for seller: ${this._sellerId}`);
|
936
|
+
}, SIMULATED_API_MIN_DELAY_MS, SIMULATED_API_MAX_DELAY_MS * 2, SIMULATED_API_ERROR_CHANCE * 1.5); // Deletion might be slower/riskier
|
937
|
+
|
938
|
+
}
|
939
|
+
|
940
|
+
/**
|
941
|
+
* Archives a product (sets its status to 'archived').
|
942
|
+
* This simulates a soft delete or deactivation.
|
943
|
+
* @param {string} productId - The unique identifier of the product to archive.
|
944
|
+
* @returns {Promise<Product>} A promise that resolves with the updated Product object with status 'archived'.
|
945
|
+
* @throws {Error} If the productId is invalid or the product is not found.
|
946
|
+
*/
|
947
|
+
async archiveProduct(productId) {
|
948
|
+
console.log(`[YapiSellerService] Attempting to archive product ID: ${productId} for seller: ${this._sellerId}`);
|
949
|
+
return this._simulateApiCall(async () => { // Use async here if updateProduct is async
|
950
|
+
_validateString(productId, 'productId');
|
951
|
+
// Use updateProduct to handle the actual status change and validation
|
952
|
+
const updatedProduct = await this.updateProduct(productId, { status: 'archived' });
|
953
|
+
console.log(`[YapiSellerService] Product ID: ${productId} archived successfully for seller: ${this._sellerId}`);
|
954
|
+
return updatedProduct; // updateProduct returns the product copy
|
955
|
+
}); // Simulate updateProduct's delay within its own call
|
956
|
+
}
|
957
|
+
|
958
|
+
/**
|
959
|
+
* Unarchives a product (sets its status to 'active').
|
960
|
+
* @param {string} productId - The unique identifier of the product to unarchive.
|
961
|
+
* @returns {Promise<Product>} A promise that resolves with the updated Product object with status 'active'.
|
962
|
+
* @throws {Error} If the productId is invalid or the product is not found or cannot be unarchived.
|
963
|
+
*/
|
964
|
+
async unarchiveProduct(productId) {
|
965
|
+
console.log(`[YapiSellerService] Attempting to unarchive product ID: ${productId} for seller: ${this._sellerId}`);
|
966
|
+
return this._simulateApiCall(async () => { // Use async here if updateProduct is async
|
967
|
+
_validateString(productId, 'productId');
|
968
|
+
// Use updateProduct to handle the actual status change and validation
|
969
|
+
// Check current status before attempting to unarchive? Optional, updateProduct handles it.
|
970
|
+
const updatedProduct = await this.updateProduct(productId, { status: 'active' });
|
971
|
+
console.log(`[YapiSellerService] Product ID: ${productId} unarchived successfully for seller: ${this._sellerId}`);
|
972
|
+
return updatedProduct; // updateProduct returns the product copy
|
973
|
+
});
|
974
|
+
}
|
975
|
+
|
976
|
+
|
977
|
+
/**
|
978
|
+
* Lists products for the seller with pagination and filtering options.
|
979
|
+
* @param {object} [options] - Options for listing products.
|
980
|
+
* @param {number} [options.page=1] - The page number (1-based index).
|
981
|
+
* @param {number} [options.limit=10] - The number of items per page.
|
982
|
+
* @param {string} [options.status] - Filter by product status ('draft', 'active', 'archived').
|
983
|
+
* @param {string} [options.category] - Filter by product category.
|
984
|
+
* @param {string} [options.sortBy='createdAt'] - Field to sort by ('createdAt', 'updatedAt', 'title', 'price', 'stock').
|
985
|
+
* @param {'asc'|'desc'} [options.sortOrder='desc'] - Sort order ('asc' or 'desc').
|
986
|
+
* @param {string} [options.searchQuery] - A search term to filter by title or description.
|
987
|
+
* @returns {Promise<{products: Product[], totalCount: number, page: number, limit: number}>} A promise resolving with product data and pagination info.
|
988
|
+
* @throws {Error} If options are invalid.
|
989
|
+
*/
|
990
|
+
async listProducts(options = {}) {
|
991
|
+
console.log(`[YapiSellerService] Attempting to list products for seller: ${this._sellerId} with options:`, options);
|
992
|
+
return this._simulateApiCall(() => {
|
993
|
+
// 1. Validate options
|
994
|
+
const page = options.page === undefined ? 1 : options.page;
|
995
|
+
const limit = options.limit === undefined ? 10 : options.limit;
|
996
|
+
const statusFilter = options.status;
|
997
|
+
const categoryFilter = options.category;
|
998
|
+
const sortBy = options.sortBy === undefined ? 'createdAt' : options.sortBy;
|
999
|
+
const sortOrder = options.sortOrder === undefined ? 'desc' : options.sortOrder;
|
1000
|
+
const searchQuery = options.searchQuery;
|
1001
|
+
|
1002
|
+
_validateNonNegativeInteger(page, 'options.page');
|
1003
|
+
if (page < 1) throw new Error('options.page must be at least 1.');
|
1004
|
+
_validateNonNegativeInteger(limit, 'options.limit');
|
1005
|
+
if (limit < 1 || limit > 100) throw new Error('options.limit must be between 1 and 100.'); // Arbitrary limit max
|
1006
|
+
|
1007
|
+
if (statusFilter !== undefined && statusFilter !== null) {
|
1008
|
+
_validateStatus(statusFilter, PRODUCT_STATUSES, 'options.status');
|
1009
|
+
}
|
1010
|
+
if (categoryFilter !== undefined && categoryFilter !== null) {
|
1011
|
+
_validateString(categoryFilter, 'options.category', true); // Allow empty string category
|
1012
|
+
}
|
1013
|
+
if (sortBy !== undefined && sortBy !== null) {
|
1014
|
+
_validateString(sortBy, 'options.sortBy');
|
1015
|
+
const validSortBy = ['createdAt', 'updatedAt', 'title', 'price', 'stock'];
|
1016
|
+
if (!validSortBy.includes(sortBy)) {
|
1017
|
+
throw new Error(`Invalid options.sortBy: "${sortBy}". Must be one of ${validSortBy.join(', ')}.`);
|
1018
|
+
}
|
1019
|
+
}
|
1020
|
+
if (sortOrder !== undefined && sortOrder !== null) {
|
1021
|
+
_validateString(sortOrder, 'options.sortOrder');
|
1022
|
+
if (sortOrder !== 'asc' && sortOrder !== 'desc') {
|
1023
|
+
throw new Error(`Invalid options.sortOrder: "${sortOrder}". Must be 'asc' or 'desc'.`);
|
1024
|
+
}
|
1025
|
+
}
|
1026
|
+
if (searchQuery !== undefined && searchQuery !== null) {
|
1027
|
+
_validateString(searchQuery, 'options.searchQuery', true);
|
1028
|
+
}
|
1029
|
+
|
1030
|
+
|
1031
|
+
// 2. Filter products relevant to this seller
|
1032
|
+
let filteredProducts = _products.filter(p => p.sellerId === this._sellerId);
|
1033
|
+
|
1034
|
+
// 3. Apply filters
|
1035
|
+
if (statusFilter !== undefined && statusFilter !== null) {
|
1036
|
+
filteredProducts = filteredProducts.filter(p => p.status === statusFilter);
|
1037
|
+
}
|
1038
|
+
if (categoryFilter !== undefined && categoryFilter !== null) {
|
1039
|
+
// Case-insensitive category filter
|
1040
|
+
filteredProducts = filteredProducts.filter(p => p.category.toLowerCase() === categoryFilter.toLowerCase());
|
1041
|
+
}
|
1042
|
+
if (searchQuery !== undefined && searchQuery !== null && searchQuery.trim().length > 0) {
|
1043
|
+
const lowerQuery = searchQuery.toLowerCase();
|
1044
|
+
filteredProducts = filteredProducts.filter(p =>
|
1045
|
+
(p.title && p.title.toLowerCase().includes(lowerQuery)) ||
|
1046
|
+
(p.description && p.description.toLowerCase().includes(lowerQuery))
|
1047
|
+
);
|
1048
|
+
}
|
1049
|
+
|
1050
|
+
|
1051
|
+
// 4. Apply sorting
|
1052
|
+
const sortMultiplier = sortOrder === 'asc' ? 1 : -1;
|
1053
|
+
filteredProducts.sort((a, b) => {
|
1054
|
+
const valueA = a[sortBy];
|
1055
|
+
const valueB = b[sortBy];
|
1056
|
+
|
1057
|
+
if (valueA === undefined || valueA === null) return -1 * sortMultiplier;
|
1058
|
+
if (valueB === undefined || valueB === null) return 1 * sortMultiplier;
|
1059
|
+
|
1060
|
+
if (typeof valueA === 'string' && typeof valueB === 'string') {
|
1061
|
+
return valueA.localeCompare(valueB) * sortMultiplier;
|
1062
|
+
}
|
1063
|
+
if (valueA < valueB) {
|
1064
|
+
return -1 * sortMultiplier;
|
1065
|
+
}
|
1066
|
+
if (valueA > valueB) {
|
1067
|
+
return 1 * sortMultiplier;
|
1068
|
+
}
|
1069
|
+
return 0; // Equal
|
1070
|
+
});
|
1071
|
+
|
1072
|
+
// 5. Apply pagination
|
1073
|
+
const totalCount = filteredProducts.length;
|
1074
|
+
const startIndex = (page - 1) * limit;
|
1075
|
+
const endIndex = startIndex + limit;
|
1076
|
+
const paginatedProducts = filteredProducts.slice(startIndex, endIndex);
|
1077
|
+
|
1078
|
+
console.log(`[YapiSellerService] Listed ${paginatedProducts.length} products (total ${totalCount}) for seller: ${this._sellerId}`);
|
1079
|
+
|
1080
|
+
// Return copies of products
|
1081
|
+
const productObjects = paginatedProducts.map(p => new Product(p.toObject()));
|
1082
|
+
|
1083
|
+
return {
|
1084
|
+
products: productObjects,
|
1085
|
+
totalCount: totalCount,
|
1086
|
+
page: page,
|
1087
|
+
limit: limit
|
1088
|
+
};
|
1089
|
+
});
|
1090
|
+
}
|
1091
|
+
|
1092
|
+
|
1093
|
+
/**
|
1094
|
+
* Gets the total count of products for the seller, optionally filtered by status.
|
1095
|
+
* @param {object} [filter] - Filter options.
|
1096
|
+
* @param {string} [filter.status] - Filter by product status ('draft', 'active', 'archived').
|
1097
|
+
* @returns {Promise<number>} A promise resolving with the total number of products.
|
1098
|
+
* @throws {Error} If filter options are invalid.
|
1099
|
+
*/
|
1100
|
+
async countProducts(filter = {}) {
|
1101
|
+
console.log(`[YapiSellerService] Attempting to count products for seller: ${this._sellerId} with filter:`, filter);
|
1102
|
+
return this._simulateApiCall(() => {
|
1103
|
+
const statusFilter = filter.status;
|
1104
|
+
|
1105
|
+
if (statusFilter !== undefined && statusFilter !== null) {
|
1106
|
+
_validateStatus(statusFilter, PRODUCT_STATUSES, 'filter.status');
|
1107
|
+
}
|
1108
|
+
|
1109
|
+
let count = _products.filter(p => p.sellerId === this._sellerId).length;
|
1110
|
+
|
1111
|
+
if (statusFilter !== undefined && statusFilter !== null) {
|
1112
|
+
count = _products.filter(p => p.sellerId === this._sellerId && p.status === statusFilter).length;
|
1113
|
+
}
|
1114
|
+
|
1115
|
+
console.log(`[YapiSellerService] Counted ${count} products for seller: ${this._sellerId}`);
|
1116
|
+
return count;
|
1117
|
+
});
|
1118
|
+
}
|
1119
|
+
|
1120
|
+
|
1121
|
+
// --- Order Management Methods ---
|
1122
|
+
|
1123
|
+
/**
|
1124
|
+
* Lists orders relevant to this seller with pagination and filtering options.
|
1125
|
+
* Orders are items where the ordered product belongs to this seller.
|
1126
|
+
* @param {object} [options] - Options for listing orders.
|
1127
|
+
* @param {number} [options.page=1] - The page number (1-based index).
|
1128
|
+
* @param {number} [options.limit=10] - The number of items per page.
|
1129
|
+
* @param {string} [options.status] - Filter by order status ('pending', 'processing', etc.).
|
1130
|
+
* @param {string} [options.orderId] - Filter by the overall order ID.
|
1131
|
+
* @param {string} [options.productId] - Filter by a specific product ID.
|
1132
|
+
* @param {Date} [options.startDate] - Filter for orders placed on or after this date.
|
1133
|
+
* @param {Date} [options.endDate] - Filter for orders placed on or before this date.
|
1134
|
+
* @param {string} [options.sortBy='orderDate'] - Field to sort by ('orderDate', 'updatedAt', 'totalPrice', 'quantity').
|
1135
|
+
* @param {'asc'|'desc'} [options.sortOrder='desc'] - Sort order ('asc' or 'desc').
|
1136
|
+
* @returns {Promise<{orders: Order[], totalCount: number, page: number, limit: number}>} A promise resolving with order data and pagination info.
|
1137
|
+
* @throws {Error} If options are invalid.
|
1138
|
+
*/
|
1139
|
+
async listOrders(options = {}) {
|
1140
|
+
console.log(`[YapiSellerService] Attempting to list orders for seller: ${this._sellerId} with options:`, options);
|
1141
|
+
return this._simulateApiCall(() => {
|
1142
|
+
// 1. Validate options
|
1143
|
+
const page = options.page === undefined ? 1 : options.page;
|
1144
|
+
const limit = options.limit === undefined ? 10 : options.limit;
|
1145
|
+
const statusFilter = options.status;
|
1146
|
+
const orderIdFilter = options.orderId;
|
1147
|
+
const productIdFilter = options.productId;
|
1148
|
+
const startDateFilter = options.startDate instanceof Date ? options.startDate : null;
|
1149
|
+
const endDateFilter = options.endDate instanceof Date ? options.endDate : null;
|
1150
|
+
const sortBy = options.sortBy === undefined ? 'orderDate' : options.sortBy;
|
1151
|
+
const sortOrder = options.sortOrder === undefined ? 'desc' : options.sortOrder;
|
1152
|
+
|
1153
|
+
|
1154
|
+
_validateNonNegativeInteger(page, 'options.page');
|
1155
|
+
if (page < 1) throw new Error('options.page must be at least 1.');
|
1156
|
+
_validateNonNegativeInteger(limit, 'options.limit');
|
1157
|
+
if (limit < 1 || limit > 100) throw new Error('options.limit must be between 1 and 100.');
|
1158
|
+
|
1159
|
+
if (statusFilter !== undefined && statusFilter !== null) {
|
1160
|
+
_validateStatus(statusFilter, ORDER_STATUSES, 'options.status');
|
1161
|
+
}
|
1162
|
+
if (orderIdFilter !== undefined && orderIdFilter !== null) {
|
1163
|
+
_validateString(orderIdFilter, 'options.orderId');
|
1164
|
+
}
|
1165
|
+
if (productIdFilter !== undefined && productIdFilter !== null) {
|
1166
|
+
_validateString(productIdFilter, 'options.productId');
|
1167
|
+
}
|
1168
|
+
if (startDateFilter !== null && !(startDateFilter instanceof Date) || isNaN(startDateFilter.getTime())) {
|
1169
|
+
throw new Error('options.startDate must be a valid Date object.');
|
1170
|
+
}
|
1171
|
+
if (endDateFilter !== null && !(endDateFilter instanceof Date) || isNaN(endDateFilter.getTime())) {
|
1172
|
+
throw new Error('options.endDate must be a valid Date object.');
|
1173
|
+
}
|
1174
|
+
if (startDateFilter && endDateFilter && startDateFilter > endDateFilter) {
|
1175
|
+
throw new Error('options.startDate cannot be after options.endDate.');
|
1176
|
+
}
|
1177
|
+
|
1178
|
+
if (sortBy !== undefined && sortBy !== null) {
|
1179
|
+
_validateString(sortBy, 'options.sortBy');
|
1180
|
+
const validSortBy = ['orderDate', 'updatedAt', 'totalPrice', 'quantity', 'itemPrice'];
|
1181
|
+
if (!validSortBy.includes(sortBy)) {
|
1182
|
+
throw new Error(`Invalid options.sortBy: "${sortBy}". Must be one of ${validSortBy.join(', ')}.`);
|
1183
|
+
}
|
1184
|
+
}
|
1185
|
+
if (sortOrder !== undefined && sortOrder !== null) {
|
1186
|
+
_validateString(sortOrder, 'options.sortOrder');
|
1187
|
+
if (sortOrder !== 'asc' && sortOrder !== 'desc') {
|
1188
|
+
throw new Error(`Invalid options.sortOrder: "${sortOrder}". Must be 'asc' or 'desc'.`);
|
1189
|
+
}
|
1190
|
+
}
|
1191
|
+
|
1192
|
+
|
1193
|
+
// 2. Filter orders relevant to this seller
|
1194
|
+
let filteredOrders = _orders.filter(order => order.sellerId === this._sellerId);
|
1195
|
+
|
1196
|
+
// 3. Apply filters
|
1197
|
+
if (statusFilter !== undefined && statusFilter !== null) {
|
1198
|
+
filteredOrders = filteredOrders.filter(order => order.status === statusFilter);
|
1199
|
+
}
|
1200
|
+
if (orderIdFilter !== undefined && orderIdFilter !== null) {
|
1201
|
+
filteredOrders = filteredOrders.filter(order => order.orderId === orderIdFilter);
|
1202
|
+
}
|
1203
|
+
if (productIdFilter !== undefined && productIdFilter !== null) {
|
1204
|
+
filteredOrders = filteredOrders.filter(order => order.productId === productIdFilter);
|
1205
|
+
}
|
1206
|
+
if (startDateFilter !== null) {
|
1207
|
+
filteredOrders = filteredOrders.filter(order => order.orderDate >= startDateFilter);
|
1208
|
+
}
|
1209
|
+
if (endDateFilter !== null) {
|
1210
|
+
// Include the end date itself
|
1211
|
+
const endOfDay = new Date(endDateFilter);
|
1212
|
+
endOfDay.setHours(23, 59, 59, 999);
|
1213
|
+
filteredOrders = filteredOrders.filter(order => order.orderDate <= endOfDay);
|
1214
|
+
}
|
1215
|
+
|
1216
|
+
|
1217
|
+
// 4. Apply sorting
|
1218
|
+
const sortMultiplier = sortOrder === 'asc' ? 1 : -1;
|
1219
|
+
filteredOrders.sort((a, b) => {
|
1220
|
+
const valueA = a[sortBy];
|
1221
|
+
const valueB = b[sortBy];
|
1222
|
+
|
1223
|
+
if (valueA === undefined || valueA === null) return -1 * sortMultiplier;
|
1224
|
+
if (valueB === undefined || valueB === null) return 1 * sortMultiplier;
|
1225
|
+
|
1226
|
+
// Handle Date comparison
|
1227
|
+
if (valueA instanceof Date && valueB instanceof Date) {
|
1228
|
+
return (valueA.getTime() - valueB.getTime()) * sortMultiplier;
|
1229
|
+
}
|
1230
|
+
// Handle string comparison
|
1231
|
+
if (typeof valueA === 'string' && typeof valueB === 'string') {
|
1232
|
+
return valueA.localeCompare(valueB) * sortMultiplier;
|
1233
|
+
}
|
1234
|
+
// Handle number comparison
|
1235
|
+
if (valueA < valueB) {
|
1236
|
+
return -1 * sortMultiplier;
|
1237
|
+
}
|
1238
|
+
if (valueA > valueB) {
|
1239
|
+
return 1 * sortMultiplier;
|
1240
|
+
}
|
1241
|
+
return 0; // Equal
|
1242
|
+
});
|
1243
|
+
|
1244
|
+
|
1245
|
+
// 5. Apply pagination
|
1246
|
+
const totalCount = filteredOrders.length;
|
1247
|
+
const startIndex = (page - 1) * limit;
|
1248
|
+
const endIndex = startIndex + limit;
|
1249
|
+
const paginatedOrders = filteredOrders.slice(startIndex, endIndex);
|
1250
|
+
|
1251
|
+
console.log(`[YapiSellerService] Listed ${paginatedOrders.length} orders (total ${totalCount}) for seller: ${this._sellerId}`);
|
1252
|
+
|
1253
|
+
// Return copies of orders
|
1254
|
+
const orderObjects = paginatedOrders.map(o => new Order(o.toObject()));
|
1255
|
+
|
1256
|
+
return {
|
1257
|
+
orders: orderObjects,
|
1258
|
+
totalCount: totalCount,
|
1259
|
+
page: page,
|
1260
|
+
limit: limit
|
1261
|
+
};
|
1262
|
+
});
|
1263
|
+
}
|
1264
|
+
|
1265
|
+
/**
|
1266
|
+
* Retrieves a specific order item by its ID.
|
1267
|
+
* Note: This retrieves a single item within a potentially larger order.
|
1268
|
+
* @param {string} orderItemId - The unique identifier of the order item.
|
1269
|
+
* @returns {Promise<Order>} A promise that resolves with the Order object.
|
1270
|
+
* @throws {Error} If the orderItemId is invalid or the order item is not found for this seller.
|
1271
|
+
*/
|
1272
|
+
async getOrderItemById(orderItemId) {
|
1273
|
+
console.log(`[YapiSellerService] Attempting to get order item by ID: ${orderItemId} for seller: ${this._sellerId}`);
|
1274
|
+
return this._simulateApiCall(() => {
|
1275
|
+
_validateString(orderItemId, 'orderItemId');
|
1276
|
+
|
1277
|
+
const orderItem = _orders.find(o => o.id === orderItemId && o.sellerId === this._sellerId);
|
1278
|
+
|
1279
|
+
if (!orderItem) {
|
1280
|
+
console.error(`[YapiSellerService] Order item with ID ${orderItemId} not found for seller: ${this._sellerId}.`);
|
1281
|
+
throw new Error(`Order item with ID ${orderItemId} not found.`);
|
1282
|
+
}
|
1283
|
+
|
1284
|
+
console.log(`[YapiSellerService] Successfully retrieved order item with ID: ${orderItemId}`);
|
1285
|
+
// Return a copy
|
1286
|
+
return new Order(orderItem.toObject());
|
1287
|
+
});
|
1288
|
+
}
|
1289
|
+
|
1290
|
+
/**
|
1291
|
+
* Updates the status of a specific order item.
|
1292
|
+
* @param {string} orderItemId - The unique identifier of the order item to update.
|
1293
|
+
* @param {string} newStatus - The new status for the order item ('pending', 'processing', etc.).
|
1294
|
+
* @returns {Promise<Order>} A promise that resolves with the updated Order object.
|
1295
|
+
* @throws {Error} If the orderItemId is invalid, newStatus is invalid, or the order item is not found.
|
1296
|
+
*/
|
1297
|
+
async updateOrderItemStatus(orderItemId, newStatus) {
|
1298
|
+
console.log(`[YapiSellerService] Attempting to update status of order item ID: ${orderItemId} to "${newStatus}" for seller: ${this._sellerId}`);
|
1299
|
+
return this._simulateApiCall(() => {
|
1300
|
+
_validateString(orderItemId, 'orderItemId');
|
1301
|
+
_validateStatus(newStatus, ORDER_STATUSES, 'newStatus');
|
1302
|
+
|
1303
|
+
const orderItem = _orders.find(o => o.id === orderItemId && o.sellerId === this._sellerId);
|
1304
|
+
|
1305
|
+
if (!orderItem) {
|
1306
|
+
console.error(`[YapiSellerService] Order item with ID ${orderItemId} not found for status update for seller: ${this._sellerId}.`);
|
1307
|
+
throw new Error(`Order item with ID ${orderItemId} not found.`);
|
1308
|
+
}
|
1309
|
+
|
1310
|
+
// Apply status update using the Order class method
|
1311
|
+
orderItem.updateStatus(newStatus);
|
1312
|
+
|
1313
|
+
console.log(`[YapiSellerService] Order item ID: ${orderItemId} status updated to "${newStatus}" for seller: ${this._sellerId}`);
|
1314
|
+
// Return a copy of the updated order item
|
1315
|
+
return new Order(orderItem.toObject());
|
1316
|
+
});
|
1317
|
+
}
|
1318
|
+
|
1319
|
+
|
1320
|
+
/**
|
1321
|
+
* Gets the total count of orders for the seller, optionally filtered by status.
|
1322
|
+
* @param {object} [filter] - Filter options.
|
1323
|
+
* @param {string} [filter.status] - Filter by order status ('pending', 'processing', etc.).
|
1324
|
+
* @returns {Promise<number>} A promise resolving with the total number of orders.
|
1325
|
+
* @throws {Error} If filter options are invalid.
|
1326
|
+
*/
|
1327
|
+
async countOrders(filter = {}) {
|
1328
|
+
console.log(`[YapiSellerService] Attempting to count orders for seller: ${this._sellerId} with filter:`, filter);
|
1329
|
+
return this._simulateApiCall(() => {
|
1330
|
+
const statusFilter = filter.status;
|
1331
|
+
|
1332
|
+
if (statusFilter !== undefined && statusFilter !== null) {
|
1333
|
+
_validateStatus(statusFilter, ORDER_STATUSES, 'filter.status');
|
1334
|
+
}
|
1335
|
+
|
1336
|
+
let count = _orders.filter(o => o.sellerId === this._sellerId).length;
|
1337
|
+
|
1338
|
+
if (statusFilter !== undefined && statusFilter !== null) {
|
1339
|
+
count = _orders.filter(o => o.sellerId === this._sellerId && o.status === statusFilter).length;
|
1340
|
+
}
|
1341
|
+
|
1342
|
+
console.log(`[YapiSellerService] Counted ${count} orders for seller: ${this._sellerId}`);
|
1343
|
+
return count;
|
1344
|
+
});
|
1345
|
+
}
|
1346
|
+
|
1347
|
+
|
1348
|
+
// --- Inventory Methods (Basic) ---
|
1349
|
+
|
1350
|
+
/**
|
1351
|
+
* Updates the stock level for a specific product.
|
1352
|
+
* The change quantity can be positive (add stock) or negative (remove stock).
|
1353
|
+
* @param {string} productId - The unique identifier of the product.
|
1354
|
+
* @param {number} quantityChange - The amount to change the stock by. Can be positive or negative integer.
|
1355
|
+
* @returns {Promise<Product>} A promise that resolves with the updated Product object.
|
1356
|
+
* @throws {Error} If productId is invalid, quantityChange is not a valid integer, or the product is not found, or if the change results in negative stock.
|
1357
|
+
*/
|
1358
|
+
async updateStock(productId, quantityChange) {
|
1359
|
+
console.log(`[YapiSellerService] Attempting to update stock for product ID: ${productId} by ${quantityChange} for seller: ${this._sellerId}`);
|
1360
|
+
return this._simulateApiCall(() => {
|
1361
|
+
_validateString(productId, 'productId');
|
1362
|
+
if (typeof quantityChange !== 'number' || !Number.isInteger(quantityChange)) {
|
1363
|
+
throw new Error('quantityChange must be an integer.');
|
1364
|
+
}
|
1365
|
+
|
1366
|
+
const product = _products.find(p => p.id === productId && p.sellerId === this._sellerId);
|
1367
|
+
|
1368
|
+
if (!product) {
|
1369
|
+
console.error(`[YapiSellerService] Product with ID ${productId} not found for stock update for seller: ${this._sellerId}.`);
|
1370
|
+
throw new Error(`Product with ID ${productId} not found.`);
|
1371
|
+
}
|
1372
|
+
|
1373
|
+
const newStock = product.stock + quantityChange;
|
1374
|
+
if (newStock < 0) {
|
1375
|
+
console.error(`[YapiSellerService] Stock update for product ID ${productId} failed: Resulting stock would be negative (${newStock}).`);
|
1376
|
+
throw new Error(`Cannot update stock: Resulting stock quantity (${newStock}) cannot be negative.`);
|
1377
|
+
}
|
1378
|
+
|
1379
|
+
product.stock = newStock;
|
1380
|
+
product.updatedAt = new Date(); // Mark product as updated
|
1381
|
+
console.log(`[YapiSellerService] Stock updated for product ID: ${productId}. New stock: ${product.stock} for seller: ${this._sellerId}`);
|
1382
|
+
|
1383
|
+
// Return a copy of the updated product
|
1384
|
+
return new Product(product.toObject());
|
1385
|
+
});
|
1386
|
+
}
|
1387
|
+
|
1388
|
+
/**
|
1389
|
+
* Gets the current stock level for a specific product.
|
1390
|
+
* @param {string} productId - The unique identifier of the product.
|
1391
|
+
* @returns {Promise<number>} A promise that resolves with the current stock quantity.
|
1392
|
+
* @throws {Error} If productId is invalid or the product is not found.
|
1393
|
+
*/
|
1394
|
+
async getStockLevel(productId) {
|
1395
|
+
console.log(`[YapiSellerService] Attempting to get stock level for product ID: ${productId} for seller: ${this._sellerId}`);
|
1396
|
+
return this._simulateApiCall(() => {
|
1397
|
+
_validateString(productId, 'productId');
|
1398
|
+
|
1399
|
+
const product = _products.find(p => p.id === productId && p.sellerId === this._sellerId);
|
1400
|
+
|
1401
|
+
if (!product) {
|
1402
|
+
console.error(`[YapiSellerService] Product with ID ${productId} not found for stock check for seller: ${this._sellerId}.`);
|
1403
|
+
throw new Error(`Product with ID ${productId} not found.`);
|
1404
|
+
}
|
1405
|
+
|
1406
|
+
console.log(`[YapiSellerService] Stock level for product ID: ${productId} is ${product.stock} for seller: ${this._sellerId}`);
|
1407
|
+
return product.stock;
|
1408
|
+
});
|
1409
|
+
}
|
1410
|
+
|
1411
|
+
/**
|
1412
|
+
* Lists products for the seller that are currently below a specified stock threshold.
|
1413
|
+
* Only lists products with 'active' status by default.
|
1414
|
+
* @param {number} threshold - The stock quantity threshold. Products with stock less than this will be listed.
|
1415
|
+
* @returns {Promise<Product[]>} A promise that resolves with an array of Product objects with low stock.
|
1416
|
+
* @throws {Error} If the threshold is invalid.
|
1417
|
+
*/
|
1418
|
+
async listLowStockProducts(threshold) {
|
1419
|
+
console.log(`[YapiSellerService] Attempting to list low stock products (threshold < ${threshold}) for seller: ${this._sellerId}`);
|
1420
|
+
return this._simulateApiCall(() => {
|
1421
|
+
_validateNonNegativeInteger(threshold, 'threshold');
|
1422
|
+
if (threshold < 0) throw new Error('Threshold cannot be negative.');
|
1423
|
+
|
1424
|
+
|
1425
|
+
const lowStockProducts = _products.filter(p =>
|
1426
|
+
p.sellerId === this._sellerId &&
|
1427
|
+
p.status === 'active' && // Typically only active products need stock monitoring
|
1428
|
+
p.stock < threshold
|
1429
|
+
);
|
1430
|
+
|
1431
|
+
console.log(`[YapiSellerService] Found ${lowStockProducts.length} products below stock threshold ${threshold} for seller: ${this._sellerId}`);
|
1432
|
+
// Return copies of products
|
1433
|
+
return lowStockProducts.map(p => new Product(p.toObject()));
|
1434
|
+
});
|
1435
|
+
}
|
1436
|
+
|
1437
|
+
|
1438
|
+
// --- Additional Utility/Simulation Methods (Private) ---
|
1439
|
+
|
1440
|
+
/**
|
1441
|
+
* Internal helper to find a product index by ID.
|
1442
|
+
* @param {string} productId - The product ID.
|
1443
|
+
* @returns {number} The index of the product in the _products array, or -1 if not found.
|
1444
|
+
* @private
|
1445
|
+
*/
|
1446
|
+
_findProductIndexById(productId) {
|
1447
|
+
// Note: Real API wouldn't expose indices, this is purely for simulation data manipulation
|
1448
|
+
return _products.findIndex(p => p.id === productId && p.sellerId === this._sellerId);
|
1449
|
+
}
|
1450
|
+
|
1451
|
+
/**
|
1452
|
+
* Internal helper to find an order item index by ID.
|
1453
|
+
* @param {string} orderItemId - The order item ID.
|
1454
|
+
* @returns {number} The index of the order item in the _orders array, or -1 if not found.
|
1455
|
+
* @private
|
1456
|
+
*/
|
1457
|
+
_findOrderItemIndexById(orderItemId) {
|
1458
|
+
// Note: Real API wouldn't expose indices, this is purely for simulation data manipulation
|
1459
|
+
return _orders.findIndex(o => o.id === orderItemId && o.sellerId === this._sellerId);
|
1460
|
+
}
|
1461
|
+
|
1462
|
+
/**
|
1463
|
+
* Simulates clearing all in-memory data for the seller.
|
1464
|
+
* Useful for testing or resetting the simulation state.
|
1465
|
+
* In a real service, this method would not exist or would require high privileges.
|
1466
|
+
* @returns {Promise<void>} A promise that resolves when data is cleared.
|
1467
|
+
*/
|
1468
|
+
async _clearSimulatedData() {
|
1469
|
+
console.warn(`[YapiSellerService - Simulation] Clearing simulated data for seller: ${this._sellerId}`);
|
1470
|
+
return this._simulateApiCall(() => {
|
1471
|
+
_sellerProfile = null; // Reset seller profile
|
1472
|
+
_products = []; // Clear products
|
1473
|
+
_orders = []; // Clear orders
|
1474
|
+
console.warn(`[YapiSellerService - Simulation] Simulated data cleared for seller: ${this._sellerId}`);
|
1475
|
+
}, 50, 100); // Faster simulation clear
|
1476
|
+
}
|
1477
|
+
}
|
1478
|
+
|
1479
|
+
// --- Exports ---
|
1480
|
+
|
1481
|
+
/**
|
1482
|
+
* @exports YapiSellerService
|
1483
|
+
*/
|
1484
|
+
module.exports = YapiSellerService;
|
1485
|
+
|
1486
|
+
// Note: In a real ES Module environment, you would use `export default YapiSellerService;`
|
1487
|
+
// and potentially export the data classes if needed, e.g., `export { YapiSellerService, Product, Order, SellerProfile };`
|
1488
|
+
// Using module.exports for Node.js CommonJS compatibility, common in older npm packages.
|
1489
|
+
|
1490
|
+
// Example Usage (Illustrative - would typically be in a separate test or example file)
|
1491
|
+
/*
|
1492
|
+
async function runExample() {
|
1493
|
+
const sellerId = 'SELLER123';
|
1494
|
+
const sellerService = new YapiSellerService(sellerId);
|
1495
|
+
|
1496
|
+
try {
|
1497
|
+
// Get profile
|
1498
|
+
const profile = await sellerService.getProfile();
|
1499
|
+
console.log('\n--- Seller Profile ---');
|
1500
|
+
console.log(profile.toObject());
|
1501
|
+
|
1502
|
+
// Update profile
|
1503
|
+
await sellerService.updateProfile({ phone: '+1-555-9876', description: 'Updated description.' });
|
1504
|
+
const updatedProfile = await sellerService.getProfile();
|
1505
|
+
console.log('\n--- Updated Seller Profile ---');
|
1506
|
+
console.log(updatedProfile.toObject());
|
1507
|
+
|
1508
|
+
// List products
|
1509
|
+
console.log('\n--- Listing Products (Page 1) ---');
|
1510
|
+
const productList = await sellerService.listProducts({ page: 1, limit: 2, status: 'active', sortBy: 'title', sortOrder: 'asc' });
|
1511
|
+
console.log(`Total active products: ${productList.totalCount}`);
|
1512
|
+
productList.products.forEach(p => console.log(p.toObject()));
|
1513
|
+
|
1514
|
+
console.log('\n--- Listing Products (Page 2) ---');
|
1515
|
+
const productList2 = await sellerService.listProducts({ page: 2, limit: 2, status: 'active', sortBy: 'title', sortOrder: 'asc' });
|
1516
|
+
productList2.products.forEach(p => console.log(p.toObject()));
|
1517
|
+
|
1518
|
+
// Get a specific product
|
1519
|
+
if (productList.products.length > 0) {
|
1520
|
+
const firstProductId = productList.products[0].id;
|
1521
|
+
console.log(`\n--- Getting Product ID: ${firstProductId} ---`);
|
1522
|
+
const specificProduct = await sellerService.getProductById(firstProductId);
|
1523
|
+
console.log(specificProduct.toObject());
|
1524
|
+
|
1525
|
+
// Update a product
|
1526
|
+
console.log(`\n--- Updating Stock for Product ID: ${firstProductId} ---`);
|
1527
|
+
const updatedProduct = await sellerService.updateStock(firstProductId, -5); // Sell 5 units
|
1528
|
+
console.log(updatedProduct.toObject());
|
1529
|
+
const updatedProduct2 = await sellerService.updateStock(firstProductId, 10); // Restock 10 units
|
1530
|
+
console.log(updatedProduct2.toObject());
|
1531
|
+
|
1532
|
+
|
1533
|
+
// Archive a product
|
1534
|
+
if (productList.products.length > 1) {
|
1535
|
+
const secondProductId = productList.products[1].id;
|
1536
|
+
console.log(`\n--- Archiving Product ID: ${secondProductId} ---`);
|
1537
|
+
await sellerService.archiveProduct(secondProductId);
|
1538
|
+
console.log(`Product ${secondProductId} status is now archived.`);
|
1539
|
+
}
|
1540
|
+
|
1541
|
+
|
1542
|
+
} else {
|
1543
|
+
console.log('\nNo active products found to demonstrate get/update/archive.');
|
1544
|
+
}
|
1545
|
+
|
1546
|
+
|
1547
|
+
// List orders
|
1548
|
+
console.log('\n--- Listing Orders (Page 1) ---');
|
1549
|
+
const orderList = await sellerService.listOrders({ limit: 3, sortBy: 'orderDate' });
|
1550
|
+
console.log(`Total orders: ${orderList.totalCount}`);
|
1551
|
+
orderList.orders.forEach(o => console.log(o.toObject()));
|
1552
|
+
|
1553
|
+
// Get a specific order item
|
1554
|
+
if (orderList.orders.length > 0) {
|
1555
|
+
const firstOrderItemId = orderList.orders[0].id;
|
1556
|
+
console.log(`\n--- Getting Order Item ID: ${firstOrderItemId} ---`);
|
1557
|
+
const specificOrderItem = await sellerService.getOrderItemById(firstOrderItemId);
|
1558
|
+
console.log(specificOrderItem.toObject());
|
1559
|
+
|
1560
|
+
// Update order status
|
1561
|
+
console.log(`\n--- Updating Status for Order Item ID: ${firstOrderItemId} ---`);
|
1562
|
+
const updatedOrderItem = await sellerService.updateOrderItemStatus(firstOrderItemId, 'shipped');
|
1563
|
+
console.log(updatedOrderItem.toObject());
|
1564
|
+
} else {
|
1565
|
+
console.log('\nNo orders found to demonstrate get/update.');
|
1566
|
+
}
|
1567
|
+
|
1568
|
+
|
1569
|
+
// Count products by status
|
1570
|
+
console.log('\n--- Counting Products by Status ---');
|
1571
|
+
const activeCount = await sellerService.countProducts({ status: 'active' });
|
1572
|
+
const archivedCount = await sellerService.countProducts({ status: 'archived' });
|
1573
|
+
const draftCount = await sellerService.countProducts({ status: 'draft' });
|
1574
|
+
const totalProductCount = await sellerService.countProducts();
|
1575
|
+
console.log(`Active products: ${activeCount}`);
|
1576
|
+
console.log(`Archived products: ${archivedCount}`);
|
1577
|
+
console.log(`Draft products: ${draftCount}`);
|
1578
|
+
console.log(`Total products: ${totalProductCount}`);
|
1579
|
+
|
1580
|
+
// Count orders by status
|
1581
|
+
console.log('\n--- Counting Orders by Status ---');
|
1582
|
+
const pendingCount = await sellerService.countOrders({ status: 'pending' });
|
1583
|
+
const shippedCount = await sellerService.countOrders({ status: 'shipped' });
|
1584
|
+
const totalOrderCount = await sellerService.countOrders();
|
1585
|
+
console.log(`Pending orders: ${pendingCount}`);
|
1586
|
+
console.log(`Shipped orders: ${shippedCount}`);
|
1587
|
+
console.log(`Total order items: ${totalOrderCount}`);
|
1588
|
+
|
1589
|
+
|
1590
|
+
// List low stock products
|
1591
|
+
console.log('\n--- Listing Low Stock Products (Threshold 100) ---');
|
1592
|
+
const lowStock = await sellerService.listLowStockProducts(100);
|
1593
|
+
lowStock.forEach(p => console.log(`Low Stock: ${p.title} (${p.stock})`));
|
1594
|
+
|
1595
|
+
// Simulate an error case (e.g., get non-existent product)
|
1596
|
+
console.log('\n--- Demonstrating Error Handling (Get non-existent product) ---');
|
1597
|
+
try {
|
1598
|
+
await sellerService.getProductById('non-existent-id');
|
1599
|
+
} catch (error) {
|
1600
|
+
console.error('Caught expected error:', error.message);
|
1601
|
+
}
|
1602
|
+
|
1603
|
+
// Simulate an error case (e.g., invalid update data)
|
1604
|
+
console.log('\n--- Demonstrating Error Handling (Invalid update data) ---');
|
1605
|
+
try {
|
1606
|
+
await sellerService.updateProfile({ name: '' }); // Invalid empty string
|
1607
|
+
} catch (error) {
|
1608
|
+
console.error('Caught expected error:', error.message);
|
1609
|
+
}
|
1610
|
+
|
1611
|
+
|
1612
|
+
} catch (error) {
|
1613
|
+
console.error('\nAn unexpected error occurred during example execution:', error);
|
1614
|
+
} finally {
|
1615
|
+
// Clean up simulated data if needed (optional)
|
1616
|
+
// await sellerService._clearSimulatedData();
|
1617
|
+
}
|
1618
|
+
}
|
1619
|
+
|
1620
|
+
// runExample();
|
1621
|
+
*/
|