@universis/accommodation 1.1.2 → 1.1.5

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.
@@ -5,7 +5,7 @@
5
5
  "title": "StudentAccommodationCard",
6
6
  "hidden": false,
7
7
  "sealed": false,
8
- "version": "1.5",
8
+ "version": "1.9",
9
9
  "source": "StudentAccommodationCardBase",
10
10
  "view": "StudentAccommodationCardData",
11
11
  "classPath": "./models/StudentAccommodationCard",
@@ -16,14 +16,14 @@
16
16
  "description": "Ο κωδικός της εγγραφής",
17
17
  "type": "Counter",
18
18
  "primary": true
19
- },
19
+ },
20
20
  {
21
21
  "name": "action",
22
22
  "title": "Action",
23
23
  "description": "The action associated with this card.",
24
24
  "type": "AccommodationRequestAction",
25
25
  "nullable": false
26
- },
26
+ },
27
27
  {
28
28
  "name": "student",
29
29
  "title": "Φοιτητής",
@@ -47,80 +47,75 @@
47
47
  "type": "AcademicYear",
48
48
  "expandable": true,
49
49
  "nullable": false
50
- },
51
-
52
- {
53
- "name": "serialNumber",
54
- "title": "Number",
55
- "type": "Integer",
56
- "nullable": false,
57
- "value": "javascript:return this.newid();"
58
- },
59
- {
60
- "name": "dateCreated",
61
- "title": "Date Created",
62
- "description": "The date on which this item was created.",
63
- "type": "DateTime",
64
- "value": "javascript:return (new Date());",
65
- "readonly": true
66
- },
67
- {
68
- "@id": "http://schema.org/validFrom",
69
- "name": "validFrom",
70
- "title": "validFrom",
71
- "description": "The date when the item becomes valid.",
72
- "type": "DateTime",
73
- "nullable": false
74
- },
75
- {
76
- "@id": "http://schema.org/validThrough",
77
- "name": "validThrough",
78
- "title": "validThrough",
79
- "description": "The date after when the item is not valid.",
80
- "type": "DateTime",
81
- "nullable": false
82
- },
83
- {
84
- "@id": "http://schema.org/cardStatus",
85
- "name": "cardStatus",
86
- "title": "cardStatus",
87
- "description": "Indicates the current disposition of the Card.",
88
- "type": "ActionStatusType",
89
- "value": "javascript:return { alternateName:'PotentialActionStatus' };"
90
- },
91
- {
92
- "name": "dateCancelled",
93
- "title": "Date Cancelled",
94
- "description": "The date on which this item was cancelled.",
95
- "type": "DateTime",
96
- "nullable": true
97
- },
98
- {
99
- "name": "cancelReason",
100
- "title": "Λόγοι ακύρωσης",
101
- "description": "Λόγοι ακύρωσης",
102
- "type": "Text",
103
- "size": 255,
104
- "many": false,
105
- "nullable": true
106
- },
107
- {
108
- "name": "dateModified",
109
- "title": "Ημερομηνία τροποποίησης",
110
- "description": "Ημερομηνία τροποποίησης",
111
- "type": "DateTime",
112
- "nullable": false,
113
- "value": "javascript:return new Date();",
114
- "calculation": "javascript:return this.now();"
115
- }
116
- ],
117
- "constraints": [
50
+ },
51
+ {
52
+ "name": "serialNumber",
53
+ "title": "Number",
54
+ "type": "Integer",
55
+ "nullable": false,
56
+ "value": "javascript:return this.newid();"
57
+ },
118
58
  {
119
- "type": "unique",
120
- "fields": [
121
- "student",
122
- "academicYear"
123
- ]
59
+ "name": "dateCreated",
60
+ "title": "Date Created",
61
+ "description": "The date on which this item was created.",
62
+ "type": "DateTime",
63
+ "value": "javascript:return (new Date());",
64
+ "readonly": true
65
+ },
66
+ {
67
+ "@id": "http://schema.org/validFrom",
68
+ "name": "validFrom",
69
+ "title": "validFrom",
70
+ "description": "The date when the item becomes valid.",
71
+ "type": "DateTime",
72
+ "nullable": false
73
+ },
74
+ {
75
+ "@id": "http://schema.org/validThrough",
76
+ "name": "validThrough",
77
+ "title": "validThrough",
78
+ "description": "The date after when the item is not valid.",
79
+ "type": "DateTime",
80
+ "nullable": false
81
+ },
82
+ {
83
+ "@id": "http://schema.org/cardStatus",
84
+ "name": "cardStatus",
85
+ "title": "cardStatus",
86
+ "description": "Indicates the current disposition of the Card.",
87
+ "type": "ActionStatusType",
88
+ "value": "javascript:return { alternateName:'PotentialActionStatus' };"
89
+ },
90
+ {
91
+ "name": "dateCancelled",
92
+ "title": "Date Cancelled",
93
+ "description": "The date on which this item was cancelled.",
94
+ "type": "DateTime",
95
+ "nullable": true
96
+ },
97
+ {
98
+ "name": "cancelReason",
99
+ "title": "Λόγοι ακύρωσης",
100
+ "description": "Λόγοι ακύρωσης",
101
+ "type": "Text",
102
+ "size": 255,
103
+ "many": false,
104
+ "nullable": true
105
+ },
106
+ {
107
+ "name": "dateModified",
108
+ "title": "Ημερομηνία τροποποίησης",
109
+ "description": "Ημερομηνία τροποποίησης",
110
+ "type": "DateTime",
111
+ "nullable": false,
112
+ "value": "javascript:return new Date();",
113
+ "calculation": "javascript:return this.now();"
114
+ },
115
+ {
116
+ "name": "isActive",
117
+ "readonly": true,
118
+ "virtual": true
124
119
  }
