dscf-core 0.3.3 → 0.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 442564f7e8acce4f8eae9f074f58b03f8f026189660334c5c58cb2f67cce461c
4
- data.tar.gz: 1c283579ec50ad95225bf1f6f9fd76d71decedf6387289ce99c314fefdae5d8f
3
+ metadata.gz: '07479b97a14690388c3ad7d6d8104d07e5d0495115a89732b0098eedbfb7cb53'
4
+ data.tar.gz: b3fc923aec6c9d9d1f9b6a8e75f97aecd4b89337b44659bb479c6c9b9ac8e6a8
5
5
  SHA512:
6
- metadata.gz: 8d13fd5bb155bc6b292bcbe46c28e35d5e3be4b070cb95183b30cc7da0abaef0f955b000af3a2282138b5272f98a181569e45ae322c047e695cf229daba194ec
7
- data.tar.gz: 56540f927754630b63a813a573e81f7c84cc2d94f462851536dc04b59cf6ecb1c85ee32e3156d43263e0de6256cd986dedc160d7d13da0f123cf8818a3b6df7e
6
+ metadata.gz: '0369e0fe5f6914f227ebd6e0dc3fe96c299ce1e50978d4a592b15c114f47928974a1b4a27bbff14ae5814449bc08038eabdb02a816daef451f05c45a0ca3feb8'
7
+ data.tar.gz: 91aff4a210139997fd2ca7272bee083c8a4aa0d7407a699da923e2677aac7fa1ebfa8a0de3ab1c71f99669eca5d595ac8ceaf1cee0cd0ec3b33423a5195e9949
@@ -166,8 +166,6 @@ module Dscf
166
166
  end
167
167
  end
168
168
 
169
- private
170
-
171
169
  # Check if current action needs auditing
172
170
  def audit_needed?
173
171
  return false if _audit_configs.empty?
@@ -0,0 +1,15 @@
1
+ module Dscf
2
+ module Core
3
+ module RoleAssignable
4
+ extend ActiveSupport::Concern
5
+
6
+ def assign_default_role(user)
7
+ role = Role.find_or_create_by!(code: "USER") do |r|
8
+ r.name = "User"
9
+ end
10
+
11
+ UserRole.find_or_create_by!(user: user, role: role)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,109 @@
1
+ module Dscf
2
+ module Core
3
+ class AccountManagementController < ApplicationController
4
+ include Dscf::Core::Common
5
+
6
+ def index
7
+ authorize @clazz.new, :index?, policy_class: Dscf::Core::AccountManagementPolicy
8
+
9
+ data = policy_scope(@clazz)
10
+ data = data.includes(eager_loaded_associations) if eager_loaded_associations.present?
11
+ data = filter_records(data)
12
+
13
+ total_count = data.count if params[:page]
14
+ data = data.then(&paginate) if params[:page]
15
+
16
+ options = {}
17
+ includes = serializer_includes_for_action(:index)
18
+ options[:include] = includes if includes.present?
19
+
20
+ if params[:page]
21
+ total_pages = (total_count.to_f / per_page).ceil
22
+ count = data.is_a?(Array) ? data.length : data.count
23
+ options[:pagination] = {
24
+ current_page: page_no,
25
+ per_page: per_page,
26
+ count: count,
27
+ total_count: total_count,
28
+ total_pages: total_pages,
29
+ links: pagination_links(total_pages)
30
+ }
31
+ end
32
+
33
+ render_success(data: data, serializer_options: options)
34
+ end
35
+
36
+ def suspend
37
+ @obj = find_record
38
+ authorize @obj, :suspend?, policy_class: Dscf::Core::AccountManagementPolicy
39
+
40
+ return render_error(errors: "User is already suspended", status: :unprocessable_entity) if @obj.suspended?
41
+
42
+ if @obj.update(status: :suspended, suspended_at: Time.current, suspension_reason: params[:suspension_reason])
43
+ notification = Dscf::Core::Notification.create!(
44
+ notifiable: @obj,
45
+ recipient: @obj,
46
+ notification_type: :suspension,
47
+ title: "Account Suspended",
48
+ body: params[:suspension_reason] || "Your account has been suspended"
49
+ )
50
+ Dscf::Core::NotificationService.deliver(notification)
51
+
52
+ render_success(data: @obj)
53
+ else
54
+ render_error(errors: @obj.errors.full_messages[0], status: :unprocessable_entity)
55
+ end
56
+ end
57
+
58
+ def activate
59
+ @obj = find_record
60
+ authorize @obj, :activate?, policy_class: Dscf::Core::AccountManagementPolicy
61
+
62
+ return render_error(errors: "User is already active", status: :unprocessable_entity) if @obj.active?
63
+
64
+ if @obj.update(status: :active, suspended_at: nil)
65
+ notification = Dscf::Core::Notification.create!(
66
+ notifiable: @obj,
67
+ recipient: @obj,
68
+ notification_type: :activation,
69
+ title: "Account Activated",
70
+ body: params[:reason] || "Your account has been activated"
71
+ )
72
+ Dscf::Core::NotificationService.deliver(notification)
73
+
74
+ render_success(data: @obj)
75
+ else
76
+ render_error(errors: @obj.errors.full_messages[0], status: :unprocessable_entity)
77
+ end
78
+ end
79
+
80
+ private
81
+
82
+ def set_clazz
83
+ @clazz = Dscf::Core::User
84
+ end
85
+
86
+ def model_params
87
+ params.permit(:suspension_reason, :reason)
88
+ end
89
+
90
+ def find_record
91
+ @clazz.find(params[:id])
92
+ end
93
+
94
+ def eager_loaded_associations
95
+ [:user_profile]
96
+ end
97
+
98
+ def default_serializer_includes
99
+ {
100
+ index: [:user_profile]
101
+ }
102
+ end
103
+
104
+ def allowed_order_columns
105
+ %w[id email phone status suspended_at created_at updated_at]
106
+ end
107
+ end
108
+ end
109
+ end
@@ -1,6 +1,8 @@
1
1
  module Dscf
