washday-sdk 1.6.61 → 1.6.63

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.
@@ -8,13 +8,90 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  });
9
9
  };
10
10
  const ATTENDANCE_API = 'api/attendance';
11
+ const ATTENDANCE_SOURCES = ["web_pos", "pos_v2", "unknown"];
12
+ const LOCATION_PERMISSION_STATUSES = [
13
+ "granted",
14
+ "denied",
15
+ "unavailable",
16
+ "not_requested",
17
+ "error",
18
+ ];
19
+ const stringValue = (value) => (typeof value === "string" && value.trim() ? value.trim() : undefined);
20
+ const numberValue = (value) => (typeof value === "number" && Number.isFinite(value) ? value : undefined);
21
+ const sourceValue = (value) => (ATTENDANCE_SOURCES.includes(value) ? value : undefined);
22
+ const permissionStatusValue = (value) => (LOCATION_PERMISSION_STATUSES.includes(value)
23
+ ? value
24
+ : undefined);
25
+ const dateValue = (value) => {
26
+ if (value instanceof Date && !Number.isNaN(value.getTime()))
27
+ return value;
28
+ if (typeof value !== "string")
29
+ return undefined;
30
+ return Number.isNaN(new Date(value).getTime()) ? undefined : value;
31
+ };
32
+ const buildSafeAttendanceLocation = (location) => {
33
+ if (!location)
34
+ return undefined;
35
+ const permissionStatus = permissionStatusValue(location.permissionStatus);
36
+ if (!permissionStatus)
37
+ return undefined;
38
+ const safeLocation = {
39
+ permissionStatus,
40
+ capturedAt: dateValue(location.capturedAt),
41
+ errorCode: stringValue(location.errorCode),
42
+ errorMessage: stringValue(location.errorMessage),
43
+ };
44
+ if (permissionStatus === "granted") {
45
+ safeLocation.latitude = numberValue(location.latitude);
46
+ safeLocation.longitude = numberValue(location.longitude);
47
+ safeLocation.accuracy = numberValue(location.accuracy);
48
+ safeLocation.altitude = numberValue(location.altitude);
49
+ safeLocation.altitudeAccuracy = numberValue(location.altitudeAccuracy);
50
+ safeLocation.heading = numberValue(location.heading);
51
+ safeLocation.speed = numberValue(location.speed);
52
+ }
53
+ const filteredEntries = Object.entries(safeLocation).filter(([, value]) => value !== undefined);
54
+ return filteredEntries.length
55
+ ? Object.fromEntries(filteredEntries)
56
+ : undefined;
57
+ };
58
+ const buildSafeAttendanceMetadata = (metadata) => {
59
+ if (!metadata)
60
+ return undefined;
61
+ const safeMetadata = {
62
+ source: sourceValue(metadata.source),
63
+ platform: stringValue(metadata.platform),
64
+ appVersion: stringValue(metadata.appVersion),
65
+ deviceId: stringValue(metadata.deviceId),
66
+ language: stringValue(metadata.language),
67
+ timezone: stringValue(metadata.timezone),
68
+ screenWidth: numberValue(metadata.screenWidth),
69
+ screenHeight: numberValue(metadata.screenHeight),
70
+ location: buildSafeAttendanceLocation(metadata.location),
71
+ };
72
+ const filteredEntries = Object.entries(safeMetadata).filter(([, value]) => value !== undefined);
73
+ return filteredEntries.length
74
+ ? Object.fromEntries(filteredEntries)
75
+ : undefined;
76
+ };
77
+ const buildAttendancePayload = (data) => {
78
+ const metadata = buildSafeAttendanceMetadata(data.metadata);
79
+ const payload = Object.assign({}, data);
80
+ if (metadata) {
81
+ payload.metadata = metadata;
82
+ }
83
+ else {
84
+ delete payload.metadata;
85
+ }
86
+ return payload;
87
+ };
11
88
  export const clockIn = function (data) {
12
89
  return __awaiter(this, void 0, void 0, function* () {
13
90
  try {
14
91
  const config = {
15
92
  headers: { Authorization: `Bearer ${this.apiToken}` }
16
93
  };
17
- return yield this.axiosInstance.post(`${ATTENDANCE_API}/clock-in`, data, config);
94
+ return yield this.axiosInstance.post(`${ATTENDANCE_API}/clock-in`, buildAttendancePayload(data), config);
18
95
  }
19
96
  catch (error) {
20
97
  console.error('Error clock-in:', error);
@@ -28,7 +105,7 @@ export const clockOut = function (data) {
28
105
  const config = {
29
106
  headers: { Authorization: `Bearer ${this.apiToken}` }
30
107
  };
31
- return yield this.axiosInstance.post(`${ATTENDANCE_API}/clock-out`, data, config);
108
+ return yield this.axiosInstance.post(`${ATTENDANCE_API}/clock-out`, buildAttendancePayload(data), config);
32
109
  }
33
110
  catch (error) {
34
111
  console.error('Error clock-out:', error);
@@ -14,7 +14,8 @@ export const updateById = function (id, data) {
14
14
  const config = {
15
15
  headers: { Authorization: `Bearer ${this.apiToken}` }
16
16
  };
17
- return yield this.axiosInstance.put(`${ATTENDANCE_API}/${id}`, data, config);
17
+ const { timestamp, notes, reason } = data;
18
+ return yield this.axiosInstance.put(`${ATTENDANCE_API}/${id}`, Object.assign(Object.assign(Object.assign({}, (timestamp !== undefined ? { timestamp } : {})), (notes !== undefined ? { notes } : {})), { reason }), config);
18
19
  }
19
20
  catch (error) {
20
21
  console.error('Error updating attendance entry:', error);
package/dist/index.js CHANGED
@@ -7,4 +7,5 @@ export * from './interfaces/Auth';
7
7
  export * from './interfaces/Customer';
8
8
  export * from './interfaces/DomainUrls';
9
9
  export * from './interfaces/GoogleMaps';
10
+ export * from './interfaces/Attendance';
10
11
  export { WashdayClient, utils };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "washday-sdk",
3
- "version": "1.6.61",
3
+ "version": "1.6.63",
4
4
  "description": "Washday utilities functions and API",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -1,36 +1,140 @@
1
1
  import { WashdayClientInstance } from "../../interfaces/Api";
2
- import axiosInstance from "../axiosInstance";
2
+ import type {
3
+ AttendanceClientMetadata,
4
+ AttendanceLocationMetadata,
5
+ AttendanceLocationPermissionStatus,
6
+ AttendanceSource,
7
+ ClockInRequest,
8
+ ClockOutRequest,
9
+ } from "../../interfaces/Attendance";
3
10
 
4
11
  const ATTENDANCE_API = 'api/attendance';
12
+ const ATTENDANCE_SOURCES: AttendanceSource[] = ["web_pos", "pos_v2", "unknown"];
13
+ const LOCATION_PERMISSION_STATUSES: AttendanceLocationPermissionStatus[] = [
14
+ "granted",
15
+ "denied",
16
+ "unavailable",
17
+ "not_requested",
18
+ "error",
19
+ ];
5
20
 
6
- export const clockIn = async function (this: WashdayClientInstance, data: {
7
- storeId: string;
8
- userId?: string;
9
- notes?: string;
10
- }): Promise<any> {
21
+ const stringValue = (value: unknown): string | undefined => (
22
+ typeof value === "string" && value.trim() ? value.trim() : undefined
23
+ );
24
+
25
+ const numberValue = (value: unknown): number | undefined => (
26
+ typeof value === "number" && Number.isFinite(value) ? value : undefined
27
+ );
28
+
29
+ const sourceValue = (value: unknown): AttendanceSource | undefined => (
30
+ ATTENDANCE_SOURCES.includes(value as AttendanceSource) ? value as AttendanceSource : undefined
31
+ );
32
+
33
+ const permissionStatusValue = (value: unknown): AttendanceLocationPermissionStatus | undefined => (
34
+ LOCATION_PERMISSION_STATUSES.includes(value as AttendanceLocationPermissionStatus)
35
+ ? value as AttendanceLocationPermissionStatus
36
+ : undefined
37
+ );
38
+
39
+ const dateValue = (value: unknown): string | Date | undefined => {
40
+ if (value instanceof Date && !Number.isNaN(value.getTime())) return value;
41
+ if (typeof value !== "string") return undefined;
42
+ return Number.isNaN(new Date(value).getTime()) ? undefined : value;
43
+ };
44
+
45
+ const buildSafeAttendanceLocation = (
46
+ location?: AttendanceLocationMetadata & Record<string, unknown>
47
+ ): AttendanceLocationMetadata | undefined => {
48
+ if (!location) return undefined;
49
+
50
+ const permissionStatus = permissionStatusValue(location.permissionStatus);
51
+ if (!permissionStatus) return undefined;
52
+
53
+ const safeLocation: AttendanceLocationMetadata = {
54
+ permissionStatus,
55
+ capturedAt: dateValue(location.capturedAt),
56
+ errorCode: stringValue(location.errorCode),
57
+ errorMessage: stringValue(location.errorMessage),
58
+ };
59
+
60
+ if (permissionStatus === "granted") {
61
+ safeLocation.latitude = numberValue(location.latitude);
62
+ safeLocation.longitude = numberValue(location.longitude);
63
+ safeLocation.accuracy = numberValue(location.accuracy);
64
+ safeLocation.altitude = numberValue(location.altitude);
65
+ safeLocation.altitudeAccuracy = numberValue(location.altitudeAccuracy);
66
+ safeLocation.heading = numberValue(location.heading);
67
+ safeLocation.speed = numberValue(location.speed);
68
+ }
69
+
70
+ const filteredEntries = Object.entries(safeLocation).filter(
71
+ ([, value]) => value !== undefined
72
+ );
73
+
74
+ return filteredEntries.length
75
+ ? Object.fromEntries(filteredEntries) as AttendanceLocationMetadata
76
+ : undefined;
77
+ };
78
+
79
+ const buildSafeAttendanceMetadata = (
80
+ metadata?: AttendanceClientMetadata & Record<string, unknown>
81
+ ): AttendanceClientMetadata | undefined => {
82
+ if (!metadata) return undefined;
83
+
84
+ const safeMetadata: AttendanceClientMetadata = {
85
+ source: sourceValue(metadata.source),
86
+ platform: stringValue(metadata.platform),
87
+ appVersion: stringValue(metadata.appVersion),
88
+ deviceId: stringValue(metadata.deviceId),
89
+ language: stringValue(metadata.language),
90
+ timezone: stringValue(metadata.timezone),
91
+ screenWidth: numberValue(metadata.screenWidth),
92
+ screenHeight: numberValue(metadata.screenHeight),
93
+ location: buildSafeAttendanceLocation(metadata.location as any),
94
+ };
95
+
96
+ const filteredEntries = Object.entries(safeMetadata).filter(
97
+ ([, value]) => value !== undefined
98
+ );
99
+
100
+ return filteredEntries.length
101
+ ? Object.fromEntries(filteredEntries) as AttendanceClientMetadata
102
+ : undefined;
103
+ };
104
+
105
+ const buildAttendancePayload = <T extends ClockInRequest | ClockOutRequest>(data: T): T => {
106
+ const metadata = buildSafeAttendanceMetadata(data.metadata as any);
107
+ const payload = { ...data } as T;
108
+
109
+ if (metadata) {
110
+ payload.metadata = metadata;
111
+ } else {
112
+ delete payload.metadata;
113
+ }
114
+
115
+ return payload;
116
+ };
117
+
118
+ export const clockIn = async function (this: WashdayClientInstance, data: ClockInRequest): Promise<any> {
11
119
  try {
12
120
  const config = {
13
121
  headers: { Authorization: `Bearer ${this.apiToken}` }
14
122
  };
15
- return await this.axiosInstance.post(`${ATTENDANCE_API}/clock-in`, data, config);
123
+ return await this.axiosInstance.post(`${ATTENDANCE_API}/clock-in`, buildAttendancePayload(data), config);
16
124
  } catch (error) {
17
125
  console.error('Error clock-in:', error);
18
126
  throw error;
19
127
  }
20
128
  };
21
129
 
22
- export const clockOut = async function (this: WashdayClientInstance, data: {
23
- userId?: string;
24
- storeId?: string; // ✅ AÑADIDO - requerido por backend
25
- notes?: string;
26
- }): Promise<any> {
130
+ export const clockOut = async function (this: WashdayClientInstance, data: ClockOutRequest): Promise<any> {
27
131
  try {
28
132
  const config = {
29
133
  headers: { Authorization: `Bearer ${this.apiToken}` }
30
134
  };
31
- return await this.axiosInstance.post(`${ATTENDANCE_API}/clock-out`, data, config);
135
+ return await this.axiosInstance.post(`${ATTENDANCE_API}/clock-out`, buildAttendancePayload(data), config);
32
136
  } catch (error) {
33
137
  console.error('Error clock-out:', error);
34
138
  throw error;
35
139
  }
36
- };
140
+ };
@@ -1,20 +1,27 @@
1
1
  import { WashdayClientInstance } from "../../interfaces/Api";
2
- import axiosInstance from "../axiosInstance";
3
2
 
4
3
  const ATTENDANCE_API = 'api/attendance';
5
4
 
6
- export const updateById = async function (this: WashdayClientInstance, id: string, data: {
5
+ export interface UpdateAttendanceEntryRequest {
7
6
  timestamp?: string;
8
7
  notes?: string;
8
+ reason: string;
9
9
  editorId?: string;
10
- }): Promise<any> {
10
+ }
11
+
12
+ export const updateById = async function (this: WashdayClientInstance, id: string, data: UpdateAttendanceEntryRequest): Promise<any> {
11
13
  try {
12
14
  const config = {
13
15
  headers: { Authorization: `Bearer ${this.apiToken}` }
14
16
  };
15
- return await this.axiosInstance.put(`${ATTENDANCE_API}/${id}`, data, config);
17
+ const { timestamp, notes, reason } = data;
18
+ return await this.axiosInstance.put(`${ATTENDANCE_API}/${id}`, {
19
+ ...(timestamp !== undefined ? { timestamp } : {}),
20
+ ...(notes !== undefined ? { notes } : {}),
21
+ reason,
22
+ }, config);
16
23
  } catch (error) {
17
24
  console.error('Error updating attendance entry:', error);
18
25
  throw error;
19
26
  }
20
- };
27
+ };
package/src/index.ts CHANGED
@@ -7,6 +7,7 @@ export * from './interfaces/Auth';
7
7
  export * from './interfaces/Customer';
8
8
  export * from './interfaces/DomainUrls';
9
9
  export * from './interfaces/GoogleMaps';
10
+ export * from './interfaces/Attendance';
10
11
  export type { StartMPOAuthFlowRequest } from './api/integrations/post';
11
12
  export type {
12
13
  CreateCFDISubscriptionCheckoutSessionRequest,
@@ -1,6 +1,62 @@
1
1
  import { IUser } from "./User";
2
2
  import { IStore } from "./Store";
3
3
 
4
+ export type AttendanceSource = "web_pos" | "pos_v2" | "unknown";
5
+
6
+ export type AttendanceLocationPermissionStatus =
7
+ | "granted"
8
+ | "denied"
9
+ | "unavailable"
10
+ | "not_requested"
11
+ | "error";
12
+
13
+ export interface AttendanceLocationMetadata {
14
+ latitude?: number;
15
+ longitude?: number;
16
+ accuracy?: number;
17
+ altitude?: number;
18
+ altitudeAccuracy?: number;
19
+ heading?: number;
20
+ speed?: number;
21
+ permissionStatus?: AttendanceLocationPermissionStatus;
22
+ capturedAt?: string | Date;
23
+ errorCode?: string;
24
+ errorMessage?: string;
25
+ }
26
+
27
+ export interface AttendanceClientMetadata {
28
+ platform?: string;
29
+ appVersion?: string;
30
+ deviceId?: string;
31
+ source?: AttendanceSource;
32
+ language?: string;
33
+ timezone?: string;
34
+ screenWidth?: number;
35
+ screenHeight?: number;
36
+ location?: AttendanceLocationMetadata;
37
+ }
38
+
39
+ export interface AttendanceServerMetadata extends AttendanceClientMetadata {
40
+ ipAddress?: string;
41
+ cfConnectingIp?: string;
42
+ xForwardedFor?: string;
43
+ userAgent?: string;
44
+ }
45
+
46
+ export interface ClockInRequest {
47
+ storeId: string;
48
+ userId?: string;
49
+ notes?: string;
50
+ metadata?: AttendanceClientMetadata;
51
+ }
52
+
53
+ export interface ClockOutRequest {
54
+ userId?: string;
55
+ storeId?: string;
56
+ notes?: string;
57
+ metadata?: AttendanceClientMetadata;
58
+ }
59
+
4
60
  export interface IAttendanceRecord {
5
61
  _id?: string;
6
62
  employee: IUser | string;
@@ -8,9 +64,12 @@ export interface IAttendanceRecord {
8
64
  type: 'IN' | 'OUT';
9
65
  timestamp: Date;
10
66
  ipAddress?: string;
67
+ metadata?: AttendanceServerMetadata;
11
68
  notes?: string;
12
69
  editedBy?: IUser | string;
13
70
  editedAt?: Date;
71
+ lastEditReason?: string;
72
+ revision?: number;
14
73
  autoClosed?: boolean;
15
74
  createdAt?: Date;
16
75
  updatedAt?: Date;
@@ -29,6 +88,36 @@ export interface IAttendanceStatus {
29
88
  clockInStore?: IStore | string;
30
89
  }
31
90
 
91
+ export interface AttendanceShiftDuration {
92
+ hours: number;
93
+ minutes: number;
94
+ totalMinutes: number;
95
+ }
96
+
97
+ export interface AttendanceReportTableInfoRow {
98
+ _id?: string;
99
+ employee: IUser | string | {
100
+ _id?: string;
101
+ name?: string;
102
+ email?: string;
103
+ };
104
+ clockInTime?: Date | string;
105
+ clockOutTime?: Date | string | null;
106
+ shiftDuration?: AttendanceShiftDuration | null;
107
+ clockInStore?: string;
108
+ clockOutStore?: string | null;
109
+ notes?: string;
110
+ clockInIpAddress?: string;
111
+ clockOutIpAddress?: string;
112
+ clockInMetadata?: AttendanceServerMetadata | null;
113
+ clockOutMetadata?: AttendanceServerMetadata | null;
114
+ clockInLocation?: AttendanceLocationMetadata | null;
115
+ clockOutLocation?: AttendanceLocationMetadata | null;
116
+ clockInLocationSummary?: string;
117
+ clockOutLocationSummary?: string;
118
+ autoClosed?: boolean;
119
+ }
120
+
32
121
  export interface IAttendanceReport {
33
122
  storeId: string;
34
123
  storeName?: string;
@@ -42,4 +131,12 @@ export interface IAttendanceReport {
42
131
  startDate: Date;
43
132
  endDate: Date;
44
133
  };
45
- }
134
+ tableInfo?: AttendanceReportTableInfoRow[];
135
+ totalRows?: number;
136
+ pagination?: {
137
+ currentPage: number;
138
+ totalPages: number;
139
+ totalRecords: number;
140
+ limit?: number;
141
+ };
142
+ }
@@ -13,6 +13,7 @@ export interface IPermissionMutationType {
13
13
 
14
14
  export interface IPermission {
15
15
  canEditOrders: IPermissionMutationType,
16
+ canManageAttendance: IPermissionMutationType,
16
17
  mutateOrderPayments: IPermissionMutationType,
17
18
  markOrderAsCollectedWithPendingAmount: IPermissionMutationType,
18
19
  markOrderAsUncollected: IPermissionMutationType,
@@ -0,0 +1,113 @@
1
+ import { clockIn, clockOut } from "../src/api/attendance/post";
2
+
3
+ describe("attendance metadata requests", () => {
4
+ it("forwards client-safe clock-in metadata and strips IP fields", async () => {
5
+ const post = jest.fn().mockResolvedValue({ data: { data: {} } });
6
+ const client = {
7
+ apiToken: "token-123",
8
+ axiosInstance: { post },
9
+ } as any;
10
+
11
+ await clockIn.call(client, {
12
+ storeId: "store-1",
13
+ userId: "user-1",
14
+ metadata: {
15
+ source: "web_pos",
16
+ platform: "MacIntel",
17
+ language: "es-MX",
18
+ timezone: "America/Mexico_City",
19
+ screenWidth: 1440,
20
+ screenHeight: 900,
21
+ location: {
22
+ permissionStatus: "granted",
23
+ latitude: 19.4326,
24
+ longitude: -99.1332,
25
+ accuracy: 24,
26
+ capturedAt: "2026-05-26T14:00:00.000Z",
27
+ unknown: "drop-me",
28
+ },
29
+ ipAddress: "1.1.1.1",
30
+ cfConnectingIp: "1.1.1.2",
31
+ xForwardedFor: "1.1.1.3",
32
+ },
33
+ } as any);
34
+
35
+ expect(post).toHaveBeenCalledWith(
36
+ "api/attendance/clock-in",
37
+ {
38
+ storeId: "store-1",
39
+ userId: "user-1",
40
+ metadata: {
41
+ source: "web_pos",
42
+ platform: "MacIntel",
43
+ language: "es-MX",
44
+ timezone: "America/Mexico_City",
45
+ screenWidth: 1440,
46
+ screenHeight: 900,
47
+ location: {
48
+ permissionStatus: "granted",
49
+ latitude: 19.4326,
50
+ longitude: -99.1332,
51
+ accuracy: 24,
52
+ capturedAt: "2026-05-26T14:00:00.000Z",
53
+ },
54
+ },
55
+ },
56
+ {
57
+ headers: { Authorization: "Bearer token-123" },
58
+ },
59
+ );
60
+ });
61
+
62
+ it("forwards client-safe clock-out metadata and strips IP fields", async () => {
63
+ const post = jest.fn().mockResolvedValue({ data: { data: {} } });
64
+ const client = {
65
+ apiToken: "token-123",
66
+ axiosInstance: { post },
67
+ } as any;
68
+
69
+ await clockOut.call(client, {
70
+ storeId: "store-1",
71
+ userId: "user-1",
72
+ metadata: {
73
+ source: "pos_v2",
74
+ platform: "ios",
75
+ appVersion: "2.5.0",
76
+ deviceId: "device-1",
77
+ location: {
78
+ permissionStatus: "denied",
79
+ latitude: 19.4326,
80
+ longitude: -99.1332,
81
+ accuracy: 24,
82
+ capturedAt: "2026-05-26T14:00:00.000Z",
83
+ errorCode: "1",
84
+ errorMessage: "Permission denied",
85
+ },
86
+ ipAddress: "1.1.1.1",
87
+ },
88
+ } as any);
89
+
90
+ expect(post).toHaveBeenCalledWith(
91
+ "api/attendance/clock-out",
92
+ {
93
+ storeId: "store-1",
94
+ userId: "user-1",
95
+ metadata: {
96
+ source: "pos_v2",
97
+ platform: "ios",
98
+ appVersion: "2.5.0",
99
+ deviceId: "device-1",
100
+ location: {
101
+ permissionStatus: "denied",
102
+ capturedAt: "2026-05-26T14:00:00.000Z",
103
+ errorCode: "1",
104
+ errorMessage: "Permission denied",
105
+ },
106
+ },
107
+ },
108
+ {
109
+ headers: { Authorization: "Bearer token-123" },
110
+ },
111
+ );
112
+ });
113
+ });
@@ -0,0 +1,30 @@
1
+ import { updateById } from "../src/api/attendance/put";
2
+
3
+ describe("attendance update requests", () => {
4
+ it("sends reason with editable attendance fields", async () => {
5
+ const put = jest.fn().mockResolvedValue({ data: { data: {} } });
6
+ const client = {
7
+ apiToken: "token-123",
8
+ axiosInstance: { put },
9
+ } as any;
10
+
11
+ await updateById.call(client, "entry-1", {
12
+ timestamp: "2026-05-26T14:00:00.000Z",
13
+ notes: "Corrected note",
14
+ reason: "El empleado olvidó marcar a tiempo",
15
+ editorId: "ignored-by-backend",
16
+ } as any);
17
+
18
+ expect(put).toHaveBeenCalledWith(
19
+ "api/attendance/entry-1",
20
+ {
21
+ timestamp: "2026-05-26T14:00:00.000Z",
22
+ notes: "Corrected note",
23
+ reason: "El empleado olvidó marcar a tiempo",
24
+ },
25
+ {
26
+ headers: { Authorization: "Bearer token-123" },
27
+ },
28
+ );
29
+ });
30
+ });