better_model 1.0.0 → 1.2.0
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.
- checksums.yaml +4 -4
- data/README.md +427 -85
- data/lib/better_model/archivable.rb +10 -10
- data/lib/better_model/predicable.rb +39 -52
- data/lib/better_model/sortable.rb +4 -0
- data/lib/better_model/state_transition.rb +106 -0
- data/lib/better_model/stateable/configurator.rb +300 -0
- data/lib/better_model/stateable/errors.rb +45 -0
- data/lib/better_model/stateable/guard.rb +87 -0
- data/lib/better_model/stateable/transition.rb +143 -0
- data/lib/better_model/stateable.rb +354 -0
- data/lib/better_model/traceable.rb +498 -0
- data/lib/better_model/validatable/business_rule_validator.rb +47 -0
- data/lib/better_model/validatable/configurator.rb +245 -0
- data/lib/better_model/validatable/order_validator.rb +77 -0
- data/lib/better_model/validatable.rb +270 -0
- data/lib/better_model/version.rb +1 -1
- data/lib/better_model/version_record.rb +63 -0
- data/lib/better_model.rb +15 -2
- data/lib/generators/better_model/stateable/install_generator.rb +37 -0
- data/lib/generators/better_model/stateable/stateable_generator.rb +50 -0
- data/lib/generators/better_model/stateable/templates/README +38 -0
- data/lib/generators/better_model/stateable/templates/install_migration.rb.tt +20 -0
- data/lib/generators/better_model/stateable/templates/migration.rb.tt +9 -0
- data/lib/generators/better_model/traceable/templates/create_table_migration.rb.tt +28 -0
- data/lib/generators/better_model/traceable/traceable_generator.rb +77 -0
- metadata +22 -3
|
@@ -115,7 +115,7 @@ module BetterModel
|
|
|
115
115
|
|
|
116
116
|
# Applica default scope SOLO se configurato
|
|
117
117
|
if archivable_config[:skip_archived_by_default]
|
|
118
|
-
default_scope -> {
|
|
118
|
+
default_scope -> { where(archived_at: nil) }
|
|
119
119
|
end
|
|
120
120
|
end
|
|
121
121
|
end
|
|
@@ -124,19 +124,19 @@ module BetterModel
|
|
|
124
124
|
#
|
|
125
125
|
# @return [ActiveRecord::Relation]
|
|
126
126
|
def archived_only
|
|
127
|
-
raise
|
|
127
|
+
raise ArchivableNotEnabledError unless archivable_enabled?
|
|
128
128
|
unscoped.archived
|
|
129
129
|
end
|
|
130
130
|
|
|
131
131
|
# Helper: alias per archived_at_today
|
|
132
132
|
def archived_today
|
|
133
|
-
raise
|
|
133
|
+
raise ArchivableNotEnabledError unless archivable_enabled?
|
|
134
134
|
archived_at_today
|
|
135
135
|
end
|
|
136
136
|
|
|
137
137
|
# Helper: alias per archived_at_this_week
|
|
138
138
|
def archived_this_week
|
|
139
|
-
raise
|
|
139
|
+
raise ArchivableNotEnabledError unless archivable_enabled?
|
|
140
140
|
archived_at_this_week
|
|
141
141
|
end
|
|
142
142
|
|
|
@@ -145,7 +145,7 @@ module BetterModel
|
|
|
145
145
|
# @param duration [ActiveSupport::Duration] Durata (es: 7.days)
|
|
146
146
|
# @return [ActiveRecord::Relation]
|
|
147
147
|
def archived_recently(duration = 7.days)
|
|
148
|
-
raise
|
|
148
|
+
raise ArchivableNotEnabledError unless archivable_enabled?
|
|
149
149
|
archived_at_within(duration)
|
|
150
150
|
end
|
|
151
151
|
|
|
@@ -164,10 +164,10 @@ module BetterModel
|
|
|
164
164
|
# @param by [Integer, Object] ID utente o oggetto user (opzionale)
|
|
165
165
|
# @param reason [String] Motivo dell'archiviazione (opzionale)
|
|
166
166
|
# @return [self]
|
|
167
|
-
# @raise [
|
|
167
|
+
# @raise [ArchivableNotEnabledError] se archivable non è attivo
|
|
168
168
|
# @raise [AlreadyArchivedError] se già archiviato
|
|
169
169
|
def archive!(by: nil, reason: nil)
|
|
170
|
-
raise
|
|
170
|
+
raise ArchivableNotEnabledError unless self.class.archivable_enabled?
|
|
171
171
|
raise AlreadyArchivedError, "Record is already archived" if archived?
|
|
172
172
|
|
|
173
173
|
self.archived_at = Time.current
|
|
@@ -186,10 +186,10 @@ module BetterModel
|
|
|
186
186
|
# Ripristina record archiviato
|
|
187
187
|
#
|
|
188
188
|
# @return [self]
|
|
189
|
-
# @raise [
|
|
189
|
+
# @raise [ArchivableNotEnabledError] se archivable non è attivo
|
|
190
190
|
# @raise [NotArchivedError] se non archiviato
|
|
191
191
|
def restore!
|
|
192
|
-
raise
|
|
192
|
+
raise ArchivableNotEnabledError unless self.class.archivable_enabled?
|
|
193
193
|
raise NotArchivedError, "Record is not archived" unless archived?
|
|
194
194
|
|
|
195
195
|
self.archived_at = nil
|
|
@@ -241,7 +241,7 @@ module BetterModel
|
|
|
241
241
|
class AlreadyArchivedError < ArchivableError; end
|
|
242
242
|
class NotArchivedError < ArchivableError; end
|
|
243
243
|
|
|
244
|
-
class
|
|
244
|
+
class ArchivableNotEnabledError < ArchivableError
|
|
245
245
|
def initialize(msg = nil)
|
|
246
246
|
super(msg || "Archivable is not enabled. Add 'archivable do...end' to your model.")
|
|
247
247
|
end
|
|
@@ -58,6 +58,9 @@ module BetterModel
|
|
|
58
58
|
column = columns_hash[field_name.to_s]
|
|
59
59
|
next unless column
|
|
60
60
|
|
|
61
|
+
# Base predicates available for all column types
|
|
62
|
+
define_base_predicates(field_name, column.type)
|
|
63
|
+
|
|
61
64
|
case column.type
|
|
62
65
|
when :string, :text
|
|
63
66
|
define_string_predicates(field_name)
|
|
@@ -68,18 +71,14 @@ module BetterModel
|
|
|
68
71
|
when :date, :datetime, :time, :timestamp
|
|
69
72
|
define_date_predicates(field_name)
|
|
70
73
|
when :jsonb, :json
|
|
71
|
-
# JSONB/JSON:
|
|
72
|
-
define_base_predicates(field_name)
|
|
74
|
+
# JSONB/JSON: PostgreSQL-specific predicates
|
|
73
75
|
define_postgresql_jsonb_predicates(field_name)
|
|
74
76
|
else
|
|
75
77
|
# Check for array columns (PostgreSQL)
|
|
76
78
|
if column.respond_to?(:array?) && column.array?
|
|
77
|
-
define_base_predicates(field_name)
|
|
78
79
|
define_postgresql_array_predicates(field_name)
|
|
79
|
-
else
|
|
80
|
-
# Default: genera solo predicati base
|
|
81
|
-
define_base_predicates(field_name)
|
|
82
80
|
end
|
|
81
|
+
# Unknown types only get base predicates
|
|
83
82
|
end
|
|
84
83
|
end
|
|
85
84
|
end
|
|
@@ -143,7 +142,7 @@ module BetterModel
|
|
|
143
142
|
end
|
|
144
143
|
|
|
145
144
|
# Genera predicati base: _eq, _not_eq, _present
|
|
146
|
-
def define_base_predicates(field_name)
|
|
145
|
+
def define_base_predicates(field_name, column_type = nil)
|
|
147
146
|
table = arel_table
|
|
148
147
|
field = table[field_name]
|
|
149
148
|
|
|
@@ -151,24 +150,27 @@ module BetterModel
|
|
|
151
150
|
scope :"#{field_name}_eq", ->(value) { where(field.eq(value)) }
|
|
152
151
|
scope :"#{field_name}_not_eq", ->(value) { where(field.not_eq(value)) }
|
|
153
152
|
|
|
154
|
-
# Presence
|
|
155
|
-
|
|
153
|
+
# Presence - skip for string/text types as they get specialized version
|
|
154
|
+
# String types need to check for both nil and empty string
|
|
155
|
+
unless [ :string, :text ].include?(column_type)
|
|
156
|
+
scope :"#{field_name}_present", -> { where(field.not_eq(nil)) }
|
|
157
|
+
end
|
|
156
158
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
)
|
|
159
|
+
scopes_to_register = [ :"#{field_name}_eq", :"#{field_name}_not_eq" ]
|
|
160
|
+
scopes_to_register << :"#{field_name}_present" unless [ :string, :text ].include?(column_type)
|
|
161
|
+
|
|
162
|
+
register_predicable_scopes(*scopes_to_register)
|
|
162
163
|
end
|
|
163
164
|
|
|
164
|
-
# Genera predicati per campi stringa (
|
|
165
|
+
# Genera predicati per campi stringa (12 scope)
|
|
166
|
+
# Base predicates (_eq, _not_eq) are defined separately
|
|
167
|
+
# _present is defined here to handle both nil and empty strings
|
|
165
168
|
def define_string_predicates(field_name)
|
|
166
169
|
table = arel_table
|
|
167
170
|
field = table[field_name]
|
|
168
171
|
|
|
169
|
-
#
|
|
170
|
-
scope :"#{field_name}
|
|
171
|
-
scope :"#{field_name}_not_eq", ->(value) { where(field.not_eq(value)) }
|
|
172
|
+
# String-specific presence check (checks both nil and empty string)
|
|
173
|
+
scope :"#{field_name}_present", -> { where(field.not_eq(nil).and(field.not_eq(""))) }
|
|
172
174
|
|
|
173
175
|
# Pattern matching (4)
|
|
174
176
|
scope :"#{field_name}_matches", ->(pattern) { where(field.matches(pattern)) }
|
|
@@ -203,14 +205,11 @@ module BetterModel
|
|
|
203
205
|
scope :"#{field_name}_in", ->(values) { where(field.in(Array(values))) }
|
|
204
206
|
scope :"#{field_name}_not_in", ->(values) { where.not(field.in(Array(values))) }
|
|
205
207
|
|
|
206
|
-
# Presence (
|
|
207
|
-
scope :"#{field_name}_present", -> { where(field.not_eq(nil).and(field.not_eq(""))) }
|
|
208
|
+
# Presence (2) - _present is overridden above for string-specific behavior
|
|
208
209
|
scope :"#{field_name}_blank", -> { where(field.eq(nil).or(field.eq(""))) }
|
|
209
210
|
scope :"#{field_name}_null", -> { where(field.eq(nil)) }
|
|
210
211
|
|
|
211
212
|
register_predicable_scopes(
|
|
212
|
-
:"#{field_name}_eq",
|
|
213
|
-
:"#{field_name}_not_eq",
|
|
214
213
|
:"#{field_name}_matches",
|
|
215
214
|
:"#{field_name}_start",
|
|
216
215
|
:"#{field_name}_end",
|
|
@@ -226,14 +225,13 @@ module BetterModel
|
|
|
226
225
|
)
|
|
227
226
|
end
|
|
228
227
|
|
|
229
|
-
# Genera predicati per campi numerici (
|
|
228
|
+
# Genera predicati per campi numerici (8 scope)
|
|
229
|
+
# Base predicates (_eq, _not_eq, _present) are defined separately
|
|
230
230
|
def define_numeric_predicates(field_name)
|
|
231
231
|
table = arel_table
|
|
232
232
|
field = table[field_name]
|
|
233
233
|
|
|
234
|
-
# Comparison (
|
|
235
|
-
scope :"#{field_name}_eq", ->(value) { where(field.eq(value)) }
|
|
236
|
-
scope :"#{field_name}_not_eq", ->(value) { where(field.not_eq(value)) }
|
|
234
|
+
# Comparison (4)
|
|
237
235
|
scope :"#{field_name}_lt", ->(value) { where(field.lt(value)) }
|
|
238
236
|
scope :"#{field_name}_lteq", ->(value) { where(field.lteq(value)) }
|
|
239
237
|
scope :"#{field_name}_gt", ->(value) { where(field.gt(value)) }
|
|
@@ -247,12 +245,7 @@ module BetterModel
|
|
|
247
245
|
scope :"#{field_name}_in", ->(values) { where(field.in(Array(values))) }
|
|
248
246
|
scope :"#{field_name}_not_in", ->(values) { where.not(field.in(Array(values))) }
|
|
249
247
|
|
|
250
|
-
# Presence (1)
|
|
251
|
-
scope :"#{field_name}_present", -> { where(field.not_eq(nil)) }
|
|
252
|
-
|
|
253
248
|
register_predicable_scopes(
|
|
254
|
-
:"#{field_name}_eq",
|
|
255
|
-
:"#{field_name}_not_eq",
|
|
256
249
|
:"#{field_name}_lt",
|
|
257
250
|
:"#{field_name}_lteq",
|
|
258
251
|
:"#{field_name}_gt",
|
|
@@ -265,32 +258,27 @@ module BetterModel
|
|
|
265
258
|
)
|
|
266
259
|
end
|
|
267
260
|
|
|
268
|
-
# Genera predicati per campi booleani (
|
|
261
|
+
# Genera predicati per campi booleani (2 scope)
|
|
262
|
+
# Base predicates (_eq, _not_eq, _present) are defined separately
|
|
269
263
|
def define_boolean_predicates(field_name)
|
|
270
264
|
table = arel_table
|
|
271
265
|
field = table[field_name]
|
|
272
266
|
|
|
273
|
-
# Comparison (2)
|
|
274
|
-
scope :"#{field_name}_eq", ->(value) { where(field.eq(value)) }
|
|
275
|
-
scope :"#{field_name}_not_eq", ->(value) { where(field.not_eq(value)) }
|
|
276
|
-
|
|
277
267
|
# Boolean shortcuts (2)
|
|
278
268
|
scope :"#{field_name}_true", -> { where(field.eq(true)) }
|
|
279
269
|
scope :"#{field_name}_false", -> { where(field.eq(false)) }
|
|
280
270
|
|
|
281
|
-
# Presence (1)
|
|
282
|
-
scope :"#{field_name}_present", -> { where(field.not_eq(nil)) }
|
|
283
|
-
|
|
284
271
|
register_predicable_scopes(
|
|
285
|
-
:"#{field_name}_eq",
|
|
286
|
-
:"#{field_name}_not_eq",
|
|
287
272
|
:"#{field_name}_true",
|
|
288
|
-
:"#{field_name}_false"
|
|
289
|
-
:"#{field_name}_present"
|
|
273
|
+
:"#{field_name}_false"
|
|
290
274
|
)
|
|
291
275
|
end
|
|
292
276
|
|
|
293
277
|
# Genera predicati per campi array PostgreSQL (3 scope)
|
|
278
|
+
#
|
|
279
|
+
# NOTA: Questo metodo non è coperto da test automatici perché richiede
|
|
280
|
+
# PostgreSQL. I test vengono eseguiti su SQLite per performance.
|
|
281
|
+
# Testare manualmente su PostgreSQL con: rails console RAILS_ENV=test
|
|
294
282
|
def define_postgresql_array_predicates(field_name)
|
|
295
283
|
return unless postgresql_adapter?
|
|
296
284
|
|
|
@@ -343,6 +331,10 @@ module BetterModel
|
|
|
343
331
|
end
|
|
344
332
|
|
|
345
333
|
# Genera predicati per campi JSONB PostgreSQL (4 scope)
|
|
334
|
+
#
|
|
335
|
+
# NOTA: Questo metodo non è coperto da test automatici perché richiede
|
|
336
|
+
# PostgreSQL con supporto JSONB. I test vengono eseguiti su SQLite per performance.
|
|
337
|
+
# Testare manualmente su PostgreSQL con: rails console RAILS_ENV=test
|
|
346
338
|
def define_postgresql_jsonb_predicates(field_name)
|
|
347
339
|
return unless postgresql_adapter?
|
|
348
340
|
|
|
@@ -391,14 +383,13 @@ module BetterModel
|
|
|
391
383
|
)
|
|
392
384
|
end
|
|
393
385
|
|
|
394
|
-
# Genera predicati per campi data/datetime (
|
|
386
|
+
# Genera predicati per campi data/datetime (17 scope)
|
|
387
|
+
# Base predicates (_eq, _not_eq, _present) are defined separately
|
|
395
388
|
def define_date_predicates(field_name)
|
|
396
389
|
table = arel_table
|
|
397
390
|
field = table[field_name]
|
|
398
391
|
|
|
399
|
-
# Comparison (
|
|
400
|
-
scope :"#{field_name}_eq", ->(value) { where(field.eq(value)) }
|
|
401
|
-
scope :"#{field_name}_not_eq", ->(value) { where(field.not_eq(value)) }
|
|
392
|
+
# Comparison (4)
|
|
402
393
|
scope :"#{field_name}_lt", ->(value) { where(field.lt(value)) }
|
|
403
394
|
scope :"#{field_name}_lteq", ->(value) { where(field.lteq(value)) }
|
|
404
395
|
scope :"#{field_name}_gt", ->(value) { where(field.gt(value)) }
|
|
@@ -440,15 +431,12 @@ module BetterModel
|
|
|
440
431
|
where(field.gteq(time_ago))
|
|
441
432
|
}
|
|
442
433
|
|
|
443
|
-
# Presence (
|
|
444
|
-
scope :"#{field_name}_present", -> { where(field.not_eq(nil)) }
|
|
434
|
+
# Presence (3) - _present is defined in base predicates
|
|
445
435
|
scope :"#{field_name}_blank", -> { where(field.eq(nil)) }
|
|
446
436
|
scope :"#{field_name}_null", -> { where(field.eq(nil)) }
|
|
447
437
|
scope :"#{field_name}_not_null", -> { where(field.not_eq(nil)) }
|
|
448
438
|
|
|
449
439
|
register_predicable_scopes(
|
|
450
|
-
:"#{field_name}_eq",
|
|
451
|
-
:"#{field_name}_not_eq",
|
|
452
440
|
:"#{field_name}_lt",
|
|
453
441
|
:"#{field_name}_lteq",
|
|
454
442
|
:"#{field_name}_gt",
|
|
@@ -465,7 +453,6 @@ module BetterModel
|
|
|
465
453
|
:"#{field_name}_past",
|
|
466
454
|
:"#{field_name}_future",
|
|
467
455
|
:"#{field_name}_within",
|
|
468
|
-
:"#{field_name}_present",
|
|
469
456
|
:"#{field_name}_blank",
|
|
470
457
|
:"#{field_name}_null",
|
|
471
458
|
:"#{field_name}_not_null"
|
|
@@ -188,6 +188,10 @@ module BetterModel
|
|
|
188
188
|
end
|
|
189
189
|
|
|
190
190
|
# Genera SQL per gestione NULL multi-database
|
|
191
|
+
#
|
|
192
|
+
# NOTA: Il blocco MySQL else qui sotto non è coperto da test automatici
|
|
193
|
+
# perché i test vengono eseguiti su SQLite. Testare manualmente su MySQL
|
|
194
|
+
# con: rails console RAILS_ENV=test
|
|
191
195
|
def nulls_order_sql(field_name, direction, nulls_position)
|
|
192
196
|
quoted_field = connection.quote_column_name(field_name)
|
|
193
197
|
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterModel
|
|
4
|
+
# StateTransition - Base ActiveRecord model for state transition history
|
|
5
|
+
#
|
|
6
|
+
# Questo è un modello abstract. Le classi concrete vengono generate dinamicamente
|
|
7
|
+
# per ogni tabella (state_transitions, order_transitions, etc.).
|
|
8
|
+
#
|
|
9
|
+
# Schema della tabella:
|
|
10
|
+
# t.string :transitionable_type, null: false
|
|
11
|
+
# t.integer :transitionable_id, null: false
|
|
12
|
+
# t.string :event, null: false
|
|
13
|
+
# t.string :from_state, null: false
|
|
14
|
+
# t.string :to_state, null: false
|
|
15
|
+
# t.json :metadata
|
|
16
|
+
# t.datetime :created_at, null: false
|
|
17
|
+
#
|
|
18
|
+
# Utilizzo:
|
|
19
|
+
# # Tutte le transizioni di un modello
|
|
20
|
+
# order.state_transitions
|
|
21
|
+
#
|
|
22
|
+
# # Query globali (tramite classi dinamiche)
|
|
23
|
+
# BetterModel::StateTransitions.for_model(Order)
|
|
24
|
+
# BetterModel::OrderTransitions.by_event(:confirm)
|
|
25
|
+
#
|
|
26
|
+
class StateTransition < ActiveRecord::Base
|
|
27
|
+
# Default table name (can be overridden by dynamic subclasses)
|
|
28
|
+
self.table_name = "state_transitions"
|
|
29
|
+
|
|
30
|
+
# Polymorphic association
|
|
31
|
+
belongs_to :transitionable, polymorphic: true
|
|
32
|
+
|
|
33
|
+
# Validations
|
|
34
|
+
validates :event, :from_state, :to_state, presence: true
|
|
35
|
+
|
|
36
|
+
# Scopes
|
|
37
|
+
|
|
38
|
+
# Scope per modello specifico
|
|
39
|
+
#
|
|
40
|
+
# @param model_class [Class] Classe del modello
|
|
41
|
+
# @return [ActiveRecord::Relation]
|
|
42
|
+
#
|
|
43
|
+
scope :for_model, ->(model_class) {
|
|
44
|
+
where(transitionable_type: model_class.name)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# Scope per evento specifico
|
|
48
|
+
#
|
|
49
|
+
# @param event [Symbol, String] Nome dell'evento
|
|
50
|
+
# @return [ActiveRecord::Relation]
|
|
51
|
+
#
|
|
52
|
+
scope :by_event, ->(event) {
|
|
53
|
+
where(event: event.to_s)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
# Scope per stato di partenza
|
|
57
|
+
#
|
|
58
|
+
# @param state [Symbol, String] Stato di partenza
|
|
59
|
+
# @return [ActiveRecord::Relation]
|
|
60
|
+
#
|
|
61
|
+
scope :from_state, ->(state) {
|
|
62
|
+
where(from_state: state.to_s)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
# Scope per stato di arrivo
|
|
66
|
+
#
|
|
67
|
+
# @param state [Symbol, String] Stato di arrivo
|
|
68
|
+
# @return [ActiveRecord::Relation]
|
|
69
|
+
#
|
|
70
|
+
scope :to_state, ->(state) {
|
|
71
|
+
where(to_state: state.to_s)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# Scope per transizioni recenti
|
|
75
|
+
#
|
|
76
|
+
# @param duration [ActiveSupport::Duration] Durata (es. 7.days)
|
|
77
|
+
# @return [ActiveRecord::Relation]
|
|
78
|
+
#
|
|
79
|
+
scope :recent, ->(duration = 7.days) {
|
|
80
|
+
where("created_at >= ?", duration.ago)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# Scope per transizioni in un periodo
|
|
84
|
+
#
|
|
85
|
+
# @param start_time [Time, Date] Inizio periodo
|
|
86
|
+
# @param end_time [Time, Date] Fine periodo
|
|
87
|
+
# @return [ActiveRecord::Relation]
|
|
88
|
+
#
|
|
89
|
+
scope :between, ->(start_time, end_time) {
|
|
90
|
+
where(created_at: start_time..end_time)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
# Metodi di istanza
|
|
94
|
+
|
|
95
|
+
# Formatted description della transizione
|
|
96
|
+
#
|
|
97
|
+
# @return [String]
|
|
98
|
+
#
|
|
99
|
+
def description
|
|
100
|
+
"#{transitionable_type}##{transitionable_id}: #{from_state} -> #{to_state} (#{event})"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Alias per retrocompatibilità
|
|
104
|
+
alias_method :to_s, :description
|
|
105
|
+
end
|
|
106
|
+
end
|