@blackcode_sa/metaestetics-api 1.12.62 → 1.12.64
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/admin/index.d.mts +4 -2
- package/dist/admin/index.d.ts +4 -2
- package/dist/admin/index.js +4 -45
- package/dist/admin/index.mjs +4 -45
- package/dist/backoffice/index.d.mts +86 -1
- package/dist/backoffice/index.d.ts +86 -1
- package/dist/backoffice/index.js +308 -0
- package/dist/backoffice/index.mjs +306 -0
- package/dist/index.d.mts +99 -3
- package/dist/index.d.ts +99 -3
- package/dist/index.js +545 -281
- package/dist/index.mjs +867 -603
- package/package.json +119 -119
- package/src/__mocks__/firstore.ts +10 -10
- package/src/admin/aggregation/README.md +79 -79
- package/src/admin/aggregation/appointment/README.md +128 -128
- package/src/admin/aggregation/appointment/appointment.aggregation.service.ts +1844 -1844
- package/src/admin/aggregation/appointment/index.ts +1 -1
- package/src/admin/aggregation/clinic/README.md +52 -52
- package/src/admin/aggregation/clinic/clinic.aggregation.service.ts +703 -703
- package/src/admin/aggregation/clinic/index.ts +1 -1
- package/src/admin/aggregation/forms/README.md +13 -13
- package/src/admin/aggregation/forms/filled-forms.aggregation.service.ts +322 -322
- package/src/admin/aggregation/forms/index.ts +1 -1
- package/src/admin/aggregation/index.ts +8 -8
- package/src/admin/aggregation/patient/README.md +27 -27
- package/src/admin/aggregation/patient/index.ts +1 -1
- package/src/admin/aggregation/patient/patient.aggregation.service.ts +141 -141
- package/src/admin/aggregation/practitioner/README.md +42 -42
- package/src/admin/aggregation/practitioner/index.ts +1 -1
- package/src/admin/aggregation/practitioner/practitioner.aggregation.service.ts +433 -433
- package/src/admin/aggregation/practitioner-invite/index.ts +1 -1
- package/src/admin/aggregation/practitioner-invite/practitioner-invite.aggregation.service.ts +961 -961
- package/src/admin/aggregation/procedure/README.md +43 -43
- package/src/admin/aggregation/procedure/index.ts +1 -1
- package/src/admin/aggregation/procedure/procedure.aggregation.service.ts +702 -702
- package/src/admin/aggregation/reviews/index.ts +1 -1
- package/src/admin/aggregation/reviews/reviews.aggregation.service.ts +641 -689
- package/src/admin/booking/README.md +125 -125
- package/src/admin/booking/booking.admin.ts +1037 -1037
- package/src/admin/booking/booking.calculator.ts +712 -712
- package/src/admin/booking/booking.types.ts +59 -59
- package/src/admin/booking/index.ts +3 -3
- package/src/admin/booking/timezones-problem.md +185 -185
- package/src/admin/calendar/README.md +7 -7
- package/src/admin/calendar/calendar.admin.service.ts +345 -345
- package/src/admin/calendar/index.ts +1 -1
- package/src/admin/documentation-templates/document-manager.admin.ts +260 -260
- package/src/admin/documentation-templates/index.ts +1 -1
- package/src/admin/free-consultation/free-consultation-utils.admin.ts +148 -148
- package/src/admin/free-consultation/index.ts +1 -1
- package/src/admin/index.ts +75 -75
- package/src/admin/logger/index.ts +78 -78
- package/src/admin/mailing/README.md +95 -95
- package/src/admin/mailing/appointment/appointment.mailing.service.ts +732 -732
- package/src/admin/mailing/appointment/index.ts +1 -1
- package/src/admin/mailing/appointment/templates/patient/appointment-confirmed.html +40 -40
- package/src/admin/mailing/base.mailing.service.ts +208 -208
- package/src/admin/mailing/index.ts +3 -3
- package/src/admin/mailing/practitionerInvite/existing-practitioner-invite.mailing.ts +611 -611
- package/src/admin/mailing/practitionerInvite/index.ts +2 -2
- package/src/admin/mailing/practitionerInvite/practitionerInvite.mailing.ts +395 -395
- package/src/admin/mailing/practitionerInvite/templates/existing-practitioner-invitation.template.ts +155 -155
- package/src/admin/mailing/practitionerInvite/templates/invitation.template.ts +101 -101
- package/src/admin/mailing/practitionerInvite/templates/invite-accepted-notification.template.ts +228 -228
- package/src/admin/mailing/practitionerInvite/templates/invite-rejected-notification.template.ts +242 -242
- package/src/admin/notifications/index.ts +1 -1
- package/src/admin/notifications/notifications.admin.ts +710 -710
- package/src/admin/requirements/README.md +128 -128
- package/src/admin/requirements/index.ts +1 -1
- package/src/admin/requirements/patient-requirements.admin.service.ts +475 -475
- package/src/admin/users/index.ts +1 -1
- package/src/admin/users/user-profile.admin.ts +405 -405
- package/src/backoffice/constants/certification.constants.ts +13 -13
- package/src/backoffice/constants/index.ts +1 -1
- package/src/backoffice/errors/backoffice.errors.ts +181 -181
- package/src/backoffice/errors/index.ts +1 -1
- package/src/backoffice/expo-safe/README.md +26 -26
- package/src/backoffice/expo-safe/index.ts +41 -41
- package/src/backoffice/index.ts +5 -5
- package/src/backoffice/services/FIXES_README.md +102 -102
- package/src/backoffice/services/README.md +40 -40
- package/src/backoffice/services/brand.service.ts +256 -256
- package/src/backoffice/services/category.service.ts +318 -318
- package/src/backoffice/services/constants.service.ts +385 -385
- package/src/backoffice/services/documentation-template.service.ts +202 -202
- package/src/backoffice/services/index.ts +11 -8
- package/src/backoffice/services/migrate-products.ts +116 -116
- package/src/backoffice/services/product.service.ts +553 -553
- package/src/backoffice/services/requirement.service.ts +235 -235
- package/src/backoffice/services/subcategory.service.ts +395 -395
- package/src/backoffice/services/technology.service.ts +1083 -1070
- package/src/backoffice/types/README.md +12 -12
- package/src/backoffice/types/admin-constants.types.ts +69 -69
- package/src/backoffice/types/brand.types.ts +29 -29
- package/src/backoffice/types/category.types.ts +62 -62
- package/src/backoffice/types/documentation-templates.types.ts +28 -28
- package/src/backoffice/types/index.ts +10 -10
- package/src/backoffice/types/procedure-product.types.ts +38 -38
- package/src/backoffice/types/product.types.ts +240 -240
- package/src/backoffice/types/requirement.types.ts +63 -63
- package/src/backoffice/types/static/README.md +18 -18
- package/src/backoffice/types/static/blocking-condition.types.ts +21 -21
- package/src/backoffice/types/static/certification.types.ts +37 -37
- package/src/backoffice/types/static/contraindication.types.ts +19 -19
- package/src/backoffice/types/static/index.ts +6 -6
- package/src/backoffice/types/static/pricing.types.ts +16 -16
- package/src/backoffice/types/static/procedure-family.types.ts +14 -14
- package/src/backoffice/types/static/treatment-benefit.types.ts +22 -22
- package/src/backoffice/types/subcategory.types.ts +34 -34
- package/src/backoffice/types/technology.types.ts +163 -161
- package/src/backoffice/validations/index.ts +1 -1
- package/src/backoffice/validations/schemas.ts +164 -163
- package/src/config/__mocks__/firebase.ts +99 -99
- package/src/config/firebase.ts +78 -78
- package/src/config/index.ts +9 -9
- package/src/errors/auth.error.ts +6 -6
- package/src/errors/auth.errors.ts +200 -200
- package/src/errors/clinic.errors.ts +32 -32
- package/src/errors/firebase.errors.ts +47 -47
- package/src/errors/user.errors.ts +99 -99
- package/src/index.backup.ts +407 -407
- package/src/index.ts +6 -6
- package/src/locales/en.ts +31 -31
- package/src/recommender/admin/index.ts +1 -1
- package/src/recommender/admin/services/recommender.service.admin.ts +5 -5
- package/src/recommender/front/index.ts +1 -1
- package/src/recommender/front/services/onboarding.service.ts +5 -5
- package/src/recommender/front/services/recommender.service.ts +3 -3
- package/src/recommender/index.ts +1 -1
- package/src/services/PATIENTAUTH.MD +197 -197
- package/src/services/README.md +106 -106
- package/src/services/__tests__/auth/auth.mock.test.ts +17 -17
- package/src/services/__tests__/auth/auth.setup.ts +293 -293
- package/src/services/__tests__/auth.service.test.ts +346 -346
- package/src/services/__tests__/base.service.test.ts +77 -77
- package/src/services/__tests__/user.service.test.ts +528 -528
- package/src/services/appointment/README.md +17 -17
- package/src/services/appointment/appointment.service.ts +2505 -2082
- package/src/services/appointment/index.ts +1 -1
- package/src/services/appointment/utils/appointment.utils.ts +552 -552
- package/src/services/appointment/utils/extended-procedure.utils.ts +314 -314
- package/src/services/appointment/utils/form-initialization.utils.ts +225 -225
- package/src/services/appointment/utils/recommended-procedure.utils.ts +195 -195
- package/src/services/appointment/utils/zone-management.utils.ts +353 -353
- package/src/services/appointment/utils/zone-photo.utils.ts +152 -152
- package/src/services/auth/auth.service.ts +989 -989
- package/src/services/auth/auth.v2.service.ts +961 -961
- package/src/services/auth/index.ts +7 -7
- package/src/services/auth/utils/error.utils.ts +90 -90
- package/src/services/auth/utils/firebase.utils.ts +49 -49
- package/src/services/auth/utils/index.ts +21 -21
- package/src/services/auth/utils/practitioner.utils.ts +125 -125
- package/src/services/base.service.ts +41 -41
- package/src/services/calendar/calendar.service.ts +1077 -1077
- package/src/services/calendar/calendar.v2.service.ts +1683 -1683
- package/src/services/calendar/calendar.v3.service.ts +313 -313
- package/src/services/calendar/externalCalendar.service.ts +178 -178
- package/src/services/calendar/index.ts +5 -5
- package/src/services/calendar/synced-calendars.service.ts +743 -743
- package/src/services/calendar/utils/appointment.utils.ts +265 -265
- package/src/services/calendar/utils/calendar-event.utils.ts +646 -646
- package/src/services/calendar/utils/clinic.utils.ts +237 -237
- package/src/services/calendar/utils/docs.utils.ts +157 -157
- package/src/services/calendar/utils/google-calendar.utils.ts +697 -697
- package/src/services/calendar/utils/index.ts +8 -8
- package/src/services/calendar/utils/patient.utils.ts +198 -198
- package/src/services/calendar/utils/practitioner.utils.ts +221 -221
- package/src/services/calendar/utils/synced-calendar.utils.ts +472 -472
- package/src/services/clinic/README.md +204 -204
- package/src/services/clinic/__tests__/clinic-admin.service.test.ts +287 -287
- package/src/services/clinic/__tests__/clinic-group.service.test.ts +352 -352
- package/src/services/clinic/__tests__/clinic.service.test.ts +354 -354
- package/src/services/clinic/billing-transactions.service.ts +217 -217
- package/src/services/clinic/clinic-admin.service.ts +202 -202
- package/src/services/clinic/clinic-group.service.ts +310 -310
- package/src/services/clinic/clinic.service.ts +708 -708
- package/src/services/clinic/index.ts +5 -5
- package/src/services/clinic/practitioner-invite.service.ts +519 -519
- package/src/services/clinic/utils/admin.utils.ts +551 -551
- package/src/services/clinic/utils/clinic-group.utils.ts +646 -646
- package/src/services/clinic/utils/clinic.utils.ts +949 -949
- package/src/services/clinic/utils/filter.utils.d.ts +23 -23
- package/src/services/clinic/utils/filter.utils.ts +446 -446
- package/src/services/clinic/utils/index.ts +11 -11
- package/src/services/clinic/utils/photos.utils.ts +188 -188
- package/src/services/clinic/utils/search.utils.ts +84 -84
- package/src/services/clinic/utils/tag.utils.ts +124 -124
- package/src/services/documentation-templates/documentation-template.service.ts +537 -537
- package/src/services/documentation-templates/filled-document.service.ts +587 -587
- package/src/services/documentation-templates/index.ts +2 -2
- package/src/services/index.ts +13 -13
- package/src/services/media/index.ts +1 -1
- package/src/services/media/media.service.ts +418 -418
- package/src/services/notifications/__tests__/notification.service.test.ts +242 -242
- package/src/services/notifications/index.ts +1 -1
- package/src/services/notifications/notification.service.ts +215 -215
- package/src/services/patient/README.md +48 -48
- package/src/services/patient/To-Do.md +43 -43
- package/src/services/patient/__tests__/patient.service.test.ts +294 -294
- package/src/services/patient/index.ts +2 -2
- package/src/services/patient/patient.service.ts +883 -883
- package/src/services/patient/patientRequirements.service.ts +285 -285
- package/src/services/patient/utils/aesthetic-analysis.utils.ts +176 -176
- package/src/services/patient/utils/clinic.utils.ts +80 -80
- package/src/services/patient/utils/docs.utils.ts +142 -142
- package/src/services/patient/utils/index.ts +9 -9
- package/src/services/patient/utils/location.utils.ts +126 -126
- package/src/services/patient/utils/medical-stuff.utils.ts +143 -143
- package/src/services/patient/utils/medical.utils.ts +458 -458
- package/src/services/patient/utils/practitioner.utils.ts +260 -260
- package/src/services/patient/utils/profile.utils.ts +510 -510
- package/src/services/patient/utils/sensitive.utils.ts +260 -260
- package/src/services/patient/utils/token.utils.ts +211 -211
- package/src/services/practitioner/README.md +145 -145
- package/src/services/practitioner/index.ts +1 -1
- package/src/services/practitioner/practitioner.service.ts +1742 -1742
- package/src/services/procedure/README.md +163 -163
- package/src/services/procedure/index.ts +1 -1
- package/src/services/procedure/procedure.service.ts +1682 -1682
- package/src/services/reviews/index.ts +1 -1
- package/src/services/reviews/reviews.service.ts +636 -683
- package/src/services/user/index.ts +1 -1
- package/src/services/user/user.service.ts +489 -489
- package/src/services/user/user.v2.service.ts +466 -466
- package/src/types/appointment/index.ts +481 -453
- package/src/types/calendar/index.ts +258 -258
- package/src/types/calendar/synced-calendar.types.ts +66 -66
- package/src/types/clinic/index.ts +489 -489
- package/src/types/clinic/practitioner-invite.types.ts +91 -91
- package/src/types/clinic/preferences.types.ts +159 -159
- package/src/types/clinic/to-do +3 -3
- package/src/types/documentation-templates/index.ts +308 -308
- package/src/types/index.ts +44 -44
- package/src/types/notifications/README.md +77 -77
- package/src/types/notifications/index.ts +265 -265
- package/src/types/patient/aesthetic-analysis.types.ts +66 -66
- package/src/types/patient/allergies.ts +58 -58
- package/src/types/patient/index.ts +275 -273
- package/src/types/patient/medical-info.types.ts +152 -152
- package/src/types/patient/patient-requirements.ts +92 -92
- package/src/types/patient/token.types.ts +61 -61
- package/src/types/practitioner/index.ts +206 -206
- package/src/types/procedure/index.ts +181 -181
- package/src/types/profile/index.ts +39 -39
- package/src/types/reviews/index.ts +130 -132
- package/src/types/tz-lookup.d.ts +4 -4
- package/src/types/user/index.ts +38 -38
- package/src/utils/TIMESTAMPS.md +176 -176
- package/src/utils/TimestampUtils.ts +241 -241
- package/src/utils/index.ts +1 -1
- package/src/validations/appointment.schema.ts +574 -574
- package/src/validations/calendar.schema.ts +225 -225
- package/src/validations/clinic.schema.ts +493 -493
- package/src/validations/common.schema.ts +25 -25
- package/src/validations/documentation-templates/index.ts +1 -1
- package/src/validations/documentation-templates/template.schema.ts +220 -220
- package/src/validations/documentation-templates.schema.ts +10 -10
- package/src/validations/index.ts +20 -20
- package/src/validations/media.schema.ts +10 -10
- package/src/validations/notification.schema.ts +90 -90
- package/src/validations/patient/aesthetic-analysis.schema.ts +55 -55
- package/src/validations/patient/medical-info.schema.ts +125 -125
- package/src/validations/patient/patient-requirements.schema.ts +84 -84
- package/src/validations/patient/token.schema.ts +29 -29
- package/src/validations/patient.schema.ts +217 -216
- package/src/validations/practitioner.schema.ts +222 -222
- package/src/validations/procedure-product.schema.ts +41 -41
- package/src/validations/procedure.schema.ts +124 -124
- package/src/validations/profile-info.schema.ts +41 -41
- package/src/validations/reviews.schema.ts +189 -195
- package/src/validations/schemas.ts +104 -104
- 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
|
+
}
|