2
2
  module Core
3
3
  class AuthController < ApplicationController
4
+ include Dscf::Core::RoleAssignable
5
+
4
6
  skip_before_action :authenticate_user, only: %i[login signup refresh]
5
7
  skip_before_action :validate_token_expiry, only: %i[login signup refresh]
6
8
  skip_before_action :validate_device_consistency, only: %i[login signup refresh]
@@ -122,14 +124,6 @@ module Dscf
122
124
  def refresh_params
123
125
  params.permit(:refresh_token)
124
126
  end
125
-
126
- def assign_default_role(user)
127
- role = Role.find_or_create_by!(code: "USER") do |r|
128
- r.name = "User"
129
- end
130
-
131
- UserRole.find_or_create_by!(user: user, role: role)
132
- end
133
127
  end
134
128
  end
135
129
  end
@@ -0,0 +1,30 @@
1
+ module Dscf
2
+ module Core
3
+ class FaydaVerificationsController < ApplicationController
4
+ skip_before_action :authenticate_user, only: %i[verify]
5
+ skip_before_action :validate_token_expiry, only: %i[verify]
6
+ skip_before_action :validate_device_consistency, only: %i[verify]
7
+ skip_before_action :authorize_action!, only: %i[verify]
8
+
9
+ def verify
10
+ skip_authorization
11
+
12
+ fayda_number = params[:fayda_number].presence
13
+
14
+ return render_error(errors: "fayda_number is required", status: :unprocessable_entity) unless fayda_number
15
+ return render_error(errors: "Invalid fayda_number format", status: :unprocessable_entity) unless fayda_number.match?(/\A\d{12}\z/)
16
+
17
+ render_success(
18
+ data: {
19
+ verified: true,
20
+ fayda_number: fayda_number,
21
+ full_name: "Mock Person",
22
+ gender: "Male",
23
+ date_of_birth: "1990-01-01",
24
+ verification_date: Time.zone.now.iso8601
25
+ }
26
+ )
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,11 @@
1
+ module Dscf
2
+ module Core
3
+ module Notifiable
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ has_many :notifications, as: :notifiable, dependent: :destroy, class_name: "Dscf::Core::Notification"
8
+ end
9
+ end
10
+ end
11
+ end
@@ -8,6 +8,8 @@ module Dscf
8
8
 
9
9
  has_one_attached :logo
10
10
 
