@blackcode_sa/metaestetics-api 1.12.59 → 1.12.61

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 (267) hide show
  1. package/dist/admin/index.d.mts +36 -4
  2. package/dist/admin/index.d.ts +36 -4
  3. package/dist/admin/index.js +147 -26
  4. package/dist/admin/index.mjs +147 -26
  5. package/dist/index.d.mts +9 -1
  6. package/dist/index.d.ts +9 -1
  7. package/package.json +119 -119
  8. package/src/__mocks__/firstore.ts +10 -10
  9. package/src/admin/aggregation/README.md +79 -79
  10. package/src/admin/aggregation/appointment/README.md +128 -128
  11. package/src/admin/aggregation/appointment/appointment.aggregation.service.ts +1844 -1689
  12. package/src/admin/aggregation/appointment/index.ts +1 -1
  13. package/src/admin/aggregation/clinic/README.md +52 -52
  14. package/src/admin/aggregation/clinic/clinic.aggregation.service.ts +703 -703
  15. package/src/admin/aggregation/clinic/index.ts +1 -1
  16. package/src/admin/aggregation/forms/README.md +13 -13
  17. package/src/admin/aggregation/forms/filled-forms.aggregation.service.ts +322 -322
  18. package/src/admin/aggregation/forms/index.ts +1 -1
  19. package/src/admin/aggregation/index.ts +8 -8
  20. package/src/admin/aggregation/patient/README.md +27 -27
  21. package/src/admin/aggregation/patient/index.ts +1 -1
  22. package/src/admin/aggregation/patient/patient.aggregation.service.ts +141 -141
  23. package/src/admin/aggregation/practitioner/README.md +42 -42
  24. package/src/admin/aggregation/practitioner/index.ts +1 -1
  25. package/src/admin/aggregation/practitioner/practitioner.aggregation.service.ts +433 -433
  26. package/src/admin/aggregation/practitioner-invite/index.ts +1 -1
  27. package/src/admin/aggregation/practitioner-invite/practitioner-invite.aggregation.service.ts +961 -961
  28. package/src/admin/aggregation/procedure/README.md +43 -43
  29. package/src/admin/aggregation/procedure/index.ts +1 -1
  30. package/src/admin/aggregation/procedure/procedure.aggregation.service.ts +702 -702
  31. package/src/admin/aggregation/reviews/index.ts +1 -1
  32. package/src/admin/aggregation/reviews/reviews.aggregation.service.ts +641 -641
  33. package/src/admin/booking/README.md +125 -125
  34. package/src/admin/booking/booking.admin.ts +1037 -1037
  35. package/src/admin/booking/booking.calculator.ts +712 -712
  36. package/src/admin/booking/booking.types.ts +59 -59
  37. package/src/admin/booking/index.ts +3 -3
  38. package/src/admin/booking/timezones-problem.md +185 -185
  39. package/src/admin/calendar/README.md +7 -7
  40. package/src/admin/calendar/calendar.admin.service.ts +345 -345
  41. package/src/admin/calendar/index.ts +1 -1
  42. package/src/admin/documentation-templates/document-manager.admin.ts +260 -260
  43. package/src/admin/documentation-templates/index.ts +1 -1
  44. package/src/admin/free-consultation/free-consultation-utils.admin.ts +148 -148
  45. package/src/admin/free-consultation/index.ts +1 -1
  46. package/src/admin/index.ts +75 -75
  47. package/src/admin/logger/index.ts +78 -78
  48. package/src/admin/mailing/README.md +95 -95
  49. package/src/admin/mailing/appointment/appointment.mailing.service.ts +732 -732
  50. package/src/admin/mailing/appointment/index.ts +1 -1
  51. package/src/admin/mailing/appointment/templates/patient/appointment-confirmed.html +40 -40
  52. package/src/admin/mailing/base.mailing.service.ts +208 -208
  53. package/src/admin/mailing/index.ts +3 -3
  54. package/src/admin/mailing/practitionerInvite/existing-practitioner-invite.mailing.ts +611 -611
  55. package/src/admin/mailing/practitionerInvite/index.ts +2 -2
  56. package/src/admin/mailing/practitionerInvite/practitionerInvite.mailing.ts +395 -395
  57. package/src/admin/mailing/practitionerInvite/templates/existing-practitioner-invitation.template.ts +155 -155
  58. package/src/admin/mailing/practitionerInvite/templates/invitation.template.ts +101 -101
  59. package/src/admin/mailing/practitionerInvite/templates/invite-accepted-notification.template.ts +228 -228
  60. package/src/admin/mailing/practitionerInvite/templates/invite-rejected-notification.template.ts +242 -242
  61. package/src/admin/notifications/index.ts +1 -1
  62. package/src/admin/notifications/notifications.admin.ts +710 -710
  63. package/src/admin/requirements/README.md +128 -128
  64. package/src/admin/requirements/index.ts +1 -1
  65. package/src/admin/requirements/patient-requirements.admin.service.ts +475 -475
  66. package/src/admin/users/index.ts +1 -1
  67. package/src/admin/users/user-profile.admin.ts +405 -405
  68. package/src/backoffice/constants/certification.constants.ts +13 -13
  69. package/src/backoffice/constants/index.ts +1 -1
  70. package/src/backoffice/errors/backoffice.errors.ts +181 -181
  71. package/src/backoffice/errors/index.ts +1 -1
  72. package/src/backoffice/expo-safe/README.md +26 -26
  73. package/src/backoffice/expo-safe/index.ts +41 -41
  74. package/src/backoffice/index.ts +5 -5
  75. package/src/backoffice/services/FIXES_README.md +102 -102
  76. package/src/backoffice/services/README.md +40 -40
  77. package/src/backoffice/services/brand.service.ts +256 -256
  78. package/src/backoffice/services/category.service.ts +318 -318
  79. package/src/backoffice/services/constants.service.ts +385 -385
  80. package/src/backoffice/services/documentation-template.service.ts +202 -202
  81. package/src/backoffice/services/index.ts +8 -8
  82. package/src/backoffice/services/migrate-products.ts +116 -116
  83. package/src/backoffice/services/product.service.ts +553 -553
  84. package/src/backoffice/services/requirement.service.ts +235 -235
  85. package/src/backoffice/services/subcategory.service.ts +395 -395
  86. package/src/backoffice/services/technology.service.ts +1070 -1070
  87. package/src/backoffice/types/README.md +12 -12
  88. package/src/backoffice/types/admin-constants.types.ts +69 -69
  89. package/src/backoffice/types/brand.types.ts +29 -29
  90. package/src/backoffice/types/category.types.ts +62 -62
  91. package/src/backoffice/types/documentation-templates.types.ts +28 -28
  92. package/src/backoffice/types/index.ts +10 -10
  93. package/src/backoffice/types/procedure-product.types.ts +38 -38
  94. package/src/backoffice/types/product.types.ts +240 -240
  95. package/src/backoffice/types/requirement.types.ts +63 -63
  96. package/src/backoffice/types/static/README.md +18 -18
  97. package/src/backoffice/types/static/blocking-condition.types.ts +21 -21
  98. package/src/backoffice/types/static/certification.types.ts +37 -37
  99. package/src/backoffice/types/static/contraindication.types.ts +19 -19
  100. package/src/backoffice/types/static/index.ts +6 -6
  101. package/src/backoffice/types/static/pricing.types.ts +16 -16
  102. package/src/backoffice/types/static/procedure-family.types.ts +14 -14
  103. package/src/backoffice/types/static/treatment-benefit.types.ts +22 -22
  104. package/src/backoffice/types/subcategory.types.ts +34 -34
  105. package/src/backoffice/types/technology.types.ts +161 -161
  106. package/src/backoffice/validations/index.ts +1 -1
  107. package/src/backoffice/validations/schemas.ts +163 -163
  108. package/src/config/__mocks__/firebase.ts +99 -99
  109. package/src/config/firebase.ts +78 -78
  110. package/src/config/index.ts +9 -9
  111. package/src/errors/auth.error.ts +6 -6
  112. package/src/errors/auth.errors.ts +200 -200
  113. package/src/errors/clinic.errors.ts +32 -32
  114. package/src/errors/firebase.errors.ts +47 -47
  115. package/src/errors/user.errors.ts +99 -99
  116. package/src/index.backup.ts +407 -407
  117. package/src/index.ts +6 -6
  118. package/src/locales/en.ts +31 -31
  119. package/src/recommender/admin/index.ts +1 -1
  120. package/src/recommender/admin/services/recommender.service.admin.ts +5 -5
  121. package/src/recommender/front/index.ts +1 -1
  122. package/src/recommender/front/services/onboarding.service.ts +5 -5
  123. package/src/recommender/front/services/recommender.service.ts +3 -3
  124. package/src/recommender/index.ts +1 -1
  125. package/src/services/PATIENTAUTH.MD +197 -197
  126. package/src/services/README.md +106 -106
  127. package/src/services/__tests__/auth/auth.mock.test.ts +17 -17
  128. package/src/services/__tests__/auth/auth.setup.ts +293 -293
  129. package/src/services/__tests__/auth.service.test.ts +346 -346
  130. package/src/services/__tests__/base.service.test.ts +77 -77
  131. package/src/services/__tests__/user.service.test.ts +528 -528
  132. package/src/services/appointment/README.md +17 -17
  133. package/src/services/appointment/appointment.service.ts +2082 -2082
  134. package/src/services/appointment/index.ts +1 -1
  135. package/src/services/appointment/utils/appointment.utils.ts +552 -552
  136. package/src/services/appointment/utils/extended-procedure.utils.ts +314 -314
  137. package/src/services/appointment/utils/form-initialization.utils.ts +225 -225
  138. package/src/services/appointment/utils/recommended-procedure.utils.ts +195 -195
  139. package/src/services/appointment/utils/zone-management.utils.ts +353 -353
  140. package/src/services/appointment/utils/zone-photo.utils.ts +152 -152
  141. package/src/services/auth/auth.service.ts +989 -989
  142. package/src/services/auth/auth.v2.service.ts +961 -961
  143. package/src/services/auth/index.ts +7 -7
  144. package/src/services/auth/utils/error.utils.ts +90 -90
  145. package/src/services/auth/utils/firebase.utils.ts +49 -49
  146. package/src/services/auth/utils/index.ts +21 -21
  147. package/src/services/auth/utils/practitioner.utils.ts +125 -125
  148. package/src/services/base.service.ts +41 -41
  149. package/src/services/calendar/calendar.service.ts +1077 -1077
  150. package/src/services/calendar/calendar.v2.service.ts +1683 -1683
  151. package/src/services/calendar/calendar.v3.service.ts +313 -313
  152. package/src/services/calendar/externalCalendar.service.ts +178 -178
  153. package/src/services/calendar/index.ts +5 -5
  154. package/src/services/calendar/synced-calendars.service.ts +743 -743
  155. package/src/services/calendar/utils/appointment.utils.ts +265 -265
  156. package/src/services/calendar/utils/calendar-event.utils.ts +646 -646
  157. package/src/services/calendar/utils/clinic.utils.ts +237 -237
  158. package/src/services/calendar/utils/docs.utils.ts +157 -157
  159. package/src/services/calendar/utils/google-calendar.utils.ts +697 -697
  160. package/src/services/calendar/utils/index.ts +8 -8
  161. package/src/services/calendar/utils/patient.utils.ts +198 -198
  162. package/src/services/calendar/utils/practitioner.utils.ts +221 -221
  163. package/src/services/calendar/utils/synced-calendar.utils.ts +472 -472
  164. package/src/services/clinic/README.md +204 -204
  165. package/src/services/clinic/__tests__/clinic-admin.service.test.ts +287 -287
  166. package/src/services/clinic/__tests__/clinic-group.service.test.ts +352 -352
  167. package/src/services/clinic/__tests__/clinic.service.test.ts +354 -354
  168. package/src/services/clinic/billing-transactions.service.ts +217 -217
  169. package/src/services/clinic/clinic-admin.service.ts +202 -202
  170. package/src/services/clinic/clinic-group.service.ts +310 -310
  171. package/src/services/clinic/clinic.service.ts +708 -708
  172. package/src/services/clinic/index.ts +5 -5
  173. package/src/services/clinic/practitioner-invite.service.ts +519 -519
  174. package/src/services/clinic/utils/admin.utils.ts +551 -551
  175. package/src/services/clinic/utils/clinic-group.utils.ts +646 -646
  176. package/src/services/clinic/utils/clinic.utils.ts +949 -949
  177. package/src/services/clinic/utils/filter.utils.d.ts +23 -23
  178. package/src/services/clinic/utils/filter.utils.ts +446 -446
  179. package/src/services/clinic/utils/index.ts +11 -11
  180. package/src/services/clinic/utils/photos.utils.ts +188 -188
  181. package/src/services/clinic/utils/search.utils.ts +84 -84
  182. package/src/services/clinic/utils/tag.utils.ts +124 -124
  183. package/src/services/documentation-templates/documentation-template.service.ts +537 -537
  184. package/src/services/documentation-templates/filled-document.service.ts +587 -587
  185. package/src/services/documentation-templates/index.ts +2 -2
  186. package/src/services/index.ts +13 -13
  187. package/src/services/media/index.ts +1 -1
  188. package/src/services/media/media.service.ts +418 -418
  189. package/src/services/notifications/__tests__/notification.service.test.ts +242 -242
  190. package/src/services/notifications/index.ts +1 -1
  191. package/src/services/notifications/notification.service.ts +215 -215
  192. package/src/services/patient/README.md +48 -48
  193. package/src/services/patient/To-Do.md +43 -43
  194. package/src/services/patient/__tests__/patient.service.test.ts +294 -294
  195. package/src/services/patient/index.ts +2 -2
  196. package/src/services/patient/patient.service.ts +883 -883
  197. package/src/services/patient/patientRequirements.service.ts +285 -285
  198. package/src/services/patient/utils/aesthetic-analysis.utils.ts +176 -176
  199. package/src/services/patient/utils/clinic.utils.ts +80 -80
  200. package/src/services/patient/utils/docs.utils.ts +142 -142
  201. package/src/services/patient/utils/index.ts +9 -9
  202. package/src/services/patient/utils/location.utils.ts +126 -126
  203. package/src/services/patient/utils/medical-stuff.utils.ts +143 -143
  204. package/src/services/patient/utils/medical.utils.ts +458 -458
  205. package/src/services/patient/utils/practitioner.utils.ts +260 -260
  206. package/src/services/patient/utils/profile.utils.ts +510 -510
  207. package/src/services/patient/utils/sensitive.utils.ts +260 -260
  208. package/src/services/patient/utils/token.utils.ts +211 -211
  209. package/src/services/practitioner/README.md +145 -145
  210. package/src/services/practitioner/index.ts +1 -1
  211. package/src/services/practitioner/practitioner.service.ts +1742 -1742
  212. package/src/services/procedure/README.md +163 -163
  213. package/src/services/procedure/index.ts +1 -1
  214. package/src/services/procedure/procedure.service.ts +1682 -1682
  215. package/src/services/reviews/index.ts +1 -1
  216. package/src/services/reviews/reviews.service.ts +636 -636
  217. package/src/services/user/index.ts +1 -1
  218. package/src/services/user/user.service.ts +489 -489
  219. package/src/services/user/user.v2.service.ts +466 -466
  220. package/src/types/appointment/index.ts +453 -453
  221. package/src/types/calendar/index.ts +258 -258
  222. package/src/types/calendar/synced-calendar.types.ts +66 -66
  223. package/src/types/clinic/index.ts +489 -489
  224. package/src/types/clinic/practitioner-invite.types.ts +91 -91
  225. package/src/types/clinic/preferences.types.ts +159 -159
  226. package/src/types/clinic/to-do +3 -3
  227. package/src/types/documentation-templates/index.ts +308 -308
  228. package/src/types/index.ts +44 -44
  229. package/src/types/notifications/README.md +77 -77
  230. package/src/types/notifications/index.ts +265 -265
  231. package/src/types/patient/aesthetic-analysis.types.ts +66 -66
  232. package/src/types/patient/allergies.ts +58 -58
  233. package/src/types/patient/index.ts +273 -273
  234. package/src/types/patient/medical-info.types.ts +152 -152
  235. package/src/types/patient/patient-requirements.ts +92 -81
  236. package/src/types/patient/token.types.ts +61 -61
  237. package/src/types/practitioner/index.ts +206 -206
  238. package/src/types/procedure/index.ts +181 -181
  239. package/src/types/profile/index.ts +39 -39
  240. package/src/types/reviews/index.ts +130 -130
  241. package/src/types/tz-lookup.d.ts +4 -4
  242. package/src/types/user/index.ts +38 -38
  243. package/src/utils/TIMESTAMPS.md +176 -176
  244. package/src/utils/TimestampUtils.ts +241 -241
  245. package/src/utils/index.ts +1 -1
  246. package/src/validations/appointment.schema.ts +574 -574
  247. package/src/validations/calendar.schema.ts +225 -225
  248. package/src/validations/clinic.schema.ts +493 -493
  249. package/src/validations/common.schema.ts +25 -25
  250. package/src/validations/documentation-templates/index.ts +1 -1
  251. package/src/validations/documentation-templates/template.schema.ts +220 -220
  252. package/src/validations/documentation-templates.schema.ts +10 -10
  253. package/src/validations/index.ts +20 -20
  254. package/src/validations/media.schema.ts +10 -10
  255. package/src/validations/notification.schema.ts +90 -90
  256. package/src/validations/patient/aesthetic-analysis.schema.ts +55 -55
  257. package/src/validations/patient/medical-info.schema.ts +125 -125
  258. package/src/validations/patient/patient-requirements.schema.ts +84 -75
  259. package/src/validations/patient/token.schema.ts +29 -29
  260. package/src/validations/patient.schema.ts +216 -216
  261. package/src/validations/practitioner.schema.ts +222 -222
  262. package/src/validations/procedure-product.schema.ts +41 -41
  263. package/src/validations/procedure.schema.ts +124 -124
  264. package/src/validations/profile-info.schema.ts +41 -41
  265. package/src/validations/reviews.schema.ts +189 -189
  266. package/src/validations/schemas.ts +104 -104
  267. package/src/validations/shared.schema.ts +78 -78
