dscf-banking 0.1.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.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +28 -0
  4. data/Rakefile +8 -0
  5. data/app/controllers/dscf/banking/application_controller.rb +6 -0
  6. data/app/controllers/dscf/banking/interest_configurations_controller.rb +41 -0
  7. data/app/controllers/dscf/banking/interest_rate_tiers_controller.rb +30 -0
  8. data/app/controllers/dscf/banking/interest_rate_types_controller.rb +27 -0
  9. data/app/controllers/dscf/banking/product_approvals_controller.rb +36 -0
  10. data/app/controllers/dscf/banking/product_categories_controller.rb +27 -0
  11. data/app/controllers/dscf/banking/virtual_account_products_controller.rb +74 -0
  12. data/app/jobs/dscf/banking/application_job.rb +6 -0
  13. data/app/mailers/dscf/banking/application_mailer.rb +8 -0
  14. data/app/models/concerns/dscf/banking/auditable.rb +123 -0
  15. data/app/models/dscf/banking/application_record.rb +7 -0
  16. data/app/models/dscf/banking/interest_configuration.rb +85 -0
  17. data/app/models/dscf/banking/interest_rate_tier.rb +24 -0
  18. data/app/models/dscf/banking/interest_rate_type.rb +22 -0
  19. data/app/models/dscf/banking/product_approval.rb +23 -0
  20. data/app/models/dscf/banking/product_audit_log.rb +46 -0
  21. data/app/models/dscf/banking/product_category.rb +22 -0
  22. data/app/models/dscf/banking/virtual_account_product.rb +50 -0
  23. data/app/serializers/dscf/banking/interest_configuration_serializer.rb +11 -0
  24. data/app/serializers/dscf/banking/interest_rate_tier_serializer.rb +8 -0
  25. data/app/serializers/dscf/banking/interest_rate_type_serializer.rb +5 -0
  26. data/app/serializers/dscf/banking/product_approval_serializer.rb +11 -0
  27. data/app/serializers/dscf/banking/product_category_serializer.rb +5 -0
  28. data/app/serializers/dscf/banking/virtual_account_product_serializer.rb +10 -0
  29. data/config/routes.rb +12 -0
  30. data/db/migrate/20250830211002_create_dscf_banking_product_categories.rb +14 -0
  31. data/db/migrate/20250830211027_create_dscf_banking_interest_rate_types.rb +17 -0
  32. data/db/migrate/20250831063605_add_code_and_remove_fields_from_interest_rate_types.rb +12 -0
  33. data/db/migrate/20250831064917_create_dscf_banking_virtual_account_products.rb +26 -0
  34. data/db/migrate/20250831072627_create_dscf_banking_interest_configurations.rb +26 -0
  35. data/db/migrate/20250831080745_create_dscf_banking_interest_rate_tiers.rb +18 -0
  36. data/db/migrate/20250831082356_create_dscf_banking_product_approvals.rb +22 -0
  37. data/db/migrate/20250831083907_create_dscf_banking_product_audit_logs.rb +18 -0
  38. data/db/migrate/20250831084706_allow_null_virtual_account_product_id_in_product_audit_logs.rb +5 -0
  39. data/lib/dscf/banking/engine.rb +24 -0
  40. data/lib/dscf/banking/version.rb +5 -0
  41. data/lib/dscf/banking.rb +10 -0
  42. data/lib/tasks/dscf/banking_tasks.rake +4 -0
  43. data/spec/factories/dscf/banking/interest_configurations.rb +36 -0
  44. data/spec/factories/dscf/banking/interest_rate_tiers.rb +34 -0
  45. data/spec/factories/dscf/banking/interest_rate_types.rb +31 -0
  46. data/spec/factories/dscf/banking/product_approvals.rb +40 -0
  47. data/spec/factories/dscf/banking/product_audit_logs.rb +34 -0
  48. data/spec/factories/dscf/banking/product_categories.rb +26 -0
  49. data/spec/factories/dscf/banking/virtual_account_products.rb +46 -0
  50. metadata +510 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 3bc685fe54cfe3ac3a4eaa5f241c70e71fabbb659daed56ff93ba9d40762c262