11
+ include Dscf::Core::Notifiable
12
+
11
13
  has_many :documents, as: :documentable, dependent: :destroy, class_name: "Dscf::Core::Document"
12
14
 
13
15
  validates :name, presence: true
@@ -20,7 +22,7 @@ module Dscf
20
22
 
21
23
  def self.ransackable_attributes(_auth_object = nil)
22
24
  %w[business_type_id contact_email contact_phone created_at description id id_value name tin_number
23
- updated_at user_id is_system]
25
+ updated_at user_id is_system]
24
26
  end
25
27
 
26
28
  def self.ransackable_associations(_auth_object = nil)
@@ -0,0 +1,32 @@
1
+ module Dscf
2
+ module Core
3
+ class Notification < ApplicationRecord
4
+ self.table_name = "dscf_core_notifications"
5
+
6
+ belongs_to :notifiable, polymorphic: true
7
+ belongs_to :recipient, polymorphic: true
8
+
9
+ enum :status, {pending: 0, delivered: 1, failed: 2, read: 3}, default: :pending
10
+ enum :notification_type, {
11
+ approval: 0, rejection: 1, modification: 2,
12
+ suspension: 3, activation: 4, listing: 5, general: 6
13
+ }
14
+
15
+ validates :notification_type, presence: true
16
+ validates :title, presence: true
17
+ validates :body, presence: true
18
+ validates :recipient, presence: true
19
+
20
+ scope :unread, -> { where(read_at: nil) }
21
+ scope :for_recipient, ->(recipient) { where(recipient: recipient) }
22
+
23
+ def self.ransackable_attributes(_auth_object = nil)
24
+ %w[id notification_type status title body read_at delivered_at created_at updated_at]
25
+ end
26
+
27
+ def self.ransackable_associations(_auth_object = nil)
28
+ %w[notifiable recipient]
29
+ end
30
+ end
31
+ end
32
+ end
@@ -10,13 +10,17 @@ module Dscf
10
10
  has_many :documents, as: :documentable, dependent: :destroy, class_name: "Dscf::Core::Document"
11
11
  has_one :user_profile, dependent: :destroy, class_name: "Dscf::Core::UserProfile"
12
12
 
13
+ enum :status, {active: 0, suspended: 1}, default: :active
14
+
15
+ scope :active, -> { where(status: :active) }
16
+ scope :suspended, -> { where(status: :suspended) }
17
+
13
18
  accepts_nested_attributes_for :user_profile
14
19
 
15
20
  before_create :set_default_temp_password
16
21
 
17
22
  # --- RBAC Helper Methods ---
18
23
 
19
- # rubocop:disable Naming/PredicateName
20
24
  def has_role?(role_code)
21
25
  active_role_codes.include?(role_code.to_s.upcase)
22
26
  end
@@ -36,7 +40,6 @@ module Dscf
36
40
  def has_any_permission?(*permission_codes)
37
41
  permission_codes.flatten.any? { |code| has_permission?(code) }
38
42
  end
39
- # rubocop:enable Naming/PredicateName
40
43
 
41
44
  def super_admin?
42
45
  active_role_codes.include?("SUPER_ADMIN")
@@ -71,7 +74,7 @@ module Dscf
71
74
  end
72
75
 
73
76
  def self.ransackable_attributes(_auth_object = nil)
74
- %w[id email phone verified_at created_at updated_at]
77
+ %w[id email phone status suspended_at suspension_reason verified_at created_at updated_at]
75
78
  end
76
79
 
77
80
  def self.ransackable_associations(_auth_object = nil)
@@ -0,0 +1,17 @@
1
+ module Dscf
2
+ module Core
3
+ class AccountManagementPolicy < ApplicationPolicy
4
+ def index?
5
+ user.has_permission?("account_management.index")
6
+ end
7
+
8
+ def suspend?
9
+ user.has_permission?("account_management.suspend")
10
+ end
11
+
12
+ def activate?
13
+ user.has_permission?("account_management.activate")
14
+ end
15
+ end
16
+ end
17
+ end
@@ -1,7 +1,7 @@
1
1
  module Dscf
2
2
  module Core
3
3
  class UserSerializer < ActiveModel::Serializer
4
- attributes :id, :email, :phone, :verified_at, :created_at, :updated_at
4
+ attributes :id, :email, :phone, :status, :suspended_at, :suspension_reason, :verified_at, :created_at, :updated_at
5
5
 
