@shyntech-proximity/inventories 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +135 -0
  2. package/package.json +19 -0
  3. package/src/index.js +536 -0
package/README.md ADDED
@@ -0,0 +1,135 @@
1
+ # Inventory Backend API Documentation
2
+
3
+ **Base URL:** `/api` (example)
4
+ **Auth:** Currently relies on device headers (`x-device-sig`, `x-ws-token`) and IP-based identification for proximity features.
5
+
6
+ **Models:**
7
+
8
+ * **Vendor**: Contains vendor_info and inventory arrays.
9
+ * **DeviceScan**: Records devices’ last scan (SSID info, timestamp, etc.).
10
+
11
+ ---
12
+
13
+ ## 1. Common Headers
14
+
15
+ | Header | Description | Required |
16
+ | -------------- | -------------------------- | -------------------------------------- |
17
+ | `x-device-sig` | Unique device signature | Optional (used in proximity detection) |
18
+ | `x-ws-token` | Session or workspace token | Optional |
19
+ | `public_ip` | Client IP address | Optional |
20
+
21
+ ---
22
+
23
+ ## 2. Inventory Endpoints
24
+
25
+ ### 2.1 Get Inventory by Item ID
26
+
27
+ **GET** `/inventory/:itemId`
28
+
29
+ | Parameter | Type | Required | Description |
30
+ | --------- | ------ | -------- | ------------------------ |
31
+ | `itemId` | String | Yes | Unique inventory item ID |
32
+
33
+ **Response:**
34
+
35
+ | Field | Type | Description |
36
+ | -------- | ------ | ------------------------------------------- |
37
+ | `vendor` | Object | Vendor info containing `vendor_info` fields |
38
+ | `item` | Object | Inventory item matching `itemId` |
39
+
40
+ **Error Codes:**
41
+
42
+ | Status | Message |
43
+ | ------ | ------------------------ |
44
+ | 404 | Inventory item not found |
45
+ | 500 | Internal server error |
46
+
47
+ ---
48
+
49
+ ### 2.2 Get Inventory by Proximity
50
+
51
+ **GET** `/inventory`
52
+
53
+ **Headers used:** `x-device-sig`, `public_ip`, `x-ws-token`
54
+
55
+ **Response:**
56
+
57
+ * List of vendors matching the last device scan SSIDs
58
+
59
+ | Field | Type | Description |
60
+ | ------------- | ------ | ----------------------- |
61
+ | `vendor_id` | String | Vendor identifier |
62
+ | `vendor_name` | String | Name of vendor |
63
+ | `address` | String | Vendor address |
64
+ | `inventory` | Array | List of inventory items |
65
+
66
+ **Error Codes:**
67
+
68
+ | Status | Message |
69
+ | ------ | ------------------------------------------ |
70
+ | 404 | No recent proximity data found for this IP |
71
+ | 500 | Internal server error |
72
+
73
+ ---
74
+
75
+ ### 2.3 Search Inventory
76
+
77
+ **GET** `/search?query=<query>`
78
+
79
+ | Query Parameter | Type | Required | Description |
80
+ | --------------- | ------ | -------- | -------------------------------------------------------------- |
81
+ | `query` | String | Yes | Fuzzy search term for inventory, vendor name, address, or tags |
82
+
83
+ **Response:**
84
+
85
+ | Field | Type | Description |
86
+ | --------- | ------ | ------------------------------ |
87
+ | `query` | String | Search term |
88
+ | `results` | Array | List of vendors matching query |
89
+
90
+ **Error Codes:**
91
+
92
+ | Status | Message |
93
+ | ------ | --------------------------- |
94
+ | 400 | Query parameter is required |
95
+ | 500 | Internal server error |
96
+
97
+ ---
98
+
99
+ ### 2.4 Create / Update / Delete Inventory
100
+
101
+ | Method | Endpoint | Request Body | Response | Notes |
102
+ | ------ | ---------------------------------- | ----------------------------------------- | ----------------------------- | ---------------------------- |
103
+ | POST | `/vendor/:vendorId/inventory` | `{ item_id, name, category, price, ... }` | 200, added item | Add single inventory item |
104
+ | POST | `/vendor/:vendorId/inventory/bulk` | `{ items: [{ item_id, name, ...}, ...] }` | 200, items added | Add multiple inventory items |
105
+ | PUT | `/inventory/:itemId` | `{ field: value, ... }` | 200, updated item | Update fields of single item |
106
+ | PUT | `/inventory/:vendorId/bulk` | `[ { item_id, field: value }, ... ]` | 200, summary of updated items | Bulk update inventory items |
107
+ | DELETE | `/inventory/:itemId` | None | 200, deleted item | Delete single item |
108
+ | DELETE | `/inventory` | `{ itemIds: [id1, id2, ...] }` | 200, `deletedCount` | Bulk delete items |
109
+
110
+ ---
111
+
112
+ ## 3. Error Response Format
113
+
114
+ | Field | Type | Description |
115
+ | --------- | ------ | ------------------------------------------------ |
116
+ | `error` | String | Error message |
117
+ | `message` | String | Optional descriptive message for success/failure |
118
+
119
+ **Example:**
120
+
121
+ ```json
122
+ {
123
+ "error": "Vendor not found"
124
+ }
125
+ ```
126
+
127
+ ---
128
+
129
+ ## 4. Notes / Guidelines
130
+
131
+ 1. **Proximity detection:** `GET /inventory` uses device signature, token, or IP as a fallback to retrieve recent Wi-Fi scans.
132
+ 2. **Bulk operations:** Partial success is possible; the response includes per-item update status.
133
+ 3. **Consistency:** All update operations return the new state of the object after modification.
134
+ 4. **Pagination:** Not implemented yet for search; consider adding `limit` and `offset` query params for large datasets.
135
+ 5. **Logging:** Server logs device signatures and SSIDs for audit/debugging.
package/package.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@shyntech-proximity/inventories",
3
+ "version": "1.0.0",
4
+ "main": "src/index.js",
5
+ "files": [
6
+ "dist",
7
+ "src"
8
+ ],
9
+ "scripts": {
10
+ "build": "echo 'no build'",
11
+ "prepublishOnly": "npm run build"
12
+ },
13
+ "publishConfig": {
14
+ "@shyntech:registry": "https://gitlab.com/api/v4/projects/78314798/packages/npm/"
15
+ },
16
+ "devDependencies": {
17
+ "typescript": "^5.9.3"
18
+ }
19
+ }
package/src/index.js ADDED
@@ -0,0 +1,536 @@
1
+ import express from "express";
2
+ import { DeviceScan } from "../models/DeviceScan.js";
3
+ import { Vendor } from "../models/Vendor.js";
4
+
5
+ const router = express.Router();
6
+ router.proximityCache = {};
7
+ /**
8
+ * Browser client calls this endpoint.
9
+ * Server finds last scan from same IP and returns matching vendor inventories.
10
+ */
11
+
12
+ // đź§  Fallback Matching Logic
13
+ function findProximityKey(req) {
14
+ const ip = req.headers["public_ip"];
15
+ const deviceSig = req.headers["x-device-sig"];
16
+ const token = req.headers["x-ws-token"];
17
+ console.log("Device: ", deviceSig);
18
+ if (deviceSig) return deviceSig;
19
+ if (ip) return ip;
20
+ if (token) return token;
21
+ return null;
22
+ }
23
+
24
+
25
+
26
+
27
+
28
+
29
+ router.get("/inventory/:itemId", async (req, res) => {
30
+ try {
31
+ const itemId = req.params.itemId;
32
+
33
+ // Find the vendor that contains the inventory item
34
+ const vendor = await Vendor.findOne(
35
+ { "inventory.item_id": itemId },
36
+ { "inventory.$": 1, vendor_info: 1 } // project only the matching inventory item and vendor_info
37
+ ).lean();
38
+
39
+ if (!vendor || !vendor.inventory || vendor.inventory.length === 0) {
40
+ return res.status(404).json({ error: "Inventory item not found" });
41
+ }
42
+
43
+ res.json({
44
+ vendor: vendor.vendor_info,
45
+ item: vendor.inventory[0]
46
+ });
47
+ } catch (error) {
48
+ console.error(error);
49
+ res.status(500).json({ error: "Internal server error" });
50
+ }
51
+ });
52
+
53
+
54
+
55
+
56
+
57
+ router.get("/search", async (req, res) => {
58
+ try {
59
+ const { query } = req.query;
60
+
61
+ if (!query || query.trim() === "") {
62
+ return res.status(400).json({ error: "Query parameter is required" });
63
+ }
64
+
65
+ const regex = new RegExp(query, "i"); // case-insensitive fuzzy match
66
+
67
+ // Search inventory items
68
+ const vendors = await Vendor.find({
69
+ $or: [
70
+ { "vendor_info.name": regex },
71
+ { "vendor_info.address": regex },
72
+ { "inventory.name": regex },
73
+ { "inventory.category": regex },
74
+ { "inventory.tags": regex }
75
+ ]
76
+ }).lean();
77
+
78
+ const results = vendors.map(vendor => {
79
+ // Filter inventory items in vendor that match the query
80
+ const matchingItems = vendor.inventory.filter(item =>
81
+ regex.test(item.name) ||
82
+ regex.test(item.category) ||
83
+ item.tags.some(tag => regex.test(tag))
84
+ );
85
+
86
+ return {
87
+ vendor_id: vendor.vendor_info.vendor_id,
88
+ vendor_name: vendor.vendor_info.name,
89
+ address: vendor.vendor_info.address,
90
+ inventory: matchingItems
91
+ };
92
+ }).filter(vendor => vendor.inventory.length > 0 || regex.test(vendor.vendor_name) || regex.test(vendor.address));
93
+
94
+ res.json({
95
+ query,
96
+ results
97
+ });
98
+ } catch (error) {
99
+ console.error(error);
100
+ res.status(500).json({ error: "Internal server error" });
101
+ }
102
+ });
103
+
104
+
105
+
106
+
107
+
108
+ router.get("/inventory", async (req, res) => {
109
+ const deviceId = findProximityKey(req)|| req.socket.remoteAddress;
110
+
111
+ try {
112
+ const lastScan = await DeviceScan.findOne({ device_id: deviceId })
113
+ .sort({ timestamp: -1 })
114
+ .lean();
115
+ if (!lastScan) {
116
+ return res.status(404).json({ message: "No recent proximity data found for this IP" });
117
+ }
118
+ const ssids = lastScan?.detected_wifi.map(w => w.ssid);
119
+ console.log(ssids)
120
+ const vendors = await Vendor.find({ ssid: { $in: ssids } }).lean();
121
+ console.log(vendors);
122
+ res.json(vendors);
123
+ } catch (error) {
124
+ console.error(error);
125
+ res.status(500).json({ error: "Internal server error" });
126
+ }
127
+ });
128
+
129
+
130
+
131
+
132
+ // =========================
133
+ // Get vendor by phoneNumber
134
+ // =========================
135
+ router.get("/vendor", async (req, res) => {
136
+ try {
137
+ const { phoneNumber } = req.query;
138
+ console.log(phoneNumber);
139
+ if (!phoneNumber) {
140
+ return res
141
+ .status(400)
142
+ .json({ error: "phoneNumber query parameter is required" });
143
+ }
144
+
145
+ // Find vendor using vendor_info.phoneNumber
146
+ const vendor = await Vendor.findOne({ "phoneNumber": phoneNumber })
147
+ .lean();
148
+
149
+ if (!vendor) {
150
+ return res.status(404).json({ error: "Vendor not found" });
151
+ }
152
+
153
+ res.json(vendor);
154
+
155
+ } catch (error) {
156
+ console.error("Error fetching vendor by phoneNumber:", error);
157
+ res.status(500).json({ error: "Internal server error" });
158
+ }
159
+ });
160
+
161
+
162
+
163
+
164
+
165
+
166
+ // Get inventory for a specific vendor
167
+ router.get("/vendor/:vendorId/inventory", async (req, res) => {
168
+ try {
169
+ const vendorId = req.params.vendorId;
170
+ // Find the vendor by vendor_info.vendor_id
171
+ const vendor = await Vendor.findOne({ "vendor_info.vendor_id": vendorId })
172
+ .select("inventory") // only return inventory and vendor_info
173
+ .lean();
174
+
175
+ if (!vendor) {
176
+ return res.status(404).json({ error: "Vendor not found" });
177
+ }
178
+
179
+ res.json(vendor.inventory);
180
+ } catch (error) {
181
+ console.error(error);
182
+ res.status(500).json({ error: "Internal server error" });
183
+ }
184
+ });
185
+
186
+
187
+
188
+
189
+
190
+
191
+
192
+ // STORE MANAGEMENT STARTS HERE
193
+ router.get("/vendor/:vendorId", async (req, res) => {
194
+ try {
195
+ const vendorId = req.params.vendorId;
196
+ // vendor_id is inside vendor_info
197
+ const vendor = await Vendor.findOne({ "vendor_info.vendor_id": vendorId }).lean();
198
+
199
+ if (!vendor) {
200
+ return res.status(404).json({ error: "Vendor not found" });
201
+ }
202
+
203
+ res.json(vendor);
204
+ } catch (error) {
205
+ console.error(error);
206
+ res.status(500).json({ error: "Internal server error" });
207
+ }
208
+ });
209
+
210
+
211
+
212
+
213
+ // POST /vendor
214
+ // body: { vendor_id, name, address, contact, gps_location, opening_hours, inventory }
215
+ router.post("/vendor", async (req, res) => {
216
+ try {
217
+ const vendorData = req.body;
218
+
219
+ // Check if vendor already exists
220
+ const existing = await Vendor.findOne({ vendor_id: vendorData.vendor_id });
221
+ if (existing) {
222
+ return res.status(400).json({ error: "Vendor with this ID already exists" });
223
+ }
224
+
225
+ const newVendor = await Vendor.create(vendorData);
226
+ res.status(201).json({
227
+ message: "Vendor created successfully",
228
+ vendor: newVendor,
229
+ });
230
+ } catch (error) {
231
+ console.error(error);
232
+ res.status(500).json({ error: "Internal server error" });
233
+ }
234
+ });
235
+
236
+
237
+
238
+
239
+
240
+ // body: fields to update: { name, address, contact, gps_location, opening_hours, inventory }
241
+ // PUT /vendor/:vendorId
242
+ router.put("/vendor/:vendorId", async (req, res) => {
243
+ try {
244
+ const { vendorId } = req.params;
245
+ const updateData = req.body;
246
+
247
+ // Check if a vendor with this vendor_id exists
248
+ const existingVendor = await Vendor.findOne({ "vendor_info.vendor_id": vendorId });
249
+ if (!existingVendor) {
250
+ return res.status(404).json({ error: "Vendor not found" });
251
+ }
252
+
253
+ // Flatten nested updateData to update specific fields (so we don’t overwrite everything)
254
+ const setFields = {};
255
+ for (const [key, value] of Object.entries(updateData)) {
256
+ setFields[`vendor_info.${key}`] = value;
257
+ }
258
+
259
+ // Update vendor info
260
+ const updatedVendor = await Vendor.findOneAndUpdate(
261
+ { "vendor_info.vendor_id": vendorId },
262
+ { $set: setFields },
263
+ { new: true }
264
+ ).lean();
265
+
266
+ res.json({
267
+ message: "Vendor updated successfully",
268
+ vendor: updatedVendor,
269
+ });
270
+ } catch (error) {
271
+ console.error("Error updating vendor:", error);
272
+ res.status(500).json({ error: "Internal server error" });
273
+ }
274
+ });
275
+
276
+
277
+
278
+
279
+
280
+ // DELETE /vendor/:vendorId
281
+ router.delete("/vendor/:vendorId", async (req, res) => {
282
+ try {
283
+ const { vendorId } = req.params;
284
+
285
+ const deletedVendor = await Vendor.findOneAndDelete({ vendor_id: vendorId }).lean();
286
+
287
+ if (!deletedVendor) {
288
+ return res.status(404).json({ error: "Vendor not found" });
289
+ }
290
+
291
+ res.json({
292
+ message: "Vendor deleted successfully",
293
+ vendor: deletedVendor,
294
+ });
295
+ } catch (error) {
296
+ console.error(error);
297
+ res.status(500).json({ error: "Internal server error" });
298
+ }
299
+ });
300
+
301
+
302
+
303
+
304
+
305
+ //INVENTORY STARTS HERE
306
+
307
+
308
+
309
+ // Add a single inventory item to a vendor
310
+ router.post("/vendor/:vendorId/inventory", async (req, res) => {
311
+ try {
312
+ const vendorId = req.params.vendorId;
313
+ const newItem = req.body; // e.g., { item_id, name, category, price, ... }
314
+
315
+ const updatedVendor = await Vendor.findOneAndUpdate(
316
+ { vendor_id: vendorId },
317
+ { $push: { inventory: newItem } },
318
+ { new: true, projection: { inventory: 1, vendor_info: 1 } }
319
+ ).lean();
320
+
321
+ if (!updatedVendor) {
322
+ return res.status(404).json({ error: "Vendor not found" });
323
+ }
324
+
325
+ res.json({
326
+ message: "Inventory item added successfully",
327
+ vendor: updatedVendor.vendor_info,
328
+ item: newItem,
329
+ });
330
+ } catch (error) {
331
+ console.error(error);
332
+ res.status(500).json({ error: "Internal server error" });
333
+ }
334
+ });
335
+
336
+
337
+
338
+
339
+
340
+
341
+
342
+ // Add multiple inventory items to a vendor
343
+ router.post("/vendor/:vendorId/inventory/bulk", async (req, res) => {
344
+ try {
345
+ const vendorId = req.params.vendorId;
346
+ const newItems = req.body.items; // e.g., [{ item_id, name, ... }, {...}]
347
+
348
+ if (!Array.isArray(newItems) || newItems.length === 0) {
349
+ return res.status(400).json({ error: "Items array is required" });
350
+ }
351
+
352
+ const updatedVendor = await Vendor.findOneAndUpdate(
353
+ { vendor_id: vendorId },
354
+ { $push: { inventory: { $each: newItems } } },
355
+ { new: true, projection: { inventory: 1, vendor_info: 1 } }
356
+ ).lean();
357
+
358
+ if (!updatedVendor) {
359
+ return res.status(404).json({ error: "Vendor not found" });
360
+ }
361
+
362
+ res.json({
363
+ message: `${newItems.length} inventory items added successfully`,
364
+ vendor: updatedVendor.vendor_info,
365
+ items: newItems,
366
+ });
367
+ } catch (error) {
368
+ console.error(error);
369
+ res.status(500).json({ error: "Internal server error" });
370
+ }
371
+ });
372
+
373
+
374
+
375
+
376
+
377
+
378
+ router.put("/inventory/:itemId", async (req, res) => {
379
+ try {
380
+ const itemId = req.params.itemId;
381
+ const updateData = req.body; // e.g., { price: 300, quantity_available: 50 }
382
+
383
+ // Find the vendor containing the item and update the matching inventory element
384
+ const result = await Vendor.findOneAndUpdate(
385
+ { "inventory.item_id": itemId },
386
+ { $set: Object.fromEntries(
387
+ Object.entries(updateData).map(([key, value]) => [`inventory.$.${key}`, value])
388
+ )
389
+ },
390
+ { new: true, projection: { "inventory.$": 1, vendor_info: 1 } } // return updated item
391
+ ).lean();
392
+
393
+ if (!result || !result.inventory || result.inventory.length === 0) {
394
+ return res.status(404).json({ error: "Inventory item not found" });
395
+ }
396
+
397
+ res.json({
398
+ message: "Inventory item updated successfully",
399
+ vendor: result.vendor_info,
400
+ item: result.inventory[0],
401
+ });
402
+ } catch (error) {
403
+ console.error(error);
404
+ res.status(500).json({ error: "Internal server error" });
405
+ }
406
+ });
407
+
408
+
409
+
410
+
411
+
412
+
413
+
414
+ router.put("/inventory/:vendorId/bulk", async (req, res) => {
415
+ try {
416
+ const { vendorId } = req.params;
417
+ const itemsToUpdate = req.body;
418
+
419
+ if (!Array.isArray(itemsToUpdate) || itemsToUpdate.length === 0) {
420
+ return res.status(400).json({ error: "Items array is required" });
421
+ }
422
+
423
+ // Validate the vendor first
424
+ const vendor = await Vendor.findOne({ "vendor_info.vendor_id": vendorId });
425
+ if (!vendor) {
426
+ return res.status(404).json({ error: `Vendor ${vendorId} not found` });
427
+ }
428
+
429
+ // Perform updates in parallel
430
+ const updateResults = await Promise.all(
431
+ itemsToUpdate.map(async (item) => {
432
+ const { item_id, ...updates } = item;
433
+ const updateQuery = {};
434
+
435
+ // Dynamically build the update fields
436
+ for (const [key, value] of Object.entries(updates)) {
437
+ updateQuery[`inventory.$.${key}`] = value;
438
+ }
439
+
440
+ const result = await Vendor.updateOne(
441
+ { "vendor_info.vendor_id": vendorId, "inventory.item_id": item_id },
442
+ { $set: updateQuery }
443
+ );
444
+
445
+ return { item_id, matched: result.matchedCount, modified: result.modifiedCount };
446
+ })
447
+ );
448
+
449
+ // Fetch the fresh vendor document after updates
450
+ const updatedVendor = await Vendor.findOne({ "vendor_info.vendor_id": vendorId }).lean();
451
+
452
+
453
+ console.log(updateResults)
454
+
455
+ res.json({
456
+ message: `${updateResults.filter(r => r.modified > 0).length} of ${itemsToUpdate.length} items updated successfully`,
457
+ updateResults,
458
+ vendor: updatedVendor?.vendor_info,
459
+ inventory: updatedVendor?.inventory,
460
+ });
461
+
462
+ } catch (error) {
463
+ console.error("Bulk update error:", error);
464
+ res.status(500).json({ error: "Internal server error" });
465
+ }
466
+ });
467
+
468
+
469
+
470
+
471
+
472
+
473
+ // DELETE /inventory/:itemId
474
+ router.delete("/inventory/:itemId", async (req, res) => {
475
+ try {
476
+ const { itemId } = req.params;
477
+
478
+ const result = await Vendor.findOneAndUpdate(
479
+ { "inventory.item_id": itemId },
480
+ { $pull: { inventory: { item_id: itemId } } },
481
+ { new: true, projection: { vendor_info: 1 } }
482
+ ).lean();
483
+
484
+ if (!result) {
485
+ return res.status(404).json({ error: "Inventory item not found" });
486
+ }
487
+
488
+ res.json({
489
+ message: "Inventory item deleted successfully",
490
+ vendor: result.vendor_info,
491
+ });
492
+ } catch (error) {
493
+ console.error(error);
494
+ res.status(500).json({ error: "Internal server error" });
495
+ }
496
+ });
497
+
498
+
499
+
500
+
501
+
502
+
503
+ // DELETE /inventory
504
+ // body: { itemIds: ["INV001", "INV002", ...] }
505
+ router.delete("/inventory", async (req, res) => {
506
+ try {
507
+ const { itemIds } = req.body;
508
+
509
+ if (!Array.isArray(itemIds) || itemIds.length === 0) {
510
+ return res.status(400).json({ error: "itemIds array is required" });
511
+ }
512
+
513
+ const result = await Vendor.updateMany(
514
+ { "inventory.item_id": { $in: itemIds } },
515
+ { $pull: { inventory: { item_id: { $in: itemIds } } } }
516
+ );
517
+
518
+ res.json({
519
+ message: "Inventory items deleted successfully",
520
+ deletedCount: result.modifiedCount,
521
+ });
522
+ } catch (error) {
523
+ console.error(error);
524
+ res.status(500).json({ error: "Internal server error" });
525
+ }
526
+ });
527
+
528
+
529
+
530
+
531
+
532
+
533
+
534
+ export default router;
535
+
536
+