@blackcode_sa/metaestetics-api 1.12.6 → 1.12.8

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@blackcode_sa/metaestetics-api",
3
3
  "private": false,
4
- "version": "1.12.6",
4
+ "version": "1.12.8",
5
5
  "description": "Firebase authentication service with anonymous upgrade support",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.mjs",
@@ -95,7 +95,6 @@
95
95
  "typescript": "^5.7.3"
96
96
  },
97
97
  "dependencies": {
98
- "@blackcode_sa/metaestetics-api": "^1.12.5",
99
98
  "axios": "^1.8.3",
100
99
  "expo-server-sdk": "^3.13.0",
101
100
  "firebase-admin": "^13.0.2",
@@ -0,0 +1,217 @@
1
+ import {
2
+ collection,
3
+ getDocs,
4
+ query,
5
+ orderBy,
6
+ limit,
7
+ startAfter,
8
+ where,
9
+ DocumentSnapshot,
10
+ QueryConstraint,
11
+ } from 'firebase/firestore';
12
+ import { BaseService } from '../base.service';
13
+ import {
14
+ BillingTransaction,
15
+ BillingTransactionType,
16
+ CLINIC_GROUPS_COLLECTION,
17
+ } from '../../types/clinic';
18
+
19
+ /**
20
+ * Service for managing billing transactions
21
+ * Provides read-only access to billing transaction history
22
+ */
23
+ export class BillingTransactionsService extends BaseService {
24
+ private readonly BILLING_TRANSACTIONS_COLLECTION = 'billingTransactions';
25
+
26
+ /**
27
+ * Get billing transactions for a clinic group with pagination
28
+ * @param clinicGroupId - The clinic group ID
29
+ * @param options - Query options
30
+ * @returns Promise with transactions and pagination info
31
+ */
32
+ async getBillingTransactions(
33
+ clinicGroupId: string,
34
+ options: {
35
+ limit?: number;
36
+ startAfter?: DocumentSnapshot;
37
+ transactionType?: BillingTransactionType;
38
+ } = {},
39
+ ): Promise<{
40
+ transactions: BillingTransaction[];
41
+ lastDoc: DocumentSnapshot | null;
42
+ hasMore: boolean;
43
+ }> {
44
+ try {
45
+ const { limit: queryLimit = 50, startAfter: startAfterDoc, transactionType } = options;
46
+
47
+ // Build query constraints
48
+ const constraints: QueryConstraint[] = [];
49
+
50
+ // Add transaction type filter if specified
51
+ if (transactionType) {
52
+ constraints.push(where('type', '==', transactionType));
53
+ }
54
+
55
+ // Add ordering
56
+ constraints.push(orderBy('timestamp', 'desc')); // Most recent first
57
+
58
+ // Add pagination
59
+ if (startAfterDoc) {
60
+ constraints.push(startAfter(startAfterDoc));
61
+ }
62
+
63
+ constraints.push(limit(queryLimit + 1)); // Get one extra to check if there are more
64
+
65
+ // Execute query
66
+ const transactionsRef = collection(
67
+ this.db,
68
+ CLINIC_GROUPS_COLLECTION,
69
+ clinicGroupId,
70
+ this.BILLING_TRANSACTIONS_COLLECTION,
71
+ );
72
+
73
+ const q = query(transactionsRef, ...constraints);
74
+ const querySnapshot = await getDocs(q);
75
+
76
+ // Process results
77
+ const docs = querySnapshot.docs;
78
+ const hasMore = docs.length > queryLimit;
79
+ const transactions = docs.slice(0, queryLimit).map(doc => ({
80
+ id: doc.id,
81
+ ...doc.data(),
82
+ })) as BillingTransaction[];
83
+
84
+ const lastDoc = transactions.length > 0 ? docs[transactions.length - 1] : null;
85
+
86
+ return {
87
+ transactions,
88
+ lastDoc,
89
+ hasMore,
90
+ };
91
+ } catch (error) {
92
+ console.error(
93
+ `Error fetching billing transactions for clinic group ${clinicGroupId}:`,
94
+ error,
95
+ );
96
+ throw new Error(
97
+ `Failed to fetch billing transactions: ${
98
+ error instanceof Error ? error.message : 'Unknown error'
99
+ }`,
100
+ );
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Get recent billing transactions (last 10)
106
+ * @param clinicGroupId - The clinic group ID
107
+ * @returns Promise with recent transactions
108
+ */
109
+ async getRecentBillingTransactions(clinicGroupId: string): Promise<BillingTransaction[]> {
110
+ const result = await this.getBillingTransactions(clinicGroupId, { limit: 10 });
111
+ return result.transactions;
112
+ }
113
+
114
+ /**
115
+ * Get billing transactions by type
116
+ * @param clinicGroupId - The clinic group ID
117
+ * @param transactionType - The transaction type to filter by
118
+ * @param options - Additional query options
119
+ * @returns Promise with filtered transactions
120
+ */
121
+ async getBillingTransactionsByType(
122
+ clinicGroupId: string,
123
+ transactionType: BillingTransactionType,
124
+ options: {
125
+ limit?: number;
126
+ startAfter?: DocumentSnapshot;
127
+ } = {},
128
+ ): Promise<{
129
+ transactions: BillingTransaction[];
130
+ lastDoc: DocumentSnapshot | null;
131
+ hasMore: boolean;
132
+ }> {
133
+ return this.getBillingTransactions(clinicGroupId, {
134
+ ...options,
135
+ transactionType,
136
+ });
137
+ }
138
+
139
+ /**
140
+ * Get subscription-related transactions only
141
+ * @param clinicGroupId - The clinic group ID
142
+ * @param options - Query options
143
+ * @returns Promise with subscription transactions
144
+ */
145
+ async getSubscriptionTransactions(
146
+ clinicGroupId: string,
147
+ options: {
148
+ limit?: number;
149
+ startAfter?: DocumentSnapshot;
150
+ } = {},
151
+ ): Promise<{
152
+ transactions: BillingTransaction[];
153
+ lastDoc: DocumentSnapshot | null;
154
+ hasMore: boolean;
155
+ }> {
156
+ try {
157
+ const { limit: queryLimit = 50, startAfter: startAfterDoc } = options;
158
+
159
+ // Subscription-related transaction types
160
+ const subscriptionTypes = [
161
+ BillingTransactionType.SUBSCRIPTION_CREATED,
162
+ BillingTransactionType.SUBSCRIPTION_ACTIVATED,
163
+ BillingTransactionType.SUBSCRIPTION_RENEWED,
164
+ BillingTransactionType.SUBSCRIPTION_UPDATED,
165
+ BillingTransactionType.SUBSCRIPTION_CANCELED,
166
+ BillingTransactionType.SUBSCRIPTION_REACTIVATED,
167
+ BillingTransactionType.SUBSCRIPTION_DELETED,
168
+ ];
169
+
170
+ const constraints: QueryConstraint[] = [
171
+ where('type', 'in', subscriptionTypes),
172
+ orderBy('timestamp', 'desc'),
173
+ ];
174
+
175
+ if (startAfterDoc) {
176
+ constraints.push(startAfter(startAfterDoc));
177
+ }
178
+
179
+ constraints.push(limit(queryLimit + 1));
180
+
181
+ const transactionsRef = collection(
182
+ this.db,
183
+ CLINIC_GROUPS_COLLECTION,
184
+ clinicGroupId,
185
+ this.BILLING_TRANSACTIONS_COLLECTION,
186
+ );
187
+
188
+ const q = query(transactionsRef, ...constraints);
189
+ const querySnapshot = await getDocs(q);
190
+
191
+ const docs = querySnapshot.docs;
192
+ const hasMore = docs.length > queryLimit;
193
+ const transactions = docs.slice(0, queryLimit).map(doc => ({
194
+ id: doc.id,
195
+ ...doc.data(),
196
+ })) as BillingTransaction[];
197
+
198
+ const lastDoc = transactions.length > 0 ? docs[transactions.length - 1] : null;
199
+
200
+ return {
201
+ transactions,
202
+ lastDoc,
203
+ hasMore,
204
+ };
205
+ } catch (error) {
206
+ console.error(
207
+ `Error fetching subscription transactions for clinic group ${clinicGroupId}:`,
208
+ error,
209
+ );
210
+ throw new Error(
211
+ `Failed to fetch subscription transactions: ${
212
+ error instanceof Error ? error.message : 'Unknown error'
213
+ }`,
214
+ );
215
+ }
216
+ }
217
+ }
@@ -11,8 +11,8 @@ import {
11
11
  Timestamp,
12
12
  serverTimestamp,
13
13
  FieldValue,
14
- } from "firebase/firestore";
15
- import { BaseService } from "../base.service";
14
+ } from 'firebase/firestore';
15
+ import { BaseService } from '../base.service';
16
16
  import {
17
17
  ClinicGroup,
18
18
  CreateClinicGroupData,
@@ -22,31 +22,28 @@ import {
22
22
  CreateAdminTokenData,
23
23
  ClinicGroupSetupData,
24
24
  UpdateClinicGroupData,
25
- } from "../../types/clinic";
26
- import { ClinicAdminService } from "./clinic-admin.service";
27
- import { geohashForLocation } from "geofire-common";
28
- import {
29
- clinicGroupSchema,
30
- createClinicGroupSchema,
31
- } from "../../validations/clinic.schema";
32
- import { z } from "zod";
33
- import { Auth } from "firebase/auth";
34
- import { Firestore } from "firebase/firestore";
35
- import { FirebaseApp } from "firebase/app";
36
- import * as ClinicGroupUtils from "./utils/clinic-group.utils";
37
- import { uploadPhoto } from "./utils/photos.utils";
25
+ BillingTransaction,
26
+ BillingTransactionType,
27
+ } from '../../types/clinic';
28
+ import { ClinicAdminService } from './clinic-admin.service';
29
+ import { BillingTransactionsService } from './billing-transactions.service';
30
+ import { geohashForLocation } from 'geofire-common';
31
+ import { clinicGroupSchema, createClinicGroupSchema } from '../../validations/clinic.schema';
32
+ import { z } from 'zod';
33
+ import { Auth } from 'firebase/auth';
34
+ import { Firestore } from 'firebase/firestore';
35
+ import { FirebaseApp } from 'firebase/app';
36
+ import * as ClinicGroupUtils from './utils/clinic-group.utils';
37
+ import { uploadPhoto } from './utils/photos.utils';
38
38
 
39
39
  export class ClinicGroupService extends BaseService {
40
40
  private clinicAdminService: ClinicAdminService;
41
+ private billingTransactionsService: BillingTransactionsService;
41
42
 
42
- constructor(
43
- db: Firestore,
44
- auth: Auth,
45
- app: FirebaseApp,
46
- clinicAdminService: ClinicAdminService
47
- ) {
43
+ constructor(db: Firestore, auth: Auth, app: FirebaseApp, clinicAdminService: ClinicAdminService) {
48
44
  super(db, auth, app);
49
45
  this.clinicAdminService = clinicAdminService;
46
+ this.billingTransactionsService = new BillingTransactionsService(db, auth, app);
50
47
  }
51
48
 
52
49
  /**
@@ -55,7 +52,7 @@ export class ClinicGroupService extends BaseService {
55
52
  async createClinicGroup(
56
53
  data: CreateClinicGroupData,
57
54
  ownerId: string,
58
- isDefault: boolean = false
55
+ isDefault: boolean = false,
59
56
  ): Promise<ClinicGroup> {
60
57
  return ClinicGroupUtils.createClinicGroup(
61
58
  this.db,
@@ -63,7 +60,7 @@ export class ClinicGroupService extends BaseService {
63
60
  ownerId,
64
61
  isDefault,
65
62
  this.clinicAdminService,
66
- this.app
63
+ this.app,
67
64
  );
68
65
  }
69
66
 
@@ -84,10 +81,7 @@ export class ClinicGroupService extends BaseService {
84
81
  /**
85
82
  * Ažurira grupaciju klinika
86
83
  */
87
- async updateClinicGroup(
88
- groupId: string,
89
- data: Partial<ClinicGroup>
90
- ): Promise<ClinicGroup> {
84
+ async updateClinicGroup(groupId: string, data: Partial<ClinicGroup>): Promise<ClinicGroup> {
91
85
  return ClinicGroupUtils.updateClinicGroup(this.db, groupId, data, this.app);
92
86
  }
93
87
 
@@ -95,24 +89,14 @@ export class ClinicGroupService extends BaseService {
95
89
  * Dodaje admina u grupaciju
96
90
  */
97
91
  async addAdminToGroup(groupId: string, adminId: string): Promise<void> {
98
- return ClinicGroupUtils.addAdminToGroup(
99
- this.db,
100
- groupId,
101
- adminId,
102
- this.app
103
- );
92
+ return ClinicGroupUtils.addAdminToGroup(this.db, groupId, adminId, this.app);
104
93
  }
105
94
 
106
95
  /**
107
96
  * Uklanja admina iz grupacije
108
97
  */
109
98
  async removeAdminFromGroup(groupId: string, adminId: string): Promise<void> {
110
- return ClinicGroupUtils.removeAdminFromGroup(
111
- this.db,
112
- groupId,
113
- adminId,
114
- this.app
115
- );
99
+ return ClinicGroupUtils.removeAdminFromGroup(this.db, groupId, adminId, this.app);
116
100
  }
117
101
 
118
102
  /**
@@ -129,32 +113,29 @@ export class ClinicGroupService extends BaseService {
129
113
  * @param setupData - The setup data for the clinic group
130
114
  * @returns The updated clinic group
131
115
  */
132
- async setupClinicGroup(
133
- groupId: string,
134
- setupData: ClinicGroupSetupData
135
- ): Promise<ClinicGroup> {
136
- console.log("[CLINIC_GROUP] Setting up clinic group", { groupId });
116
+ async setupClinicGroup(groupId: string, setupData: ClinicGroupSetupData): Promise<ClinicGroup> {
117
+ console.log('[CLINIC_GROUP] Setting up clinic group', { groupId });
137
118
 
138
119
  // Get the clinic group
139
120
  const clinicGroup = await this.getClinicGroup(groupId);
140
121
  if (!clinicGroup) {
141
- console.error("[CLINIC_GROUP] Clinic group not found", { groupId });
122
+ console.error('[CLINIC_GROUP] Clinic group not found', { groupId });
142
123
  throw new Error(`Clinic group with ID ${groupId} not found`);
143
124
  }
144
125
 
145
126
  // Process logo if it's a data URL
146
127
  let logoUrl = setupData.logo;
147
- if (logoUrl && typeof logoUrl === "string" && logoUrl.startsWith("data:")) {
148
- console.log("[CLINIC_GROUP] Processing logo in setupClinicGroup");
128
+ if (logoUrl && typeof logoUrl === 'string' && logoUrl.startsWith('data:')) {
129
+ console.log('[CLINIC_GROUP] Processing logo in setupClinicGroup');
149
130
  try {
150
131
  const uploadedLogoUrl = await uploadPhoto(
151
132
  logoUrl,
152
- "clinic-groups",
133
+ 'clinic-groups',
153
134
  groupId,
154
- "logo",
155
- this.app
135
+ 'logo',
136
+ this.app,
156
137
  );
157
- console.log("[CLINIC_GROUP] Logo processed in setupClinicGroup", {
138
+ console.log('[CLINIC_GROUP] Logo processed in setupClinicGroup', {
158
139
  uploadedLogoUrl,
159
140
  });
160
141
 
@@ -163,10 +144,7 @@ export class ClinicGroupService extends BaseService {
163
144
  logoUrl = uploadedLogoUrl;
164
145
  }
165
146
  } catch (error) {
166
- console.error(
167
- "[CLINIC_GROUP] Error processing logo in setupClinicGroup:",
168
- error
169
- );
147
+ console.error('[CLINIC_GROUP] Error processing logo in setupClinicGroup:', error);
170
148
  // Continue with update even if logo upload fails
171
149
  }
172
150
  }
@@ -182,7 +160,7 @@ export class ClinicGroupService extends BaseService {
182
160
  businessIdentificationNumber: setupData.businessIdentificationNumber,
183
161
  };
184
162
 
185
- console.log("[CLINIC_GROUP] Updating clinic group with setup data");
163
+ console.log('[CLINIC_GROUP] Updating clinic group with setup data');
186
164
 
187
165
  // Update the clinic group
188
166
  return this.updateClinicGroup(groupId, updateData);
@@ -194,64 +172,30 @@ export class ClinicGroupService extends BaseService {
194
172
  async createAdminToken(
195
173
  groupId: string,
196
174
  creatorAdminId: string,
197
- data?: CreateAdminTokenData
175
+ data?: CreateAdminTokenData,
198
176
  ): Promise<AdminToken> {
199
- return ClinicGroupUtils.createAdminToken(
200
- this.db,
201
- groupId,
202
- creatorAdminId,
203
- this.app,
204
- data
205
- );
177
+ return ClinicGroupUtils.createAdminToken(this.db, groupId, creatorAdminId, this.app, data);
206
178
  }
207
179
 
208
180
  /**
209
181
  * Verifikuje i koristi admin token
210
182
  */
211
- async verifyAndUseAdminToken(
212
- groupId: string,
213
- token: string,
214
- userRef: string
215
- ): Promise<boolean> {
216
- return ClinicGroupUtils.verifyAndUseAdminToken(
217
- this.db,
218
- groupId,
219
- token,
220
- userRef,
221
- this.app
222
- );
183
+ async verifyAndUseAdminToken(groupId: string, token: string, userRef: string): Promise<boolean> {
184
+ return ClinicGroupUtils.verifyAndUseAdminToken(this.db, groupId, token, userRef, this.app);
223
185
  }
224
186
 
225
187
  /**
226
188
  * Briše admin token
227
189
  */
228
- async deleteAdminToken(
229
- groupId: string,
230
- tokenId: string,
231
- adminId: string
232
- ): Promise<void> {
233
- return ClinicGroupUtils.deleteAdminToken(
234
- this.db,
235
- groupId,
236
- tokenId,
237
- adminId,
238
- this.app
239
- );
190
+ async deleteAdminToken(groupId: string, tokenId: string, adminId: string): Promise<void> {
191
+ return ClinicGroupUtils.deleteAdminToken(this.db, groupId, tokenId, adminId, this.app);
240
192
  }
241
193
 
242
194
  /**
243
195
  * Dohvata aktivne admin tokene
244
196
  */
245
- async getActiveAdminTokens(
246
- groupId: string,
247
- adminId: string
248
- ): Promise<AdminToken[]> {
249
- return ClinicGroupUtils.getActiveAdminTokens(
250
- this.db,
251
- groupId,
252
- adminId,
253
- this.app
254
- );
197
+ async getActiveAdminTokens(groupId: string, adminId: string): Promise<AdminToken[]> {
198
+ return ClinicGroupUtils.getActiveAdminTokens(this.db, groupId, adminId, this.app);
255
199
  }
256
200
 
257
201
  // TODO: Add a method to get all admin tokens for a clinic group (not just active ones)
@@ -274,9 +218,9 @@ export class ClinicGroupService extends BaseService {
274
218
  onboardingData: {
275
219
  completed?: boolean;
276
220
  step?: number;
277
- }
221
+ },
278
222
  ): Promise<ClinicGroup> {
279
- console.log("[CLINIC_GROUP] Updating onboarding status", {
223
+ console.log('[CLINIC_GROUP] Updating onboarding status', {
280
224
  groupId,
281
225
  onboardingData,
282
226
  });
@@ -294,7 +238,7 @@ export class ClinicGroupService extends BaseService {
294
238
  * @returns The updated clinic group
295
239
  */
296
240
  async setOnboardingStep(groupId: string, step: number): Promise<ClinicGroup> {
297
- console.log("[CLINIC_GROUP] Setting onboarding step", { groupId, step });
241
+ console.log('[CLINIC_GROUP] Setting onboarding step', { groupId, step });
298
242
 
299
243
  return this.setOnboarding(groupId, { step, completed: false });
300
244
  }
@@ -306,8 +250,61 @@ export class ClinicGroupService extends BaseService {
306
250
  * @returns The updated clinic group
307
251
  */
308
252
  async completeOnboarding(groupId: string): Promise<ClinicGroup> {
309
- console.log("[CLINIC_GROUP] Completing onboarding", { groupId });
253
+ console.log('[CLINIC_GROUP] Completing onboarding', { groupId });
310
254
 
311
255
  return this.setOnboarding(groupId, { completed: true });
312
256
  }
257
+
258
+ /**
259
+ * Get billing transactions for a clinic group
260
+ *
261
+ * @param groupId - The clinic group ID
262
+ * @param options - Query options for pagination and filtering
263
+ * @returns Promise with billing transactions and pagination info
264
+ */
265
+ async getBillingTransactions(
266
+ groupId: string,
267
+ options: {
268
+ limit?: number;
269
+ startAfter?: any;
270
+ transactionType?: BillingTransactionType;
271
+ } = {},
272
+ ): Promise<{
273
+ transactions: BillingTransaction[];
274
+ lastDoc: any;
275
+ hasMore: boolean;
276
+ }> {
277
+ return this.billingTransactionsService.getBillingTransactions(groupId, options);
278
+ }
279
+
280
+ /**
281
+ * Get recent billing transactions for a clinic group (last 10)
282
+ *
283
+ * @param groupId - The clinic group ID
284
+ * @returns Promise with recent billing transactions
285
+ */
286
+ async getRecentBillingTransactions(groupId: string): Promise<BillingTransaction[]> {
287
+ return this.billingTransactionsService.getRecentBillingTransactions(groupId);
288
+ }
289
+
290
+ /**
291
+ * Get subscription-related billing transactions for a clinic group
292
+ *
293
+ * @param groupId - The clinic group ID
294
+ * @param options - Query options for pagination
295
+ * @returns Promise with subscription transactions and pagination info
296
+ */
297
+ async getSubscriptionTransactions(
298
+ groupId: string,
299
+ options: {
300
+ limit?: number;
301
+ startAfter?: any;
302
+ } = {},
303
+ ): Promise<{
304
+ transactions: BillingTransaction[];
305
+ lastDoc: any;
306
+ hasMore: boolean;
307
+ }> {
308
+ return this.billingTransactionsService.getSubscriptionTransactions(groupId, options);
309
+ }
313
310
  }
@@ -1,4 +1,5 @@
1
- export * from "./clinic.service";
2
- export * from "./clinic-admin.service";
3
- export * from "./clinic-group.service";
4
- export * from "./practitioner-invite.service";
1
+ export * from './clinic.service';
2
+ export * from './clinic-admin.service';
3
+ export * from './clinic-group.service';
4
+ export * from './practitioner-invite.service';
5
+ export * from './billing-transactions.service';