payservedb 8.3.8 โ 8.4.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.
- package/ZOHO_INTEGRATION_SCHEMA.md +644 -0
- package/index.js +267 -266
- package/package.json +1 -1
- package/src/models/facilityInvoice.js +25 -8
- package/src/models/facilityInvoicePayment.js +5 -0
- package/src/models/water_meter_account.js +7 -1
- package/src/models/water_meter_settings.js +7 -3
- package/src/models/zohoIntegration.js +262 -0
- package/.idea/material_theme_project_new.xml +0 -12
- package/.idea/modules.xml +0 -8
- package/.idea/psdb.iml +0 -12
- package/.idea/vcs.xml +0 -6
|
@@ -0,0 +1,644 @@
|
|
|
1
|
+
# Zoho Integration Schema - Implementation Summary
|
|
2
|
+
|
|
3
|
+
## ๐ Overview
|
|
4
|
+
|
|
5
|
+
The `ZohoIntegration` schema has been successfully created and added to the PayServe database package (psdb). This schema stores Zoho Books API credentials and synchronization data on a per-facility basis.
|
|
6
|
+
|
|
7
|
+
**Package Version:** `8.3.8` (updated from 8.3.7)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## ๐๏ธ Schema Structure
|
|
12
|
+
|
|
13
|
+
### Location
|
|
14
|
+
- **File:** `psdb/src/models/zohoIntegration.js`
|
|
15
|
+
- **Model Name:** `ZohoIntegration`
|
|
16
|
+
- **Collection:** `zohointegrations`
|
|
17
|
+
|
|
18
|
+
### Key Features
|
|
19
|
+
- โ
One integration per facility (unique constraint on `facilityId`)
|
|
20
|
+
- โ
Encrypted sensitive fields (clientSecret, refreshToken, accessToken)
|
|
21
|
+
- โ
Automatic sync tracking and statistics
|
|
22
|
+
- โ
Token expiry management
|
|
23
|
+
- โ
Connection status monitoring
|
|
24
|
+
- โ
Audit trail support
|
|
25
|
+
- โ
Built-in helper methods
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## ๐ Schema Fields
|
|
30
|
+
|
|
31
|
+
### Core Identification
|
|
32
|
+
```javascript
|
|
33
|
+
{
|
|
34
|
+
facilityId: ObjectId (required, unique, indexed)
|
|
35
|
+
- Links to Facility collection
|
|
36
|
+
- One Zoho config per facility
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Zoho API Credentials
|
|
41
|
+
```javascript
|
|
42
|
+
{
|
|
43
|
+
clientId: String (required)
|
|
44
|
+
- Zoho OAuth client ID
|
|
45
|
+
- Example: "1000.XXXXXXXXXXXXX"
|
|
46
|
+
|
|
47
|
+
clientSecret: String (required, encrypted)
|
|
48
|
+
- Zoho OAuth client secret
|
|
49
|
+
- MUST be encrypted before storage
|
|
50
|
+
|
|
51
|
+
refreshToken: String (required, encrypted)
|
|
52
|
+
- Zoho OAuth refresh token
|
|
53
|
+
- MUST be encrypted before storage
|
|
54
|
+
- Example: "1000.XXXXXXXXXXXXX"
|
|
55
|
+
|
|
56
|
+
accessToken: String (encrypted, nullable)
|
|
57
|
+
- Current access token
|
|
58
|
+
- Auto-refreshed by system
|
|
59
|
+
- MUST be encrypted before storage
|
|
60
|
+
|
|
61
|
+
organizationId: String (required)
|
|
62
|
+
- Zoho Books organization ID
|
|
63
|
+
- Example: "902250663"
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Integration Status
|
|
68
|
+
```javascript
|
|
69
|
+
{
|
|
70
|
+
isActive: Boolean (default: true)
|
|
71
|
+
- Whether integration is active
|
|
72
|
+
|
|
73
|
+
connectionStatus: Enum (default: "pending")
|
|
74
|
+
- Values: "connected", "disconnected", "error", "pending"
|
|
75
|
+
- Current connection state
|
|
76
|
+
|
|
77
|
+
connectionError: String (nullable)
|
|
78
|
+
- Last connection error message
|
|
79
|
+
|
|
80
|
+
lastConnectionTest: Date (nullable)
|
|
81
|
+
- Timestamp of last connection test
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Token Management
|
|
86
|
+
```javascript
|
|
87
|
+
{
|
|
88
|
+
tokenExpiresAt: Date (nullable)
|
|
89
|
+
- When current access token expires
|
|
90
|
+
|
|
91
|
+
lastTokenRefreshAt: Date (nullable)
|
|
92
|
+
- Last successful token refresh
|
|
93
|
+
|
|
94
|
+
tokenRefreshCount: Number (default: 0)
|
|
95
|
+
- Total token refreshes performed
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Sync Statistics
|
|
100
|
+
```javascript
|
|
101
|
+
{
|
|
102
|
+
lastSyncedAt: Date (nullable)
|
|
103
|
+
- Last successful sync timestamp
|
|
104
|
+
|
|
105
|
+
totalInvoicesSynced: Number (default: 0)
|
|
106
|
+
- Total invoices synced to Zoho
|
|
107
|
+
|
|
108
|
+
totalPaymentsSynced: Number (default: 0)
|
|
109
|
+
- Total payments synced to Zoho
|
|
110
|
+
|
|
111
|
+
totalCustomersSynced: Number (default: 0)
|
|
112
|
+
- Total customers synced to Zoho
|
|
113
|
+
|
|
114
|
+
successfulSyncs: Number (default: 0)
|
|
115
|
+
- Count of successful sync operations
|
|
116
|
+
|
|
117
|
+
failedSyncs: Number (default: 0)
|
|
118
|
+
- Count of failed sync operations
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Sync Configuration
|
|
123
|
+
```javascript
|
|
124
|
+
{
|
|
125
|
+
autoSyncEnabled: Boolean (default: true)
|
|
126
|
+
- Enable automatic synchronization
|
|
127
|
+
|
|
128
|
+
syncFrequency: Enum (default: "realtime")
|
|
129
|
+
- Values: "realtime", "hourly", "daily", "manual"
|
|
130
|
+
- How often to sync
|
|
131
|
+
|
|
132
|
+
lastSyncError: String (nullable)
|
|
133
|
+
- Last sync error message
|
|
134
|
+
|
|
135
|
+
syncInvoicesOnCreation: Boolean (default: true)
|
|
136
|
+
- Auto-sync when invoice created
|
|
137
|
+
|
|
138
|
+
syncPaymentsOnReceipt: Boolean (default: true)
|
|
139
|
+
- Auto-sync when payment received
|
|
140
|
+
|
|
141
|
+
markInvoicesAsSent: Boolean (default: true)
|
|
142
|
+
- Mark invoices as "sent" in Zoho
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### API Configuration
|
|
147
|
+
```javascript
|
|
148
|
+
{
|
|
149
|
+
apiBaseUrl: String (default: "https://www.zohoapis.com/books/v3")
|
|
150
|
+
- Zoho API base URL
|
|
151
|
+
|
|
152
|
+
requestTimeout: Number (default: 30000)
|
|
153
|
+
- API request timeout in milliseconds
|
|
154
|
+
- Min: 1000ms
|
|
155
|
+
|
|
156
|
+
maxRetries: Number (default: 3)
|
|
157
|
+
- Max retry attempts for failed requests
|
|
158
|
+
- Min: 0, Max: 10
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Audit Fields
|
|
163
|
+
```javascript
|
|
164
|
+
{
|
|
165
|
+
createdBy: ObjectId (nullable)
|
|
166
|
+
- User who created configuration
|
|
167
|
+
|
|
168
|
+
updatedBy: ObjectId (nullable)
|
|
169
|
+
- User who last updated configuration
|
|
170
|
+
|
|
171
|
+
lastModifiedBy: ObjectId (nullable)
|
|
172
|
+
- User who last modified configuration
|
|
173
|
+
|
|
174
|
+
createdAt: Date (auto)
|
|
175
|
+
- Creation timestamp
|
|
176
|
+
|
|
177
|
+
updatedAt: Date (auto)
|
|
178
|
+
- Last update timestamp
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Metadata
|
|
183
|
+
```javascript
|
|
184
|
+
{
|
|
185
|
+
notes: String (nullable)
|
|
186
|
+
- Admin notes about integration
|
|
187
|
+
|
|
188
|
+
metadata: Mixed (default: {})
|
|
189
|
+
- Additional custom data
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## ๐ Security Considerations
|
|
196
|
+
|
|
197
|
+
### Fields That MUST Be Encrypted
|
|
198
|
+
|
|
199
|
+
โ ๏ธ **CRITICAL:** These fields contain sensitive data and MUST be encrypted before storage:
|
|
200
|
+
|
|
201
|
+
1. `clientSecret`
|
|
202
|
+
2. `refreshToken`
|
|
203
|
+
3. `accessToken`
|
|
204
|
+
|
|
205
|
+
### Recommended Encryption Approach
|
|
206
|
+
|
|
207
|
+
```javascript
|
|
208
|
+
const crypto = require('crypto');
|
|
209
|
+
|
|
210
|
+
// Encryption function (use environment variable for key)
|
|
211
|
+
function encrypt(text) {
|
|
212
|
+
const algorithm = 'aes-256-cbc';
|
|
213
|
+
const key = Buffer.from(process.env.ENCRYPTION_KEY, 'hex');
|
|
214
|
+
const iv = crypto.randomBytes(16);
|
|
215
|
+
|
|
216
|
+
const cipher = crypto.createCipheriv(algorithm, key, iv);
|
|
217
|
+
let encrypted = cipher.update(text, 'utf8', 'hex');
|
|
218
|
+
encrypted += cipher.final('hex');
|
|
219
|
+
|
|
220
|
+
return iv.toString('hex') + ':' + encrypted;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Decryption function
|
|
224
|
+
function decrypt(text) {
|
|
225
|
+
const algorithm = 'aes-256-cbc';
|
|
226
|
+
const key = Buffer.from(process.env.ENCRYPTION_KEY, 'hex');
|
|
227
|
+
|
|
228
|
+
const parts = text.split(':');
|
|
229
|
+
const iv = Buffer.from(parts[0], 'hex');
|
|
230
|
+
const encryptedText = parts[1];
|
|
231
|
+
|
|
232
|
+
const decipher = crypto.createDecipheriv(algorithm, key, iv);
|
|
233
|
+
let decrypted = decipher.update(encryptedText, 'hex', 'utf8');
|
|
234
|
+
decrypted += decipher.final('utf8');
|
|
235
|
+
|
|
236
|
+
return decrypted;
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## ๐ ๏ธ Built-in Methods
|
|
243
|
+
|
|
244
|
+
### 1. `toSafeObject()`
|
|
245
|
+
Returns a safe version of the document with masked sensitive fields.
|
|
246
|
+
|
|
247
|
+
```javascript
|
|
248
|
+
const config = await ZohoIntegration.findOne({ facilityId });
|
|
249
|
+
const safeConfig = config.toSafeObject();
|
|
250
|
+
|
|
251
|
+
// Output:
|
|
252
|
+
// {
|
|
253
|
+
// clientSecret: "โขโขโขโขโขโขโขโข7e7d",
|
|
254
|
+
// refreshToken: "โขโขโขโขโขโขโขโข54d",
|
|
255
|
+
// accessToken: "โขโขโขโขโขโขโขโขxyz",
|
|
256
|
+
// ...other fields
|
|
257
|
+
// }
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### 2. `needsTokenRefresh()`
|
|
261
|
+
Checks if the access token needs refreshing (5-minute buffer).
|
|
262
|
+
|
|
263
|
+
```javascript
|
|
264
|
+
const config = await ZohoIntegration.findOne({ facilityId });
|
|
265
|
+
|
|
266
|
+
if (config.needsTokenRefresh()) {
|
|
267
|
+
// Refresh the token
|
|
268
|
+
const newToken = await refreshZohoToken(config);
|
|
269
|
+
config.accessToken = encrypt(newToken);
|
|
270
|
+
config.tokenExpiresAt = new Date(Date.now() + 3600000);
|
|
271
|
+
await config.save();
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### 3. `incrementSyncCounters(type, success)`
|
|
276
|
+
Increments sync statistics after each operation.
|
|
277
|
+
|
|
278
|
+
```javascript
|
|
279
|
+
const config = await ZohoIntegration.findOne({ facilityId });
|
|
280
|
+
|
|
281
|
+
// After successful invoice sync
|
|
282
|
+
await config.incrementSyncCounters('invoice', true);
|
|
283
|
+
|
|
284
|
+
// After failed payment sync
|
|
285
|
+
await config.incrementSyncCounters('payment', false);
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
## ๐ Virtual Properties
|
|
291
|
+
|
|
292
|
+
### `syncSuccessRate`
|
|
293
|
+
Calculates success rate as a percentage.
|
|
294
|
+
|
|
295
|
+
```javascript
|
|
296
|
+
const config = await ZohoIntegration.findOne({ facilityId });
|
|
297
|
+
console.log(`Success Rate: ${config.syncSuccessRate}%`);
|
|
298
|
+
// Output: "Success Rate: 98.50%"
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
## ๐ Indexes
|
|
304
|
+
|
|
305
|
+
The schema includes optimized indexes for common queries:
|
|
306
|
+
|
|
307
|
+
```javascript
|
|
308
|
+
// Compound index for facility + active status
|
|
309
|
+
{ facilityId: 1, isActive: 1 }
|
|
310
|
+
|
|
311
|
+
// Index for connection status queries
|
|
312
|
+
{ connectionStatus: 1 }
|
|
313
|
+
|
|
314
|
+
// Index for sorting by last sync
|
|
315
|
+
{ lastSyncedAt: -1 }
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
## ๐ป Usage Examples
|
|
321
|
+
|
|
322
|
+
### Example 1: Create New Configuration
|
|
323
|
+
|
|
324
|
+
```javascript
|
|
325
|
+
const { ZohoIntegration } = require('payservedb');
|
|
326
|
+
|
|
327
|
+
async function createZohoConfig(facilityId, credentials, userId) {
|
|
328
|
+
try {
|
|
329
|
+
const config = new ZohoIntegration({
|
|
330
|
+
facilityId,
|
|
331
|
+
clientId: credentials.clientId,
|
|
332
|
+
clientSecret: encrypt(credentials.clientSecret),
|
|
333
|
+
refreshToken: encrypt(credentials.refreshToken),
|
|
334
|
+
organizationId: credentials.organizationId,
|
|
335
|
+
connectionStatus: 'pending',
|
|
336
|
+
createdBy: userId,
|
|
337
|
+
updatedBy: userId
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
await config.save();
|
|
341
|
+
return config.toSafeObject();
|
|
342
|
+
} catch (error) {
|
|
343
|
+
if (error.code === 11000) {
|
|
344
|
+
throw new Error('Zoho integration already exists for this facility');
|
|
345
|
+
}
|
|
346
|
+
throw error;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
### Example 2: Get Configuration (Safe)
|
|
352
|
+
|
|
353
|
+
```javascript
|
|
354
|
+
async function getZohoConfig(facilityId) {
|
|
355
|
+
const config = await ZohoIntegration.findOne({
|
|
356
|
+
facilityId,
|
|
357
|
+
isActive: true
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
if (!config) {
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Return safe version (masked secrets)
|
|
365
|
+
return config.toSafeObject();
|
|
366
|
+
}
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### Example 3: Update Configuration
|
|
370
|
+
|
|
371
|
+
```javascript
|
|
372
|
+
async function updateZohoConfig(facilityId, updates, userId) {
|
|
373
|
+
const config = await ZohoIntegration.findOne({ facilityId });
|
|
374
|
+
|
|
375
|
+
if (!config) {
|
|
376
|
+
throw new Error('Configuration not found');
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Encrypt sensitive fields if provided
|
|
380
|
+
if (updates.clientSecret) {
|
|
381
|
+
updates.clientSecret = encrypt(updates.clientSecret);
|
|
382
|
+
}
|
|
383
|
+
if (updates.refreshToken) {
|
|
384
|
+
updates.refreshToken = encrypt(updates.refreshToken);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
updates.updatedBy = userId;
|
|
388
|
+
updates.lastModifiedBy = userId;
|
|
389
|
+
|
|
390
|
+
Object.assign(config, updates);
|
|
391
|
+
await config.save();
|
|
392
|
+
|
|
393
|
+
return config.toSafeObject();
|
|
394
|
+
}
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
### Example 4: Test Connection
|
|
398
|
+
|
|
399
|
+
```javascript
|
|
400
|
+
async function testZohoConnection(facilityId) {
|
|
401
|
+
const config = await ZohoIntegration.findOne({ facilityId });
|
|
402
|
+
|
|
403
|
+
if (!config) {
|
|
404
|
+
throw new Error('Configuration not found');
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
try {
|
|
408
|
+
// Test connection to Zoho API
|
|
409
|
+
const response = await axios.get(
|
|
410
|
+
`${config.apiBaseUrl}/organizations`,
|
|
411
|
+
{
|
|
412
|
+
headers: {
|
|
413
|
+
'Authorization': `Zoho-oauthtoken ${decrypt(config.accessToken)}`
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
// Update status
|
|
419
|
+
config.connectionStatus = 'connected';
|
|
420
|
+
config.lastConnectionTest = new Date();
|
|
421
|
+
config.connectionError = null;
|
|
422
|
+
await config.save();
|
|
423
|
+
|
|
424
|
+
return { success: true };
|
|
425
|
+
} catch (error) {
|
|
426
|
+
config.connectionStatus = 'error';
|
|
427
|
+
config.connectionError = error.message;
|
|
428
|
+
config.lastConnectionTest = new Date();
|
|
429
|
+
await config.save();
|
|
430
|
+
|
|
431
|
+
return { success: false, error: error.message };
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
### Example 5: Disconnect Integration
|
|
437
|
+
|
|
438
|
+
```javascript
|
|
439
|
+
async function disconnectZoho(facilityId, userId) {
|
|
440
|
+
const config = await ZohoIntegration.findOne({ facilityId });
|
|
441
|
+
|
|
442
|
+
if (!config) {
|
|
443
|
+
throw new Error('Configuration not found');
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
config.isActive = false;
|
|
447
|
+
config.connectionStatus = 'disconnected';
|
|
448
|
+
config.updatedBy = userId;
|
|
449
|
+
await config.save();
|
|
450
|
+
|
|
451
|
+
return { success: true };
|
|
452
|
+
}
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
### Example 6: Get Integration Statistics
|
|
456
|
+
|
|
457
|
+
```javascript
|
|
458
|
+
async function getZohoStats(facilityId) {
|
|
459
|
+
const config = await ZohoIntegration.findOne({ facilityId });
|
|
460
|
+
|
|
461
|
+
if (!config) {
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return {
|
|
466
|
+
totalInvoicesSynced: config.totalInvoicesSynced,
|
|
467
|
+
totalPaymentsSynced: config.totalPaymentsSynced,
|
|
468
|
+
totalCustomersSynced: config.totalCustomersSynced,
|
|
469
|
+
successfulSyncs: config.successfulSyncs,
|
|
470
|
+
failedSyncs: config.failedSyncs,
|
|
471
|
+
successRate: config.syncSuccessRate,
|
|
472
|
+
lastSyncedAt: config.lastSyncedAt,
|
|
473
|
+
connectionStatus: config.connectionStatus,
|
|
474
|
+
isActive: config.isActive
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
---
|
|
480
|
+
|
|
481
|
+
## ๐ Next Steps
|
|
482
|
+
|
|
483
|
+
### 1. Update Backend Package
|
|
484
|
+
|
|
485
|
+
```bash
|
|
486
|
+
cd backend_main
|
|
487
|
+
npm install payservedb@8.3.8
|
|
488
|
+
# or
|
|
489
|
+
npm update payservedb
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
### 2. Create Controllers
|
|
493
|
+
|
|
494
|
+
Create the following files in `backend_main`:
|
|
495
|
+
- `src/controllers/integrations/zohoIntegrationController.js`
|
|
496
|
+
- `src/routes/integrations/zohoIntegrationRoutes.js`
|
|
497
|
+
|
|
498
|
+
### 3. Implement Encryption
|
|
499
|
+
|
|
500
|
+
Add encryption utilities:
|
|
501
|
+
- `src/utils/encryption.js`
|
|
502
|
+
|
|
503
|
+
Generate encryption key:
|
|
504
|
+
```bash
|
|
505
|
+
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
Add to `.env`:
|
|
509
|
+
```env
|
|
510
|
+
ENCRYPTION_KEY=your_generated_key_here
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
### 4. API Endpoints to Create
|
|
514
|
+
|
|
515
|
+
```
|
|
516
|
+
GET /api/facility/:facilityId/integrations/zoho/config
|
|
517
|
+
POST /api/facility/:facilityId/integrations/zoho/config
|
|
518
|
+
PUT /api/facility/:facilityId/integrations/zoho/config
|
|
519
|
+
DELETE /api/facility/:facilityId/integrations/zoho/config
|
|
520
|
+
POST /api/facility/:facilityId/integrations/zoho/test
|
|
521
|
+
GET /api/facility/:facilityId/integrations/zoho/stats
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
### 5. Frontend Integration
|
|
525
|
+
|
|
526
|
+
Update the `IntegrationsTab.js` component to:
|
|
527
|
+
- Call the new API endpoints
|
|
528
|
+
- Display real configuration data
|
|
529
|
+
- Handle encryption (send plain, receive masked)
|
|
530
|
+
- Update UI based on connection status
|
|
531
|
+
|
|
532
|
+
---
|
|
533
|
+
|
|
534
|
+
## ๐ Migration Notes
|
|
535
|
+
|
|
536
|
+
### For Existing Facilities
|
|
537
|
+
|
|
538
|
+
If you need to migrate existing Zoho configurations:
|
|
539
|
+
|
|
540
|
+
```javascript
|
|
541
|
+
async function migrateExistingConfigs() {
|
|
542
|
+
// If you have configs stored elsewhere
|
|
543
|
+
const oldConfigs = await OldModel.find({});
|
|
544
|
+
|
|
545
|
+
for (const old of oldConfigs) {
|
|
546
|
+
await ZohoIntegration.create({
|
|
547
|
+
facilityId: old.facilityId,
|
|
548
|
+
clientId: old.clientId,
|
|
549
|
+
clientSecret: encrypt(old.clientSecret),
|
|
550
|
+
refreshToken: encrypt(old.refreshToken),
|
|
551
|
+
organizationId: old.organizationId,
|
|
552
|
+
connectionStatus: 'pending',
|
|
553
|
+
createdAt: old.createdAt || new Date()
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
---
|
|
560
|
+
|
|
561
|
+
## ๐งช Testing
|
|
562
|
+
|
|
563
|
+
### Unit Test Example
|
|
564
|
+
|
|
565
|
+
```javascript
|
|
566
|
+
const { ZohoIntegration } = require('payservedb');
|
|
567
|
+
|
|
568
|
+
describe('ZohoIntegration Model', () => {
|
|
569
|
+
it('should create config with unique facilityId', async () => {
|
|
570
|
+
const config = await ZohoIntegration.create({
|
|
571
|
+
facilityId: mongoose.Types.ObjectId(),
|
|
572
|
+
clientId: '1000.TEST123',
|
|
573
|
+
clientSecret: encrypt('secret123'),
|
|
574
|
+
refreshToken: encrypt('refresh123'),
|
|
575
|
+
organizationId: '123456'
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
expect(config).toBeDefined();
|
|
579
|
+
expect(config.isActive).toBe(true);
|
|
580
|
+
expect(config.connectionStatus).toBe('pending');
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
it('should mask sensitive fields in toSafeObject', () => {
|
|
584
|
+
const config = new ZohoIntegration({
|
|
585
|
+
clientSecret: 'very_secret_key',
|
|
586
|
+
refreshToken: 'refresh_token_123'
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
const safe = config.toSafeObject();
|
|
590
|
+
expect(safe.clientSecret).toContain('โขโขโขโขโขโขโขโข');
|
|
591
|
+
expect(safe.refreshToken).toContain('โขโขโขโขโขโขโขโข');
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
it('should calculate sync success rate', () => {
|
|
595
|
+
const config = new ZohoIntegration({
|
|
596
|
+
successfulSyncs: 95,
|
|
597
|
+
failedSyncs: 5
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
expect(config.syncSuccessRate).toBe('95.00');
|
|
601
|
+
});
|
|
602
|
+
});
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
---
|
|
606
|
+
|
|
607
|
+
## โ ๏ธ Important Warnings
|
|
608
|
+
|
|
609
|
+
1. **NEVER** return decrypted secrets to the frontend
|
|
610
|
+
2. **ALWAYS** use `toSafeObject()` when sending to API responses
|
|
611
|
+
3. **ENCRYPT** sensitive fields before saving to database
|
|
612
|
+
4. **VALIDATE** all inputs before storing
|
|
613
|
+
5. **AUDIT** all configuration changes
|
|
614
|
+
6. **MONITOR** connection status regularly
|
|
615
|
+
7. **ROTATE** tokens periodically for security
|
|
616
|
+
|
|
617
|
+
---
|
|
618
|
+
|
|
619
|
+
## ๐ Related Documentation
|
|
620
|
+
|
|
621
|
+
- **Zoho API Documentation:** [https://www.zoho.com/books/api/v3/](https://www.zoho.com/books/api/v3/)
|
|
622
|
+
- **OAuth 2.0 Guide:** `backend_main/src/services/integrations/zoho/README.md`
|
|
623
|
+
- **Frontend Component:** `app_main/src/components/facility/settings_management/IntegrationsTab.js`
|
|
624
|
+
- **Payment Integration:** `backend_main/src/controllers/integrations/zoho/PAYMENT_GUIDE.md`
|
|
625
|
+
|
|
626
|
+
---
|
|
627
|
+
|
|
628
|
+
## โ
Status
|
|
629
|
+
|
|
630
|
+
- โ
Schema created and validated
|
|
631
|
+
- โ
Model exported in payservedb package
|
|
632
|
+
- โ
Package version bumped to 8.3.8
|
|
633
|
+
- โ
Indexes defined for performance
|
|
634
|
+
- โ
Helper methods implemented
|
|
635
|
+
- โ
Security considerations documented
|
|
636
|
+
- โณ Controllers and routes (pending)
|
|
637
|
+
- โณ Encryption utilities (pending)
|
|
638
|
+
- โณ Frontend integration (pending)
|
|
639
|
+
|
|
640
|
+
---
|
|
641
|
+
|
|
642
|
+
**Last Updated:** 2025-10-28
|
|
643
|
+
**Schema Version:** 1.0.0
|
|
644
|
+
**Package Version:** 8.3.8
|