125
120
  ],
126
121
  "privileges": [
@@ -143,7 +138,7 @@
143
138
  "type": "self",
144
139
  "account": "Students",
145
140
  "filter": "student/user eq me()"
146
- }
141
+ }
147
142
  ],
148
143
  "eventListeners": [
149
144
  {
@@ -159,5 +154,4 @@
159
154
  "type": "./listeners/SendEmailAfterCardStatusChangeListener"
160
155
  }
161
156
  ]
162
- }
163
-
157
+ }
@@ -259,6 +259,42 @@ export default [
259
259
  'write'
260
260
  ]
261
261
  },
262
+ {
263
+ 'scope': [
264
+ 'accommodation'
265
+ ],
266
+ 'resource': '/api/RoomTypes',
267
+ 'access': [
268
+ 'read'
269
+ ]
270
+ },
271
+ {
272
+ 'scope': [
273
+ 'accommodation'
274
+ ],
275
+ 'resource': '/api/Genders',
276
+ 'access': [
277
+ 'read'
278
+ ]
279
+ },
280
+ {
281
+ 'scope': [
282
+ 'accommodation'
283
+ ],
284
+ 'resource': '/api/Regions',
285
+ 'access': [
286
+ 'read'
287
+ ]
288
+ },
289
+ {
290
+ 'scope': [
291
+ 'accommodation'
292
+ ],
293
+ 'resource': '/api/Countries',
294
+ 'access': [
295
+ 'read'
296
+ ]
297
+ },
262
298
  {
263
299
  'scope': [
264
300
  'accommodation'
@@ -282,7 +318,7 @@ export default [
282
318
  'scope': [
283
319
  'students'
284
320
  ],
285
- 'resource': '/api/Accommodations/?$',
321
+ 'resource': '/api/AccommodationPlaces/?$',
286
322
  'access': [
287
323
  'read'
288
324
  ]
@@ -13,18 +13,35 @@ export function beforeSave(event, callback) {
13
13
  /**
14
14
  * @type {Student|DataObject|*}
15
15
  */
16
- const student = context.model('Student').convert(event.target.student);
17
- // const isActive = await student.silent().isActive();
16
+ const previous = event.previous;
17
+ let student = context.model('Student').convert(event.target.student);
18
+ if (!student){
19
+ if (previous){
20
+ student = context.model('Student').convert(previous.student);
21
+ }else{
22
+ throw new DataError('ERR_STATUS','Student cannot be found');
23
+ }
24
+ }
18
25
  const isActive = await student.is(':active');
19
- if (!isActive) {
26
+
27
+ if (!isActive && event.state===1)
28
+ {
20
29
  throw new DataError('ERR_STATUS',
21
30
  'Invalid student status. Student Accommodation Card may be published upon an active student only.'+student
22
31
  , null,
23
32
  'Student', 'studentStatus');
24
33
  }
25
-
26
- const studentObject = await context.model('Student').where('id').equal(student.id).silent().flatten().getItem();
27
-
34
+ if (Object.prototype.hasOwnProperty.call(event.target, 'CardStatus')) {
35
+ const previousActionStatus = await context.model('ActionStatusType').find(previous.cardStatus).select('alternateName').value();
36
+ const targetActionStatus = await context.model('ActionStatusType').find(event.target.cardStatus).select('alternateName').value();
37
+ if (!isActive && previousActionStatus !== targetActionStatus && targetActionStatus!=='CancelledActionStatus') {
38
+ throw new DataError('ERR_STATUS',
39
+ 'The student accommodation card cannot be activated for an inactive student' + student
40
+ , null,
41
+ 'Student', 'studentStatus');
42
+ }
43
+ }
44
+
28
45
  const academicYear = context.model('AcademicYear').convert(event.target.academicYear).getId();
29
46
  if (Number.isInteger(academicYear) === false) {
30
47
  throw new DataError('E_VALUE', 'Invalid academic year. Expected number.', null, 'AcademicYear', 'id');
@@ -1,6 +1,7 @@
1
1
 
2
- import { DataObject, EdmMapping } from '@themost/data';
3
- import { DataError, DataNotFoundError } from "@themost/common"
2
+ import { EdmMapping } from '@themost/data';
3
+ import { DataError, DataNotFoundError, Args, TraceUtils, LangUtils } from "@themost/common"
4
+ import EnableAttachmentModel from './EnableAttachmentModel';
4
5
 
5
6
  /**
6
7
  * @class
@@ -20,7 +21,7 @@ import { DataError, DataNotFoundError } from "@themost/common"
20
21
  * @property {User|any} student
21
22
  */
22
23
  @EdmMapping.entityType('StudentAccommodationCard')
23
- class StudentAccommodationCard extends DataObject {
24
+ class StudentAccommodationCard extends EnableAttachmentModel {
24
25
  /**
25
26
  * @constructor
26
27
  */
@@ -35,58 +36,180 @@ class StudentAccommodationCard extends DataObject {
35
36
 
36
37
  async cancel(data) {
37
38
 
38
- if (!data.cancelReason) {
39
- throw new DataError('E_CANCEL_REASON', 'Cancel Reason should be provided');
40
- }
39
+ Args.notEmpty(data.cancelReason, 'Cancel Reason');
41
40
 
41
+ if ( !LangUtils.isDate(data.dateCancelled) ) {
42
+ throw new DataError('E_STATE', 'Cancel date can not be empty or invalid date');
43
+ }
42
44
  await this.context.executeInTransactionAsync(async () => {
43
45
  // find StudentAccommodationCard from id
44
- const StudentAccommodationCard = await this.context.model('StudentAccommodationCard')
46
+ const currentStudentAccommodationCard = await this.context.model('StudentAccommodationCard')
45
47
  .where('id')
46
48
  .equal(this.getId())
47
- .select('id', 'active', 'action', 'student', 'academicPeriod', 'academicYear')
48
- .flatten()
49
+ .select('id', 'serialNumber', 'action', 'student', 'cardStatus', 'academicYear', 'isActive', 'validThrough', 'accommodationRoom')
50
+ .expand('cardStatus')
49
51
  .getItem();
50
52
 
51
- // validate existance and permission
52
- if (StudentAccommodationCard == null) {
53
- throw new DataNotFoundError('The specified Student Accommodation Card cannot be found or is inaccessible');
54
- }
53
+ Args.notNull(currentStudentAccommodationCard, 'The specified Student Accommodation Card' )
55
54
  // StudentAccommodationCard already cancelled throw error
56
- if (StudentAccommodationCard.active === false) {
57
- throw new DataError('E_ACTIVE', 'The specified Student Accommodation Card is already cancelled')
55
+ if ( currentStudentAccommodationCard?.cardStatus?.alternateName === 'CompletedActionStatus') {
56
+ throw new DataError('E_ACTIVE', 'The specified Student Accommodation Card in not active')
58
57
  }
58
+ // cancel StudentAccommodationCard
59
+ currentStudentAccommodationCard.cardStatus = { alternateName: 'CompletedActionStatus' };
60
+ currentStudentAccommodationCard.cancelReason = data.cancelReason;
61
+ currentStudentAccommodationCard.dateCancelled = data.dateCancelled;
62
+ await this.context.model('StudentAccommodationCard').save(currentStudentAccommodationCard);
63
+ const room = currentStudentAccommodationCard.accommodationRoom?.id || currentStudentAccommodationCard?.accommodationRoom;
64
+ Args.notNull(room, 'Accommodation room can not be empty');
65
+ Args.notNumber(room, 'Accommodation room should be a number');
66
+ await calculateOccupancy(this.context, room);
67
+ },(err)=>{
68
+ TraceUtils.log(err);
69
+ });
70
+ }
59
71
 
60
- //validate AccommodationRequestAction
61
- if (StudentAccommodationCard.action == null) {
62
- throw new DataError('E_ACTION', 'The Student Accommodation Card is not linked to a Accommodation Request Action')
63
- }
72
+ @EdmMapping.param('data', 'Object', false, true)
73
+ @EdmMapping.action('changeRoom', 'StudentAccommodationCard')
64
74
 
65
- // cancel StudentAccommodationCard
66
- StudentAccommodationCard.cardStatus = {alternateName: "CancelledActionStatus"};
67
- StudentAccommodationCard.cancelReason = data.cancelReason;
68
- await this.context.model('StudentAccommodationCard').save(StudentAccommodationCard);
69
-
70
- const action = await this.context.model('AccommodationRequestAction').where('id').equal(StudentAccommodationCard.action).select('id', 'actionStatus/alternateName as requestStatus').getItem();
71
- //validate AccommodationRequestAction
72
- if (action == null) {
73
- throw new DataError('E_ACTION', 'The specified AccommodationRequestAction cannot be found or is inaccessible')
75
+ async changeRoom(data) {
76
+
77
+ const room = data.accommodationRoom?.id || data?.accommodationRoom;
78
+ Args.notNull(room, 'Accommodation room can not be empty');
79
+ Args.notNumber(room, 'Accommodation room should be a number');
80
+
81
+ if ( !LangUtils.isDate(data.validThrough) ) {
82
+ throw new DataError('E_STATE', 'Valid through date can not be empty or it is not a valid date');
83
+ }
84
+
85
+ Args.notEmpty(data.cancelReason, 'Cancel Reason cannot be empty');
86
+
87
+ if ( !LangUtils.isDate(data.dateCancelled) ) {
88
+ throw new DataError('E_STATE', 'Cancel date can not be empty or invalid date');
89
+ }
90
+
91
+ await this.context.executeInTransactionAsync(async () => {
92
+ const currentStudentAccommodationCard = await this.context.model('StudentAccommodationCard')
93
+ .where('id')
94
+ .equal(this.getId())
95
+ .select('id', 'serialNumber', 'action', 'student', 'cardStatus', 'academicYear', 'isActive', 'validThrough', 'accommodationRoom')
96
+ .expand('cardStatus')
97
+ .getItem();
98
+
99
+ Args.notNull(currentStudentAccommodationCard, 'The specified Student Accommodation Card cannot be found or is inaccessible');
100
+
101
+ if (currentStudentAccommodationCard?.cardStatus?.alternateName !== 'ActiveActionStatus') {
102
+ throw new DataError('E_STATE', 'The accommodation room cannot be changed for inactive accommodation card');
74
103
  }
104
+ const newCard = {
105
+ action: currentStudentAccommodationCard.action,
106
+ student: currentStudentAccommodationCard.student,
107
+ accommodationRoom: room,
108
+ academicYear: currentStudentAccommodationCard.academicYear,
109
+ validFrom: data.validThrough,
110
+ validThrough: currentStudentAccommodationCard.validThrough,
111
+ cardStatus: {
112
+ alternateName: 'ActiveActionStatus'
113
+ }
114
+ }
115
+
116
+ currentStudentAccommodationCard.cancelReason = data.cancelReason;
117
+ currentStudentAccommodationCard.dateCancelled = data.dateCancelled;
118
+ currentStudentAccommodationCard.validThrough = data.dateCancelled;
119
+ delete currentStudentAccommodationCard.cardStatus ;
120
+ currentStudentAccommodationCard.cardStatus= { alternateName : 'CompletedActionStatus'};
75
121
 
76
- if (action.requestStatus !== 'CompletedActionStatus') {
77
- throw new DataError('E_ACTION', 'The specified AccommodationRequestAction is not in Completed state')
122
+ await this.context.model('StudentAccommodationCard').save(currentStudentAccommodationCard);
123
+ const currentAccommodationRoom = currentStudentAccommodationCard.accommodationRoom?.id || currentStudentAccommodationCard?.accommodationRoom;
124
+ Args.notNull(currentAccommodationRoom, 'The specified Student Accommodation Card does not contain any room');
125
+ await calculateOccupancy(this.context, currentAccommodationRoom);
126
+ await this.context.model('StudentAccommodationCards').save(newCard);
127
+ await calculateOccupancy(this.context, room);
128
+ },(err)=>{
129
+ TraceUtils.log(err);
130
+ });
131
+ }
132
+
133
+ @EdmMapping.param('data', 'Object', false, true)
134
+ @EdmMapping.action('addToRoom', 'StudentAccommodationCard')
135
+
136
+ async addToRoom(data) {
137
+ const room = data.accommodationRoom?.id || data?.accommodationRoom;
138
+ Args.notNull(room, 'Accommodation room can not be empty');
139
+ Args.notNumber(room, 'Accommodation room should be a number');
140
+ if (!LangUtils.isDate(data.validFrom)) {
141
+ throw new DataError('E_STATE', 'Valid through date can not be empty or it is not a valid date');
142
+ }
143
+ await this.context.executeInTransactionAsync(async () => {
144
+ const currentStudentAccommodationCard = await this.context.model('StudentAccommodationCard')
145
+ .where('id')
146
+ .equal(this.getId())
147
+ .select('id', 'serialNumber', 'action', 'student', 'cardStatus', 'academicYear', 'isActive', 'validThrough')
148
+ .expand('cardStatus')
149
+ .getItem();
150
+ Args.notNull(currentStudentAccommodationCard, 'The specified Student Accommodation Card cannot be found or is inaccessible');
151
+ if (currentStudentAccommodationCard?.cardStatus?.alternateName !== 'ActiveActionStatus') {
152
+ throw new DataError('E_STATE', 'Add to room action can only be applied when student accommodation card status is Active');
78
153
  }
154
+ currentStudentAccommodationCard.accommodationRoom = room;
155
+ currentStudentAccommodationCard.validFrom = data.validFrom;
156
+ await this.context.model('StudentAccommodationCard').save(currentStudentAccommodationCard);
157
+ await calculateOccupancy(this.context, room); // calculate occupancy after new room updated
158
+ }, (err) => {
159
+ TraceUtils.log(err);
160
+ });
161
+ }
79
162
 
80
- action.actionStatus = {
81
- alternateName: "ActiveActionStatus"
163
+ @EdmMapping.param('data', 'Object', false, true)
164
+ @EdmMapping.action('revertToPending', 'StudentAccommodationCard')
165
+
166
+ async revertToPending(data) {
167
+ await this.context.executeInTransactionAsync(async () => {
168
+ const currentStudentAccommodationCard = await this.context.model('StudentAccommodationCard')
169
+ .where('id')
170
+ .equal(this.getId())
171
+ .select('id', 'serialNumber', 'action', 'student', 'cardStatus', 'academicYear', 'isActive', 'validThrough', 'accommodationRoom')
172
+ .expand('cardStatus')
173
+ .getItem();
174
+
175
+ Args.notNull(currentStudentAccommodationCard, 'The specified Student Accommodation Card cannot be found or is inaccessible');
176
+ if (currentStudentAccommodationCard?.cardStatus?.alternateName !== 'ActiveActionStatus') {
177
+ throw new DataError('E_STATE', 'Inactive accommodation card');
82
178
  }
83
179
 
84
- delete action.requestStatus;
85
- await this.context.model('AccommodationRequestAction').save(action);
180
+ const accommodationRoom = currentStudentAccommodationCard.accommodationRoom?.id || currentStudentAccommodationCard?.accommodationRoom;
181
+ currentStudentAccommodationCard.cardStatus = { alternateName: 'ActiveActionStatus' };
182
+ currentStudentAccommodationCard.accommodationRoom = null;
183
+ await this.context.model('StudentAccommodationCard').save(currentStudentAccommodationCard);
184
+ await calculateOccupancy(this.context, accommodationRoom);
86
185
  });
87
186
  }
88
187
 
89
188
 
90
189
  }
190
+ async function calculateOccupancy(context, accommodationRoom) {
191
+ if (accommodationRoom == null) {
192
+ return;
193
+ }
194
+ const currentOccupancy = await context.model('StudentAccommodationCard')
195
+ .where('accommodationRoom').equal(accommodationRoom)
196
+ .and('cardStatus/alternateName').equal('ActiveActionStatus')
197
+ .and('accommodationRoom/outOfService').equal(0)
198
+ .and('accommodationRoom/roomType/accessibleType').equal(1)
199
+ .select('count(id) as total', 'accommodationRoom/occupancy as maxOccupancy')
200
+ .groupBy('accommodationRoom/occupancy')
201
+ .silent()
202
+ .getItem();
203
+
204
+ const totalOccupancy = (currentOccupancy && currentOccupancy.total) || 0;
205
+ if (totalOccupancy && totalOccupancy > currentOccupancy.maxOccupancy) {
206
+ throw new DataError('E_STATE', 'No room space for new resident');
207
+ }
208
+
209
+ await context.model('AccommodationRoom').save({
210
+ id: accommodationRoom,
211
+ currentOccupancy: totalOccupancy
212
+ });
213
+ }
91
214
 
92
215
  module.exports = StudentAccommodationCard;