6
6
  has_many :user_roles, serializer: Dscf::Core::UserRoleSerializer
7
7
  has_many :roles, serializer: Dscf::Core::RoleSerializer
@@ -0,0 +1,34 @@
1
+ require "securerandom"
2
+
3
+ module Dscf
4
+ module Core
5
+ class NotificationService
6
+ class << self
7
+ def deliver(notification)
8
+ adapter = resolve_adapter
9
+ adapter.send_sms(notification.recipient.phone, notification.body)
10
+ notification.update!(status: :delivered, delivered_at: Time.current)
11
+ end
12
+
13
+ def resolve_adapter
14
+ adapter_name = Rails.application.config.x.sms_adapter || :stub
15
+
16
+ case adapter_name.to_sym
17
+ when :stub then Adapters::SmsStub
18
+ else raise ArgumentError, "Unknown SMS adapter: #{adapter_name}"
19
+ end
20
+ end
21
+ end
22
+
23
+ module Adapters
24
+ class SmsStub
25
+ class << self
26
+ def send_sms(_phone, _message)
27
+ {success: true, message_id: "stub-#{SecureRandom.hex(6)}"}
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
data/config/routes.rb CHANGED
@@ -35,4 +35,13 @@ Dscf::Core::Engine.routes.draw do
35
35
 
36
36
  resources :addresses, only: %i[index show create update]
37
37
  resources :business_types, only: %i[index show create update]
38
+
39
+ post "fayda/verify", to: "fayda_verifications#verify"
40
+
41
+ resources :account_management, only: [:index], controller: "account_management" do
42
+ member do
43
+ post :suspend
44
+ post :activate
45
+ end
46
+ end
38
47
  end
@@ -0,0 +1,21 @@
1
+ class CreateDscfCoreNotifications < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :dscf_core_notifications do |t|
4
+ t.references :notifiable, polymorphic: true, null: false, index: false
5
+ t.references :recipient, polymorphic: true, null: false, index: false
6
+ t.integer :notification_type, null: false
7
+ t.string :title, null: false
8
+ t.text :body, null: false
9
+ t.integer :status, default: 0, null: false
10
+ t.datetime :read_at
11
+ t.datetime :delivered_at
12
+
13
+ t.timestamps
14
+ end
15
+
16
+ add_index :dscf_core_notifications, :status
17
+ add_index :dscf_core_notifications, :notification_type
18
+ add_index :dscf_core_notifications, %i[recipient_type recipient_id], name: "idx_dscf_core_notifications_on_recipient"
19
+ add_index :dscf_core_notifications, %i[notifiable_type notifiable_id], name: "idx_dscf_core_notifications_on_notifiable"
20
+ end
21
+ end
@@ -0,0 +1,7 @@
1
+ class AddStatusToDscfCoreUsers < ActiveRecord::Migration[8.0]
2
+ def change
3
+ add_column :dscf_core_users, :status, :integer, default: 0, null: false
4
+ add_column :dscf_core_users, :suspended_at, :datetime, null: true
5
+ add_column :dscf_core_users, :suspension_reason, :text, null: true
6
+ end
7
+ end
@@ -1,5 +1,5 @@
1
1
  module Dscf
2
2
  module Core
3
- VERSION = "0.3.3".freeze
3
+ VERSION = "0.3.5".freeze
4
4
  end
5
5
  end
@@ -0,0 +1,13 @@
1
+ FactoryBot.define do
2
+ factory :notification, class: "Dscf::Core::Notification" do
3
+ association :notifiable, factory: :user
4
+ association :recipient, factory: :user
5
+
6
+ notification_type { :approval }
7
+ title { "Notification #{SecureRandom.hex(4)}" }
8
+ body { Faker::Lorem.paragraph }
9
+ status { :pending }
10
+ read_at { nil }
11
+ delivered_at { nil }
12
+ end
13
+ end
@@ -6,5 +6,8 @@ FactoryBot.define do
6
6
  verified_at { Time.now }
7
7
  temp_password { false }
8
8
  password { "password" }
9
+
10
+ suspended_at { nil }
11
+ suspension_reason { nil }
9
12
  end
