@bizmap/sdk 0.0.120 → 0.0.121
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/dist/main.d.ts +251 -901
- package/dist/main.js +340 -619
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -6,16 +6,16 @@ var companyUserRoles = z.enum([
|
|
|
6
6
|
"rcpst",
|
|
7
7
|
"cshr",
|
|
8
8
|
"a/r",
|
|
9
|
-
"
|
|
9
|
+
"ceo"
|
|
10
10
|
// "admin",
|
|
11
11
|
]);
|
|
12
|
-
var employeeRoles = companyUserRoles.exclude(["
|
|
13
|
-
var adminRoles = companyUserRoles.extract(["
|
|
12
|
+
var employeeRoles = companyUserRoles.exclude(["ceo"]);
|
|
13
|
+
var adminRoles = companyUserRoles.extract(["ceo"]);
|
|
14
14
|
var healthcareProviderRoles = companyUserRoles.extract([
|
|
15
15
|
"physAsst",
|
|
16
16
|
"doc"
|
|
17
17
|
]);
|
|
18
|
-
var
|
|
18
|
+
var taskDistAlgs = z.enum(["RR", "LOR"]);
|
|
19
19
|
var companyServiceSelectors = z.enum(["scheduler", "doctor"]);
|
|
20
20
|
|
|
21
21
|
// src/enums/Global.ts
|
|
@@ -46,7 +46,7 @@ var serviceProviders = z4.enum([
|
|
|
46
46
|
]);
|
|
47
47
|
|
|
48
48
|
// src/schemas/company/CompanyDetails.ts
|
|
49
|
-
import * as
|
|
49
|
+
import * as z16 from "zod";
|
|
50
50
|
|
|
51
51
|
// src/schemas/company/components/Billing.ts
|
|
52
52
|
import * as z8 from "zod";
|
|
@@ -146,24 +146,22 @@ var CompanyState = z7.object({
|
|
|
146
146
|
current: z7.number(),
|
|
147
147
|
lastModified: Timestamp.optional()
|
|
148
148
|
}),
|
|
149
|
-
maxStaff: z7.object({
|
|
150
|
-
current: z7.int().min(1),
|
|
151
|
-
lastModified: Timestamp.optional()
|
|
152
|
-
}),
|
|
153
149
|
usedStorageInBytes: z7.object({
|
|
154
150
|
current: z7.number(),
|
|
155
151
|
lastModified: Timestamp.optional()
|
|
156
152
|
}),
|
|
153
|
+
tierId: z7.object({
|
|
154
|
+
current: z7.string(),
|
|
155
|
+
lastModified: Timestamp.optional()
|
|
156
|
+
}),
|
|
157
|
+
count: z7.object({
|
|
158
|
+
invoiceNo: z7.int().positive(),
|
|
159
|
+
tktNo: z7.int().min(1)
|
|
160
|
+
}),
|
|
161
|
+
// distRoleIds: z.array(z.string()),
|
|
157
162
|
/** Data retention period in months */
|
|
158
163
|
// drpInMonths: z.int().min(1),
|
|
159
|
-
|
|
160
|
-
* @note This should only be null when the cycle resets
|
|
161
|
-
*/
|
|
162
|
-
invoiceNoCounter: z7.int().min(1),
|
|
163
|
-
/** A counter for the company's ticket no. */
|
|
164
|
-
tktNoCounter: z7.int().min(1),
|
|
165
|
-
lastModified: Timestamp.optional(),
|
|
166
|
-
createdAt: Timestamp
|
|
164
|
+
...TimeLog.shape
|
|
167
165
|
});
|
|
168
166
|
|
|
169
167
|
// src/schemas/company/components/Billing.ts
|
|
@@ -174,10 +172,10 @@ var createMod = (resource) => PriceModList.refine(
|
|
|
174
172
|
);
|
|
175
173
|
var CompanyBilling = z8.object({
|
|
176
174
|
_id: CompanyState.shape._id,
|
|
177
|
-
additionalFees: createMod("additional fee"),
|
|
178
|
-
discounts: createMod("discount"),
|
|
175
|
+
additionalFees: createMod("additional fee").optional(),
|
|
176
|
+
discounts: createMod("discount").optional(),
|
|
179
177
|
/** Optionally deductable from bills */
|
|
180
|
-
prepayments: createMod("pre-payment"),
|
|
178
|
+
prepayments: createMod("pre-payment").optional(),
|
|
181
179
|
offeredServices: z8.record(z8.uuidv4(), PriceTag.omit({ uid: true })).superRefine((data, ctx) => {
|
|
182
180
|
const services = Object.entries(data);
|
|
183
181
|
if (services.length > 150) {
|
|
@@ -185,8 +183,8 @@ var CompanyBilling = z8.object({
|
|
|
185
183
|
"Creating more services than the max allowed services (150) is prohibited."
|
|
186
184
|
);
|
|
187
185
|
}
|
|
188
|
-
}),
|
|
189
|
-
|
|
186
|
+
}).optional(),
|
|
187
|
+
...TimeLog.shape
|
|
190
188
|
});
|
|
191
189
|
var MutableCompanyBilling = CompanyBilling.omit({
|
|
192
190
|
uid: true,
|
|
@@ -200,14 +198,21 @@ import { Address, PhoneNumber } from "@wavy/util";
|
|
|
200
198
|
// src/schemas/company/utils/CompanyIndustry.ts
|
|
201
199
|
import * as z9 from "zod";
|
|
202
200
|
var CompanyIndustry = z9.object({
|
|
203
|
-
healthcare: z9.
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
201
|
+
healthcare: z9.object({
|
|
202
|
+
medicalCare: z9.object({
|
|
203
|
+
roles: z9.enum(["doc", "physAsst"])
|
|
204
|
+
})
|
|
205
|
+
// "pharmaceuticals": z.object({
|
|
206
|
+
// })
|
|
207
|
+
})
|
|
207
208
|
});
|
|
208
209
|
|
|
209
210
|
// src/schemas/company/utils/utils.ts
|
|
210
211
|
var getInvalidSectorError = (industry, sector) => `Invalid sector: "${sector}" isn't a sector under the "${industry}" industry.`;
|
|
212
|
+
var COMPANY_USER_RELATIONSHIP = {
|
|
213
|
+
doc: ["physAsst"],
|
|
214
|
+
physAsst: ["doc"]
|
|
215
|
+
};
|
|
211
216
|
|
|
212
217
|
// src/schemas/company/components/Identity.ts
|
|
213
218
|
var CompanyIdentity = z10.object({
|
|
@@ -220,33 +225,26 @@ var CompanyIdentity = z10.object({
|
|
|
220
225
|
);
|
|
221
226
|
}
|
|
222
227
|
}),
|
|
223
|
-
address: Address
|
|
228
|
+
address: Address,
|
|
224
229
|
logo: z10.string().optional(),
|
|
225
|
-
industry: CompanyIndustry.keyof()
|
|
230
|
+
industry: CompanyIndustry.keyof(),
|
|
226
231
|
sector: z10.string(),
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
}),
|
|
232
|
-
// Can only be changed by the server
|
|
233
|
-
tier: z10.object({
|
|
234
|
-
current: tiers,
|
|
235
|
-
lastModified: Timestamp.optional()
|
|
236
|
-
}),
|
|
237
|
-
isVerified: z10.boolean(),
|
|
232
|
+
email: z10.email().max(45),
|
|
233
|
+
phoneNumber: PhoneNumber.optional(),
|
|
234
|
+
emailVerified: z10.boolean(),
|
|
235
|
+
verified: z10.boolean(),
|
|
238
236
|
legal: z10.object({
|
|
239
237
|
regNo: z10.string().max(20).readonly(),
|
|
240
238
|
// The company's registration number
|
|
241
239
|
trn: z10.string().regex(/[0-9]{3}-[0-9]{3}-[0-9]{3}/).nullish(),
|
|
242
240
|
gctRegNo: z10.string().max(15).nullish()
|
|
243
241
|
}).optional(),
|
|
244
|
-
|
|
242
|
+
...TimeLog.shape
|
|
245
243
|
}).superRefine((data, ctx) => {
|
|
246
244
|
const hasLegalInfo = "legal" in data;
|
|
247
|
-
if (data.
|
|
245
|
+
if (data.verified && !hasLegalInfo) {
|
|
248
246
|
ctx.addIssue("Verified companies must have the legal field defined.");
|
|
249
|
-
} else if (!data.
|
|
247
|
+
} else if (!data.verified && hasLegalInfo) {
|
|
250
248
|
ctx.addIssue("Unverified companies can't have the legal field defined.");
|
|
251
249
|
}
|
|
252
250
|
if (!CompanyIndustry.shape[data.industry].safeParse(data.sector).success) {
|
|
@@ -261,172 +259,53 @@ var MutableCompanyIdentity = CompanyIdentity.pick({
|
|
|
261
259
|
alias: true
|
|
262
260
|
});
|
|
263
261
|
|
|
264
|
-
// src/schemas/company/components/Notifications.ts
|
|
265
|
-
import * as z12 from "zod";
|
|
266
|
-
|
|
267
|
-
// src/schemas/Global.ts
|
|
268
|
-
import * as z11 from "zod";
|
|
269
|
-
|
|
270
|
-
// src/constants.ts
|
|
271
|
-
var NOTIF_ID_DELIM = "::";
|
|
272
|
-
var MAX_NAME_LENGTH = 21;
|
|
273
|
-
|
|
274
|
-
// src/schemas/Global.ts
|
|
275
|
-
var TierList = z11.record(
|
|
276
|
-
tiers,
|
|
277
|
-
z11.object({
|
|
278
|
-
cost: z11.number().min(0),
|
|
279
|
-
perks: z11.array(z11.string()).min(1)
|
|
280
|
-
})
|
|
281
|
-
).superRefine((data, ctx) => {
|
|
282
|
-
if (data.basic.cost !== 0) {
|
|
283
|
-
ctx.addIssue("The basic tier must always cost $0.00");
|
|
284
|
-
}
|
|
285
|
-
});
|
|
286
|
-
var Notification = z11.object({
|
|
287
|
-
_id: z11.string().superRefine((data, ctx) => {
|
|
288
|
-
const isUUidV4 = z11.uuidv4().safeParse(data).success;
|
|
289
|
-
if (isUUidV4) return;
|
|
290
|
-
let [from, to, nonce] = data.split(NOTIF_ID_DELIM);
|
|
291
|
-
nonce = nonce?.trim() || "";
|
|
292
|
-
const parsedFrom = z11.uuidv4().or(z11.uuidv7()).safeParse(from);
|
|
293
|
-
if (!parsedFrom.success) {
|
|
294
|
-
ctx.addIssue({
|
|
295
|
-
code: "custom",
|
|
296
|
-
message: "Invalid UUID",
|
|
297
|
-
path: ["from"]
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
|
-
const parsedTo = z11.uuidv4().safeParse(to);
|
|
301
|
-
if (!parsedTo.success) {
|
|
302
|
-
ctx.addIssue({
|
|
303
|
-
code: "custom",
|
|
304
|
-
message: "Invalid UUID",
|
|
305
|
-
path: ["to"]
|
|
306
|
-
});
|
|
307
|
-
}
|
|
308
|
-
if (nonce.length < 4 || nonce.length > 7) {
|
|
309
|
-
ctx.addIssue({
|
|
310
|
-
code: "custom",
|
|
311
|
-
message: "Invalid length",
|
|
312
|
-
path: ["nonce"]
|
|
313
|
-
});
|
|
314
|
-
}
|
|
315
|
-
}),
|
|
316
|
-
code: z11.literal([
|
|
317
|
-
"INVITE_RESPONSE",
|
|
318
|
-
"COMPANY_INVITE",
|
|
319
|
-
"DEV_MESSAGE",
|
|
320
|
-
"ALERT"
|
|
321
|
-
]),
|
|
322
|
-
payload: z11.string(),
|
|
323
|
-
isArchived: z11.boolean().optional(),
|
|
324
|
-
rcpt: z11.uuidv4().or(z11.uuidv7()).nullable(),
|
|
325
|
-
src: z11.object({
|
|
326
|
-
_id: z11.uuidv4().or(z11.uuidv7()),
|
|
327
|
-
name: z11.string(),
|
|
328
|
-
photoUrl: z11.string().nullish()
|
|
329
|
-
}),
|
|
330
|
-
createdAt: Timestamp
|
|
331
|
-
}).superRefine((data, ctx) => {
|
|
332
|
-
const expectMessage = () => {
|
|
333
|
-
if ((data.payload?.length ?? 0) < 3) {
|
|
334
|
-
ctx.addIssue("The payload must have at least (3) characters.");
|
|
335
|
-
}
|
|
336
|
-
};
|
|
337
|
-
switch (data.code) {
|
|
338
|
-
case "COMPANY_INVITE":
|
|
339
|
-
if (!z11.jwt().safeParse(data.payload).success) {
|
|
340
|
-
ctx.addIssue("The payload of company invite must be a valid jwt.");
|
|
341
|
-
}
|
|
342
|
-
break;
|
|
343
|
-
case "DEV_MESSAGE":
|
|
344
|
-
expectMessage();
|
|
345
|
-
break;
|
|
346
|
-
case "ALERT":
|
|
347
|
-
expectMessage();
|
|
348
|
-
break;
|
|
349
|
-
case "INVITE_RESPONSE":
|
|
350
|
-
if (!InviteResponse.safeParse(data.payload).success) {
|
|
351
|
-
ctx.addIssue(
|
|
352
|
-
`An invite response must be one of the following: ${InviteResponse.options.join()}.`
|
|
353
|
-
);
|
|
354
|
-
}
|
|
355
|
-
break;
|
|
356
|
-
default:
|
|
357
|
-
return data.code;
|
|
358
|
-
}
|
|
359
|
-
});
|
|
360
|
-
var CreditCurrency = z11.object({
|
|
361
|
-
uid: z11.uuidv4(),
|
|
362
|
-
cost: z11.object({ value: z11.number().min(0), currency: acceptedCurrencies }),
|
|
363
|
-
value: z11.int().min(0),
|
|
364
|
-
...TimeLog.shape
|
|
365
|
-
});
|
|
366
|
-
var PaymentDetails = z11.object({
|
|
367
|
-
// Used to track the payments recorded by each paymentCollector
|
|
368
|
-
uid: z11.uuidv4(),
|
|
369
|
-
method: paymentMethods,
|
|
370
|
-
currency: acceptedCurrencies,
|
|
371
|
-
amount: z11.number().min(1),
|
|
372
|
-
// Rate chart id (used to identify the rate chart used to convert the amount )
|
|
373
|
-
// rcid: z.uuidv4(),
|
|
374
|
-
...TimeLog.shape
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
// src/schemas/company/components/Notifications.ts
|
|
378
|
-
var CompanyNotifications = z12.array(Notification).refine(
|
|
379
|
-
(data) => data.every((notif) => notif.code !== "COMPANY_INVITE"),
|
|
380
|
-
"A company can't receive a company invite."
|
|
381
|
-
);
|
|
382
|
-
|
|
383
262
|
// src/schemas/company/components/Preferences.ts
|
|
384
|
-
import * as
|
|
385
|
-
var CompanyPreferences =
|
|
263
|
+
import * as z11 from "zod";
|
|
264
|
+
var CompanyPreferences = z11.object({
|
|
386
265
|
_id: CompanyState.shape._id,
|
|
387
266
|
/** The user that's allowed to record the client's services. */
|
|
388
267
|
serviceSelector: companyServiceSelectors,
|
|
389
|
-
/**
|
|
390
|
-
|
|
391
|
-
* @note
|
|
392
|
-
* - only allowed when `serviceSelector === "scheduler"`
|
|
393
|
-
* */
|
|
394
|
-
enforcePaidAppointments: z13.boolean(),
|
|
268
|
+
/** Pay per task */
|
|
269
|
+
ppt: z11.boolean(),
|
|
395
270
|
/**
|
|
396
271
|
* @property RR (Round Robin): Even distribution.
|
|
397
272
|
* @property LOR (Least Outstanding Requests): Distribute based on availability.
|
|
398
273
|
*/
|
|
399
|
-
|
|
400
|
-
servicesDeployed:
|
|
401
|
-
|
|
274
|
+
taskDistAlg: taskDistAlgs,
|
|
275
|
+
servicesDeployed: z11.boolean(),
|
|
276
|
+
...TimeLog.shape
|
|
402
277
|
}).refine(
|
|
403
|
-
(data) => !data.
|
|
404
|
-
|
|
278
|
+
(data) => !data.ppt || data.ppt && data.serviceSelector === "scheduler",
|
|
279
|
+
`Pay per task is only allowed when the service selector is the "${"scheduler"}".`
|
|
405
280
|
);
|
|
406
281
|
var MutableCompanyPreferences = CompanyPreferences.omit({
|
|
407
|
-
|
|
282
|
+
_id: true,
|
|
408
283
|
lastModified: true
|
|
409
284
|
});
|
|
410
285
|
|
|
411
286
|
// src/schemas/company/components/Staff.ts
|
|
412
|
-
import
|
|
413
|
-
|
|
414
|
-
// src/schemas/company/components/User.ts
|
|
415
|
-
import * as z16 from "zod";
|
|
287
|
+
import { distinct } from "@wavy/fn";
|
|
288
|
+
import * as z15 from "zod";
|
|
416
289
|
|
|
417
290
|
// src/functions/helper-functions.ts
|
|
418
291
|
import { camelCaseToLetter, upperFirst } from "@wavy/fn";
|
|
419
292
|
|
|
420
293
|
// src/schemas/profiles/Client.ts
|
|
421
|
-
import * as
|
|
294
|
+
import * as z13 from "zod";
|
|
422
295
|
|
|
423
296
|
// src/schemas/profiles/User.ts
|
|
424
297
|
import { UserModel } from "@wavy/util";
|
|
425
|
-
import * as
|
|
426
|
-
|
|
298
|
+
import * as z12 from "zod";
|
|
299
|
+
|
|
300
|
+
// src/constants.ts
|
|
301
|
+
var NOTIF_ID_DELIM = "::";
|
|
302
|
+
var MAX_NAME_LENGTH = 21;
|
|
303
|
+
|
|
304
|
+
// src/schemas/profiles/User.ts
|
|
305
|
+
var UserDetails = z12.object({
|
|
427
306
|
...UserModel.shape,
|
|
428
|
-
_id:
|
|
429
|
-
name:
|
|
307
|
+
_id: z12.uuidv4(),
|
|
308
|
+
name: z12.string().transform((data) => {
|
|
430
309
|
const [firstName = "", lastName = ""] = data.split(" ");
|
|
431
310
|
return (firstName + " " + lastName).trim();
|
|
432
311
|
}).superRefine((data, ctx) => {
|
|
@@ -443,58 +322,59 @@ var UserDetails = z14.object({
|
|
|
443
322
|
});
|
|
444
323
|
}),
|
|
445
324
|
email: UserModel.shape.email,
|
|
446
|
-
publicKey:
|
|
325
|
+
publicKey: z12.string().optional(),
|
|
326
|
+
availableStorageInBytes: z12.number().optional(),
|
|
447
327
|
// notifications: z.array(Notification),
|
|
448
|
-
lastSignedIn:
|
|
328
|
+
lastSignedIn: z12.object({
|
|
449
329
|
auto: Timestamp,
|
|
450
330
|
manual: Timestamp
|
|
451
331
|
}).partial(),
|
|
452
|
-
companyUids:
|
|
332
|
+
companyUids: z12.array(CompanyState.shape._id).min(1).optional(),
|
|
453
333
|
// resumeToken: z.uuidv4().optional(),
|
|
454
334
|
...TimeLog.shape
|
|
455
335
|
}).omit({ uid: true });
|
|
456
336
|
|
|
457
337
|
// src/schemas/profiles/Client.ts
|
|
458
338
|
import { Address as Address2, PhoneNumber as PhoneNumber2 } from "@wavy/util";
|
|
459
|
-
var Name =
|
|
339
|
+
var Name = z13.string().trim().min(2).max(MAX_NAME_LENGTH).transform((d) => d.replace(/-+/g, "-").replace(/'+/g, "'")).superRefine((data, ctx) => {
|
|
460
340
|
const invalidCharIdx = data.search(/[^a-z'-]/i);
|
|
461
341
|
if (invalidCharIdx >= 0) {
|
|
462
342
|
const char = data[invalidCharIdx].includes('"') ? `(${data[invalidCharIdx]})` : `"${data[invalidCharIdx]}"`;
|
|
463
343
|
ctx.addIssue(`${char} is not an allowed character.`);
|
|
464
344
|
}
|
|
465
345
|
});
|
|
466
|
-
var
|
|
346
|
+
var ClientDetails = UserDetails.pick({
|
|
467
347
|
_id: true,
|
|
468
348
|
createdAt: true,
|
|
469
349
|
lastModified: true,
|
|
470
350
|
phoneNumber: true,
|
|
471
351
|
photoUrl: true
|
|
472
352
|
}).safeExtend(
|
|
473
|
-
|
|
353
|
+
z13.object({
|
|
474
354
|
firstName: Name,
|
|
475
355
|
middleName: Name,
|
|
476
356
|
lastName: Name,
|
|
477
357
|
email: UserDetails.shape.email.optional(),
|
|
478
358
|
dob: Timestamp,
|
|
479
359
|
sex: genders,
|
|
480
|
-
isGlobal:
|
|
360
|
+
isGlobal: z13.boolean(),
|
|
481
361
|
origin: CompanyIdentity.shape._id,
|
|
482
|
-
visits:
|
|
362
|
+
visits: z13.record(CompanyIdentity.shape._id, z13.array(Timestamp).min(1)).optional(),
|
|
483
363
|
phoneNumber: PhoneNumber2.optional(),
|
|
484
364
|
// aka. National id
|
|
485
|
-
nid:
|
|
365
|
+
nid: z13.object({
|
|
486
366
|
trn: Trn,
|
|
487
367
|
nin: AlphaNumeric.toLowerCase().min(5).max(20),
|
|
488
368
|
passportNo: AlphaNumeric.toLowerCase().min(5).max(15)
|
|
489
369
|
}).partial().optional(),
|
|
490
370
|
// Don't hash the attributes, they can be used to help accurately identify a client by whoever is searching for
|
|
491
371
|
// the client
|
|
492
|
-
attributes:
|
|
372
|
+
attributes: z13.object({
|
|
493
373
|
motherMaidenName: Name,
|
|
494
374
|
fatherMiddleName: Name,
|
|
495
375
|
motherMiddleName: Name,
|
|
496
376
|
oldestSiblingMiddleName: Name,
|
|
497
|
-
firstSchoolName:
|
|
377
|
+
firstSchoolName: z13.string().trim().min(3).max(60)
|
|
498
378
|
}).partial().refine(
|
|
499
379
|
(data) => Object.keys(data).length > 0,
|
|
500
380
|
"At least (1) attribute must be defined."
|
|
@@ -503,7 +383,7 @@ var ClientIdentity = UserDetails.pick({
|
|
|
503
383
|
linkedTo: UserDetails.shape._id.optional()
|
|
504
384
|
}).shape
|
|
505
385
|
);
|
|
506
|
-
var ClientForm =
|
|
386
|
+
var ClientForm = ClientDetails.pick({
|
|
507
387
|
firstName: true,
|
|
508
388
|
middleName: true,
|
|
509
389
|
lastName: true,
|
|
@@ -512,24 +392,6 @@ var ClientForm = ClientIdentity.pick({
|
|
|
512
392
|
}).required();
|
|
513
393
|
|
|
514
394
|
// src/functions/helper-functions.ts
|
|
515
|
-
var findConflictingPartners = (...partners) => {
|
|
516
|
-
let hasSingleDoctor = false;
|
|
517
|
-
const conflicts = [];
|
|
518
|
-
if (!partners) return partners;
|
|
519
|
-
for (const partner of partners) {
|
|
520
|
-
if (!partner.roles || partner.roles.some((r) => !(r in COMPANY_USER_RELATIONSHIP))) {
|
|
521
|
-
conflicts.push(partner);
|
|
522
|
-
continue;
|
|
523
|
-
}
|
|
524
|
-
if (partner.roles.includes("doc") && !hasSingleDoctor) {
|
|
525
|
-
hasSingleDoctor = true;
|
|
526
|
-
} else if (partner.roles.includes("doc")) {
|
|
527
|
-
conflicts.push(partner);
|
|
528
|
-
continue;
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
return conflicts.length === 0 ? null : conflicts;
|
|
532
|
-
};
|
|
533
395
|
var getCompatibleRoles = (role) => {
|
|
534
396
|
return companyUserRoles.options.filter(
|
|
535
397
|
(r) => !COMPANY_USER_RELATIONSHIP[role]?.includes(r)
|
|
@@ -545,7 +407,7 @@ var normalizeCompanyId = (id) => {
|
|
|
545
407
|
return id.split("-").map((value) => upperFirst(value.toLowerCase())).join(" ");
|
|
546
408
|
};
|
|
547
409
|
var normalizeNidKey = (value) => {
|
|
548
|
-
value =
|
|
410
|
+
value = ClientDetails.shape.nid.unwrap().keyof().parse(value);
|
|
549
411
|
if (value === "nin") return "National Id No.";
|
|
550
412
|
if (value === "trn") return "Tax Registration No.";
|
|
551
413
|
return camelCaseToLetter(value.replace("No", "No."));
|
|
@@ -556,163 +418,103 @@ var parseClientName = ({
|
|
|
556
418
|
lastName
|
|
557
419
|
}) => [firstName, middleName[0] + ".", lastName].join(" ");
|
|
558
420
|
|
|
559
|
-
// src/schemas/company/
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
_id: z16.uuidv4(),
|
|
566
|
-
user: UserDetails.pick({ _id: true, name: true, email: true }),
|
|
421
|
+
// src/schemas/company/utils/CompanySession.ts
|
|
422
|
+
import * as z14 from "zod";
|
|
423
|
+
var CompanySession = z14.object({
|
|
424
|
+
_id: z14.uuidv4(),
|
|
425
|
+
userId: UserDetails.shape._id,
|
|
426
|
+
// user: UserDetails.pick({ _id: true, name: true, email: true }),
|
|
567
427
|
companyId: CompanyState.shape._id,
|
|
568
|
-
deviceId:
|
|
569
|
-
isAvailable:
|
|
570
|
-
value:
|
|
428
|
+
deviceId: z14.string().trim().min(15),
|
|
429
|
+
isAvailable: z14.object({
|
|
430
|
+
value: z14.boolean(),
|
|
571
431
|
lastModified: Timestamp.optional()
|
|
572
432
|
}),
|
|
573
|
-
isBusy:
|
|
574
|
-
value:
|
|
433
|
+
isBusy: z14.object({
|
|
434
|
+
value: z14.boolean(),
|
|
575
435
|
lastModified: Timestamp.optional()
|
|
576
436
|
}),
|
|
577
|
-
|
|
578
|
-
roles:
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
}
|
|
583
|
-
return newRoles.sort();
|
|
584
|
-
}).refine((roles) => {
|
|
585
|
-
return !roles.find(
|
|
586
|
-
(role) => getIncompatibleRoles(role).some((r) => roles.includes(r))
|
|
587
|
-
);
|
|
588
|
-
}, "A user is not allowed to have conflicting roles."),
|
|
589
|
-
ttl: z16.number(),
|
|
590
|
-
lastModified: Timestamp.optional(),
|
|
591
|
-
expiresAfter: z16.date()
|
|
592
|
-
});
|
|
593
|
-
var MiniCompanyUser2 = z16.object({
|
|
594
|
-
_id: UserDetails.shape._id,
|
|
595
|
-
...CompanyUserSession.pick({
|
|
596
|
-
roles: true,
|
|
597
|
-
/** Attached to help determine if a user is allowed to create a new session */
|
|
598
|
-
isBusy: true,
|
|
599
|
-
lastModified: true,
|
|
600
|
-
appointments: true
|
|
601
|
-
}).shape,
|
|
602
|
-
/** The time before the company session gets deleted */
|
|
603
|
-
lastActive: Timestamp.optional(),
|
|
604
|
-
inviteSent: Timestamp.optional(),
|
|
605
|
-
inviteAccepted: Timestamp.optional()
|
|
606
|
-
});
|
|
607
|
-
var CompanyUser2 = z16.object({
|
|
608
|
-
...MiniCompanyUser2.shape,
|
|
609
|
-
...CompanyUserSession.shape.user.shape,
|
|
610
|
-
// Only true when the user's data is retrieved from the session pool
|
|
611
|
-
// NB: This property is purely cosmetic (so no need to store this on the server)
|
|
612
|
-
isActive: z16.boolean()
|
|
437
|
+
taskCounter: z14.record(z14.literal(["undone", "done"]), z14.int().min(0)),
|
|
438
|
+
roles: CompanyMemberRoles,
|
|
439
|
+
ttl: z14.number(),
|
|
440
|
+
expiresAfter: z14.date(),
|
|
441
|
+
...TimeLog.shape
|
|
613
442
|
});
|
|
614
443
|
|
|
615
444
|
// src/schemas/company/components/Staff.ts
|
|
616
|
-
var
|
|
445
|
+
var CompanyMemberRoles = z15.array(companyUserRoles).transform((roles) => distinct(roles).sort()).refine((roles) => {
|
|
446
|
+
return !roles.find(
|
|
447
|
+
(role) => getIncompatibleRoles(role).some((r) => roles.includes(r))
|
|
448
|
+
);
|
|
449
|
+
}, "A user is not allowed to have conflicting roles.");
|
|
450
|
+
var CompanyStaff2 = z15.object({
|
|
617
451
|
_id: CompanyState.shape._id,
|
|
618
|
-
members:
|
|
619
|
-
|
|
620
|
-
|
|
452
|
+
members: z15.record(
|
|
453
|
+
UserDetails.shape._id,
|
|
454
|
+
z15.object({
|
|
455
|
+
invitedAt: Timestamp.optional(),
|
|
456
|
+
joinedAt: Timestamp,
|
|
457
|
+
lastActive: Timestamp,
|
|
458
|
+
roles: CompanyMemberRoles,
|
|
459
|
+
dailyTaskCounter: CompanySession.shape.taskCounter.optional()
|
|
460
|
+
})
|
|
621
461
|
),
|
|
622
|
-
// .superRefine(refineMembers),
|
|
623
462
|
/**
|
|
624
463
|
* @relationship one -> many
|
|
625
464
|
*@description A map of doctor `uids` to their assistants `uids` */
|
|
626
|
-
partnerMap:
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
),
|
|
630
|
-
updateQueue:
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
$REMOVE:
|
|
634
|
-
$CHANGE_ROLES:
|
|
635
|
-
newRoles:
|
|
465
|
+
partnerMap: z15.record(
|
|
466
|
+
UserDetails.shape._id,
|
|
467
|
+
z15.record(UserDetails.shape._id, z15.object({ addedAt: Timestamp }))
|
|
468
|
+
).optional(),
|
|
469
|
+
updateQueue: z15.record(
|
|
470
|
+
UserDetails.shape._id,
|
|
471
|
+
z15.object({
|
|
472
|
+
$REMOVE: z15.object({ addedAt: Timestamp }),
|
|
473
|
+
$CHANGE_ROLES: z15.object({
|
|
474
|
+
newRoles: CompanyMemberRoles,
|
|
636
475
|
addedAt: Timestamp
|
|
637
476
|
})
|
|
638
477
|
}).partial()
|
|
639
|
-
),
|
|
640
|
-
|
|
641
|
-
})
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
for (const uid of Object.keys(copy.updateQueue)) {
|
|
650
|
-
if (uid in copy.members) {
|
|
651
|
-
const userUpdates = copy.updateQueue[uid];
|
|
652
|
-
if ("$REMOVE" in userUpdates) {
|
|
653
|
-
copy.updateQueue[uid] = { $REMOVE: userUpdates.$REMOVE };
|
|
654
|
-
} else if (userUpdates.$CHANGE_ROLES && userUpdates.$CHANGE_ROLES.newRoles.length === copy.members[uid].roles.length && userUpdates.$CHANGE_ROLES.newRoles.every(
|
|
655
|
-
(role) => copy.members[uid].roles.includes(role)
|
|
656
|
-
)) {
|
|
657
|
-
delete copy.updateQueue[uid].$CHANGE_ROLES;
|
|
658
|
-
}
|
|
659
|
-
if (Object.keys(userUpdates).length < 1) delete copy.updateQueue[uid];
|
|
660
|
-
} else {
|
|
661
|
-
delete copy.updateQueue[uid];
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
for (const [docUid, assts] of Object.entries(copy.partnerMap)) {
|
|
665
|
-
if (docUid in d.members) {
|
|
666
|
-
for (const asstUid of Object.keys(assts)) {
|
|
667
|
-
if (asstUid in d.members) continue;
|
|
668
|
-
delete copy.partnerMap[docUid][asstUid];
|
|
669
|
-
}
|
|
670
|
-
} else {
|
|
671
|
-
delete copy.partnerMap[docUid];
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
return copy;
|
|
675
|
-
}).superRefine((details, ctx) => {
|
|
676
|
-
for (const [_id, user] of Object.entries(details.members)) {
|
|
677
|
-
if (user.roles.includes("doc") && _id in details.partnerMap) {
|
|
678
|
-
for (const asstUid of Object.keys(details.partnerMap[_id])) {
|
|
679
|
-
if (!details.members[asstUid].roles.includes("physAsst")) {
|
|
680
|
-
ctx.addIssue(
|
|
681
|
-
`A partner group member must have the "physAsst" role (${_id}) -> (${asstUid}).`
|
|
682
|
-
);
|
|
683
|
-
}
|
|
478
|
+
).optional(),
|
|
479
|
+
...TimeLog.shape
|
|
480
|
+
}).superRefine((details, ctx) => {
|
|
481
|
+
for (const [_id, user] of Object.entries(details.members)) {
|
|
482
|
+
if (user.roles.includes("doc") && details.partnerMap && _id in details.partnerMap) {
|
|
483
|
+
for (const asstUid of Object.keys(details.partnerMap[_id])) {
|
|
484
|
+
if (!details.members[asstUid].roles.includes("physAsst")) {
|
|
485
|
+
ctx.addIssue(
|
|
486
|
+
`A partner group member must have the "physAsst" role (${_id}) -> (${asstUid}).`
|
|
487
|
+
);
|
|
684
488
|
}
|
|
685
|
-
} else if (_id in details.partnerMap) {
|
|
686
|
-
ctx.addIssue(
|
|
687
|
-
`A partner group leader must have the "doc" role. (${_id})`
|
|
688
|
-
);
|
|
689
|
-
} else if (user.roles.includes("doc")) {
|
|
690
|
-
ctx.addIssue(
|
|
691
|
-
`All users that have the "doc" role must be the leader of a partner group. (${_id})`
|
|
692
|
-
);
|
|
693
489
|
}
|
|
490
|
+
} else if (details.partnerMap && _id in details.partnerMap) {
|
|
491
|
+
ctx.addIssue(
|
|
492
|
+
`A partner group leader must have the "doc" role. (${_id})`
|
|
493
|
+
);
|
|
494
|
+
} else if (user.roles.includes("doc")) {
|
|
495
|
+
ctx.addIssue(
|
|
496
|
+
`All users that have the "doc" role must be the leader of a partner group. (${_id})`
|
|
497
|
+
);
|
|
694
498
|
}
|
|
695
|
-
}
|
|
696
|
-
};
|
|
697
|
-
var
|
|
698
|
-
var MiniCompanyStaff = transformStaff(MiniRawCompanyStaff);
|
|
699
|
-
var MutableCompanyStaff = RawCompanyStaff.pick({
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
var MutableCompanyStaff = CompanyStaff2.pick({
|
|
700
502
|
updateQueue: true,
|
|
701
503
|
partnerMap: true
|
|
702
504
|
});
|
|
703
505
|
|
|
704
506
|
// src/schemas/company/CompanyDetails.ts
|
|
705
|
-
var CompanyDetails =
|
|
507
|
+
var CompanyDetails = z16.object({
|
|
706
508
|
_id: CompanyState.shape._id,
|
|
707
509
|
identity: CompanyIdentity.omit({ _id: true }),
|
|
708
|
-
notifications: CompanyNotifications,
|
|
709
|
-
receipts: Receipts,
|
|
510
|
+
// notifications: CompanyNotifications,
|
|
511
|
+
// receipts: Receipts,
|
|
710
512
|
state: CompanyState.omit({ _id: true }),
|
|
711
513
|
preferences: CompanyPreferences.omit({ _id: true }),
|
|
712
514
|
billing: CompanyBilling.omit({ _id: true }),
|
|
713
|
-
staff:
|
|
515
|
+
staff: CompanyStaff2.omit({ _id: true })
|
|
714
516
|
});
|
|
715
|
-
var MutableCompanyDetails =
|
|
517
|
+
var MutableCompanyDetails = z16.object({
|
|
716
518
|
_id: CompanyDetails.shape._id,
|
|
717
519
|
identity: MutableCompanyIdentity.partial().optional(),
|
|
718
520
|
preferences: MutableCompanyPreferences.partial().optional(),
|
|
@@ -720,23 +522,139 @@ var MutableCompanyDetails = z18.object({
|
|
|
720
522
|
staff: MutableCompanyStaff.partial().optional()
|
|
721
523
|
});
|
|
722
524
|
|
|
525
|
+
// src/schemas/company/components/Notifications.ts
|
|
526
|
+
import * as z18 from "zod";
|
|
527
|
+
|
|
528
|
+
// src/schemas/Global.ts
|
|
529
|
+
import * as z17 from "zod";
|
|
530
|
+
var TierList = z17.record(
|
|
531
|
+
tiers,
|
|
532
|
+
z17.object({
|
|
533
|
+
cost: z17.number().min(0),
|
|
534
|
+
perks: z17.array(z17.string()).min(1)
|
|
535
|
+
})
|
|
536
|
+
).superRefine((data, ctx) => {
|
|
537
|
+
if (data.basic.cost !== 0) {
|
|
538
|
+
ctx.addIssue("The basic tier must always cost $0.00");
|
|
539
|
+
}
|
|
540
|
+
});
|
|
541
|
+
var Notification = z17.object({
|
|
542
|
+
_id: z17.string().superRefine((data, ctx) => {
|
|
543
|
+
const isUUidV4 = z17.uuidv4().safeParse(data).success;
|
|
544
|
+
if (isUUidV4) return;
|
|
545
|
+
let [from, to, nonce] = data.split(NOTIF_ID_DELIM);
|
|
546
|
+
nonce = nonce?.trim() || "";
|
|
547
|
+
const parsedFrom = z17.uuidv4().or(z17.uuidv7()).safeParse(from);
|
|
548
|
+
if (!parsedFrom.success) {
|
|
549
|
+
ctx.addIssue({
|
|
550
|
+
code: "custom",
|
|
551
|
+
message: "Invalid UUID",
|
|
552
|
+
path: ["from"]
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
const parsedTo = z17.uuidv4().safeParse(to);
|
|
556
|
+
if (!parsedTo.success) {
|
|
557
|
+
ctx.addIssue({
|
|
558
|
+
code: "custom",
|
|
559
|
+
message: "Invalid UUID",
|
|
560
|
+
path: ["to"]
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
if (nonce.length < 4 || nonce.length > 7) {
|
|
564
|
+
ctx.addIssue({
|
|
565
|
+
code: "custom",
|
|
566
|
+
message: "Invalid length",
|
|
567
|
+
path: ["nonce"]
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
}),
|
|
571
|
+
code: z17.literal([
|
|
572
|
+
"INVITE_RESPONSE",
|
|
573
|
+
"COMPANY_INVITE",
|
|
574
|
+
"DEV_MESSAGE",
|
|
575
|
+
"ALERT"
|
|
576
|
+
]),
|
|
577
|
+
payload: z17.string(),
|
|
578
|
+
isArchived: z17.boolean().optional(),
|
|
579
|
+
rcpt: z17.uuidv4().or(z17.uuidv7()).nullable(),
|
|
580
|
+
src: z17.object({
|
|
581
|
+
_id: z17.uuidv4().or(z17.uuidv7()),
|
|
582
|
+
name: z17.string(),
|
|
583
|
+
photoUrl: z17.string().nullish()
|
|
584
|
+
}),
|
|
585
|
+
createdAt: Timestamp
|
|
586
|
+
}).superRefine((data, ctx) => {
|
|
587
|
+
const expectMessage = () => {
|
|
588
|
+
if ((data.payload?.length ?? 0) < 3) {
|
|
589
|
+
ctx.addIssue("The payload must have at least (3) characters.");
|
|
590
|
+
}
|
|
591
|
+
};
|
|
592
|
+
switch (data.code) {
|
|
593
|
+
case "COMPANY_INVITE":
|
|
594
|
+
if (!z17.jwt().safeParse(data.payload).success) {
|
|
595
|
+
ctx.addIssue("The payload of company invite must be a valid jwt.");
|
|
596
|
+
}
|
|
597
|
+
break;
|
|
598
|
+
case "DEV_MESSAGE":
|
|
599
|
+
expectMessage();
|
|
600
|
+
break;
|
|
601
|
+
case "ALERT":
|
|
602
|
+
expectMessage();
|
|
603
|
+
break;
|
|
604
|
+
case "INVITE_RESPONSE":
|
|
605
|
+
if (!InviteResponse.safeParse(data.payload).success) {
|
|
606
|
+
ctx.addIssue(
|
|
607
|
+
`An invite response must be one of the following: ${InviteResponse.options.join()}.`
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
break;
|
|
611
|
+
default:
|
|
612
|
+
return data.code;
|
|
613
|
+
}
|
|
614
|
+
});
|
|
615
|
+
var CreditCurrency = z17.object({
|
|
616
|
+
uid: z17.uuidv4(),
|
|
617
|
+
cost: z17.object({ value: z17.number().min(0), currency: acceptedCurrencies }),
|
|
618
|
+
value: z17.int().min(0),
|
|
619
|
+
...TimeLog.shape
|
|
620
|
+
});
|
|
621
|
+
var PaymentDetails = z17.object({
|
|
622
|
+
// Used to track the payments recorded by each paymentCollector
|
|
623
|
+
uid: z17.uuidv4(),
|
|
624
|
+
method: paymentMethods,
|
|
625
|
+
currency: acceptedCurrencies,
|
|
626
|
+
amount: z17.number().min(1),
|
|
627
|
+
// Rate chart id (used to identify the rate chart used to convert the amount )
|
|
628
|
+
// rcid: z.uuidv4(),
|
|
629
|
+
...TimeLog.shape
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
// src/schemas/company/components/Notifications.ts
|
|
633
|
+
var CompanyNotifications = z18.array(Notification).refine(
|
|
634
|
+
(data) => data.every((notif) => notif.code !== "COMPANY_INVITE"),
|
|
635
|
+
"A company can't receive a company invite."
|
|
636
|
+
);
|
|
637
|
+
|
|
723
638
|
// src/schemas/company/utils/CreateCompanyForm.ts
|
|
724
639
|
import * as z19 from "zod";
|
|
725
640
|
var CreateCompanyForm = z19.object({
|
|
726
|
-
_id: CompanyIdentity.shape._id,
|
|
727
641
|
alias: CompanyIdentity.shape.alias.optional(),
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
642
|
+
...CompanyIdentity.pick({
|
|
643
|
+
_id: true,
|
|
644
|
+
logo: true,
|
|
645
|
+
address: true,
|
|
646
|
+
email: true,
|
|
647
|
+
phoneNumber: true,
|
|
648
|
+
industry: true,
|
|
649
|
+
sector: true
|
|
650
|
+
}).shape
|
|
733
651
|
}).superRefine((data, ctx) => {
|
|
734
652
|
if (!CompanyIndustry.shape[data.industry].safeParse(data.sector).success) {
|
|
735
653
|
ctx.addIssue(getInvalidSectorError(data.industry, data.sector));
|
|
736
654
|
}
|
|
737
655
|
});
|
|
738
656
|
|
|
739
|
-
// src/schemas/
|
|
657
|
+
// src/schemas/task/components/Medical.ts
|
|
740
658
|
import * as z20 from "zod";
|
|
741
659
|
var Vitals = z20.record(
|
|
742
660
|
vitalKeys,
|
|
@@ -771,51 +689,20 @@ var MedicalDetails = z20.object({
|
|
|
771
689
|
).optional()
|
|
772
690
|
});
|
|
773
691
|
|
|
774
|
-
// src/schemas/
|
|
775
|
-
import { sumOf as sumOf2 } from "@wavy/fn";
|
|
692
|
+
// src/schemas/task/Task.ts
|
|
776
693
|
import * as z21 from "zod";
|
|
777
|
-
|
|
778
|
-
// src/functions/calc.ts
|
|
779
|
-
import { sumOf } from "@wavy/fn";
|
|
780
|
-
function calcBalance(components, options) {
|
|
781
|
-
const subtotal = sumOf(
|
|
782
|
-
Object.values(components.charges || {})?.map((c) => c.cost)
|
|
783
|
-
);
|
|
784
|
-
const discount = calcPriceMod(components.discounts, subtotal);
|
|
785
|
-
const additionalFees = calcPriceMod(components.additionalFees, subtotal);
|
|
786
|
-
const prepayments = calcPriceMod(components.prepayments, subtotal);
|
|
787
|
-
const payments = sumOf(components.payments?.map((p) => p.amount) ?? []);
|
|
788
|
-
const result = subtotal + additionalFees - (discount + prepayments + payments);
|
|
789
|
-
if (options?.logger) {
|
|
790
|
-
console.log({ subtotal, discount, additionalFees, prepayments, result });
|
|
791
|
-
}
|
|
792
|
-
return result;
|
|
793
|
-
}
|
|
794
|
-
var calcPriceMod = (mods, subtotal) => {
|
|
795
|
-
const castedAdjustments = !mods ? [] : Object.values(mods);
|
|
796
|
-
if (!mods || castedAdjustments.length < 1) return 0;
|
|
797
|
-
return sumOf(
|
|
798
|
-
castedAdjustments?.map(
|
|
799
|
-
(d) => (d.fixedAmount?.value || 0) + (d.percentage ? d.percentage * 0.01 * subtotal : 0)
|
|
800
|
-
)
|
|
801
|
-
);
|
|
802
|
-
};
|
|
803
|
-
|
|
804
|
-
// src/schemas/appointment/Appointment.ts
|
|
805
|
-
var TimelineActivity = z21.object({
|
|
806
|
-
postedAt: Timestamp.nullish(),
|
|
807
|
-
userUid: CompanyUser2.shape._id.nonoptional()
|
|
808
|
-
});
|
|
809
|
-
var AppointmentSeverity = z21.object({
|
|
694
|
+
var TaskSeverity = z21.object({
|
|
810
695
|
"1": z21.literal("Emergency"),
|
|
811
696
|
"2": z21.literal("Important"),
|
|
812
697
|
"3": z21.literal("Average")
|
|
813
698
|
});
|
|
814
|
-
var
|
|
699
|
+
var TaskDetails = z21.object({
|
|
815
700
|
/** A random uid that identifies the document. */
|
|
816
701
|
_id: z21.uuidv4(),
|
|
702
|
+
/** The amount of credits used to create the appointment */
|
|
703
|
+
_cost: z21.number().readonly(),
|
|
817
704
|
// This helps to identify the urgency of the appointment
|
|
818
|
-
severity: z21.string().refine((data) =>
|
|
705
|
+
severity: z21.string().refine((data) => TaskSeverity.keyof().safeParse(data).success),
|
|
819
706
|
/** The company's uid */
|
|
820
707
|
src: CompanyState.shape._id,
|
|
821
708
|
/** The ticket number */
|
|
@@ -844,56 +731,63 @@ var MiniAppointmentDetails = z21.object({
|
|
|
844
731
|
prepayments: CompanyBilling.shape.prepayments.optional(),
|
|
845
732
|
payments: z21.array(PaymentDetails).max(100).optional(),
|
|
846
733
|
/** The client's identity */
|
|
847
|
-
|
|
734
|
+
clientId: ClientDetails.shape._id,
|
|
848
735
|
/**
|
|
849
736
|
* Data that is specific to the entity (for now it's just medical data).
|
|
850
737
|
* It will only be defined for the participants that have access to it.
|
|
851
738
|
*/
|
|
852
739
|
payload: z21.object({
|
|
853
740
|
...MedicalDetails.shape,
|
|
854
|
-
lastModified:
|
|
741
|
+
lastModified: Timestamp.optional()
|
|
855
742
|
}).optional(),
|
|
856
743
|
/** Only defined when either the service provider or the client cancelled their appointment */
|
|
857
744
|
cancelled: z21.object({
|
|
858
745
|
/** The time that the confirm cancel button was clicked */
|
|
859
746
|
doneAt: Timestamp,
|
|
860
747
|
reason: Reason,
|
|
861
|
-
doneBy: companyUserRoles.extract(["doc", "physAsst"])
|
|
748
|
+
doneBy: companyUserRoles.extract(["doc", "physAsst", "rcpst"]).or(z21.literal("client"))
|
|
862
749
|
}).optional(),
|
|
863
750
|
//** Add this after consulting with company owners about payment expectations */
|
|
864
751
|
// paymentDueDate: Timestamp.nullish(),
|
|
865
752
|
// The user that is currently modifying the appointment
|
|
866
753
|
lockedBy: UserDetails.shape._id.optional(),
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
754
|
+
/** Role based access control
|
|
755
|
+
* If a role is:
|
|
756
|
+
* [1] Ommitted -> any user with that role that's from the same company can access the TaskDetails.
|
|
757
|
+
* [1] Addded -> only the users that have that role and an id in the respective array can access the TaskDetails.
|
|
758
|
+
*/
|
|
759
|
+
rbac: z21.record(companyUserRoles, z21.array(UserDetails.shape._id).optional()),
|
|
760
|
+
timeline: z21.record(
|
|
761
|
+
UserDetails.shape._id,
|
|
762
|
+
z21.object({
|
|
763
|
+
/** Removed to rely on a single source of truth (audit logs stored in s3)
|
|
764
|
+
* as it relates to a user's roles at any given time.
|
|
765
|
+
*/
|
|
766
|
+
// roles: CompanyMemberRoles,
|
|
767
|
+
postedAt: Timestamp,
|
|
768
|
+
// Use null as an indicator that this is intentionally undefined.
|
|
769
|
+
changes: z21.record(z21.string(), z21.any()).nullable()
|
|
770
|
+
}).optional()
|
|
771
|
+
),
|
|
772
|
+
// z.object({
|
|
773
|
+
// scheduler: TimelineActivity,
|
|
774
|
+
// physAsst: TimelineActivity.or(z.literal("none")),
|
|
775
|
+
// doctor: TimelineActivity,
|
|
776
|
+
// // Array used to accurately record payments made by the "a/r" and the "cshr"
|
|
777
|
+
// debtCollector: z
|
|
778
|
+
// .array(
|
|
779
|
+
// z.object({
|
|
780
|
+
// ...TimelineActivity.shape,
|
|
781
|
+
// // Payment uid -> collection time
|
|
782
|
+
// collectedPayments: z.record(z.uuidv4(), Timestamp),
|
|
783
|
+
// }),
|
|
784
|
+
// )
|
|
785
|
+
// .min(1)
|
|
786
|
+
// .nullable(),
|
|
787
|
+
// } satisfies Record<z.infer<typeof serviceProviders>, any>),
|
|
880
788
|
...TimeLog.shape
|
|
881
|
-
}).superRefine((data, ctx) => {
|
|
882
|
-
if (data.payments && data.charges && sumOf2(data.payments.map((p) => p.amount)) >= calcBalance(data)) {
|
|
883
|
-
const untrackedPayment = data.payments.find(
|
|
884
|
-
(p) => !data.timeline.debtCollector?.some(
|
|
885
|
-
(d) => p.uid in d.collectedPayments
|
|
886
|
-
)
|
|
887
|
-
);
|
|
888
|
-
if (untrackedPayment) {
|
|
889
|
-
ctx.addIssue(`Untracked payment found (${untrackedPayment.uid})`);
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
789
|
});
|
|
893
|
-
var
|
|
894
|
-
clientUid: true
|
|
895
|
-
}).extend(z21.object({ client: ClientIdentity }).shape);
|
|
896
|
-
var MutableAppointmentDetails = AppointmentDetails.safeExtend(
|
|
790
|
+
var MutableAppointmentDetails = TaskDetails.safeExtend(
|
|
897
791
|
z21.object({
|
|
898
792
|
/**
|
|
899
793
|
* @description An encrypted copy of the last state of the appointment.
|
|
@@ -902,203 +796,38 @@ var MutableAppointmentDetails = AppointmentDetails.safeExtend(
|
|
|
902
796
|
_hash: z21.string()
|
|
903
797
|
}).shape
|
|
904
798
|
);
|
|
905
|
-
var ScheduleAppointmentForm = z21.object({
|
|
906
|
-
clientId: ClientIdentity.shape._id,
|
|
907
|
-
serviceProviderId: CompanyUser2.shape._id.or(z21.literal("next")),
|
|
908
|
-
category: AppointmentDetails.shape.severity,
|
|
909
|
-
appointmentReason: AppointmentDetails.shape.reason,
|
|
910
|
-
charges: AppointmentDetails.shape.charges.optional(),
|
|
911
|
-
prepayments: AppointmentDetails.shape.prepayments.optional(),
|
|
912
|
-
payments: AppointmentDetails.shape.payments.optional(),
|
|
913
|
-
...TimeLog.shape
|
|
914
|
-
});
|
|
915
|
-
|
|
916
|
-
// src/functions/scheduleAppointment.ts
|
|
917
|
-
import { findChanges, omit, omitNils, TODO } from "@wavy/fn";
|
|
918
|
-
import { v4 } from "uuid";
|
|
919
799
|
|
|
920
|
-
// src/functions/
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
return !(uid in staff.updateQueue) && user.roles.includes(role) && user.isActive;
|
|
926
|
-
}
|
|
800
|
+
// src/functions/calc.ts
|
|
801
|
+
import { sumOf } from "@wavy/fn";
|
|
802
|
+
function calcBalance(components, options) {
|
|
803
|
+
const subtotal = sumOf(
|
|
804
|
+
Object.values(components.charges || {})?.map((c) => c.cost)
|
|
927
805
|
);
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
if (
|
|
934
|
-
|
|
935
|
-
case "RR": {
|
|
936
|
-
nextAvailable = userUids.find((uid) => {
|
|
937
|
-
const calcTotalAppts = (appointments) => appointments.ongoing + appointments.completed;
|
|
938
|
-
return calcTotalAppts(staff.members[uid].appointments) < calcTotalAppts(staff.members[userUids[0]].appointments);
|
|
939
|
-
}) || userUids[0];
|
|
940
|
-
break;
|
|
941
|
-
}
|
|
942
|
-
case "LOR": {
|
|
943
|
-
for (const uid of userUids) {
|
|
944
|
-
const user = staff.members[uid];
|
|
945
|
-
if (!nextAvailable || user.appointments.ongoing < staff.members[nextAvailable].appointments.ongoing) {
|
|
946
|
-
nextAvailable = uid;
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
break;
|
|
950
|
-
}
|
|
951
|
-
default:
|
|
952
|
-
return algorithm;
|
|
806
|
+
const discount = calcPriceMod(components.discounts, subtotal);
|
|
807
|
+
const additionalFees = calcPriceMod(components.additionalFees, subtotal);
|
|
808
|
+
const prepayments = calcPriceMod(components.prepayments, subtotal);
|
|
809
|
+
const payments = sumOf(components.payments?.map((p) => p.amount) ?? []);
|
|
810
|
+
const result = subtotal + additionalFees - (discount + prepayments + payments);
|
|
811
|
+
if (options?.logger) {
|
|
812
|
+
console.log({ subtotal, discount, additionalFees, prepayments, result });
|
|
953
813
|
}
|
|
954
|
-
return
|
|
814
|
+
return result;
|
|
955
815
|
}
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
request.preferences = CompanyPreferences.omit({
|
|
963
|
-
uid: true,
|
|
964
|
-
lastModified: true
|
|
965
|
-
}).parse(request.preferences);
|
|
966
|
-
request.billing = CompanyBilling.omit({
|
|
967
|
-
uid: true,
|
|
968
|
-
prepayments: true,
|
|
969
|
-
lastModified: true
|
|
970
|
-
}).parse(request.billing);
|
|
971
|
-
const companyUser = request.staff?.members[request.sender._id];
|
|
972
|
-
const staffDetailsCopy = CompanyStaff.parse(structuredClone(request.staff));
|
|
973
|
-
const stateCopy = CompanyState.parse(structuredClone(request.state));
|
|
974
|
-
const castedFormCharges = Object.values(request.form?.charges || {});
|
|
975
|
-
let selectedDoctor = null;
|
|
976
|
-
let selectedPhysAsst = null;
|
|
977
|
-
const availableDoctors = findAvailableStaff("doc", staffDetailsCopy);
|
|
978
|
-
const today = new Date(Date.now()).toISOString();
|
|
979
|
-
if (!companyUser || !companyUser?.roles?.includes?.("rcpst")) {
|
|
980
|
-
throw new Error("Permissions missing.");
|
|
981
|
-
}
|
|
982
|
-
if (availableDoctors.length < 1) {
|
|
983
|
-
throw new Error("Insufficient staff available.");
|
|
984
|
-
}
|
|
985
|
-
if (request.form.serviceProviderId && availableDoctors.includes(request.form.serviceProviderId)) {
|
|
986
|
-
selectedDoctor = request.form.serviceProviderId;
|
|
987
|
-
} else if (request.form.serviceProviderId && staffDetailsCopy.members[request.form.serviceProviderId]?.roles?.includes?.(
|
|
988
|
-
"doc"
|
|
989
|
-
)) {
|
|
990
|
-
throw new Error("Unavailable doctor selected.");
|
|
991
|
-
} else if (request.form.serviceProviderId) {
|
|
992
|
-
throw new Error("Invalid doctor selected.");
|
|
993
|
-
}
|
|
994
|
-
if (request.form.serviceProviderId === null || request.form.serviceProviderId === void 0) {
|
|
995
|
-
selectedDoctor = findNextAvailableStaff(
|
|
996
|
-
availableDoctors,
|
|
997
|
-
request.preferences.apptDistAlg,
|
|
998
|
-
staffDetailsCopy
|
|
999
|
-
);
|
|
1000
|
-
}
|
|
1001
|
-
if (!selectedDoctor) {
|
|
1002
|
-
throw new Error(
|
|
1003
|
-
"Internal error found. Failed to find an available doctor."
|
|
1004
|
-
);
|
|
1005
|
-
} else {
|
|
1006
|
-
selectedPhysAsst = findNextAvailableStaff(
|
|
1007
|
-
Object.keys(staffDetailsCopy.partnerMap[selectedDoctor]),
|
|
1008
|
-
"LOR",
|
|
1009
|
-
staffDetailsCopy
|
|
1010
|
-
);
|
|
1011
|
-
}
|
|
1012
|
-
if (request.preferences.serviceSelector === "scheduler" && (!request.form.charges || castedFormCharges.length < 1)) {
|
|
1013
|
-
throw new Error("Insufficient services selected.");
|
|
1014
|
-
} else if (request.preferences.serviceSelector === "scheduler" && request.billing.offeredServices.deployed && castedFormCharges.find(
|
|
1015
|
-
(charge) => !!Object.values(request.billing.offeredServices).find(
|
|
1016
|
-
(createdCharge) => findChanges(charge, createdCharge).length > 0
|
|
816
|
+
var calcPriceMod = (mods, subtotal) => {
|
|
817
|
+
const castedAdjustments = !mods ? [] : Object.values(mods);
|
|
818
|
+
if (!mods || castedAdjustments.length < 1) return 0;
|
|
819
|
+
return sumOf(
|
|
820
|
+
castedAdjustments?.map(
|
|
821
|
+
(d) => (d.fixedAmount?.value || 0) + (d.percentage ? d.percentage * 0.01 * subtotal : 0)
|
|
1017
822
|
)
|
|
1018
|
-
)
|
|
1019
|
-
|
|
1020
|
-
}
|
|
1021
|
-
if (request.preferences.enforcePaidAppointments && (!request.form.payments || request.form.payments.length < 1)) {
|
|
1022
|
-
throw new Error("Insufficient payment transactions recorded.");
|
|
1023
|
-
} else if (request.preferences.enforcePaidAppointments) {
|
|
1024
|
-
const maxIterations = Math.max(
|
|
1025
|
-
castedFormCharges.length,
|
|
1026
|
-
request.form.payments.length
|
|
1027
|
-
);
|
|
1028
|
-
let totalPaid = 0;
|
|
1029
|
-
let totalOwed = 0;
|
|
1030
|
-
for (let idx = 0; idx < maxIterations; idx++) {
|
|
1031
|
-
totalPaid += request.form.payments[idx]?.amount || 0;
|
|
1032
|
-
totalOwed += request.form.charges[idx]?.cost || 0;
|
|
1033
|
-
}
|
|
1034
|
-
if (totalPaid < totalOwed) throw new Error("Insufficient payments made.");
|
|
1035
|
-
}
|
|
1036
|
-
if (stateCopy.credits.current < request.cost) {
|
|
1037
|
-
if (companyUser.roles.some((role) => adminRoles.safeParse(role).success)) {
|
|
1038
|
-
throw new Error("Account balance has insufficient funds.");
|
|
1039
|
-
} else {
|
|
1040
|
-
throw new Error(
|
|
1041
|
-
"Something went wrong. Please contact an admin to rectify the problem."
|
|
1042
|
-
);
|
|
1043
|
-
}
|
|
1044
|
-
}
|
|
1045
|
-
stateCopy.tktNoCounter++;
|
|
1046
|
-
stateCopy.credits.current -= request.cost;
|
|
1047
|
-
stateCopy.credits.lastModified = today;
|
|
1048
|
-
stateCopy.lastModified = today;
|
|
1049
|
-
staffDetailsCopy.members[selectedDoctor].appointments.ongoing++;
|
|
1050
|
-
if (selectedPhysAsst) {
|
|
1051
|
-
staffDetailsCopy.members[selectedPhysAsst].appointments.ongoing++;
|
|
1052
|
-
}
|
|
1053
|
-
staffDetailsCopy.lastModified = today;
|
|
1054
|
-
const appointment = omitNils({
|
|
1055
|
-
_id: v4(),
|
|
1056
|
-
src: stateCopy._id,
|
|
1057
|
-
tkt: request.state.tktNoCounter,
|
|
1058
|
-
severity: request.form.category,
|
|
1059
|
-
reason: request.form.appointmentReason,
|
|
1060
|
-
discounts: Object.fromEntries(
|
|
1061
|
-
Object.entries(request.billing.discounts || {}).filter(
|
|
1062
|
-
([_, disc]) => !disc.isOptional
|
|
1063
|
-
)
|
|
1064
|
-
),
|
|
1065
|
-
additionalFees: Object.fromEntries(
|
|
1066
|
-
Object.entries(request.billing.additionalFees || {}).filter(
|
|
1067
|
-
([_, fee]) => !fee.isOptional
|
|
1068
|
-
)
|
|
1069
|
-
),
|
|
1070
|
-
payments: request.preferences.enforcePaidAppointments ? request.form.payments : null,
|
|
1071
|
-
prepayments: request.preferences.enforcePaidAppointments ? request.form?.prepayments ?? null : null,
|
|
1072
|
-
charges: request.preferences.serviceSelector === "scheduler" ? request.form?.charges ?? null : null,
|
|
1073
|
-
client: await request.findClient(request.form.clientId),
|
|
1074
|
-
_hash: "aa.aa.aa",
|
|
1075
|
-
timeline: {
|
|
1076
|
-
scheduler: { userUid: request.sender._id, postedAt: today },
|
|
1077
|
-
doctor: { userUid: selectedDoctor },
|
|
1078
|
-
physAsst: selectedPhysAsst ? { userUid: selectedPhysAsst } : "none",
|
|
1079
|
-
get debtCollector() {
|
|
1080
|
-
if (request.preferences.enforcePaidAppointments)
|
|
1081
|
-
return [{ ...this.scheduler, collectedPayments: {} }];
|
|
1082
|
-
return null;
|
|
1083
|
-
}
|
|
1084
|
-
},
|
|
1085
|
-
createdAt: today
|
|
1086
|
-
});
|
|
1087
|
-
appointment._hash = request.createHash(omit(appointment, ["_hash"]));
|
|
1088
|
-
return {
|
|
1089
|
-
updates: {
|
|
1090
|
-
state: stateCopy,
|
|
1091
|
-
staffDetails: staffDetailsCopy
|
|
1092
|
-
},
|
|
1093
|
-
appointment: MutableAppointmentDetails.parse(appointment)
|
|
1094
|
-
};
|
|
1095
|
-
}
|
|
823
|
+
);
|
|
824
|
+
};
|
|
1096
825
|
|
|
1097
826
|
// src/functions/createNotifId.ts
|
|
1098
827
|
import { random } from "@wavy/fn";
|
|
1099
|
-
import { v4
|
|
828
|
+
import { v4 } from "uuid";
|
|
1100
829
|
function createNotifId(options) {
|
|
1101
|
-
let nid =
|
|
830
|
+
let nid = v4();
|
|
1102
831
|
if (options) {
|
|
1103
832
|
nid = Notification.shape._id.parse(
|
|
1104
833
|
[
|
|
@@ -1112,21 +841,17 @@ function createNotifId(options) {
|
|
|
1112
841
|
}
|
|
1113
842
|
export {
|
|
1114
843
|
AlphaNumeric,
|
|
1115
|
-
AppointmentDetails,
|
|
1116
|
-
AppointmentSeverity,
|
|
1117
844
|
COMPANY_USER_RELATIONSHIP,
|
|
845
|
+
ClientDetails,
|
|
1118
846
|
ClientForm,
|
|
1119
|
-
ClientIdentity,
|
|
1120
847
|
CompanyBilling,
|
|
1121
848
|
CompanyDetails,
|
|
1122
849
|
CompanyIdentity,
|
|
1123
850
|
CompanyIndustry,
|
|
1124
851
|
CompanyNotifications,
|
|
1125
852
|
CompanyPreferences,
|
|
1126
|
-
CompanyStaff,
|
|
853
|
+
CompanyStaff2 as CompanyStaff,
|
|
1127
854
|
CompanyState,
|
|
1128
|
-
CompanyUser2 as CompanyUser,
|
|
1129
|
-
CompanyUserSession,
|
|
1130
855
|
CreateCompanyForm,
|
|
1131
856
|
CreditCurrency,
|
|
1132
857
|
InviteResponse,
|
|
@@ -1134,13 +859,12 @@ export {
|
|
|
1134
859
|
MAX_COMPANY_PRICE_MODS,
|
|
1135
860
|
MedicalDetails,
|
|
1136
861
|
Medicine,
|
|
1137
|
-
MiniAppointmentDetails,
|
|
1138
|
-
MiniCompanyUser2 as MiniCompanyUser,
|
|
1139
862
|
MutableAppointmentDetails,
|
|
1140
863
|
MutableCompanyBilling,
|
|
1141
864
|
MutableCompanyDetails,
|
|
1142
865
|
MutableCompanyIdentity,
|
|
1143
866
|
MutableCompanyPreferences,
|
|
867
|
+
MutableCompanyStaff,
|
|
1144
868
|
Notification,
|
|
1145
869
|
PaymentDetails,
|
|
1146
870
|
PriceMod,
|
|
@@ -1148,8 +872,9 @@ export {
|
|
|
1148
872
|
PriceTag,
|
|
1149
873
|
Reason,
|
|
1150
874
|
Receipts,
|
|
1151
|
-
ScheduleAppointmentForm,
|
|
1152
875
|
StandardTime,
|
|
876
|
+
TaskDetails,
|
|
877
|
+
TaskSeverity,
|
|
1153
878
|
TicketNo,
|
|
1154
879
|
TierList,
|
|
1155
880
|
TimeLog,
|
|
@@ -1159,16 +884,12 @@ export {
|
|
|
1159
884
|
Vitals,
|
|
1160
885
|
acceptedCurrencies,
|
|
1161
886
|
adminRoles,
|
|
1162
|
-
appointmentDistAlgs,
|
|
1163
887
|
calcBalance,
|
|
1164
888
|
calcPriceMod,
|
|
1165
889
|
companyServiceSelectors,
|
|
1166
890
|
companyUserRoles,
|
|
1167
891
|
createNotifId,
|
|
1168
892
|
employeeRoles,
|
|
1169
|
-
findAvailableStaff,
|
|
1170
|
-
findConflictingPartners,
|
|
1171
|
-
findNextAvailableStaff,
|
|
1172
893
|
genders,
|
|
1173
894
|
getCompatibleRoles,
|
|
1174
895
|
getIncompatibleRoles,
|
|
@@ -1177,8 +898,8 @@ export {
|
|
|
1177
898
|
normalizeNidKey,
|
|
1178
899
|
parseClientName,
|
|
1179
900
|
paymentMethods,
|
|
1180
|
-
scheduleAppointment,
|
|
1181
901
|
serviceProviders,
|
|
902
|
+
taskDistAlgs,
|
|
1182
903
|
tiers,
|
|
1183
904
|
vitalKeys
|
|
1184
905
|
};
|