@@ -1,353 +1,353 @@
1
- import { Firestore, getDoc, updateDoc, serverTimestamp } from 'firebase/firestore';
2
- import {
3
- ZoneItemData,
4
- AppointmentMetadata,
5
- FinalBilling,
6
- Appointment,
7
- APPOINTMENTS_COLLECTION,
8
- } from '../../../types/appointment';
9
- import { getAppointmentByIdUtil } from './appointment.utils';
10
- import { doc } from 'firebase/firestore';
11
-
12
- /**
13
- * Validates that a zone key follows the category.zone format
14
- * @param zoneKey Zone key to validate
15
- * @throws Error if format is invalid
16
- */
17
- export function validateZoneKeyFormat(zoneKey: string): void {
18
- const parts = zoneKey.split('.');
19
- if (parts.length !== 2) {
20
- throw new Error(
21
- `Invalid zone key format: "${zoneKey}". Must be "category.zone" (e.g., "face.forehead")`,
22
- );
23
- }
24
- }
25
-
26
- /**
27
- * Calculates subtotal for a zone item
28
- * @param item Zone item data
29
- * @returns Calculated subtotal
30
- */
31
- export function calculateItemSubtotal(item: Partial<ZoneItemData>): number {
32
- if (item.type === 'note') {
33
- return 0;
34
- }
35
-
36
- const quantity = item.quantity || 0;
37
-
38
- // If price override amount is set, use it as price per unit
39
- if (item.priceOverrideAmount !== undefined && item.priceOverrideAmount !== null) {
40
- return item.priceOverrideAmount * quantity;
41
- }
42
-
43
- // Calculate normally: price * quantity
44
- const price = item.price || 0;
45
- return price * quantity;
46
- }
47
-
48
- /**
49
- * Recalculates final billing based on all zone items
50
- * @param zonesData Zone items data
51
- * @param taxRate Tax rate (e.g., 0.20 for 20%)
52
- * @returns Calculated final billing
53
- */
54
- export function calculateFinalBilling(
55
- zonesData: Record<string, ZoneItemData[]>,
56
- taxRate: number = 0.081,
57
- ): FinalBilling {
58
- let subtotalAll = 0;
59
-
60
- // Sum up all zone items
61
- Object.values(zonesData).forEach(items => {
62
- items.forEach(item => {
63
- if (item.type === 'item' && item.subtotal) {
64
- subtotalAll += item.subtotal;
65
- }
66
- });
67
- });
68
-
69
- const taxPrice = subtotalAll * taxRate;
70
- const finalPrice = subtotalAll + taxPrice;
71
-
72
- // Get currency from first item (assuming all same currency)
73
- let currency: any = 'CHF'; // Default
74
-
75
- for (const items of Object.values(zonesData)) {
76
- const firstItem = items.find(i => i.type === 'item');
77
- if (firstItem) {
78
- currency = firstItem.currency || currency;
79
- break;
80
- }
81
- }
82
-
83
- return {
84
- subtotalAll,
85
- taxRate,
86
- taxPrice,
87
- finalPrice,
88
- currency,
89
- };
90
- }
91
-
92
- /**
93
- * Gets appointment and validates it exists
94
- * @param db Firestore instance
95
- * @param appointmentId Appointment ID
96
- * @returns Appointment document
97
- */
98
- export async function getAppointmentOrThrow(
99
- db: Firestore,
100
- appointmentId: string,
101
- ): Promise<Appointment> {
102
- const appointment = await getAppointmentByIdUtil(db, appointmentId);
103
- if (!appointment) {
104
- throw new Error(`Appointment with ID ${appointmentId} not found`);
105
- }
106
- return appointment;
107
- }
108
-
109
- /**
110
- * Initializes appointment metadata if it doesn't exist
111
- * @param appointment Appointment document
112
- * @returns Initialized metadata
113
- */
114
- export function initializeMetadata(appointment: Appointment): AppointmentMetadata {
115
- return (
116
- appointment.metadata || {
117
- selectedZones: null,
118
- zonePhotos: null,
119
- zonesData: null,
120
- appointmentProducts: [],
121
- extendedProcedures: [],
122
- recommendedProcedures: [],
123
- finalbilling: null,
124
- finalizationNotes: null,
125
- }
126
- );
127
- }
128
-
129
- /**
130
- * Adds an item to a specific zone
131
- * @param db Firestore instance
132
- * @param appointmentId Appointment ID
133
- * @param zoneId Zone ID (must be category.zone format)
134
- * @param item Zone item data to add (without parentZone)
135
- * @returns Updated appointment
136
- */
137
- export async function addItemToZoneUtil(
138
- db: Firestore,
139
- appointmentId: string,
140
- zoneId: string,
141
- item: Omit<ZoneItemData, 'subtotal' | 'parentZone'>,
142
- ): Promise<Appointment> {
143
- // Validate zone key format
144
- validateZoneKeyFormat(zoneId);
145
-
146
- // Get appointment
147
- const appointment = await getAppointmentOrThrow(db, appointmentId);
148
- const metadata = initializeMetadata(appointment);
149
-
150
- // Initialize zonesData if needed
151
- const zonesData = metadata.zonesData || {};
152
-
153
- // Initialize zone array if needed
154
- if (!zonesData[zoneId]) {
155
- zonesData[zoneId] = [];
156
- }
157
-
158
- // Calculate subtotal for the item
159
- const now = new Date().toISOString();
160
- const itemWithSubtotal: ZoneItemData = {
161
- ...item,
162
- parentZone: zoneId, // Set parentZone to the zone key
163
- subtotal: calculateItemSubtotal(item),
164
- createdAt: now,
165
- updatedAt: now,
166
- };
167
-
168
- // Add item to zone
169
- zonesData[zoneId].push(itemWithSubtotal);
170
-
171
- // Recalculate final billing with Swiss tax rate (8.1%)
172
- const finalbilling = calculateFinalBilling(zonesData, 0.081);
173
-
174
- // Update appointment
175
- const appointmentRef = doc(db, APPOINTMENTS_COLLECTION, appointmentId);
176
- await updateDoc(appointmentRef, {
177
- 'metadata.zonesData': zonesData,
178
- 'metadata.finalbilling': finalbilling,
179
- updatedAt: serverTimestamp(),
180
- });
181
-
182
- // Return updated appointment
183
- return getAppointmentOrThrow(db, appointmentId);
184
- }
185
-
186
- /**
187
- * Removes an item from a specific zone
188
- * @param db Firestore instance
189
- * @param appointmentId Appointment ID
190
- * @param zoneId Zone ID
191
- * @param itemIndex Index of item to remove
192
- * @returns Updated appointment
193
- */
194
- export async function removeItemFromZoneUtil(
195
- db: Firestore,
196
- appointmentId: string,
197
- zoneId: string,
198
- itemIndex: number,
199
- ): Promise<Appointment> {
200
- validateZoneKeyFormat(zoneId);
201
-
202
- const appointment = await getAppointmentOrThrow(db, appointmentId);
203
- const metadata = initializeMetadata(appointment);
204
-
205
- if (!metadata.zonesData || !metadata.zonesData[zoneId]) {
206
- throw new Error(`No items found for zone ${zoneId}`);
207
- }
208
-
209
- const items = metadata.zonesData[zoneId];
210
- if (itemIndex < 0 || itemIndex >= items.length) {
211
- throw new Error(`Invalid item index ${itemIndex} for zone ${zoneId}`);
212
- }
213
-
214
- // Remove item
215
- items.splice(itemIndex, 1);
216
-
217
- // If zone is now empty, remove it
218
- if (items.length === 0) {
219
- delete metadata.zonesData[zoneId];
220
- }
221
-
222
- // Recalculate final billing with Swiss tax rate (8.1%)
223
- const finalbilling = calculateFinalBilling(metadata.zonesData, 0.081);
224
-
225
- // Update appointment
226
- const appointmentRef = doc(db, APPOINTMENTS_COLLECTION, appointmentId);
227
- await updateDoc(appointmentRef, {
228
- 'metadata.zonesData': metadata.zonesData,
229
- 'metadata.finalbilling': finalbilling,
230
- updatedAt: serverTimestamp(),
231
- });
232
-
233
- return getAppointmentOrThrow(db, appointmentId);
234
- }
235
-
236
- /**
237
- * Updates a specific item in a zone
238
- * @param db Firestore instance
239
- * @param appointmentId Appointment ID
240
- * @param zoneId Zone ID
241
- * @param itemIndex Index of item to update
242
- * @param updates Partial updates to apply
243
- * @returns Updated appointment
244
- */
245
- export async function updateZoneItemUtil(
246
- db: Firestore,
247
- appointmentId: string,
248
- zoneId: string,
249
- itemIndex: number,
250
- updates: Partial<ZoneItemData>,
251
- ): Promise<Appointment> {
252
- validateZoneKeyFormat(zoneId);
253
-
254
- const appointment = await getAppointmentOrThrow(db, appointmentId);
255
- const metadata = initializeMetadata(appointment);
256
-
257
- if (!metadata.zonesData || !metadata.zonesData[zoneId]) {
258
- throw new Error(`No items found for zone ${zoneId}`);
259
- }
260
-
261
- const items = metadata.zonesData[zoneId];
262
- if (itemIndex < 0 || itemIndex >= items.length) {
263
- throw new Error(`Invalid item index ${itemIndex} for zone ${zoneId}`);
264
- }
265
-
266
- // Update item with updatedAt timestamp
267
- items[itemIndex] = {
268
- ...items[itemIndex],
269
- ...updates,
270
- updatedAt: new Date().toISOString(),
271
- };
272
-
273
- console.log(`[updateZoneItemUtil] BEFORE recalculation:`, {
274
- itemIndex,
275
- quantity: items[itemIndex].quantity,
276
- priceOverrideAmount: items[itemIndex].priceOverrideAmount,
277
- price: items[itemIndex].price,
278
- oldSubtotal: items[itemIndex].subtotal,
279
- });
280
-
281
- // Recalculate subtotal for this item
282
- items[itemIndex].subtotal = calculateItemSubtotal(items[itemIndex]);
283
-
284
- console.log(`[updateZoneItemUtil] AFTER recalculation:`, {
285
- itemIndex,
286
- newSubtotal: items[itemIndex].subtotal,
287
- });
288
-
289
- // Recalculate final billing with Swiss tax rate (8.1%)
290
- const finalbilling = calculateFinalBilling(metadata.zonesData, 0.081);
291
-
292
- // Update appointment
293
- const appointmentRef = doc(db, APPOINTMENTS_COLLECTION, appointmentId);
294
- await updateDoc(appointmentRef, {
295
- 'metadata.zonesData': metadata.zonesData,
296
- 'metadata.finalbilling': finalbilling,
297
- updatedAt: serverTimestamp(),
298
- });
299
-
300
- return getAppointmentOrThrow(db, appointmentId);
301
- }
302
-
303
- /**
304
- * Overrides price for a specific zone item
305
- * @param db Firestore instance
306
- * @param appointmentId Appointment ID
307
- * @param zoneId Zone ID
308
- * @param itemIndex Index of item
309
- * @param newPrice New price amount
310
- * @returns Updated appointment
311
- */
312
- export async function overridePriceForZoneItemUtil(
313
- db: Firestore,
314
- appointmentId: string,
315
- zoneId: string,
316
- itemIndex: number,
317
- newPrice: number,
318
- ): Promise<Appointment> {
319
- return updateZoneItemUtil(db, appointmentId, zoneId, itemIndex, {
320
- priceOverrideAmount: newPrice,
321
- });
322
- }
323
-
324
- /**
325
- * Updates subzones for a specific zone item
326
- * @param db Firestore instance
327
- * @param appointmentId Appointment ID
328
- * @param zoneId Zone ID
329
- * @param itemIndex Index of item
330
- * @param subzones Array of subzone keys (empty array = entire zone)
331
- * @returns Updated appointment
332
- */
333
- export async function updateSubzonesUtil(
334
- db: Firestore,
335
- appointmentId: string,
336
- zoneId: string,
337
- itemIndex: number,
338
- subzones: string[],
339
- ): Promise<Appointment> {
340
- // Validate subzone format if provided
341
- subzones.forEach(subzone => {
342
- const parts = subzone.split('.');
343
- if (parts.length !== 3) {
344
- throw new Error(
345
- `Invalid subzone format: "${subzone}". Must be "category.zone.subzone" (e.g., "face.forehead.left")`,
346
- );
347
- }
348
- });
349
-
350
- return updateZoneItemUtil(db, appointmentId, zoneId, itemIndex, {
351
- subzones,
352
- });
353
- }
1
+ import { Firestore, getDoc, updateDoc, serverTimestamp } from 'firebase/firestore';
2
+ import {
3
+ ZoneItemData,
4
+ AppointmentMetadata,
5
+ FinalBilling,
6
+ Appointment,
7
+ APPOINTMENTS_COLLECTION,
8
+ } from '../../../types/appointment';
9
+ import { getAppointmentByIdUtil } from './appointment.utils';
10
+ import { doc } from 'firebase/firestore';
11
+
12
+ /**
13
+ * Validates that a zone key follows the category.zone format
14
+ * @param zoneKey Zone key to validate
15
+ * @throws Error if format is invalid
16
+ */
17
+ export function validateZoneKeyFormat(zoneKey: string): void {
18
+ const parts = zoneKey.split('.');
19
+ if (parts.length !== 2) {
20
+ throw new Error(
21
+ `Invalid zone key format: "${zoneKey}". Must be "category.zone" (e.g., "face.forehead")`,
22
+ );
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Calculates subtotal for a zone item
28
+ * @param item Zone item data
29
+ * @returns Calculated subtotal
30
+ */
31
+ export function calculateItemSubtotal(item: Partial<ZoneItemData>): number {
32
+ if (item.type === 'note') {
33
+ return 0;
34
+ }
35
+
36
+ const quantity = item.quantity || 0;
37
+
38
+ // If price override amount is set, use it as price per unit
39
+ if (item.priceOverrideAmount !== undefined && item.priceOverrideAmount !== null) {
40
+ return item.priceOverrideAmount * quantity;
41
+ }
42
+
43
+ // Calculate normally: price * quantity
44
+ const price = item.price || 0;
45
+ return price * quantity;
46
+ }
47
+
48
+ /**
49
+ * Recalculates final billing based on all zone items
50
+ * @param zonesData Zone items data
51
+ * @param taxRate Tax rate (e.g., 0.20 for 20%)
52
+ * @returns Calculated final billing
53
+ */
54
+ export function calculateFinalBilling(
55
+ zonesData: Record<string, ZoneItemData[]>,
56
+ taxRate: number = 0.081,
57
+ ): FinalBilling {
58
+ let subtotalAll = 0;
59
+
60
+ // Sum up all zone items
61
+ Object.values(zonesData).forEach(items => {
62
+ items.forEach(item => {
63
+ if (item.type === 'item' && item.subtotal) {
64
+ subtotalAll += item.subtotal;
65
+ }
66
+ });
67
+ });
68
+
69
+ const taxPrice = subtotalAll * taxRate;
70
+ const finalPrice = subtotalAll + taxPrice;
71
+
72
+ // Get currency from first item (assuming all same currency)
73
+ let currency: any = 'CHF'; // Default
74
+
75
+ for (const items of Object.values(zonesData)) {
76
+ const firstItem = items.find(i => i.type === 'item');
77
+ if (firstItem) {
78
+ currency = firstItem.currency || currency;
79
+ break;
80
+ }
81
+ }
82
+
83
+ return {
84
+ subtotalAll,
85
+ taxRate,
86
+ taxPrice,
87
+ finalPrice,
88
+ currency,
89
+ };
90
+ }
91
+
92
+ /**
93
+ * Gets appointment and validates it exists
94
+ * @param db Firestore instance
95
+ * @param appointmentId Appointment ID
96
+ * @returns Appointment document
97
+ */
98
+ export async function getAppointmentOrThrow(
99
+ db: Firestore,
100
+ appointmentId: string,
101
+ ): Promise<Appointment> {
102
+ const appointment = await getAppointmentByIdUtil(db, appointmentId);
103
+ if (!appointment) {
104
+ throw new Error(`Appointment with ID ${appointmentId} not found`);
105
+ }
106
+ return appointment;
107
+ }
108
+
109
+ /**
110
+ * Initializes appointment metadata if it doesn't exist
111
+ * @param appointment Appointment document
112
+ * @returns Initialized metadata
113
+ */
114
+ export function initializeMetadata(appointment: Appointment): AppointmentMetadata {
115
+ return (
116
+ appointment.metadata || {
117
+ selectedZones: null,
118
+ zonePhotos: null,
119
+ zonesData: null,
120
+ appointmentProducts: [],
121
+ extendedProcedures: [],
122
+ recommendedProcedures: [],
123
+ finalbilling: null,
124
+ finalizationNotes: null,
125
+ }
126
+ );
127
+ }
128
+
129
+ /**
130
+ * Adds an item to a specific zone
131
+ * @param db Firestore instance
132
+ * @param appointmentId Appointment ID
133
+ * @param zoneId Zone ID (must be category.zone format)
134
+ * @param item Zone item data to add (without parentZone)
135
+ * @returns Updated appointment
136
+ */
137
+ export async function addItemToZoneUtil(
138
+ db: Firestore,
139
+ appointmentId: string,
140
+ zoneId: string,
141
+ item: Omit<ZoneItemData, 'subtotal' | 'parentZone'>,
142
+ ): Promise<Appointment> {
143
+ // Validate zone key format
144
+ validateZoneKeyFormat(zoneId);
145
+
146
+ // Get appointment
147
+ const appointment = await getAppointmentOrThrow(db, appointmentId);
148
+ const metadata = initializeMetadata(appointment);
149
+
150
+ // Initialize zonesData if needed
151
+ const zonesData = metadata.zonesData || {};
152
+
153
+ // Initialize zone array if needed
154
+ if (!zonesData[zoneId]) {
155
+ zonesData[zoneId] = [];
156
+ }
157
+
158
+ // Calculate subtotal for the item
159
+ const now = new Date().toISOString();
160
+ const itemWithSubtotal: ZoneItemData = {
161
+ ...item,
162
+ parentZone: zoneId, // Set parentZone to the zone key
163
+ subtotal: calculateItemSubtotal(item),
164
+ createdAt: now,
165
+ updatedAt: now,
166
+ };
167
+
168
+ // Add item to zone
169
+ zonesData[zoneId].push(itemWithSubtotal);
170
+
171
+ // Recalculate final billing with Swiss tax rate (8.1%)
172
+ const finalbilling = calculateFinalBilling(zonesData, 0.081);
173
+
174
+ // Update appointment
175
+ const appointmentRef = doc(db, APPOINTMENTS_COLLECTION, appointmentId);
176
+ await updateDoc(appointmentRef, {
177
+ 'metadata.zonesData': zonesData,
178
+ 'metadata.finalbilling': finalbilling,
179
+ updatedAt: serverTimestamp(),
180
+ });
181
+
182
+ // Return updated appointment
183
+ return getAppointmentOrThrow(db, appointmentId);
184
+ }
185
+
186
+ /**
187
+ * Removes an item from a specific zone
188
+ * @param db Firestore instance
189
+ * @param appointmentId Appointment ID
190
+ * @param zoneId Zone ID
191
+ * @param itemIndex Index of item to remove
192
+ * @returns Updated appointment
193
+ */
194
+ export async function removeItemFromZoneUtil(
195
+ db: Firestore,
196
+ appointmentId: string,
197
+ zoneId: string,
198
+ itemIndex: number,
199
+ ): Promise<Appointment> {
200
+ validateZoneKeyFormat(zoneId);
201
+
202
+ const appointment = await getAppointmentOrThrow(db, appointmentId);
203
+ const metadata = initializeMetadata(appointment);
204
+
205
+ if (!metadata.zonesData || !metadata.zonesData[zoneId]) {
206
+ throw new Error(`No items found for zone ${zoneId}`);
207
+ }
208
+
209
+ const items = metadata.zonesData[zoneId];
210
+ if (itemIndex < 0 || itemIndex >= items.length) {
211
+ throw new Error(`Invalid item index ${itemIndex} for zone ${zoneId}`);
212
+ }
213
+
214
+ // Remove item
215
+ items.splice(itemIndex, 1);
216
+
217
+ // If zone is now empty, remove it
218
+ if (items.length === 0) {
219
+ delete metadata.zonesData[zoneId];
220
+ }
221
+
222
+ // Recalculate final billing with Swiss tax rate (8.1%)
223
+ const finalbilling = calculateFinalBilling(metadata.zonesData, 0.081);
224
+
225
+ // Update appointment
226
+ const appointmentRef = doc(db, APPOINTMENTS_COLLECTION, appointmentId);
227
+ await updateDoc(appointmentRef, {
228
+ 'metadata.zonesData': metadata.zonesData,
229
+ 'metadata.finalbilling': finalbilling,
230
+ updatedAt: serverTimestamp(),
231
+ });
232
+
233
+ return getAppointmentOrThrow(db, appointmentId);
234
+ }
235
+
236
+ /**
237
+ * Updates a specific item in a zone
238
+ * @param db Firestore instance
239
+ * @param appointmentId Appointment ID
240
+ * @param zoneId Zone ID
241
+ * @param itemIndex Index of item to update
242
+ * @param updates Partial updates to apply
243
+ * @returns Updated appointment
244
+ */
245
+ export async function updateZoneItemUtil(
246
+ db: Firestore,
247
+ appointmentId: string,
248
+ zoneId: string,
249
+ itemIndex: number,
250
+ updates: Partial<ZoneItemData>,
251
+ ): Promise<Appointment> {
252
+ validateZoneKeyFormat(zoneId);
253
+
254
+ const appointment = await getAppointmentOrThrow(db, appointmentId);
255
+ const metadata = initializeMetadata(appointment);
256
+
257
+ if (!metadata.zonesData || !metadata.zonesData[zoneId]) {
258
+ throw new Error(`No items found for zone ${zoneId}`);
259
+ }
260
+
261
+ const items = metadata.zonesData[zoneId];
262
+ if (itemIndex < 0 || itemIndex >= items.length) {
263
+ throw new Error(`Invalid item index ${itemIndex} for zone ${zoneId}`);
264
+ }
265
+
266
+ // Update item with updatedAt timestamp
267
+ items[itemIndex] = {
268
+ ...items[itemIndex],
269
+ ...updates,
270
+ updatedAt: new Date().toISOString(),
271
+ };
272
+
273
+ console.log(`[updateZoneItemUtil] BEFORE recalculation:`, {
274
+ itemIndex,
275
+ quantity: items[itemIndex].quantity,
276
+ priceOverrideAmount: items[itemIndex].priceOverrideAmount,
277
+ price: items[itemIndex].price,
278
+ oldSubtotal: items[itemIndex].subtotal,
279
+ });
280
+
281
+ // Recalculate subtotal for this item
282
+ items[itemIndex].subtotal = calculateItemSubtotal(items[itemIndex]);
283
+
284
+ console.log(`[updateZoneItemUtil] AFTER recalculation:`, {
285
+ itemIndex,
286
+ newSubtotal: items[itemIndex].subtotal,
287
+ });
288
+
289
+ // Recalculate final billing with Swiss tax rate (8.1%)
290
+ const finalbilling = calculateFinalBilling(metadata.zonesData, 0.081);
291
+
292
+ // Update appointment
293
+ const appointmentRef = doc(db, APPOINTMENTS_COLLECTION, appointmentId);
294
+ await updateDoc(appointmentRef, {
295
+ 'metadata.zonesData': metadata.zonesData,
296
+ 'metadata.finalbilling': finalbilling,
297
+ updatedAt: serverTimestamp(),
298
+ });
299
+
300
+ return getAppointmentOrThrow(db, appointmentId);
301
+ }
302
+
303
+ /**
304
+ * Overrides price for a specific zone item
305
+ * @param db Firestore instance
306
+ * @param appointmentId Appointment ID
307
+ * @param zoneId Zone ID
308
+ * @param itemIndex Index of item
309
+ * @param newPrice New price amount
310
+ * @returns Updated appointment
311
+ */
312
+ export async function overridePriceForZoneItemUtil(
313
+ db: Firestore,
314
+ appointmentId: string,
315
+ zoneId: string,
316
+ itemIndex: number,
317
+ newPrice: number,
318
+ ): Promise<Appointment> {
319
+ return updateZoneItemUtil(db, appointmentId, zoneId, itemIndex, {
320
+ priceOverrideAmount: newPrice,
321
+ });
322
+ }
323
+
324
+ /**
325
+ * Updates subzones for a specific zone item
326
+ * @param db Firestore instance
327
+ * @param appointmentId Appointment ID
328
+ * @param zoneId Zone ID
329
+ * @param itemIndex Index of item
330
+ * @param subzones Array of subzone keys (empty array = entire zone)
331
+ * @returns Updated appointment
332
+ */
333
+ export async function updateSubzonesUtil(
334
+ db: Firestore,
335
+ appointmentId: string,
336
+ zoneId: string,
337
+ itemIndex: number,
338
+ subzones: string[],
339
+ ): Promise<Appointment> {
340
+ // Validate subzone format if provided
341
+ subzones.forEach(subzone => {
342
+ const parts = subzone.split('.');
343
+ if (parts.length !== 3) {
344
+ throw new Error(
345
+ `Invalid subzone format: "${subzone}". Must be "category.zone.subzone" (e.g., "face.forehead.left")`,
346
+ );
347
+ }
348
+ });
349
+
350
+ return updateZoneItemUtil(db, appointmentId, zoneId, itemIndex, {
351
+ subzones,
352
+ });
353
+ }