10
13
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dscf-core
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.3
4
+ version: 0.3.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Asrat
@@ -434,12 +434,15 @@ files:
434
434
  - app/controllers/concerns/dscf/core/json_response.rb
435
435
  - app/controllers/concerns/dscf/core/pagination.rb
436
436
  - app/controllers/concerns/dscf/core/reviewable_controller.rb
437
+ - app/controllers/concerns/dscf/core/role_assignable.rb
437
438
  - app/controllers/concerns/dscf/core/token_authenticatable.rb
439
+ - app/controllers/dscf/core/account_management_controller.rb
438
440
  - app/controllers/dscf/core/addresses_controller.rb
439
441
  - app/controllers/dscf/core/application_controller.rb
440
442
  - app/controllers/dscf/core/auth_controller.rb
441
443
  - app/controllers/dscf/core/business_types_controller.rb
442
444
  - app/controllers/dscf/core/businesses_controller.rb
445
+ - app/controllers/dscf/core/fayda_verifications_controller.rb
443
446
  - app/controllers/dscf/core/files_controller.rb
444
447
  - app/controllers/dscf/core/permissions_controller.rb
445
448
  - app/controllers/dscf/core/role_permissions_controller.rb
@@ -452,6 +455,7 @@ files:
452
455
  - app/mailers/dscf/core/application_mailer.rb
453
456
  - app/models/concerns/dscf/core/attachable.rb
454
457
  - app/models/concerns/dscf/core/auditable_model.rb
458
+ - app/models/concerns/dscf/core/notifiable.rb
455
459
  - app/models/concerns/dscf/core/reviewable_model.rb
456
460
  - app/models/concerns/dscf/core/user_authenticatable.rb
457
461
  - app/models/dscf/core/address.rb
@@ -461,6 +465,7 @@ files:
461
465
  - app/models/dscf/core/business_type.rb
462
466
  - app/models/dscf/core/document.rb
463
467
  - app/models/dscf/core/file_attachment.rb
468
+ - app/models/dscf/core/notification.rb
464
469
  - app/models/dscf/core/permission.rb
465
470
  - app/models/dscf/core/refresh_token.rb
466
471
  - app/models/dscf/core/review.rb
@@ -469,6 +474,7 @@ files:
469
474
  - app/models/dscf/core/user.rb
470
475
  - app/models/dscf/core/user_profile.rb
471
476
  - app/models/dscf/core/user_role.rb
477
+ - app/policies/dscf/core/account_management_policy.rb
472
478
  - app/policies/dscf/core/application_policy.rb
473
479
  - app/policies/dscf/core/business_policy.rb
474
480
  - app/policies/dscf/core/business_type_policy.rb
@@ -493,6 +499,7 @@ files:
493
499
  - app/services/dscf/core/file_storage.rb
494
500
  - app/services/dscf/core/file_storage/client.rb
495
501
  - app/services/dscf/core/file_storage/uploader.rb
502
+ - app/services/dscf/core/notification_service.rb
496
503
  - app/services/dscf/core/token_service.rb
497
504
  - config/initializers/jwt.rb
498
505
  - config/initializers/ransack.rb
@@ -516,6 +523,8 @@ files:
516
523
  - db/migrate/20260304000001_create_dscf_core_permissions.rb
517
524
  - db/migrate/20260304000002_create_dscf_core_role_permissions.rb
518
525
  - db/migrate/20260501000001_add_is_system_to_dscf_core_businesses.rb
526
+ - db/migrate/20260616000000_create_dscf_core_notifications.rb
527
+ - db/migrate/20260616000001_add_status_to_dscf_core_users.rb
519
528
  - lib/dscf/core.rb
520
529
  - lib/dscf/core/engine.rb
521
530
  - lib/dscf/core/permission_registry.rb
@@ -532,6 +541,7 @@ files:
532
541
  - spec/factories/dscf/core/business_types.rb
533
542
  - spec/factories/dscf/core/businesses.rb
534
543
  - spec/factories/dscf/core/documents.rb
544
+ - spec/factories/dscf/core/notifications.rb
535
545
  - spec/factories/dscf/core/permissions.rb
536
546
  - spec/factories/dscf/core/refresh_tokens.rb
537
547
  - spec/factories/dscf/core/reviews.rb