4
+ data.tar.gz: 7ba510d6818243bded3582c1ddaa66e87a92f3ebcf7bf7a1f393d83064ff811c
5
+ SHA512:
6
+ metadata.gz: 4ddabc45b7019977470f880cb364e853ba7269319ecede9d7d2f75db6c6e99fed19ca6bc915e7f613b6367d5abe0360850e2aeaf84312db58c8197091a6114ed
7
+ data.tar.gz: 7e038bfe9f2a4558e05971d4cc11374e779553fd72a953d7db25a743c7f334ed6ea758126aeaa30a9f8c63684be617178ab0f6d64eb57560f01c4ad7fbd48730
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright Asrat
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,28 @@
1
+ # Dscf::Banking
2
+ Short description and motivation.
3
+
4
+ ## Usage
5
+ How to use my plugin.
6
+
7
+ ## Installation
8
+ Add this line to your application's Gemfile:
9
+
10
+ ```ruby
11
+ gem "dscf-banking"
12
+ ```
13
+
14
+ And then execute:
15
+ ```bash
16
+ $ bundle
17
+ ```
18
+
19
+ Or install it yourself as:
20
+ ```bash
21
+ $ gem install dscf-banking
22
+ ```
23
+
24
+ ## Contributing
25
+ Contribution directions go here.
26
+
27
+ ## License
28
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require "bundler/setup"
2
+
3
+ APP_RAKEFILE = File.expand_path("spec/dummy/Rakefile", __dir__)
4
+ load "rails/tasks/engine.rake"
5
+
6
+ load "rails/tasks/statistics.rake"
7
+
8
+ require "bundler/gem_tasks"
@@ -0,0 +1,6 @@
1
+ module Dscf
2
+ module Banking
3
+ class ApplicationController < ActionController::API
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,41 @@
1
+ module Dscf::Banking
2
+ class InterestConfigurationsController < ApplicationController
3
+ include Dscf::Core::Common
4
+
5
+ private
6
+
7
+ def model_params
8
+ params.require(:payload).permit(
9
+ :virtual_account_product_id,
10
+ :interest_rate_type_id,
11
+ :annual_interest_rate,
12
+ :income_tax_rate,
13
+ :minimum_balance_for_interest,
14
+ :calculation_method,
15
+ :compounding_period,
16
+ :interest_basis,
17
+ :accrual_frequency,
18
+ :rounding_rule,
19
+ :calculation_timing,
20
+ :promotional_start_date,
21
+ :promotional_end_date,
22
+ :is_active
23
+ )
24
+ end
25
+
26
+ def eager_loaded_associations
27
+ [ :virtual_account_product, :interest_rate_type ]
28
+ end
29
+
30
+ def allowed_order_columns
31
+ %w[id base_rate effective_rate effective_from effective_to created_at updated_at is_active]
32
+ end
33
+
34
+ def default_serializer_includes
35
+ {
36
+ virtual_account_product: {},
37
+ interest_rate_type: {}
38
+ }
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,30 @@
1
+ module Dscf::Banking
2
+ class InterestRateTiersController < ApplicationController
3
+ include Dscf::Core::Common
4
+
5
+ private
6
+
7
+ def model_params
8
+ params.require(:payload).permit(
9
+ :interest_config_id,
10
+ :tier_order,
11
+ :balance_min,
12
+ :balance_max,
13
+ :interest_rate,
14
+ :description
15
+ )
16
+ end
17
+
18
+ def eager_loaded_associations
19
+ [ :interest_config ]
20
+ end
21
+
22
+ def allowed_order_columns
23
+ %w[id tier_order balance_min balance_max interest_rate created_at updated_at]
24
+ end
25
+
26
+ def default_serializer_includes
27
+ { interest_config: {} }
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,27 @@
1
+ module Dscf::Banking
2
+ class InterestRateTypesController < ApplicationController
3
+ include Dscf::Core::Common
4
+
5
+ private
6
+
7
+ def model_params
8
+ params.require(:payload).permit(
9
+ :code,
10
+ :name,
11
+ :description
12
+ )
13
+ end
14
+
15
+ def eager_loaded_associations
16
+ []
17
+ end
18
+
19
+ def allowed_order_columns
20
+ %w[id code name created_at updated_at]
21
+ end
22
+
23
+ def default_serializer_includes
24
+ {}
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,36 @@
1
+ module Dscf::Banking
2
+ class ProductApprovalsController < ApplicationController
3
+ include Dscf::Core::Common
4
+
5
+ private
6
+
7
+ def model_params
8
+ params.require(:payload).permit(
9
+ :virtual_account_product_id,
10
+ :submitted_by_id,
11
+ :submitted_at,
12
+ :reviewed_by_id,
13
+ :reviewed_at,
14
+ :status,
15
+ :comments,
16
+ :approval_level
17
+ )
18
+ end
19
+
20
+ def eager_loaded_associations
21
+ [ :virtual_account_product, :submitted_by, :reviewed_by ]
22
+ end
23
+
24
+ def allowed_order_columns
25
+ %w[id status submitted_at reviewed_at approval_level created_at updated_at]
26
+ end
27
+
28
+ def default_serializer_includes
29
+ {
30
+ virtual_account_product: {},
31
+ submitted_by: {},
32
+ reviewed_by: {}
33
+ }
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,27 @@
1
+ module Dscf::Banking
2
+ class ProductCategoriesController < ApplicationController
3
+ include Dscf::Core::Common
4
+
5
+ private
6
+
7
+ def model_params
8
+ params.require(:payload).permit(
9
+ :name,
10
+ :description,
11
+ :is_active
12
+ )
13
+ end
14
+
15
+ def eager_loaded_associations
16
+ []
17
+ end
18
+
19
+ def allowed_order_columns
20
+ %w[id name created_at updated_at is_active]
21
+ end
22
+
23
+ def default_serializer_includes
24
+ {}
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,74 @@
1
+ module Dscf::Banking
2
+ class VirtualAccountProductsController < ApplicationController
3
+ include Dscf::Core::Common
4
+
5
+ def audit_logs
6
+ product = Dscf::Banking::VirtualAccountProduct.find(params[:id])
7
+ audit_logs = Dscf::Banking::ProductAuditLog.by_product(product.id).recent
8
+
9
+ render json: {
10
+ success: true,
11
+ data: audit_logs.map do |log|
12
+ {
13
+ id: log.id,
14
+ change_type: log.change_type,
15
+ old_values: log.old_values,
16
+ new_values: log.new_values,
17
+ change_description: log.change_description,
18
+ changed_by: {
19
+ id: log.changed_by.id,
20
+ name: user_full_name(log.changed_by)
21
+ },
22
+ created_at: log.created_at
23
+ }
24
+ end
25
+ }
26
+ rescue ActiveRecord::RecordNotFound
27
+ render json: {
28
+ success: false,
29
+ error: "Product not found"
30
+ }, status: :not_found
31
+ end
32
+
33
+ private
34
+
35
+ def user_full_name(user)
36
+ if user.user_profile.present?
37
+ "#{user.user_profile.first_name} #{user.user_profile.last_name}".strip
38
+ else
39
+ user.email
40
+ end
41
+ end
42
+
43
+ def model_params
44
+ params.require(:payload).permit(
45
+ :product_code,
46
+ :product_name,
47
+ :product_category_id,
48
+ :description,
49
+ :document_reference,
50
+ :status,
51
+ :created_by_id,
52
+ :approved_by_id,
53
+ :rejection_reason,
54
+ :is_active
55
+ )
56
+ end
57
+
58
+ def eager_loaded_associations
59
+ [ :product_category, :created_by, :approved_by ]
60
+ end
61
+
62
+ def allowed_order_columns
63
+ %w[id product_code product_name status created_at updated_at is_active]
64
+ end
65
+
66
+ def default_serializer_includes
67
+ {
68
+ product_category: {},
69
+ created_by: {},
70
+ approved_by: {}
71
+ }
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,6 @@
1
+ module Dscf
2
+ module Banking
3
+ class ApplicationJob < ActiveJob::Base
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,8 @@
1
+ module Dscf
2
+ module Banking
3
+ class ApplicationMailer < ActionMailer::Base
4
+ default from: "from@example.com"
5
+ layout "mailer"
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,123 @@
1
+ module Dscf::Banking
2
+ module Auditable
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ after_create :log_creation
7
+ after_update :log_update
8
+ after_destroy :log_deletion
9
+
10
+ attr_accessor :current_user_for_audit
11
+ end
12
+
13
+ private
14
+
15
+ def log_creation
16
+ return unless should_audit?
17
+
18
+ create_audit_log(
19
+ change_type: :created,
20
+ old_values: nil,
21
+ new_values: auditable_attributes,
22
+ change_description: "#{self.class.name} created"
23
+ )
24
+ end
25
+
26
+ def log_update
27
+ return unless should_audit? && saved_changes.any?
28
+
29
+ create_audit_log(
30
+ change_type: determine_update_type,
31
+ old_values: extract_old_values,
32
+ new_values: extract_new_values,
33
+ change_description: generate_change_description
34
+ )
35
+ end
36
+
37
+ def log_deletion
38
+ return unless should_audit?
39
+
40
+ create_audit_log_for_deletion(
41
+ change_type: :deleted,
42
+ old_values: auditable_attributes,
43
+ new_values: nil,
44
+ change_description: "#{self.class.name} deleted"
45
+ )
46
+ end
47
+
48
+ def should_audit?
49
+ current_user_for_audit.present?
50
+ end
51
+
52
+ def create_audit_log(change_type:, old_values:, new_values:, change_description:)
53
+ Dscf::Banking::ProductAuditLog.create!(
54
+ virtual_account_product: self,
55
+ changed_by: current_user_for_audit,
56
+ change_type: change_type,
57
+ old_values: old_values,
58
+ new_values: new_values,
59
+ change_description: change_description
60
+ )
61
+ end
62
+
63
+ def create_audit_log_for_deletion(change_type:, old_values:, new_values:, change_description:)
64
+ # Store the product ID in old_values since we can't reference the deleted record
65
+ enhanced_old_values = old_values.merge("deleted_product_id" => self.id)
66
+
67
+ Dscf::Banking::ProductAuditLog.create!(
68
+ virtual_account_product_id: nil,
69
+ changed_by: current_user_for_audit,
70
+ change_type: change_type,
71
+ old_values: enhanced_old_values,
72
+ new_values: new_values,
73
+ change_description: change_description
74
+ )
75
+ end
76
+
77
+ def determine_update_type
78
+ if saved_changes.key?("status")
79
+ case status
80
+ when "approved"
81
+ :approved
82
+ when "rejected"
83
+ :rejected
84
+ else
85
+ :updated
86
+ end
87
+ elsif saved_changes.key?("is_active")
88
+ is_active? ? :activated : :deactivated
89
+ else
90
+ :updated
91
+ end
92
+ end
93
+
94
+ def extract_old_values
95
+ old_values = {}
96
+ saved_changes.each do |key, (old_val, _new_val)|
97
+ old_values[key] = old_val if auditable_attribute?(key)
98
+ end
99
+ old_values
100
+ end
101
+
102
+ def extract_new_values
103
+ new_values = {}
104
+ saved_changes.each do |key, (_old_val, new_val)|
105
+ new_values[key] = new_val if auditable_attribute?(key)
106
+ end
107
+ new_values
108
+ end
109
+
110
+ def auditable_attributes
111
+ attributes.select { |key, _value| auditable_attribute?(key) }
112
+ end
113
+
114
+ def auditable_attribute?(key)
115
+ !%w[id created_at updated_at].include?(key.to_s)
116
+ end
117
+
118
+ def generate_change_description
119
+ changed_fields = saved_changes.keys.select { |key| auditable_attribute?(key) }
120
+ "Updated #{changed_fields.join(', ')}"
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,7 @@
1
+ module Dscf
2
+ module Banking
3
+ class ApplicationRecord < ActiveRecord::Base
4
+ self.abstract_class = true
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,85 @@
1
+ module Dscf::Banking
2
+ class InterestConfiguration < ApplicationRecord
3
+ belongs_to :interest_rate_type, class_name: "Dscf::Banking::InterestRateType"
4
+ belongs_to :virtual_account_product, class_name: "Dscf::Banking::VirtualAccountProduct"
5
+ has_many :interest_rate_tiers, class_name: "Dscf::Banking::InterestRateTier", dependent: :destroy
6
+
7
+ validates :annual_interest_rate, presence: true, numericality: { greater_than: 0, less_than_or_equal_to: 1 }
8
+ validates :income_tax_rate, presence: true, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 1 }
9
+ validates :minimum_balance_for_interest, presence: true, numericality: { greater_than_or_equal_to: 0 }
10
+ validates :calculation_method, presence: true
11
+
12
+ validates :interest_basis, presence: true
13
+ validates :accrual_frequency, presence: true
14
+ validates :rounding_rule, presence: true
15
+ validates :calculation_timing, presence: true
16
+ validates :is_active, inclusion: { in: [ true, false ] }
17
+ validate :promotional_dates_consistency
18
+
19
+ enum :calculation_method, {
20
+ simple: 0,
21
+ compound: 1
22
+ }
23
+
24
+ enum :compounding_period, {
25
+ daily: 0,
26
+ weekly: 1,
27
+ monthly: 2,
28
+ quarterly: 3,
29
+ annually: 4
30
+ }, prefix: :compounding
31
+
32
+ enum :interest_basis, {
33
+ actual_360: 0,
34
+ actual_365: 1,
35
+ thirty_360: 2
36
+ }
37
+
38
+ enum :accrual_frequency, {
39
+ daily: 0,
40
+ weekly: 1,
41
+ monthly: 2,
42
+ quarterly: 3,
43
+ annually: 4
44
+ }, prefix: :accrual
45
+
46
+ enum :rounding_rule, {
47
+ nearest_cent: 0,
48
+ round_up: 1,
49
+ round_down: 2
50
+ }
51
+
52
+
53
+ scope :active, -> { where(is_active: true) }
54
+ scope :inactive, -> { where(is_active: false) }
55
+ scope :by_product, ->(product_id) { where(virtual_account_product_id: product_id) }
56
+ scope :by_interest_rate_type, ->(type_id) { where(interest_rate_type_id: type_id) }
57
+ scope :promotional, -> { where.not(promotional_start_date: nil, promotional_end_date: nil) }
58
+ scope :current_promotional, -> { where("promotional_start_date <= ? AND promotional_end_date >= ?", Date.current, Date.current) }
59
+
60
+ private
61
+
62
+ def promotional_dates_consistency
63
+ return unless promotional_start_date.present? || promotional_end_date.present?
64
+
65
+ if promotional_start_date.blank?
66
+ errors.add(:promotional_start_date, "must be present when promotional end date is set")
67
+ elsif promotional_end_date.blank?
68
+ errors.add(:promotional_end_date, "must be present when promotional start date is set")
69
+ elsif promotional_start_date > promotional_end_date
70
+ errors.add(:promotional_end_date, "must be after promotional start date")
71
+ end
72
+ end
73
+
74
+ def calculation_schedule_consistency
75
+ case accrual_frequency
76
+ when "weekly"
77
+ errors.add(:calculation_day_of_week, "must be specified for weekly accrual") if calculation_day_of_week.blank?
78
+ when "monthly", "quarterly"
79
+ errors.add(:calculation_day_of_month, "must be specified for monthly/quarterly accrual") if calculation_day_of_month.blank?
80
+ when "annually"
81
+ errors.add(:calculation_day_of_month, "must be specified for annual accrual") if calculation_day_of_month.blank?
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,24 @@
1
+ module Dscf::Banking
2
+ class InterestRateTier < ApplicationRecord
3
+ belongs_to :interest_config, class_name: "Dscf::Banking::InterestConfiguration"
4
+
5
+ validates :tier_order, presence: true, numericality: { greater_than: 0 }
6
+ validates :balance_min, presence: true, numericality: { greater_than_or_equal_to: 0 }
7
+ validates :balance_max, numericality: { greater_than: :balance_min }, allow_nil: true
8
+ validates :interest_rate, presence: true, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 1 }
9
+ validates :description, length: { maximum: 200 }
10
+ validates :tier_order, uniqueness: { scope: :interest_config_id }
11
+ validate :balance_max_greater_than_min
12
+
13
+ scope :ordered, -> { order(:tier_order) }
14
+ scope :by_interest_config, ->(config_id) { where(interest_config_id: config_id) }
15
+
16
+ private
17
+
18
+ def balance_max_greater_than_min
19
+ return unless balance_max.present? && balance_min.present?
20
+
21
+ errors.add(:balance_max, "must be greater than balance_min") if balance_max <= balance_min
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,22 @@
1
+ module Dscf::Banking
2
+ class InterestRateType < ApplicationRecord
3
+ self.table_name = "dscf_banking_interest_rate_types"
4
+
5
+ validates :code, presence: true, uniqueness: { case_sensitive: true }, length: { maximum: 20 }
6
+ validates :name, presence: true, length: { maximum: 50 }
7
+ validates :description, length: { maximum: 1000 }
8
+
9
+ scope :by_code, ->(code) { where(code: code) }
10
+ scope :by_name, ->(name) { where("name ILIKE ?", "%#{name}%") }
11
+
12
+ before_validation :strip_whitespace
13
+
14
+ private
15
+
16
+ def strip_whitespace
17
+ self.code = code&.strip
18
+ self.name = name&.strip
19
+ self.description = description&.strip
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,23 @@
1
+ module Dscf::Banking
2
+ class ProductApproval < ApplicationRecord
3
+ belongs_to :virtual_account_product
4
+ belongs_to :submitted_by, class_name: "Dscf::Core::User", optional: false
5
+ belongs_to :reviewed_by, class_name: "Dscf::Core::User", optional: true
6
+
7
+ enum :status, {
8
+ pending: 0,
9
+ approved: 1,
10
+ rejected: 2,
11
+ under_review: 3
12
+ }
13
+
14
+ validates :status, presence: true
15
+ validates :submitted_at, presence: true
16
+ validates :approval_level, presence: true, numericality: { greater_than: 0 }
17
+ validates :comments, length: { maximum: 1000 }
18
+
19
+ scope :by_status, ->(status) { where(status: status) }
20
+ scope :by_approval_level, ->(level) { where(approval_level: level) }
21
+ scope :pending_review, -> { where(status: [ "pending", "under_review" ]) }
22
+ end
23
+ end
@@ -0,0 +1,46 @@
1
+ module Dscf::Banking
2
+ class ProductAuditLog < ApplicationRecord
3
+ belongs_to :virtual_account_product, class_name: "Dscf::Banking::VirtualAccountProduct", optional: true
4
+ belongs_to :changed_by, class_name: "Dscf::Core::User"
5
+
6
+ enum :change_type, {
7
+ created: 0,
8
+ updated: 1,
9
+ deleted: 2,
10
+ approved: 3,
11
+ rejected: 4,
12
+ activated: 5,
13
+ deactivated: 6
14
+ }
15
+
16
+ validates :change_type, presence: true
17
+ validates :virtual_account_product_id, presence: true, unless: :deleted_change_type?
18
+
19
+ private
20
+
21
+ def deleted_change_type?
22
+ change_type == "deleted"
23
+ end
24
+ validates :changed_by_id, presence: true
25
+
26
+ scope :by_product, ->(product_id) { where(virtual_account_product_id: product_id) }
27
+ scope :by_user, ->(user_id) { where(changed_by_id: user_id) }
28
+ scope :by_change_type, ->(type) { where(change_type: type) }
29
+ scope :recent, -> { order(created_at: :desc) }
30
+
31
+ before_update :prevent_update
32
+ before_destroy :prevent_destroy
33
+
34
+ private
35
+
36
+ def prevent_update
37
+ errors.add(:base, "Audit logs cannot be updated")
38
+ raise ActiveRecord::RecordInvalid.new(self)
39
+ end
40
+
41
+ def prevent_destroy
42
+ errors.add(:base, "Audit logs cannot be deleted")
43
+ throw(:abort)
44
+ end
45
+ end
46
+ end